package authentication

import (
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/golang-jwt/jwt"
	a "github.com/karincake/apem"
	dg "github.com/karincake/apem/db-gorm-pg"
	ms "github.com/karincake/apem/ms-redis"
	d "github.com/karincake/dodol"
	l "github.com/karincake/lepet"

	pa "simrs-vx/internal/lib/auth"
	pl "simrs-vx/pkg/logger"
	p "simrs-vx/pkg/password"

	eap "simrs-vx/internal/domain/main-entities/auth-partner"
	eu "simrs-vx/internal/domain/main-entities/user"
	euf "simrs-vx/internal/domain/main-entities/user-fes"
	erc "simrs-vx/internal/domain/references/common"
)

const source = "authentication"

var authCfg AuthCfg

func init() {
	a.RegisterExtCall(GetConfig)
}

// Generates token and store in redis at one place
// just return the error code
func GenToken(input eu.LoginDto) (*d.Data, error) {
	event := pl.Event{
		Feature: "Create",
		Source:  source,
	}

	// Get User
	user := &eu.User{Name: input.Name}
	if errCode := getAndCheck(user, user, nil); errCode != "" {
		return nil, d.FieldErrors{"authentication": d.FieldError{Code: errCode, Message: pl.GenMessage(errCode)}}
	}

	// Check login attempt
	if user.LoginAttemptCount > 5 {
		if user.LastSuccessLogin != nil {
			now := time.Now()
			lastAllowdLogin := user.LastAllowdLogin
			if lastAllowdLogin.After(now.Add(-time.Hour * 1)) {
				return nil, d.FieldErrors{"authentication": d.FieldError{Code: "auth-login-tooMany", Message: pl.GenMessage("auth-login-tooMany")}}
			} else {
				tn := time.Now()
				user.LastAllowdLogin = &tn
				user.LoginAttemptCount = 0
				dg.I.Save(&user)
			}
		} else {
			tn := time.Now()
			user.LastAllowdLogin = &tn
			dg.I.Save(&user)
			return nil, d.FieldErrors{"authentication": d.FieldError{Code: "auth-login-tooMany", Message: pl.GenMessage("auth-login-tooMany")}}
		}
	}

	// Check password
	if !p.Check(input.Password, user.Password) {
		user.LoginAttemptCount++
		dg.I.Save(&user)
		return nil, d.FieldErrors{"authentication": d.FieldError{Code: "auth-login-incorrect", Message: pl.GenMessage("auth-login-incorrect")}}
	} else if user.Status_Code == erc.USCBlocked {
		return nil, d.FieldErrors{"authentication": d.FieldError{Code: "auth-login-blocked", Message: pl.GenMessage("auth-login-blocked")}}
	} else if user.Status_Code == erc.USCNew {
		return nil, d.FieldErrors{"authentication": d.FieldError{Code: "auth-login-unverified", Message: pl.GenMessage("auth-login-unverified")}}
	}

	// Data and output population
	atClaims := jwt.MapClaims{}
	outputData := d.II{}
	if err := populateRoles(user, input, atClaims, outputData, event); err != nil {
		return nil, err
	}

	// Only manual login
	tn := time.Now()
	user.LoginAttemptCount = 0
	user.LastSuccessLogin = &tn
	user.LastAllowdLogin = &tn
	dg.I.Save(&user)

	// Current data
	return &d.Data{
		Meta: d.IS{
			"source":    "authentication",
			"structure": "single-data",
			"status":    "verified",
		},
		Data: outputData,
	}, nil
}

