package authentication

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

	"github.com/golang-jwt/jwt"
	"github.com/google/uuid"
	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"

<<<<<<< HEAD
	ed "simrs-vx/internal/domain/main-entities/doctor"
	ee "simrs-vx/internal/domain/main-entities/employee"
	"simrs-vx/internal/domain/main-entities/intern"
	em "simrs-vx/internal/domain/main-entities/midwife"
	en "simrs-vx/internal/domain/main-entities/nurse"
	ep "simrs-vx/internal/domain/main-entities/pharmacist"
	eu "simrs-vx/internal/domain/main-entities/user"

=======
	eu "simrs-vx/internal/domain/main-entities/user"
	euf "simrs-vx/internal/domain/main-entities/user-fes"
>>>>>>> fc7e74b (feat/sso-auth: wip)
	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 input.Position_Code != "" {
	// 	user.Position_Code = input.Position_Code
	// }
	if errCode := getAndCheck(user, user); errCode != "" {
		return nil, d.FieldErrors{"authentication": d.FieldError{Code: errCode, Message: pl.GenMessage(errCode)}}
	}

	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")}}
		}
	}

	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")}}
	}

	// Access token prep
	id, err := uuid.NewRandom()
	if err != nil {
		panic(fmt.Sprintf(l.I.Msg("uuid-gen-fail"), err))
	}
	if input.Duration == 0 {
		input.Duration = 24 * 60
	}
	duration := time.Minute * time.Duration(input.Duration)
	aUuid := id.String()
	atExpires := time.Now().Add(duration).Unix()
	atSecretKey := authCfg.AtSecretKey

	// Create Claim
	atClaims := jwt.MapClaims{}
	atClaims["user_id"] = user.Id
	atClaims["user_name"] = user.Name
	atClaims["user_contractPosition_code"] = user.ContractPosition_Code
	atClaims["uuid"] = aUuid
	atClaims["exp"] = atExpires

	// Create output
	outputData := d.II{
		"user_id":                    strconv.Itoa(int(user.Id)),
		"user_name":                  user.Name,
		"user_contractPosition_code": user.ContractPosition_Code,
	}

	// extra
	// role := []string{}
	// switch user.ContractPosition_Code {
	// case erg.CSCEmp:
	// 	// employee
	// 	employee := ee.Employee{}
	// 	dg.I.Where("\"User_Id\" = ?", user.Id).First(&employee)
	// 	if employee.Id == 0 {
	// 		return nil, d.FieldErrors{"authentication": d.FieldError{Code: "auth-noEmployee", Message: pl.GenMessage("auth-noEmployee")}}
	// 	}
	// 	atClaims["employee_id"] = employee.Id
	// 	outputData["employee_id"] = employee.Id
	// 	role = append(role, "emp-"+string(*employee.Position_Code))

	// 	//if employee.Division_Code != nil {
	// 	//	atClaims["employee_division_code"] = employee.Division_Code
	// 	//	outputData["employee_division_code"] = employee.Division_Code
	// 	//}

	// 	// employee position
	// 	if employee.Id > 0 && employee.Position_Code != nil {
	// 		atClaims["employee_position_code"] = *employee.Position_Code
	// 		switch *employee.Position_Code {
	// 		case erg.EPCDoc:
	// 			doctor := ed.Doctor{}
	// 			dg.I.Where("\"Employee_Id\" = ?", employee.Id).First(&doctor)
	// 			if doctor.Id == 0 {
	// 				return nil, d.FieldErrors{"authentication": d.FieldError{Code: "auth-noDoctor", Message: pl.GenMessage("auth-noDoctor")}}
	// 			}
	// 			atClaims["doctor_code"] = doctor.Code
	// 			outputData["doctor_code"] = doctor.Code

