package services

import (
	"context"
	"fmt"
	"history-api/internal/dtos/request"
	"history-api/internal/dtos/response"
	"history-api/internal/gen/sqlc"
	"history-api/internal/models"
	"history-api/internal/repositories"
	"history-api/pkg/cache"
	"history-api/pkg/constants"
	"history-api/pkg/convert"
	"slices"

	"github.com/gofiber/fiber/v3"
	"github.com/jackc/pgx/v5/pgtype"
	"golang.org/x/crypto/bcrypt"
	"golang.org/x/sync/errgroup"
)

type UserService interface {
	//user
	GetUserCurrent(ctx context.Context, userId string) (*response.UserResponse, error)
	UpdateProfile(ctx context.Context, userId string, dto *request.UpdateProfileDto) (*response.UserResponse, error)
	ChangePassword(ctx context.Context, userId string, dto *request.ChangePasswordDto) error

	//admin
	DeleteUser(ctx context.Context, userId string) error
	ChangeRoleUser(ctx context.Context, claims *response.JWTClaims, dto *request.ChangeRoleDto) (*response.UserResponse, error)
	RestoreUser(ctx context.Context, userId string) (*response.UserResponse, error)
	GetUserByID(ctx context.Context, userId string) (*response.UserResponse, error)
	SearchUser(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, error)
}

type userService struct {
	userRepo repositories.UserRepository
	roleRepo repositories.RoleRepository
	c        cache.Cache
}

func NewUserService(
	userRepo repositories.UserRepository,
	roleRepo repositories.RoleRepository,
	c cache.Cache,
) UserService {
	return &userService{
		userRepo: userRepo,
		roleRepo: roleRepo,
		c:        c,
	}
}

func (u *userService) ChangePassword(ctx context.Context, userId string, dto *request.ChangePasswordDto) error {
	pgID, err := convert.StringToUUID(userId)
	if err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, err.Error())
	}
	user, err := u.userRepo.GetByID(ctx, pgID)
	if err != nil {
		return fiber.NewError(fiber.StatusNotFound, err.Error())
	}
	if user == nil {
		return fiber.NewError(fiber.StatusNotFound, "User not found")
	}

	if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(dto.OldPassword)); err != nil {
		return fiber.NewError(fiber.StatusUnauthorized, "Invalid identity or password!")
	}

	hashPassword, err := bcrypt.GenerateFromPassword([]byte(dto.NewPassword), bcrypt.DefaultCost)
	if err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, err.Error())
	}

	err = u.userRepo.UpdatePassword(ctx, sqlc.UpdateUserPasswordParams{
		ID:           pgID,
		PasswordHash: pgtype.Text{String: string(hashPassword), Valid: true},
	})
	if err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, err.Error())
	}

	return nil
}

