package auth

import (
	"net/http"
	"time"

	"donetick.com/core/config"
	uModel "donetick.com/core/internal/user/model"
	uRepo "donetick.com/core/internal/user/repo"
	"donetick.com/core/logging"
	jwt "github.com/appleboy/gin-jwt/v2"
	"github.com/gin-gonic/gin"
	"golang.org/x/crypto/bcrypt"
)

var identityKey = "id"

type signIn struct {
	Username string `form:"username" json:"username" binding:"required"`
	Password string `form:"password" json:"password" binding:"required"`
}

func CurrentUser(c *gin.Context) (*uModel.User, bool) {
	data, ok := c.Get(identityKey)
	if !ok {
		return nil, false
	}
	acc, ok := data.(*uModel.User)
	return acc, ok
}

func MustCurrentUser(c *gin.Context) *uModel.User {
	acc, ok := CurrentUser(c)
	if ok {
		return acc
	}
	panic("no account in gin.Context")
}

func NewAuthMiddleware(cfg *config.Config, userRepo *uRepo.UserRepository) (*jwt.GinJWTMiddleware, error) {
	return jwt.New(&jwt.GinJWTMiddleware{
		Realm:       "test zone",
		Key:         []byte(cfg.Jwt.Secret),
		Timeout:     cfg.Jwt.SessionTime,
		MaxRefresh:  cfg.Jwt.MaxRefresh, // 7 days as long as their token is valid they can refresh it
		IdentityKey: identityKey,
		PayloadFunc: func(data interface{}) jwt.MapClaims {
			if u, ok := data.(*uModel.User); ok {
				return jwt.MapClaims{
					identityKey: u.Username,
				}
			}
			return jwt.MapClaims{}
		},
		IdentityHandler: func(c *gin.Context) interface{} {
			claims := jwt.ExtractClaims(c)
			username, ok := claims[identityKey].(string)
			if !ok {
				return nil
			}
			user, err := userRepo.GetUserByUsername(c.Request.Context(), username)
			if err != nil {
				return nil
			}
			return user
		},
		Authenticator: func(c *gin.Context) (interface{}, error) {
			provider := c.Value("auth_provider")
			switch provider {
			case nil:
				var req signIn
				if err := c.ShouldBindJSON(&req); err != nil {
					return "", jwt.ErrMissingLoginValues
				}

				// ctx := cache.WithCacheSkip(c.Request.Context(), true)
				user, err := userRepo.GetUserByUsername(c.Request.Context(), req.Username)
				if err != nil || user.Disabled {
					return nil, jwt.ErrFailedAuthentication
				}
				err = Matches(user.Password, req.Password)
				if err != nil {
					if err != bcrypt.ErrMismatchedHashAndPassword {
						logging.FromContext(c).Warnw("middleware.jwt.Authenticator found unknown error when matches password", "err", err)
					}
					return nil, jwt.ErrFailedAuthentication
				}
				return &uModel.User{
					ID:        user.ID,
					Username:  user.Username,
					Password:  "",
					Image:     user.Image,
					CreatedAt: user.CreatedAt,
					UpdatedAt: user.UpdatedAt,
					Disabled:  user.Disabled,
					CircleID:  user.CircleID,
				}, nil
			case "3rdPartyAuth":
				// we should only reach this stage if a handler mannually call authenticator with it's context:

				var authObject *uModel.User
				v := c.Value("user_account")
				authObject = v.(*uModel.User)

				return authObject, nil

			default:
				return nil, jwt.ErrFailedAuthentication
			}
		},

		Authorizator: func(data interface{}, c *gin.Context) bool {

			if _, ok := data.(*uModel.User); ok {
				return true
			}
			return false
		},
		Unauthorized: func(c *gin.Context, code int, message string) {
			logging.FromContext(c).Info("middleware.jwt.Unauthorized", "code", code, "message", message)
			c.JSON(code, gin.H{
				"code":    code,
				"message": message,
			})
		},
		LoginResponse: func(c *gin.Context, code int, token string, expire time.Time) {
			c.JSON(http.StatusOK, gin.H{
				"code":   code,
				"token":  token,
				"expire": expire,
			})
		},
		TokenLookup:   "header: Authorization",
		TokenHeadName: "Bearer",
		TimeFunc:      time.Now,
	})
}
