package services

import (
	"context"
	"database/sql"
	"errors"
	"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"
	"github.com/jackc/pgx/v5/pgxpool"
	"golang.org/x/crypto/bcrypt"
	"golang.org/x/sync/errgroup"
)

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

	//admin
	CreateUser(ctx context.Context, dto *request.CreateUserDto) (*response.UserResponse, *fiber.Error)
	DeleteUser(ctx context.Context, userId string) *fiber.Error
	ChangeRoleUser(ctx context.Context, userId string, claims *response.JWTClaims, dto *request.ChangeRoleDto) (*response.UserResponse, *fiber.Error)
	RestoreUser(ctx context.Context, userId string) (*response.UserResponse, *fiber.Error)
	GetUserByID(ctx context.Context, userId string) (*response.UserResponse, *fiber.Error)
	SearchUser(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, *fiber.Error)
	AdminResetPassword(ctx context.Context, userId string, dto *request.ResetPasswordDto) *fiber.Error
}

type userService struct {
	userRepo repositories.UserRepository
	roleRepo repositories.RoleRepository
	c        cache.Cache
	db       *pgxpool.Pool
}

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

func (u *userService) CreateUser(ctx context.Context, dto *request.CreateUserDto) (*response.UserResponse, *fiber.Error) {
	tx, err := u.db.Begin(ctx)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction")
	}
	defer tx.Rollback(ctx)

	uRepo := u.userRepo.WithTx(tx)
	rRepo := u.roleRepo.WithTx(tx)

	existingUser, err := u.userRepo.GetByEmail(ctx, dto.Email)
	if err != nil && !errors.Is(err, sql.ErrNoRows) {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing user")
	}
	if existingUser != nil {
		return nil, fiber.NewError(fiber.StatusBadRequest, "User already exists")
	}

	hashed, err := bcrypt.GenerateFromPassword([]byte(dto.Password), bcrypt.DefaultCost)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to hash password")
	}

	user, err := uRepo.UpsertUser(ctx, sqlc.UpsertUserParams{
		Email:        dto.Email,
		PasswordHash: pgtype.Text{String: string(hashed), Valid: true},
		AuthProvider: constants.ProviderTypeLocal.String(),
	})
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create user")
	}

	userUUID, err := convert.StringToUUID(user.ID)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid user ID")
	}

	_, err = uRepo.CreateProfile(ctx, sqlc.CreateUserProfileParams{
		UserID:      userUUID,
		DisplayName: pgtype.Text{String: dto.DisplayName, Valid: true},
	})
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create user profile")
	}

	var roleIdList []pgtype.UUID
	for _, rId := range dto.Roles {
		rid, err := convert.StringToUUID(rId)
		if err == nil {
			roleIdList = append(roleIdList, rid)
		}
	}

	err = rRepo.CreateUserRole(ctx, sqlc.CreateUserRoleParams{
		UserID:  userUUID,
		Column2: roleIdList,
	})
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to assign roles")
	}

	if err := tx.Commit(ctx); err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction")
	}

	_ = u.c.PublishTask(ctx, constants.StreamEmailName, constants.TaskTypeAdminUserAction, models.AdminUserActionPayload{
		Email:    dto.Email,
		Password: dto.Password,
		Action:   "create",
	})

	finalUser, err := u.userRepo.GetByID(ctx, userUUID)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch created user")
	}

	return finalUser.ToResponse(), nil
}

func (u *userService) ChangePassword(ctx context.Context, userId string, dto *request.ChangePasswordDto) *fiber.Error {
	tx, err := u.db.Begin(ctx)
	if err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction")
	}
	defer tx.Rollback(ctx)

	uRepo := u.userRepo.WithTx(tx)

	pgID, err := convert.StringToUUID(userId)
	if err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Invalid user ID")
	}
	user, err := u.userRepo.GetByID(ctx, pgID)
	if err != nil {
		return fiber.NewError(fiber.StatusNotFound, "Failed to fetch user")
	}
	if user == nil {
		return fiber.NewError(fiber.StatusNotFound, "User not found")
	}

	if user.PasswordHash != "" {
		if dto.OldPassword == "" {
			return fiber.NewError(fiber.StatusBadRequest, "Old password required")
		}
		if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(dto.OldPassword)); err != nil {
			return fiber.NewError(fiber.StatusUnauthorized, "Invalid old password")
		}
	} else if user.PasswordHash == "" && dto.OldPassword != "" {
		return fiber.NewError(fiber.StatusBadRequest, "Invalid request: user has no password")
	}

	hashPassword, err := bcrypt.GenerateFromPassword([]byte(dto.NewPassword), bcrypt.DefaultCost)
	if err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to hash new password")
	}

	err = uRepo.UpdatePassword(ctx, sqlc.UpdateUserPasswordParams{
		ID:           pgID,
		PasswordHash: pgtype.Text{String: string(hashPassword), Valid: true},
	})
	if err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to update password")
	}

	err = uRepo.UpdateTokenVersion(ctx, sqlc.UpdateTokenVersionParams{
		ID:           pgID,
		TokenVersion: user.TokenVersion + 1,
	})
	if err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to update token version")
	}

	if err := tx.Commit(ctx); err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction")
	}
	return nil
}

