import {ConflictError, NotFoundError, ValidationError} from "../api/errors/httpErrors.js";
import {CreateUserRequest} from "../api/dtos/CreateUserRequest.js";
import {UserDto} from "../api/dtos/UserDto.js";
import {encrypt} from "../utils/encryptionUtils.js";
import {UserRepository} from "../repositories/UserRepository.js";
import {UserDtoEntityMapper} from "../mappers/UserDtoEntityMapper.js";
import {UUID} from "crypto";
import {ChangeUserPasswordRequest} from "../api/dtos/ChangeUserPasswordRequest.js";
import {UserEntity} from "../entities/UserEntity.js";
import {UserRole, UserRoleHelper} from "../api/enums/UserRole.js";

/**
 * Controls all user specific actions
 */
export class UserHandler {
  constructor(
    private userRepository: UserRepository,
    private mapper: UserDtoEntityMapper
  ) {}

  /**
   * Create a new user
   * @param requestData CreateUserRequest containing data for the user to add
   * @returns UserDto Data of the user as stored in the database
   */
  async createUser(requestData: CreateUserRequest): Promise<UserDto> {
    // check mandatory fields
      if(!requestData.userData){
          throw new ValidationError("User data is required")
      }
    this.validateUserData(requestData.userData);
    const userName = requestData.userData.userName;
      const password = requestData.password;
    if(!password || (password && password.length == 0)){
      throw new ValidationError("Password is required");
    }

    // user name must be unique
    const existingUser = await this.userRepository.findByUserName(userName);
    if (existingUser) {
      throw new ConflictError("User with this user name already exists");
    }

      // Validate role if provided
      if (requestData.userData.role && !UserRoleHelper.isValidRole(requestData.userData.role)) {
          throw new ValidationError(`Invalid role: ${requestData.userData.role}`);
      }

      // Set default role if not provided
      if (!requestData.userData.role) {
          requestData.userData.role = UserRole.USER;
      }

    const userEntity = this.mapper.toEntity(requestData.userData);
    userEntity.password = await encrypt.encryptpass(password);

    const savedUser = await this.userRepository.create(userEntity);

    return this.mapper.toDto(savedUser);
  }

    private validateUserData(dto: UserDto) : void {
        const email = dto.email;
        if (!email || (email && email.length == 0)) {
            throw new ValidationError("Email is required");
        }
        const userName = dto.userName;
        if (!userName || (userName && userName.length == 0)) {
            throw new ValidationError("Username is required");
        }
    }

    /**
     * Update user data
     * @param dto UserDto containing updates userData
     */
    async updateUserData(dto: UserDto) : Promise<UserDto> {
        // Ensure that user data is complete
        this.validateUserData(dto);
        const userId = dto.id
        if(userId === undefined || userId.length === 0){
            // user does not exist yet -> Wrong method is used
            throw new ValidationError("Cannot save user without valid userId. Use user/create to create a new user");
        }

        // Validate role if provided
        if (dto.role && !UserRoleHelper.isValidRole(dto.role)) {
            throw new ValidationError(`Invalid role: ${dto.role}`);
        }

        // First: Load current version of user from database
        const entity = await this.userRepository.findById(userId);
        if (!entity) {
            throw new ValidationError("No user with ID " + userId + " found in database!");
        }
        // merge changes into entity
        this.mapper.mergeDtoIntoEntity(dto, entity);
        // persist changes
        const savedEntity = await this.userRepository.update(entity);
        return this.mapper.toDto(savedEntity);
    }

    /**
     * Update password for a specific user
     * @param requestData ChangeUserPasswordRequest containing both password and userId
     * @return true on success
     */
    async changePassword(requestData : ChangeUserPasswordRequest) : Promise<boolean> {
        const userId = requestData.userId;
        if(!userId) {
            throw new ValidationError("User id is required to change password");
        }
        const password = requestData.password;
        if(!password || (password && password.length == 0)){
            throw new ValidationError("Password is required");
        }
        // load user from database
        let entity : UserEntity | null = await this.userRepository.findById(userId);
        if(!entity) {
            throw new ValidationError("No user with ID " + userId + " found in database!");
        }
        entity.password = password;
        const savedEntity = await this.userRepository.update(entity);

        return true;
    }


  /**
   * Load data of a specific user
   * @param userId Id of user to load
   * @returns UserDto containing the user's data
   */
  async getUserById(userId: UUID|string|undefined): Promise<UserDto> {
    if(!userId){
      throw new ValidationError("userId is required");
    }

    const userEntity = await this.userRepository.findById(userId);
    if(!userEntity){
      throw new NotFoundError("user with id" + userId + "not found!")
    }
    return this.mapper.toDto(userEntity);
  }

    /**
     * Get all users
     * @returns Array of UserDto containing all users
     */
    async getAllUsers(): Promise<UserDto[]> {
        const userEntities = await this.userRepository.findAll();
        return userEntities.map((entity) => this.mapper.toDto(entity));
    }
}
