import {
  ForbiddenException,
  HttpException,
  HttpStatus,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { User } from '../users/entities/user.entity';
import * as bcrypt from 'bcryptjs';
import { AuthEmailLoginDto } from './dto/auth-email-login.dto';
import { AuthUpdateDto } from './dto/auth-update.dto';
import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util';
import { RoleEnum } from 'src/roles/roles.enum';
import { StatusEnum } from 'src/statuses/statuses.enum';
import * as crypto from 'crypto';
import { plainToClass } from 'class-transformer';
import { Status } from 'src/statuses/entities/status.entity';
import { Role } from 'src/roles/entities/role.entity';
import { AuthProvidersEnum } from './auth-providers.enum';
import { AuthRegisterLoginDto } from './dto/auth-register-login.dto';
import { UsersService } from 'src/users/users.service';
import { ForgotService } from 'src/forgot/forgot.service';
import { MailService } from 'src/mail/mail.service';
import { CreateInviteDTO } from './dto/create-invite.dto';
import { LoggingService } from 'src/database/logging/logging.service';
import { DataSource } from 'typeorm';
import { TeamAccessService } from 'src/teams/team-access.service';
import { TeamRolesEnum } from 'src/team-roles/team-roles.enum';

@Injectable()
export class AuthService {
  constructor(
    private jwtService: JwtService,
    private usersService: UsersService,
    private forgotService: ForgotService,
    private mailService: MailService,
    private logger: LoggingService,
    private dataSource: DataSource,
    private teamAccess: TeamAccessService,
  ) {}

  async validateLogin(
    loginDto: AuthEmailLoginDto,
  ): Promise<{ token: string; user: User }> {
    const user = await this.usersService.findOne({
      email: loginDto.email,
    });

    if (!user) {
      await this.logger.info({
        event: 'user_login_fail',
        details: 'reason=user_not_found',
        userId: -1,
      });
      throw new HttpException(
        {
          status: HttpStatus.UNPROCESSABLE_ENTITY,
          errors: {
            email: 'notFound',
          },
        },
        HttpStatus.UNPROCESSABLE_ENTITY,
      );
    }

    await this.assertActiveUser(user);

    if (user.provider !== AuthProvidersEnum.email) {
      throw new HttpException(
        {
          status: HttpStatus.UNPROCESSABLE_ENTITY,
          errors: {
            email: `needLoginViaProvider:${user.provider}`,
          },
        },
        HttpStatus.UNPROCESSABLE_ENTITY,
      );
    }

    const isValidPassword = await bcrypt.compare(
      loginDto.password,
      user.password,
    );

    if (isValidPassword) {
      const token = await this.jwtService.sign({
        id: user.id,
        role: user.role,
      });

      await this.logger.info({
        event: 'user_login_success',
        details: `userId=${user.id}`,
        userId: user.id,
      });

      return { token, user: user };
    } else {
      await this.logger.info({
        event: 'user_login_fail',
        details: `userId=${user.id} reason=incorrect_password`,
        userId: user.id,
      });

      throw new HttpException(
        {
          status: HttpStatus.UNPROCESSABLE_ENTITY,
          errors: {
            password: 'incorrectPassword',
          },
        },
        HttpStatus.UNPROCESSABLE_ENTITY,
      );
    }
  }

  async register(dto: AuthRegisterLoginDto): Promise<void> {
    const hash = crypto
      .createHash('sha256')
      .update(randomStringGenerator())
      .digest('hex');

    const user = await this.usersService.create({
      email: dto.email,
      password: dto.password,
      firstName: dto.firstName,
      lastName: dto.lastName,
      role: {
        id: RoleEnum.user,
      } as Role,
      status: {
        id: StatusEnum.inactive,
      } as Status,
      hash,
    });

    await this.logger.info({
      event: 'user_create',
      details: `userId=${user.id}`,
      userId: user.id,
    });

    await this.mailService.userSignUp({
      to: user.email,
      data: {
        hash,
        firstName: user.firstName,
      },
    });
  }

  async confirmEmail(hash: string): Promise<void> {
    await this.dataSource.transaction(async (manager) => {
      const repository = manager.getRepository(User);
      const user = await repository
        .createQueryBuilder('user')
        .where('user.hash = :hash', { hash })
        .setLock('pessimistic_write')
        .getOne();
      if (!user) {
        throw new HttpException(
          {
            status: HttpStatus.NOT_FOUND,
            error: `notFound`,
          },
          HttpStatus.NOT_FOUND,
        );
      }
      user.hash = null;
      user.status = plainToClass(Status, { id: StatusEnum.active });
      await repository.save(user);
    });
  }

  async forgotPassword(email: string): Promise<void> {
    const user = await this.usersService.findOne({
      email,
    });

    if (!user) {
      throw new HttpException(
        {
          status: HttpStatus.UNPROCESSABLE_ENTITY,
          errors: {
            email: 'emailNotExists',
          },
        },
        HttpStatus.UNPROCESSABLE_ENTITY,
      );
    } else {
      const hash = crypto
        .createHash('sha256')
        .update(randomStringGenerator())
        .digest('hex');
      await this.forgotService.create({
        hash,
        user,
      });

      await this.mailService.forgotPassword({
        to: email,
        data: {
          hash,
          firstName: user.firstName,
        },
      });
    }
  }

  async resetPassword(hash: string, password: string): Promise<void> {
    const forgot = await this.forgotService.findOne({
      where: {
        hash,
      },
    });

    if (!forgot) {
      throw new HttpException(
        {
          status: HttpStatus.UNPROCESSABLE_ENTITY,
          errors: {
            hash: `notFound`,
          },
        },
        HttpStatus.UNPROCESSABLE_ENTITY,
      );
    }

    const user = forgot.user;
    user.password = password;
    await user.save();
    await this.forgotService.softDelete(forgot.id);
  }

  async me(token: string): Promise<User> {
    token = token.replace('Bearer ', '');
    let payload: any;
    let refreshToken = false;
    try {
      payload = this.jwtService.verify(token);
    } catch {
      payload = this.jwtService.decode(token);
      refreshToken = true;
    }
    if (!payload?.id) throw new UnauthorizedException();
    const user = await this.usersService.findOne({ id: payload.id });
    if (!user) throw new UnauthorizedException();
    await this.assertActiveUser(user);
    if (refreshToken) {
      const t = await this.jwtService.sign({
        id: user.id,
        role: user.role,
      });
      user['token'] = t;
    }
    await this.logger.debug({
      event: 'user_token_verification_success',
      details: `userId=${user.id}`,
      userId: user.id,
    });
    return user;
  }

  async update(user: User, userDto: AuthUpdateDto): Promise<User> {
    if (userDto.password) {
      if (userDto.oldPassword) {
        const currentUser = await this.usersService.findOne({
          id: user.id,
        });

        const isValidOldPassword = await bcrypt.compare(
          userDto.oldPassword,
          currentUser.password,
        );

        if (!isValidOldPassword) {
          throw new HttpException(
            {
              status: HttpStatus.UNPROCESSABLE_ENTITY,
              errors: {
                oldPassword: 'incorrectOldPassword',
              },
            },
            HttpStatus.UNPROCESSABLE_ENTITY,
          );
        }
      } else {
        throw new HttpException(
          {
            status: HttpStatus.UNPROCESSABLE_ENTITY,
            errors: {
              oldPassword: 'missingOldPassword',
            },
          },
          HttpStatus.UNPROCESSABLE_ENTITY,
        );
      }
    }

    await this.usersService.update(user.id, userDto);

    return this.usersService.findOne({
      id: user.id,
    });
  }

  async createTeamInvite(object: CreateInviteDTO, actorUserId: number) {
    await this.teamAccess.assertAtLeast(
      actorUserId,
      object.teamId,
      'invite_min_role',
      TeamRolesEnum.captain,
    );

    const token = await this.jwtService.sign(object, {
      expiresIn: '30d',
    });

    await this.logger.info({
      event: 'user_invite_link_create',
      details: `invitation created for team: ${object.teamName} , ${object.teamId}`,
      userId: 0,
    });

    return { token };
  }

  async getTeamFromInvite(token: string) {
    try {
      const teamInfo = this.jwtService.verify(token);
      delete teamInfo.iat;
      delete teamInfo.exp;

      await this.logger.info({
        event: 'user_invite_link_validate',
        details: `invitation validated for team: ${teamInfo.teamName} , ${teamInfo.teamId}`,
        userId: 0,
      });

      return teamInfo;
    } catch {
      await this.logger.info({
        event: 'user_invite_link_validate_fail',
        details: 'invitation validation failed',
        userId: 0,
      });

      throw new HttpException(
        'Token not valid',
        HttpStatus.UNPROCESSABLE_ENTITY,
      );
    }
  }

  private async assertActiveUser(user: User): Promise<void> {
    if (user.status?.id === StatusEnum.active) return;
    await this.logger.info({
      event: 'user_login_fail',
      details: `userId=${user.id} reason=inactive`,
      userId: user.id,
    });
    throw new ForbiddenException('User account is inactive');
  }
}
