import {
  BadRequestException,
  ForbiddenException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';

import FileType from 'file-type';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';

import { FileFolder } from 'src/core/file/interfaces/file-folder.interface';

import { ChallengeInput } from 'src/core/auth/dto/challenge.input';
import { assert } from 'src/utils/assert';
import {
  PASSWORD_REGEX,
  compareHash,
  hashPassword,
} from 'src/core/auth/auth.util';
import { Verify } from 'src/core/auth/dto/verify.entity';
import { UserExists } from 'src/core/auth/dto/user-exists.entity';
import { WorkspaceInviteHashValid } from 'src/core/auth/dto/workspace-invite-hash-valid.entity';
import { User } from 'src/core/user/user.entity';
import { Workspace } from 'src/core/workspace/workspace.entity';
import { UserService } from 'src/core/user/services/user.service';
import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspace-manager.service';
import { getImageBufferFromUrl } from 'src/utils/image';
import { FileUploadService } from 'src/core/file/services/file-upload.service';

import { TokenService } from './token.service';

export type UserPayload = {
  firstName: string;
  lastName: string;
  email: string;
};

@Injectable()
export class AuthService {
  constructor(
    private readonly tokenService: TokenService,
    private readonly userService: UserService,
    private readonly workspaceManagerService: WorkspaceManagerService,
    private readonly fileUploadService: FileUploadService,
    @InjectRepository(Workspace, 'core')
    private readonly workspaceRepository: Repository<Workspace>,
    @InjectRepository(User, 'core')
    private readonly userRepository: Repository<User>,
  ) {}

  async challenge(challengeInput: ChallengeInput) {
    const user = await this.userRepository.findOneBy({
      email: challengeInput.email,
    });

    assert(user, "This user doesn't exist", NotFoundException);
    assert(user.passwordHash, 'Incorrect login method', ForbiddenException);

    const isValid = await compareHash(
      challengeInput.password,
      user.passwordHash,
    );

    assert(isValid, 'Wrong password', ForbiddenException);

    return user;
  }

  async signUp({
    email,
    password,
    workspaceInviteHash,
    firstName,
    lastName,
    picture,
  }: {
    email: string;
    password?: string;
    firstName?: string | null;
    lastName?: string | null;
    workspaceInviteHash?: string | null;
    picture?: string | null;
  }) {
    if (!firstName) firstName = '';
    if (!lastName) lastName = '';

    const existingUser = await this.userRepository.findOneBy({
      email: email,
    });

    assert(!existingUser, 'This user already exists', ForbiddenException);

    if (password) {
      const isPasswordValid = PASSWORD_REGEX.test(password);

      assert(isPasswordValid, 'Password too weak', BadRequestException);
    }

    const passwordHash = password ? await hashPassword(password) : undefined;
    let workspace: Workspace | null;

    if (workspaceInviteHash) {
      workspace = await this.workspaceRepository.findOneBy({
        inviteHash: workspaceInviteHash,
      });

      assert(
        workspace,
        'This workspace inviteHash is invalid',
        ForbiddenException,
      );
    } else {
      const workspaceToCreate = this.workspaceRepository.create({
        displayName: '',
        domainName: '',
        inviteHash: v4(),
      });

      workspace = await this.workspaceRepository.save(workspaceToCreate);
      await this.workspaceManagerService.init(workspace.id);
    }

    const userToCreate = this.userRepository.create({
      email: email,
      firstName: firstName,
      lastName: lastName,
      canImpersonate: false,
      passwordHash,
      defaultWorkspace: workspace,
    });
    const user = await this.userRepository.save(userToCreate);
    let imagePath: string | undefined = undefined;

    if (picture) {
      const buffer = await getImageBufferFromUrl(picture);

      const type = await FileType.fromBuffer(buffer);

      const { paths } = await this.fileUploadService.uploadImage({
        file: buffer,
        filename: `${v4()}.${type?.ext}`,
        mimeType: type?.mime,
        fileFolder: FileFolder.ProfilePicture,
      });

      imagePath = paths[0];
    }
    await this.userService.createWorkspaceMember(user, imagePath);

    return user;
  }

  async verify(email: string): Promise<Verify> {
    const user = await this.userRepository.findOne({
      where: {
        email,
      },
      relations: ['defaultWorkspace'],
    });

    assert(user, "This user doesn't exist", NotFoundException);

    assert(
      user.defaultWorkspace,
      'User has no default workspace',
      NotFoundException,
    );

    // passwordHash is hidden for security reasons
    user.passwordHash = '';
    user.workspaceMember = await this.userService.loadWorkspaceMember(user);

    const accessToken = await this.tokenService.generateAccessToken(user.id);
    const refreshToken = await this.tokenService.generateRefreshToken(user.id);

    return {
      user,
      tokens: {
        accessToken,
        refreshToken,
      },
    };
  }

  async checkUserExists(email: string): Promise<UserExists> {
    const user = await this.userRepository.findOneBy({
      email,
    });

    return { exists: !!user };
  }

  async checkWorkspaceInviteHashIsValid(
    inviteHash: string,
  ): Promise<WorkspaceInviteHashValid> {
    const workspace = await this.workspaceRepository.findOneBy({
      inviteHash,
    });

    return { isValid: !!workspace };
  }

  async impersonate(userId: string) {
    const user = await this.userRepository.findOne({
      where: {
        id: userId,
      },
      relations: ['defaultWorkspace'],
    });

    assert(user, "This user doesn't exist", NotFoundException);

    if (!user.defaultWorkspace.allowImpersonation) {
      throw new ForbiddenException('Impersonation not allowed');
    }

    const accessToken = await this.tokenService.generateAccessToken(user.id);
    const refreshToken = await this.tokenService.generateRefreshToken(user.id);

    return {
      user,
      tokens: {
        accessToken,
        refreshToken,
      },
    };
  }
}
