import {
  Body,
  Controller,
  HttpException,
  HttpStatus,
  Logger,
  Post,
  Req,
  UseGuards,
} from '@nestjs/common';
import {
  ApiResponse,
  ApiOperation,
  ApiBearerAuth,
  ApiTags,
} from '@nestjs/swagger';
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
import { ErrorResponse } from '../../common/error/types/types';
import { AuthService } from './auth.service';
import {
  AccessTokenResponse,
  TokenRequest,
  TokenResponse,
  DelegationTokenRequest,
  DelegationTokenResponse,
  DelegationAccessTokenResponse,
} from './types/types';
import { retrieveAuthorizationToken } from '../../common/http/helper';
import { makeContext, retrieveRequestId, retrieveIp } from '../../common/log';
import { Request } from 'express';
import { AuthGuard } from '../../common/guards/auth/authguards';
import { RoleGuard } from '../../common/guards/role/roleguards';
import { ADMIN_ROLES, TIERS } from '../../constants';
import jwt from 'jsonwebtoken';
import { AccessToken, RefreshToken } from '../../common/token';
import { makeIDTokenKey } from '../../common/cache';
import { RedisService } from '../../gateways/redis/redis.service';

@ApiTags('auth')
@Controller('auth')
export class AuthController {
  private readonly logger = new Logger(AuthController.name);
  constructor(
    private readonly authService: AuthService,
    private readonly redisService: RedisService,
  ) {}

