package authentication

import (
	"fmt"
	"strconv"
	"time"

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

	pl "simrs-vx/pkg/logger"

	edp "simrs-vx/internal/domain/main-entities/division-position"
	ed "simrs-vx/internal/domain/main-entities/doctor"
	ee "simrs-vx/internal/domain/main-entities/employee"
	eip "simrs-vx/internal/domain/main-entities/installation-position"
	"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"
	esp "simrs-vx/internal/domain/main-entities/specialist-position"
	essp "simrs-vx/internal/domain/main-entities/subspecialist-position"
	eup "simrs-vx/internal/domain/main-entities/unit-position"
	eu "simrs-vx/internal/domain/main-entities/user"
	erg "simrs-vx/internal/domain/references/organization"

	udp "simrs-vx/internal/use-case/main-use-case/division-position"
	uip "simrs-vx/internal/use-case/main-use-case/installation-position"
	usp "simrs-vx/internal/use-case/main-use-case/specialist-position"
	ussp "simrs-vx/internal/use-case/main-use-case/subspecialist-position"
	uup "simrs-vx/internal/use-case/main-use-case/unit-position"
)

// just return the error code
func getAndCheck(input, condition any, includes any) (eCode string) {
	qry := dg.I.Where(condition)

	// WARNING THIS PRELOAD FAILS
	if includes != nil {
		if val := includes.(string); val != "" {
			qry = qry.Preload(val)
		} else if vals := includes.([]string); len(vals) > 0 {
			for _, val := range vals {
				qry = qry.Preload(val)
			}
		}
	}

	result := qry.First(&input)
	if result.Error != nil {
		return "fetch-fail"
	} else if result.RowsAffected == 0 {
		return "auth-login-incorrect"
	}

	return ""
}

func getDivisionPosition(employee_id uint, event *pl.Event) ([]string, error) {
	var result []string

	// get data division_position based on employee_id
	data, _, err := udp.ReadListData(edp.ReadListDto{FilterDto: edp.FilterDto{Employee_Id: &employee_id}}, event)
	if err != nil {
		return nil, err
	}

	if len(data) > 0 {
		for _, dp := range data {
			result = append(result, "div|"+*dp.Division_Code+"|"+dp.Code)
		}
	}

	return result, nil
}

func getInstallationPosition(employeeId uint, event *pl.Event) ([]string, error) {
	var result []string

	// get data unit_position based on employee_id
	data, _, err := uip.ReadListData(eip.ReadListDto{
		FilterDto: eip.FilterDto{Employee_Id: &employeeId},
		Includes:  "installation"}, event)
	if err != nil {
		return nil, err
	}

	if len(data) > 0 {
		for _, dp := range data {
			result = append(result, "inst|"+*dp.Installation_Code+"|"+dp.Code)
		}
	}

	return result, nil
}

func getUnitPosition(employeeId uint, event *pl.Event) ([]string, error) {
	var result []string

	// get data unit_position based on employee_id
	data, _, err := uup.ReadListData(eup.ReadListDto{FilterDto: eup.FilterDto{Employee_Id: &employeeId}}, event)
	if err != nil {
		return nil, err
	}

	if len(data) > 0 {
		for _, dp := range data {
			result = append(result, "unit|"+*dp.Unit_Code+"|"+dp.Code)
		}
	}

	return result, nil
}

func getSpecialistPosition(employeeId uint, event *pl.Event) ([]string, error) {
	var result []string

	// get data unit_position based on employee_id
	data, _, err := usp.ReadListData(esp.ReadListDto{FilterDto: esp.FilterDto{Employee_Id: &employeeId}}, event)
	if err != nil {
		return nil, err
	}

	if len(data) > 0 {
		for _, dp := range data {
			result = append(result, "spec|"+*dp.Specialist_Code+"|"+dp.Code)
		}
	}

	return result, nil
}

func getSubspecialistPosition(employeeId uint, event *pl.Event) ([]string, error) {
	var result []string

	// get data unit_position based on employee_id
	data, _, err := ussp.ReadListData(essp.ReadListDto{
		FilterDto: essp.FilterDto{Employee_Id: &employeeId},
		Includes:  "subspecialist"}, event)
	if err != nil {
		return nil, err
	}

	if len(data) > 0 {
		for _, dp := range data {
			result = append(result, "subspec|"+dp.Subspecialist.Code+"|"+dp.Code)
		}
	}

	return result, nil
}

func checkStrClaims(claim map[string]interface{}, key string) string {
	if v, exist := claim[key]; exist && v != nil {
		return v.(string)
	}
	return ""
}

func checkStrPtrClaims(claim map[string]interface{}, key string) *string {
	if v, exist := claim[key]; exist && v != nil {
		val := v.(string)
		return &val
	}
	return nil
}

func checkUntPtrClaims(claim map[string]interface{}, key string) *uint {
	if v, exist := claim[key]; exist && v != nil {
		val := uint(v.(float64))
		return &val
	}
	return nil
}

func populateRoles(user *eu.User, input eu.LoginDto, atClaims jwt.MapClaims, outputData d.II, event pl.Event) error {
	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()

	atClaims["uuid"] = aUuid
	atClaims["exp"] = atExpires
	atClaims["user_id"] = user.Id
	atClaims["user_name"] = user.Name
	atClaims["user_contractPosition_code"] = user.ContractPosition_Code

	outputData["user_id"] = user.Id
	outputData["user_name"] = user.Name
	outputData["user_contractPosition_code"] = user.ContractPosition_Code

	roles := []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 d.FieldErrors{"authentication": d.FieldError{Code: "auth-noEmployee", Message: pl.GenMessage("auth-noEmployee")}}
		}
		atClaims["employee_id"] = employee.Id
		outputData["employee_id"] = employee.Id
		roles = append(roles, "emp|"+string(*employee.Position_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 d.FieldErrors{"authentication": d.FieldError{Code: "auth-noDoctor", Message: pl.GenMessage("auth-noDoctor")}}
				}
				atClaims["doctor_code"] = doctor.Code
				outputData["doctor_code"] = doctor.Code

				// specialist
				if doctor.Unit_Code != nil {
					atClaims["unit_code"] = doctor.Unit_Code
					outputData["unit_code"] = doctor.Unit_Code
				}
				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 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 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 d.FieldErrors{"authentication": d.FieldError{Code: "auth-noPharmacist", Message: pl.GenMessage("auth-noPharmacist")}}
				}
				atClaims["pharmacist_code"] = empData.Code
				outputData["pharmacist_code"] = empData.Code
			}

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

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

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

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

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

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

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

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

	return nil
}
