import {
  Injectable,
  CanActivate,
  ExecutionContext,
  UnauthorizedException,
  InternalServerErrorException,
  Inject,
  HttpStatus
} from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { Reflector } from '@nestjs/core';
import { IncomingHttpHeaders } from 'http';

import { errors } from 'jose';

import { p3Values } from 'point3-common-tool';
import { LogtoTokenVerifier, LogtoTokenVerifierToken } from '../token';
import { ConfigService } from '@nestjs/config';

export const LogtoTokenGuardToken = Symbol('LogtoTokenGuard');

@Injectable()
export class LogtoTokenGuard implements CanActivate {
  private reflector: Reflector = new Reflector();
  constructor(
    @Inject(LogtoTokenVerifierToken)
    private tokenVerifier: LogtoTokenVerifier,

    private configService: ConfigService
  ) { }

  async canActivate(context: ExecutionContext): Promise<boolean> {
    if (this.configService.get<string>('NODE_ENV') === 'local') {
      return true;
    }

    //매타데이터에서 필요한 스코프와 역할을 가져온다.
    const requiredScopes = this.reflector.get<string[]>('requiredScopes', context.getHandler());
    const requiredRoles = this.reflector.get<string[]>('requiredRoles', context.getHandler());

    const request = this.getRequest(context);

    //헤더에서 베어러 토큰을 추출한다
    try {
      const bearerToken = this.extractBearerTokenFrom(request.headers);
      const result = await this.tokenVerifier.verifyToken(bearerToken, requiredScopes, requiredRoles);

      // request.user에 사용자 정보를 추가한다.
      request.user = {
        userId: result.sub,
        managerId: p3Values.Guid.parse(result.managerId),
        clientId: result.clientId ? p3Values.Guid.parse(result.clientId) : undefined,
      }
      return true;
    } catch (error) {
      if (error instanceof UnauthorizedException) throw error;
      if (error instanceof errors.JOSEError) throw new UnauthorizedException(error);
      if (error instanceof Error) throw new InternalServerErrorException("요청을 처리하지 못하였습니다.", `${HttpStatus.INTERNAL_SERVER_ERROR}`);

      throw new UnauthorizedException("접근이 허용되지 않습니다.");
    }
  }

  /**
   * Extracts the Bearer token from the authorization header.
   * @param headers - The incoming HTTP headers.
   * @returns The extracted token.
   * @throws UnauthorizedException if the authorization header is missing or invalid.
   */
  private extractBearerTokenFrom(headers: IncomingHttpHeaders): string {
    const bearerTokenIdentifier = 'Bearer';

    if (!headers.authorization) {
      throw new UnauthorizedException('Authorization header is missing');
    }

    if (!headers.authorization.startsWith(bearerTokenIdentifier)) {
      throw new UnauthorizedException('Authorization token type not supported');
    }

    return headers.authorization.slice(bearerTokenIdentifier.length + 1);
  };

  private getRequest(context: ExecutionContext): any {
    // Works for both REST and GraphQL
    if (context.getType<'http' | 'graphql'>() === 'graphql') {
      const gqlCtx = GqlExecutionContext.create(context);
      // depends on what you return from GraphQLModule context: ({ req }) => ({ req })
      return gqlCtx.getContext().req;
    }

    return context.switchToHttp().getRequest();
  }


}