/*
 * Copyright © 2024 Hexastack. All rights reserved.
 *
 * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
 * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
 * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
 */

import {
  BadRequestException,
  Body,
  Controller,
  Get,
  InternalServerErrorException,
  Param,
  Post,
  Req,
  Res,
  Session,
  UnauthorizedException,
  UseGuards,
  UseInterceptors,
} from '@nestjs/common';
import { CsrfCheck, CsrfGen, CsrfGenAuth } from '@tekuconcept/nestjs-csrf';
import { Request, Response } from 'express';
import { Session as ExpressSession } from 'express-session';

import { config } from '@/config';
import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
import { LoggerService } from '@/logger/logger.service';
import { Roles } from '@/utils/decorators/roles.decorator';

import { InvitationCreateDto } from '../dto/invitation.dto';
import { UserCreateDto } from '../dto/user.dto';
import { LocalAuthGuard } from '../guards/local-auth.guard';
import { InvitationService } from '../services/invitation.service';
import { UserService } from '../services/user.service';
import { ValidateAccountService } from '../services/validate-account.service';

export class BaseAuthController {
  constructor(protected readonly logger: LoggerService) {}

  /**
   * Fetches details of the currently authenticated user.
   *
   * @param req - The request object which includes user details.
   *
   * @returns The user object from the request.
   */
  @Get('me')
  me(@Req() req: Request) {
    return req.user;
  }

  /**
   * Handles user logout by clearing the session and cookies.
   *
   * @param session - The current user's session to be destroyed.
   * @param res - The response object used to clear the session cookie.
   *
   * @returns A status object indicating successful logout.
   */
  @Post('logout')
  @CsrfCheck(false)
  @CsrfGen(false)
  logout(
    @Session() session: ExpressSession,
    @Res({ passthrough: true }) res: Response,
  ) {
    res.clearCookie(config.session.name);

    session.destroy((error) => {
      if (error) {
        this.logger.error(error);
        throw new BadRequestException();
      }
    });

    return { status: 'ok' };
  }
}

@UseInterceptors(CsrfInterceptor)
@Controller('auth')
export class LocalAuthController extends BaseAuthController {
  constructor(
    logger: LoggerService,
    private readonly userService: UserService,
    private readonly validateAccountService: ValidateAccountService,
    private readonly invitationService: InvitationService,
  ) {
    super(logger);
  }

  /**
   * Handles local login process using guard for public users.
   *
   * @param req - The request object containing user details.
   *
   * @returns The logged-in user object.
   */
  @UseGuards(LocalAuthGuard)
  @Roles('public')
  @Post('local')
  @CsrfCheck(false)
  @CsrfGenAuth(true)
  login(@Req() req: Request) {
    return req.user;
  }

  /**
   * Handles the signup process for new users.
   *
   * @param userCreateDto - Data Transfer Object containing the new user's information.
   *
   * @returns Success status object upon successful signup.
   */
  @Roles('public')
  @Post('signup')
  async signup(@Body() userCreateDto: UserCreateDto) {
    try {
      await this.userService.create(userCreateDto);
      return { success: true };
    } catch (error) {
      this.logger.error(error);
      throw new BadRequestException();
    }
  }

  /**
   * Accepts an invitation and creates a new user based on the invitation token.
   * Verifies the token and ensures it matches the user details.
   *
   * @param userCreateDto - Data Transfer Object for the new user.
   * @param token - The invitation token.
   *
   * @returns Void, upon successful creation of the user.
   */
  @Roles('public')
  @Post('accept-invite/:token')
  async acceptInvite(
    @Body() userCreateDto: UserCreateDto,
    @Param('token') token: string,
  ) {
    let decodedToken: InvitationCreateDto;

    // Verify token
    try {
      decodedToken = await this.invitationService.verify(token);
    } catch (error) {
      if (error.name === 'TokenExpiredError')
        throw new UnauthorizedException('Token expired');
      else throw new BadRequestException(error.name, error.message);
    }

    // Verify token matches user
    if (decodedToken.email !== userCreateDto.email)
      throw new BadRequestException("Email doesn't match invitation email");
    if (decodedToken.roles.some((item) => !userCreateDto.roles.includes(item)))
      throw new BadRequestException('invitation roles do not match user roles');

    try {
      // Create user
      await this.userService.create({ ...userCreateDto, state: false });

      await this.validateAccountService.sendConfirmationEmail({
        email: userCreateDto.email,
        first_name: userCreateDto.first_name,
      });

      await this.invitationService.deleteOne({ email: decodedToken.email });
    } catch (e) {
      this.logger.error(
        'Could not send email',
        e.message,
        e.stack,
        'AcceptInvite',
      );
      throw new InternalServerErrorException('Could not send email');
    }
  }
}