<<<<<<< HEAD
				// specialist
				if doctor.Specialist_Code != nil {
					atClaims["specialist_code"] = doctor.Specialist_Code
					outputData["specialist_code"] = doctor.Specialist_Code
				}
				if doctor.Subspecialist_Code != nil {
					atClaims["subspecialist_code"] = doctor.Subspecialist_Code
					outputData["subspecialist_code"] = doctor.Subspecialist_Code
				}
			case erg.EPCNur:
				empData := en.Nurse{}
				dg.I.Where("\"Employee_Id\" = ?", employee.Id).First(&empData)
				if empData.Id == 0 {
					return nil, d.FieldErrors{"authentication": d.FieldError{Code: "auth-noNurse", Message: pl.GenMessage("auth-noNurse")}}
				}
				atClaims["nurse_code"] = empData.Code
				outputData["nurse_code"] = empData.Code
			case erg.EPCMwi:
				empData := em.Midwife{}
				dg.I.Where("\"Employee_Id\" = ?", employee.Id).First(&empData)
				if empData.Id == 0 {
					return nil, d.FieldErrors{"authentication": d.FieldError{Code: "auth-noMidwife", Message: pl.GenMessage("auth-noMidwife")}}
				}
				atClaims["midwife_code"] = empData.Code
				outputData["midwife_code"] = empData.Code
			case erg.EPCPha:
				empData := ep.Pharmacist{}
				dg.I.Where("\"Employee_Id\" = ?", employee.Id).First(&empData)
				if empData.Id == 0 {
					return nil, d.FieldErrors{"authentication": d.FieldError{Code: "auth-noPharmacist", Message: pl.GenMessage("auth-noPharmacist")}}
				}
				atClaims["pharmacist_code"] = empData.Code
				outputData["pharmacist_code"] = empData.Code
			}
=======
	// 			// specialist
	// 			if doctor.Specialist_Code != nil {
	// 				atClaims["specialist_code"] = doctor.Specialist_Code
	// 				outputData["specialist_code"] = doctor.Specialist_Code
	// 			}
	// 			if doctor.Subspecialist_Code != nil {
	// 				atClaims["subspecialist_code"] = doctor.Subspecialist_Code
	// 				outputData["subspecialist_code"] = doctor.Subspecialist_Code
	// 			}
	// 		case erg.EPCNur:
	// 			empData := en.Nurse{}
	// 			dg.I.Where("\"Employee_Id\" = ?", employee.Id).First(&empData)
	// 			if empData.Id == 0 {
	// 				return nil, d.FieldErrors{"authentication": d.FieldError{Code: "auth-noNurse", Message: pl.GenMessage("auth-noNurse")}}
	// 			}
	// 			atClaims["nurse_code"] = empData.Code
	// 			outputData["nurse_code"] = empData.Code
	// 		case erg.EPCMwi:
	// 			empData := em.Midwife{}
	// 			dg.I.Where("\"Employee_Id\" = ?", employee.Id).First(&empData)
	// 			if empData.Id == 0 {
	// 				return nil, d.FieldErrors{"authentication": d.FieldError{Code: "auth-noMidwife", Message: pl.GenMessage("auth-noMidwife")}}
	// 			}
	// 			atClaims["midwife_code"] = empData.Code
	// 			outputData["midwife_code"] = empData.Code
	// 		}