func (u *userService) ChangeRoleUser(ctx context.Context, claims *response.JWTClaims, dto *request.ChangeRoleDto) (*response.UserResponse, error) {
	userId, err := convert.StringToUUID(dto.UserID)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
	}

	user, err := u.userRepo.GetByID(ctx, userId)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusNotFound, err.Error())
	}
	if user == nil {
		return nil, fiber.NewError(fiber.StatusNotFound, "User not found")
	}

	newListRole, err := u.roleRepo.GetByIDs(ctx, dto.Roles)
	if err != nil {
		return nil, err
	}

	hasUserRole := false
	hasAdminRole := false
	hasBannedRole := false
	hasModRole := false

	for _, r := range newListRole {
		if r.Name == constants.USER.String() {
			hasUserRole = true
		}
		if r.Name == constants.ADMIN.String() {
			hasAdminRole = true
		}
		if r.Name == constants.BANNED.String() {
			hasBannedRole = true
		}
		if r.Name == constants.MOD.String() {
			hasModRole = true
		}
	}

	if !hasUserRole {
		return nil, fiber.NewError(fiber.StatusNotFound, "User must have the USER role")
	}

	if slices.Contains(claims.Roles, constants.MOD) && !slices.Contains(claims.Roles, constants.ADMIN) {
		if hasAdminRole {
			return nil, fiber.NewError(fiber.StatusForbidden, "MOD cannot assign ADMIN role to any user")
		}

		if dto.UserID == claims.UId && !hasModRole {
			return nil, fiber.NewError(fiber.StatusForbidden, "You can't remove MOD role of yourself")
		}

		if dto.UserID == claims.UId && hasBannedRole {
			return nil, fiber.NewError(fiber.StatusForbidden, "You can't assign BANNED role to yourself")
		}
		isTargetAdminOrMod := false
		for _, r := range user.Roles {
			if r.Name == constants.ADMIN.String() || r.Name == constants.MOD.String() {
				isTargetAdminOrMod = true
				break
			}
		}
		if isTargetAdminOrMod && hasBannedRole {
			return nil, fiber.NewError(fiber.StatusForbidden, "MOD cannot assign BANNED role to an ADMIN or MOD user")
		}
	}

	if slices.Contains(claims.Roles, constants.ADMIN) {
		if dto.UserID == claims.UId && hasBannedRole {
			return nil, fiber.NewError(fiber.StatusForbidden, "You can't assign BANNED role to yourself")
		}

		if dto.UserID == claims.UId && !hasAdminRole {
			return nil, fiber.NewError(fiber.StatusForbidden, "You can't remove ADMIN role of yourself")
		}
	}

	user.Roles = make([]*models.RoleSimple, 0)
	roleIdList := make([]pgtype.UUID, 0)
	for _, role := range newListRole {
		roleID, err := convert.StringToUUID(role.ID)
		if err != nil {
			continue
		}
		roleIdList = append(roleIdList, roleID)
		user.Roles = append(user.Roles, role.ToRoleSimple())
	}

	err = u.roleRepo.BulkDeleteRolesFromUser(ctx, userId)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
	}

	err = u.roleRepo.CreateUserRole(ctx, sqlc.CreateUserRoleParams{
		UserID:  userId,
		Column2: roleIdList,
	})
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
	}

	err = u.userRepo.UpdateTokenVersion(ctx, sqlc.UpdateTokenVersionParams{
		ID:           userId,
		TokenVersion: user.TokenVersion + 1,
	})
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
	}
	user.TokenVersion += 1

	mapCache := map[string]any{
		fmt.Sprintf("user:email:%s", user.Email): user,
		fmt.Sprintf("user:id:%s", user.ID):       user,
	}
	_ = u.c.MSet(ctx, mapCache, constants.NormalCacheDuration)

	return user.ToResponse(), nil

}

func (u *userService) DeleteUser(ctx context.Context, userId string) error {
	pgID, err := convert.StringToUUID(userId)
	if err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, err.Error())
	}
	user, err := u.userRepo.GetByID(ctx, pgID)
	if err != nil {
		return fiber.NewError(fiber.StatusNotFound, err.Error())
	}
	if user == nil {
		return fiber.NewError(fiber.StatusNotFound, "User not found")
	}
	err = u.userRepo.Delete(ctx, pgID)
	if err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, err.Error())
	}
	return nil
}

func (u *userService) UpdateProfile(ctx context.Context, userId string, dto *request.UpdateProfileDto) (*response.UserResponse, error) {
	pgID, err := convert.StringToUUID(userId)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
	}
	user, err := u.userRepo.GetByID(ctx, pgID)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusNotFound, err.Error())
	}
	if user == nil {
		return nil, fiber.NewError(fiber.StatusNotFound, "User not found")
	}

	newUser, err := u.userRepo.UpdateProfile(
		ctx,
		sqlc.UpdateUserProfileParams{
			DisplayName: convert.PtrToText(dto.DisplayName),
			FullName:    convert.PtrToText(dto.FullName),
			AvatarUrl:   convert.PtrToText(dto.AvatarUrl),
			Bio:         convert.PtrToText(dto.Bio),
			Location:    convert.PtrToText(dto.Location),
			Website:     convert.PtrToText(dto.Website),
			CountryCode: convert.PtrToText(dto.CountryCode),
			Phone:       convert.PtrToText(dto.Phone),
			UserID:      pgID,
		},
	)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
	}
	return newUser.ToResponse(), nil
}