func (u *userService) AdminResetPassword(ctx context.Context, userId string, dto *request.ResetPasswordDto) *fiber.Error {
	tx, err := u.db.Begin(ctx)
	if err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction")
	}
	defer tx.Rollback(ctx)

	uRepo := u.userRepo.WithTx(tx)

	pgID, err := convert.StringToUUID(userId)
	if err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Invalid user ID")
	}
	user, err := u.userRepo.GetByID(ctx, pgID)
	if err != nil {
		return fiber.NewError(fiber.StatusNotFound, "Failed to fetch user")
	}
	if user == nil {
		return fiber.NewError(fiber.StatusNotFound, "User not found")
	}

	hashPassword, err := bcrypt.GenerateFromPassword([]byte(dto.NewPassword), bcrypt.DefaultCost)
	if err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to hash new password")
	}

	err = uRepo.UpdatePassword(ctx, sqlc.UpdateUserPasswordParams{
		ID:           pgID,
		PasswordHash: pgtype.Text{String: string(hashPassword), Valid: true},
	})
	if err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to update password")
	}

	err = uRepo.UpdateTokenVersion(ctx, sqlc.UpdateTokenVersionParams{
		ID:           pgID,
		TokenVersion: user.TokenVersion + 1,
	})
	if err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to update token version")
	}

	if err := tx.Commit(ctx); err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction")
	}

	if dto.IsSendEmail {
		_ = u.c.PublishTask(ctx, constants.StreamEmailName, constants.TaskTypeAdminUserAction, models.AdminUserActionPayload{
			Email:    user.Email,
			Password: dto.NewPassword,
			Action:   "reset",
		})
	}

	return nil
}

func (u *userService) ChangeRoleUser(ctx context.Context, userId string, claims *response.JWTClaims, dto *request.ChangeRoleDto) (*response.UserResponse, *fiber.Error) {
	tx, err := u.db.Begin(ctx)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction")
	}
	defer tx.Rollback(ctx)

	uRepo := u.userRepo.WithTx(tx)
	rRepo := u.roleRepo.WithTx(tx)

	userUUID, err := convert.StringToUUID(userId)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid user ID")
	}

	user, err := u.userRepo.GetByID(ctx, userUUID)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusNotFound, "Failed to fetch user")
	}
	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, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch roles")
	}

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

	for _, r := range newListRole {
		if r.Name == constants.RoleTypeUser.String() {
			hasUserRole = true
		}
		if r.Name == constants.RoleTypeAdmin.String() {
			hasAdminRole = true
		}
		if r.Name == constants.RoleTypeBanned.String() {
			hasBannedRole = true
		}
		if r.Name == constants.RoleTypeMod.String() {
			hasModRole = true
		}
	}

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

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

		if userId == claims.UId && !hasModRole {
			return nil, fiber.NewError(fiber.StatusForbidden, "You cannot remove your own MOD role")
		}

		if userId == claims.UId && hasBannedRole {
			return nil, fiber.NewError(fiber.StatusForbidden, "You cannot ban yourself")
		}
		isTargetAdminOrMod := false
		for _, r := range user.Roles {
			if r.Name == constants.RoleTypeAdmin.String() || r.Name == constants.RoleTypeMod.String() {
				isTargetAdminOrMod = true
				break
			}
		}
		if isTargetAdminOrMod && hasBannedRole {
			return nil, fiber.NewError(fiber.StatusForbidden, "MOD cannot ban an ADMIN or MOD")
		}
	}

	if slices.Contains(claims.Roles, constants.RoleTypeAdmin) {
		if userId == claims.UId && hasBannedRole {
			return nil, fiber.NewError(fiber.StatusForbidden, "You cannot ban yourself")
		}

		if userId == claims.UId && !hasAdminRole {
			return nil, fiber.NewError(fiber.StatusForbidden, "You cannot remove your own ADMIN role")
		}
	}

	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 = rRepo.BulkDeleteRolesFromUser(ctx, userUUID)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to clear old roles")
	}

	err = rRepo.CreateUserRole(ctx, sqlc.CreateUserRoleParams{
		UserID:  userUUID,
		Column2: roleIdList,
	})
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create new roles")
	}

	err = uRepo.UpdateTokenVersion(ctx, sqlc.UpdateTokenVersionParams{
		ID:           userUUID,
		TokenVersion: user.TokenVersion + 1,
	})
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update token version")
	}
	user.TokenVersion += 1

	err = tx.Commit(ctx)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction")
	}

	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) *fiber.Error {
	pgID, err := convert.StringToUUID(userId)
	if err != nil {
		return fiber.NewError(fiber.StatusInternalServerError, "Invalid user ID")
	}
	user, err := u.userRepo.GetByID(ctx, pgID)
	if err != nil {
		return fiber.NewError(fiber.StatusNotFound, "Failed to fetch user")
	}
	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, "Failed to delete user")
	}
	return nil
}

func (u *userService) UpdateProfile(ctx context.Context, userId string, dto *request.UpdateProfileDto) (*response.UserResponse, *fiber.Error) {
	pgID, err := convert.StringToUUID(userId)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid user ID")
	}
	user, err := u.userRepo.GetByID(ctx, pgID)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusNotFound, "Failed to fetch user")
	}
	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, "Failed to update profile")
	}
	return newUser.ToResponse(), nil
}

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

	user, err := u.userRepo.GetByIDWithoutDeleted(ctx, pgID)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusNotFound, "Failed to fetch user")
	}
	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, "Failed to restore user")
	}
	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, *fiber.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, fiber.NewError(fiber.StatusInternalServerError, "Failed to search users")
	}

	users := models.UsersEntityToResponse(rows)

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

func (u *userService) GetUserByID(ctx context.Context, userId string) (*response.UserResponse, *fiber.Error) {
	pgID, err := convert.StringToUUID(userId)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid user ID")
	}
	user, err := u.userRepo.GetByID(ctx, pgID)
	if err != nil {
		return nil, fiber.NewError(fiber.StatusNotFound, "Failed to fetch user")
	}
	if user == nil {
		return nil, fiber.NewError(fiber.StatusNotFound, "User not found")
	}
	return user.ToResponse(), nil
}
