package system

import (
	"context"
	"time"

	"golang.org/x/crypto/bcrypt"
)

type User struct {
	ID                 uint
	CreatedAt          time.Time
	UpdatedAt          time.Time
	UUID               string
	Username           string
	Password           string
	NickName           string
	HeaderImg          string
	AuthorityID        uint
	Authority          Authority
	Authorities        []Authority
	DeptID             uint
	Department         *Department
	Departments        []Department
	Positions          []Position
	Phone              string
	Email              string
	Enable             int
	OriginSetting      map[string]any
	MustChangePassword bool
	PasswordUpdatedAt  *time.Time
}
type UserListFilter struct {
	Username, NickName, Phone, Email, OrderKey string
	Desc                                       bool
}

type UserRepo interface {
	FindUserByUsername(context.Context, string) (*User, error)
	FindUserByID(context.Context, uint) (*User, error)
	FindUserByUUID(context.Context, string) (*User, error)
	HasAuthorityMenu(context.Context, uint, string) (bool, error)
	MenusByAuthority(context.Context, uint) ([]*Menu, error)
	ListUsers(context.Context, int, int, *UserListFilter) ([]*User, int64, error)
	CreateUser(context.Context, *User) (*User, error)
	CreateUserWithAuthorities(context.Context, *User, []uint) (*User, error)
	UpdateUser(context.Context, *User) error
	UpdateSelfUser(context.Context, *User) error
	UpdateUserWithAuthorities(context.Context, *User, []uint) error
	DeleteUser(context.Context, uint) error
	UpdatePassword(context.Context, uint, string, bool) error
	SetUserAuthorities(context.Context, uint, []uint) error
	SetUserAuthority(context.Context, uint, uint) error
	SetUserSetting(context.Context, uint, map[string]any) error
	ListAuthorities(context.Context) ([]*Authority, error)
	FillDepartmentNamePaths(context.Context, *User) error
}

type UserUsecase struct{ repo UserRepo }

func NewUserUsecase(repo UserRepo) *UserUsecase { return &UserUsecase{repo: repo} }

func (uc *UserUsecase) Login(ctx context.Context, username, password string) (*User, error) {
	u, err := uc.repo.FindUserByUsername(ctx, username)
	if err != nil || u == nil || bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)) != nil {
		return nil, ErrInvalidCredentials
	}
	uc.fallbackDefaultRouter(ctx, u)
	return u, nil
}

func (uc *UserUsecase) User(ctx context.Context, id uint) (*User, error) {
	user, err := uc.repo.FindUserByID(ctx, id)
	if err != nil {
		return nil, err
	}
	_ = uc.repo.FillDepartmentNamePaths(ctx, user)
	uc.fallbackDefaultRouter(ctx, user)
	return user, nil
}

// UserByUUID resolves the identity from the UUID embedded in the JWT rather
// than from the mutable numeric ID used by administrative records.
func (uc *UserUsecase) UserByUUID(ctx context.Context, uuid string) (*User, error) {
	user, err := uc.repo.FindUserByUUID(ctx, uuid)
	if err != nil {
		return nil, err
	}
	_ = uc.repo.FillDepartmentNamePaths(ctx, user)
	uc.fallbackDefaultRouter(ctx, user)
	return user, nil
}

func (uc *UserUsecase) fallbackDefaultRouter(ctx context.Context, user *User) {
	if user == nil {
		return
	}
	hasMenu, err := uc.repo.HasAuthorityMenu(ctx, user.AuthorityID, user.Authority.DefaultRouter)
	if err != nil {
		return
	}
	if !hasMenu {
		user.Authority.DefaultRouter = "404"
	}
}

func (uc *UserUsecase) Menus(ctx context.Context, authorityID uint) ([]*Menu, error) {
	return uc.repo.MenusByAuthority(ctx, authorityID)
}

func (uc *UserUsecase) ListUsers(ctx context.Context, page, pageSize int, filter *UserListFilter) ([]*User, int64, error) {
	return uc.repo.ListUsers(ctx, page, pageSize, filter)
}
func (uc *UserUsecase) CreateUser(ctx context.Context, user *User, authorityIDs []uint) (*User, error) {
	hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
	if err != nil {
		return nil, err
	}
	user.Password = string(hash)
	return uc.repo.CreateUserWithAuthorities(ctx, user, authorityIDs)
}
func (uc *UserUsecase) UpdateUser(ctx context.Context, user *User, authorityIDs []uint) error {
	// Role associations are updated first only when authorityIds was supplied,
	// and then updates the basic user fields as a separate operation. Preserve
	// that ordering and partial-failure behavior instead of coupling both writes
	// into the data-layer transaction used by lower-level callers.
	if len(authorityIDs) > 0 {
		if err := uc.repo.SetUserAuthorities(ctx, user.ID, authorityIDs); err != nil {
			return err
		}
	}
	return uc.repo.UpdateUser(ctx, user)
}

func (uc *UserUsecase) UpdateSelfUser(ctx context.Context, user *User) error {
	return uc.repo.UpdateSelfUser(ctx, user)
}

func (uc *UserUsecase) DeleteUser(ctx context.Context, id uint) error {
	return uc.repo.DeleteUser(ctx, id)
}
func (uc *UserUsecase) ResetPassword(ctx context.Context, id uint, password string) error {
	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return err
	}
	return uc.repo.UpdatePassword(ctx, id, string(hash), false)
}
func (uc *UserUsecase) ChangePassword(ctx context.Context, id uint, oldPassword, newPassword string) error {
	user, err := uc.repo.FindUserByID(ctx, id)
	if err != nil {
		return err
	}
	if bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(oldPassword)) != nil {
		return ErrInvalidCredentials
	}
	hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
	if err != nil {
		return err
	}
	return uc.repo.UpdatePassword(ctx, id, string(hash), true)
}
func (uc *UserUsecase) Authorities(ctx context.Context) ([]*Authority, error) {
	return uc.repo.ListAuthorities(ctx)
}
func (uc *UserUsecase) SetUserAuthorities(ctx context.Context, id uint, authorityIDs []uint) error {
	if len(authorityIDs) == 0 {
		return ErrInvalidCredentials
	}
	return uc.repo.SetUserAuthorities(ctx, id, authorityIDs)
}
func (uc *UserUsecase) SetUserAuthority(ctx context.Context, id, authorityID uint) error {
	return uc.repo.SetUserAuthority(ctx, id, authorityID)
}
func (uc *UserUsecase) SetUserSetting(ctx context.Context, id uint, setting map[string]any) error {
	return uc.repo.SetUserSetting(ctx, id, setting)
}