>>>>>>> fc7e74b (feat/sso-auth: wip)

	// 		errorGetPosition := d.FieldErrors{"authentication": d.FieldError{Code: "auth-getData-failed", Message: pl.GenMessage("auth-getData-failed")}}

	// 		// division position
	// 		divisionPositions, err := getDivisionPosition(employee.Id, &event)
	// 		if err != nil {
	// 			return nil, errorGetPosition
	// 		}

	// 		// installation position
	// 		installationPositions, err := getInstallationPosition(employee.Id, &event)
	// 		if err != nil {
	// 			return nil, errorGetPosition
	// 		}

	// 		// unit position
	// 		unitPositions, err := getUnitPosition(employee.Id, &event)
	// 		if err != nil {
	// 			return nil, errorGetPosition
	// 		}

	// 		// specialist position
	// 		specialistPositions, err := getSpecialistPosition(employee.Id, &event)
	// 		if err != nil {
	// 			return nil, errorGetPosition
	// 		}

	// 		// subspecialist position
	// 		subspecialistPositions, err := getSubspecialistPosition(employee.Id, &event)
	// 		if err != nil {
	// 			return nil, errorGetPosition
	// 		}

	// 		role = append(role, divisionPositions...)
	// 		role = append(role, installationPositions...)
	// 		role = append(role, unitPositions...)
	// 		role = append(role, specialistPositions...)
	// 		role = append(role, subspecialistPositions...)
	// 		// atClaims["division_positions"] = divsionPositions
	// 		// outputData["division_positions"] = divsionPositions
	// 	}
	// case erg.CSCInt:
	// 	intern := intern.Intern{}
	// 	dg.I.Where("\"User_Id\" = ?", user.Id).First(&intern)
	// 	role = append(role, "int-"+string(*intern.Position_Code))
	// case erg.CSCSys:
	// 	role = append(role, "system")
	// }
	// atClaims["roles"] = role
	// outputData["roles"] = role
	if err := populateRoles(user, atClaims, outputData, event); err != nil {
		return nil, err
	}

	// Generate jwt
	at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims)
	ats, err := at.SignedString([]byte(atSecretKey))
	if err != nil {
		return nil, d.FieldErrors{"user": d.FieldError{Code: "token-sign-err", Message: pl.GenMessage("token-sign-err")}}
	}
	outputData["accessToken"] = ats

	// Save to redis
	now := time.Now()
	atx := time.Unix(atExpires, 0) //converting Unix to UTC(to Time object)
	err = ms.I.Set(aUuid, strconv.Itoa(int(user.Id)), atx.Sub(now)).Err()
	if err != nil {
		panic(fmt.Sprintf(l.I.Msg("redis-store-fail"), err.Error()))
	}

	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 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)
}

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); errCode != "" {
		return nil, d.FieldErrors{"authentication": d.FieldError{Code: errCode, Message: pl.GenMessage(errCode)}}
	}
	if userFes.AuthPartner.SecretKey != input.AuthPartner_SecretKey {
		return nil, d.FieldErrors{"authentication": d.FieldError{Code: "auth-secretKey-invalid", Message: pl.GenMessage("auth-secretKey-invalid")}}
	}

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

	// Access token prep
	id, err := uuid.NewRandom()
	if err != nil {
		panic(fmt.Sprintf(l.I.Msg("uuid-gen-fail"), err))
	}
	if input.Duration == 0 {
		input.Duration = 24 * 60
	}
	duration := time.Minute * time.Duration(input.Duration)
	aUuid := id.String()
	atExpires := time.Now().Add(duration).Unix()
	atSecretKey := authCfg.AtSecretKey

	// Create Claim
	atClaims := jwt.MapClaims{}
	atClaims["user_id"] = user.Id
	atClaims["user_name"] = user.Name
	atClaims["user_contractPosition_code"] = user.ContractPosition_Code
	atClaims["uuid"] = aUuid
	atClaims["exp"] = atExpires

	// Create output
	outputData := d.II{
		"user_id":                    strconv.Itoa(int(user.Id)),
		"user_name":                  user.Name,
		"user_contractPosition_code": user.ContractPosition_Code,
	}

	// extra
	if err := populateRoles(user, atClaims, outputData, event); err != nil {
		return nil, err
	}

	// Generate jwt
	at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims)
	ats, err := at.SignedString([]byte(atSecretKey))
	if err != nil {
		return nil, d.FieldErrors{"user": d.FieldError{Code: "token-sign-err", Message: pl.GenMessage("token-sign-err")}}
	}
	outputData["accessToken"] = ats

	// Save to redis
	now := time.Now()
	atx := time.Unix(atExpires, 0) //converting Unix to UTC(to Time object)
	err = ms.I.Set(aUuid, strconv.Itoa(int(user.Id)), atx.Sub(now)).Err()
	if err != nil {
		panic(fmt.Sprintf(l.I.Msg("redis-store-fail"), err.Error()))
	}

	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
}