func GenTokenFes(input euf.LoginDto) (*d.Data, error) {
	event := pl.Event{
		Feature: "Create",
		Source:  source,
	}

	// Get User Fes
	userFes := &euf.UserFes{Name: input.Name, AuthPartner_Code: input.AuthPartner_Code}
	if errCode := getAndCheck(userFes, userFes, "AuthPartner"); errCode != "" {
		return nil, d.FieldErrors{"authentication": d.FieldError{Code: errCode, Message: pl.GenMessage(errCode)}}
	}
	// Preload above fails and I am so done, do it manually
	authPartner := eap.AuthPartner{}
	if err := dg.I.Where("\"Code\" = ?", userFes.AuthPartner_Code).First(&authPartner).Error; err != nil {
		return nil, d.FieldErrors{"authentication": d.FieldError{Code: "auth-secretKey-invalid", Message: pl.GenMessage("auth-secretKey-invalid")}}
	}
	if authPartner.SecretKey != input.AuthPartner_SecretKey {
		return nil, d.FieldErrors{"authentication": d.FieldError{Code: "auth-secretKey-invalid", Message: pl.GenMessage("auth-secretKey-invalid")}}
	}

	// Get User
	user := &eu.User{Name: userFes.User_Name}
	if errCode := getAndCheck(user, user, nil); errCode != "" {
		return nil, d.FieldErrors{"authentication": d.FieldError{Code: errCode, Message: pl.GenMessage(errCode)}}
	}

	// Data and output population
	atClaims := jwt.MapClaims{}
	outputData := d.II{}
	primInput := eu.LoginDto{Duration: input.Duration}
	if err := populateRoles(user, primInput, atClaims, outputData, event); err != nil {
		return nil, err
	}

	// Current data
	return &d.Data{
		Meta: d.IS{
			"source":    "authentication",
			"structure": "single-data",
			"status":    "verified",
		},
		Data: outputData,
	}, nil
}

func RevokeToken(uuid string) {
	ms.I.Del(uuid)
}

func VerifyToken(r *http.Request, tokenType TokenType) (data *jwt.Token, errCode, errDetail string) {
	auth := r.Header.Get("Authorization")
	if auth == "" {
		return nil, "auth-missingHeader", ""
	}
	authArr := strings.Split(auth, " ")
	if len(authArr) == 2 {
		auth = authArr[1]
	}

	token, err := jwt.Parse(auth, func(token *jwt.Token) (any, error) {
		//Make sure that the token method conform to "SigningMethodHMAC"
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf(l.I.Msg("token-sign-unexcpeted"), token.Header["alg"])
		}
		if tokenType == AccessToken {
			return []byte(authCfg.AtSecretKey), nil
		} else {
			return []byte(authCfg.RtSecretKey), nil
		}
	})
	if err != nil {
		return nil, "token-parse-fail", err.Error()
	}
	return token, "", ""
}

func ExtractToken(r *http.Request, tokenType TokenType) (data *pa.AuthInfo, err error) {
	token, errCode, errDetail := VerifyToken(r, tokenType)
	if errCode != "" {
		return nil, d.FieldError{Code: errCode, Message: pl.GenMessage(errCode, errDetail)}
	}
	claims, ok := token.Claims.(jwt.MapClaims)
	if ok && token.Valid {
		accessUuid, ok := claims["uuid"].(string)
		if !ok {
			return nil, d.FieldError{Code: "token-invalid", Message: pl.GenMessage("token-invalid", "uuid not available")}
		}
		user_id, myErr := strconv.ParseInt(fmt.Sprintf("%.f", claims["user_id"]), 10, 64)
		if myErr != nil {
			return nil, d.FieldError{Code: "token-invalid", Message: pl.GenMessage("token-invalid", "uuid is not available")}
		}
		accessUuidRedis := ms.I.Get(accessUuid)
		if accessUuidRedis.String() == "" {
			return nil, d.FieldError{Code: "token-unidentified", Message: pl.GenMessage("token-unidentified")}
		}

		data = &pa.AuthInfo{
			Uuid:      accessUuid,
			User_Id:   uint(user_id),
			User_Name: fmt.Sprintf("%v", claims["user_name"]),
		}

		data.User_ContractPosition_Code = checkStrClaims(claims, "contractPosition_code")
		data.Employee_Position_Code = checkStrPtrClaims(claims, "employee_position_code")
		data.Doctor_Code = checkStrPtrClaims(claims, "doctor_code")
		data.Nurse_Code = checkStrPtrClaims(claims, "nurse_code")
		data.Midwife_Code = checkStrPtrClaims(claims, "midwife_code")
		data.Nutritionist_Code = checkStrPtrClaims(claims, "nutritionist_code")
		data.Laborant_Code = checkStrPtrClaims(claims, "laborant_code")
		data.Pharmachist_Code = checkStrPtrClaims(claims, "pharmachist_code")
		data.Intern_Position_Code = checkStrPtrClaims(claims, "intern_position_code")
		data.Employee_Id = checkUntPtrClaims(claims, "employee_id")
		return
	}
	return nil, d.FieldError{Code: "token", Message: "token-invalid"}
}

func GetConfig() {
	a.ParseCfg(&authCfg)
}