  @Post('token')
  @ApiResponse({
    status: HttpStatus.OK,
    type: TokenResponse,
    description: '成功時のレスポンス',
  })
  @ApiResponse({
    status: HttpStatus.UNAUTHORIZED,
    description: '認証エラー/同意済み利用規約が最新でない場合',
    type: ErrorResponse,
  })
  @ApiResponse({
    status: HttpStatus.INTERNAL_SERVER_ERROR,
    description: '想定外のサーバーエラー',
    type: ErrorResponse,
  })
  @ApiOperation({
    description:
      'AzureADB2Cでのサインイン後に払いだされるIDトークンを元に認証用のアクセストークンとリフレッシュトークンを生成します',
    operationId: 'token',
  })
  async token(
    @Body() body: TokenRequest,
    @Req() req: Request,
  ): Promise<TokenResponse> {
    const ip = retrieveIp(req);
    if (!ip) {
      throw new HttpException(
        makeErrorResponse('E000401'),
        HttpStatus.UNAUTHORIZED,
      );
    }

    const requestId = retrieveRequestId(req);
    if (!requestId) {
      throw new HttpException(
        makeErrorResponse('E000501'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }

    const context = makeContext('anonymous', requestId);
    this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);

    const idToken = await this.authService.getVerifiedIdToken(
      context,
      body.idToken,
    );

    const isVerified = await this.authService.isVerifiedUser(context, idToken);
    if (!isVerified) {
      throw new HttpException(
        makeErrorResponse('E010201'),
        HttpStatus.BAD_REQUEST,
      );
    }

    const key = makeIDTokenKey(body.idToken);
    const isTokenExists = await this.redisService.get<boolean>(context, key);
    if (isTokenExists) {
      // IDトークンがキャッシュに存在する場合エラー
      throw new HttpException(
        makeErrorResponse('E000106'),
        HttpStatus.UNAUTHORIZED,
      );
    }

    // 同意済み利用規約バージョンが最新かチェック
    const isAcceptedLatestVersion =
      await this.authService.isAcceptedLatestVersion(context, idToken);

    // 最新でなければエラー
    if (!isAcceptedLatestVersion) {
      throw new HttpException(
        makeErrorResponse('E010209'),
        HttpStatus.UNAUTHORIZED,
      );
    }

    // IDトークンをキャッシュに保存(idTokenの有効期限をADB2Cの有効期限と合わせる(300秒))
    await this.redisService.set(context, key, true, 300);

    const refreshToken = await this.authService.generateRefreshToken(
      context,
      idToken,
      body.type,
    );

    const accessToken = await this.authService.generateAccessToken(
      context,
      refreshToken,
    );

    return {
      accessToken,
      refreshToken,
    };
  }

  @Post('accessToken')
  @ApiBearerAuth()
  @ApiResponse({
    status: HttpStatus.OK,
    type: AccessTokenResponse,
    description: '成功時のレスポンス',
  })
  @ApiResponse({
    status: HttpStatus.UNAUTHORIZED,
    description: '認証エラー',
    type: ErrorResponse,
  })
  @ApiResponse({
    status: HttpStatus.INTERNAL_SERVER_ERROR,
    description: '想定外のサーバーエラー',
    type: ErrorResponse,
  })
  @ApiOperation({
    operationId: 'accessToken',
    description: 'リフレッシュトークンを元にアクセストークンを再生成します',
  })
  async accessToken(@Req() req: Request): Promise<AccessTokenResponse> {
    const refreshToken = retrieveAuthorizationToken(req);
    if (!refreshToken) {
      throw new HttpException(
        makeErrorResponse('E000107'),
        HttpStatus.UNAUTHORIZED,
      );
    }

    const ip = retrieveIp(req);
    if (!ip) {
      throw new HttpException(
        makeErrorResponse('E000401'),
        HttpStatus.UNAUTHORIZED,
      );
    }

    const requestId = retrieveRequestId(req);
    if (!requestId) {
      throw new HttpException(
        makeErrorResponse('E000501'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }

    const context = makeContext('anonymous', requestId);
    this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);

    const accessToken = await this.authService.generateAccessToken(
      context,
      refreshToken,
    );
    return { accessToken };
  }

  @Post('delegation/token')
  @ApiBearerAuth()
  @ApiResponse({
    status: HttpStatus.OK,
    type: DelegationTokenResponse,
    description: '成功時のレスポンス',
  })
  @ApiResponse({
    status: HttpStatus.UNAUTHORIZED,
    description: '認証エラー',
    type: ErrorResponse,
  })
  @ApiResponse({
    status: HttpStatus.BAD_REQUEST,
    description: '指定したアカウントが代行操作を許可していない場合',
    type: ErrorResponse,
  })
  @ApiResponse({
    status: HttpStatus.INTERNAL_SERVER_ERROR,
    description: '想定外のサーバーエラー',
    type: ErrorResponse,
  })
  @ApiOperation({
    operationId: 'delegationToken',
    description:
      '代行操作用のリフレッシュトークン・アクセストークンを生成します',
  })
  @UseGuards(AuthGuard)
  @UseGuards(
    RoleGuard.requireds({
      roles: [ADMIN_ROLES.ADMIN],
      tiers: [TIERS.TIER4],
    }),
  )
  async delegationToken(
    @Req() req: Request,
    @Body() body: DelegationTokenRequest,
  ): Promise<DelegationTokenResponse> {
    const { delegatedAccountId } = body;
    const token = retrieveAuthorizationToken(req);
    if (!token) {
      throw new HttpException(
        makeErrorResponse('E000107'),
        HttpStatus.UNAUTHORIZED,
      );
    }

    const ip = retrieveIp(req);
    if (!ip) {
      throw new HttpException(
        makeErrorResponse('E000401'),
        HttpStatus.UNAUTHORIZED,
      );
    }

    const requestId = retrieveRequestId(req);
    if (!requestId) {
      throw new HttpException(
        makeErrorResponse('E000501'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }

    const decodedAccessToken = jwt.decode(token, { json: true });
    if (!decodedAccessToken) {
      throw new HttpException(
        makeErrorResponse('E000101'),
        HttpStatus.UNAUTHORIZED,
      );
    }
    const { userId } = decodedAccessToken as AccessToken;

    const context = makeContext('anonymous', requestId);
    this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);

    const refreshToken = await this.authService.generateDelegationRefreshToken(
      context,
      userId,
      delegatedAccountId,
    );
    const accessToken = await this.authService.generateDelegationAccessToken(
      context,
      refreshToken,
    );

    return { accessToken, refreshToken };
  }

  @Post('delegation/access-token')
  @ApiBearerAuth()
  @ApiResponse({
    status: HttpStatus.OK,
    type: DelegationAccessTokenResponse,
    description: '成功時のレスポンス',
  })
  @ApiResponse({
    status: HttpStatus.UNAUTHORIZED,
    description: '認証エラー',
    type: ErrorResponse,
  })
  @ApiResponse({
    status: HttpStatus.INTERNAL_SERVER_ERROR,
    description: '想定外のサーバーエラー',
    type: ErrorResponse,
  })
  @ApiOperation({
    operationId: 'delegationAccessToken',
    description: '代行操作用のアクセストークンを再生成します',
  })
  async delegationAccessToken(
    @Req() req: Request,
  ): Promise<DelegationAccessTokenResponse> {
    const refreshToken = retrieveAuthorizationToken(req);
    if (!refreshToken) {
      throw new HttpException(
        makeErrorResponse('E000107'),
        HttpStatus.UNAUTHORIZED,
      );
    }

    const ip = retrieveIp(req);
    if (!ip) {
      throw new HttpException(
        makeErrorResponse('E000401'),
        HttpStatus.UNAUTHORIZED,
      );
    }

    const requestId = retrieveRequestId(req);
    if (!requestId) {
      throw new HttpException(
        makeErrorResponse('E000501'),
        HttpStatus.INTERNAL_SERVER_ERROR,
      );
    }

    const decodedRefreshToken = jwt.decode(refreshToken, { json: true });
    if (!decodedRefreshToken) {
      throw new HttpException(
        makeErrorResponse('E000101'),
        HttpStatus.UNAUTHORIZED,
      );
    }
    const { userId, delegateUserId } = decodedRefreshToken as RefreshToken;

    const context = makeContext('anonymous', requestId);
    this.logger.log(`[${context.getTrackingId()}] ip : ${ip}`);

    const accessToken = await this.authService.updateDelegationAccessToken(
      context,
      delegateUserId,
      userId,
      refreshToken,
    );

    return { accessToken };
  }
}