func (u *userService) GetUserCurrent(ctx context.Context, userId string) (*response.UserResponse, error) {
	pgID, err := convert.StringToUUID(userId)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
	}
	user, err := u.userRepo.GetByID(ctx, pgID)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusNotFound, err.Error())
	}
	return user.ToResponse(), nil
}

func (u *userService) RestoreUser(ctx context.Context, userId string) (*response.UserResponse, error) {
	pgID, err := convert.StringToUUID(userId)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
	}

	user, err := u.userRepo.GetByIDWithoutDeleted(ctx, pgID)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusNotFound, err.Error())
	}
	if user == nil {
		return nil, fiber.NewError(fiber.StatusNotFound, "User not found")
	}

	err = u.userRepo.Restore(ctx, pgID)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
	}
	user.IsDeleted = false
	return user.ToResponse(), nil
}

func (m *userService) fillSearchArgs(arg *sqlc.SearchUsersParams, dto *request.SearchUserDto) {
	if dto.Sort != "" {
		arg.Sort = pgtype.Text{String: dto.Sort, Valid: true}
	} else {
		arg.Sort = pgtype.Text{String: "id", Valid: true}
	}

	arg.Order = pgtype.Text{String: "asc", Valid: true}
	if dto.Order == "desc" {
		arg.Order = pgtype.Text{String: "desc", Valid: true}
	}

	if dto.AuthProvider != "" {
		arg.AuthProvider = pgtype.Text{String: dto.AuthProvider, Valid: true}
	}

	if dto.CreatedFrom != nil {
		arg.CreatedFrom = pgtype.Timestamptz{Time: *dto.CreatedFrom, Valid: true}
	}

	if dto.CreatedTo != nil {
		arg.CreatedTo = pgtype.Timestamptz{Time: *dto.CreatedTo, Valid: true}
	}

	if dto.IsDeleted != nil {
		arg.IsDeleted = pgtype.Bool{Bool: *dto.IsDeleted, Valid: true}
	}

	if len(dto.RoleIDs) > 0 {
		for _, id := range dto.RoleIDs {
			if u, err := convert.StringToUUID(id); err == nil {
				arg.RoleIds = append(arg.RoleIds, u)
			}
		}
	}

	if dto.Search != "" {
		arg.SearchText = pgtype.Text{String: dto.Search, Valid: true}
	}
}

func (u *userService) SearchUser(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, error) {
	if dto.Page < 1 {
		dto.Page = 1
	}
	if dto.Limit == 0 {
		dto.Limit = 20
	}
	offset := (dto.Page - 1) * dto.Limit

	arg := sqlc.SearchUsersParams{
		Limit:  int32(dto.Limit),
		Offset: int32(offset),
	}

	u.fillSearchArgs(&arg, dto)

	var rows []*models.UserEntity
	var totalRecords int64

	g, gCtx := errgroup.WithContext(ctx)

	g.Go(func() error {
		var err error
		rows, err = u.userRepo.Search(gCtx, arg)
		return err
	})

	g.Go(func() error {
		countArg := sqlc.CountUsersParams{
			RoleIds:      arg.RoleIds,
			AuthProvider: arg.AuthProvider,
			CreatedFrom:  arg.CreatedFrom,
			CreatedTo:    arg.CreatedTo,
			IsDeleted:    arg.IsDeleted,
			SearchText:   arg.SearchText,
		}
		var err error
		totalRecords, err = u.userRepo.Count(gCtx, countArg)
		return err
	})

	if err := g.Wait(); err != nil {
		return nil, err
	}

	return response.BuildPaginatedResponse(rows, totalRecords, dto.Page, dto.Limit), nil
}

func (u *userService) GetUserByID(ctx context.Context, userId string) (*response.UserResponse, error) {
	pgID, err := convert.StringToUUID(userId)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
	}
	user, err := u.userRepo.GetByID(ctx, pgID)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusNotFound, err.Error())
	}
	return user.ToResponse(), nil
}
