import { Injectable, UnauthorizedException } from "@nestjs/common";
import { jwtVerify, createRemoteJWKSet } from "jose";

import { LogtoVerifierConfig } from "../client/config";
import * as token from "./access-token";

export const LogtoTokenVerifierToken = Symbol.for("LogtoTokenVerifier");

@Injectable()
export class LogtoTokenVerifier {
    constructor(private readonly config: LogtoVerifierConfig) { }

    /**
     * 토큰을 검증하고 필요에 따라 필수 스코프와 역할을 확인합니다.
     * @param token 검증할 토큰입니다.
     * @param requiredScopes 선택적으로 확인할 스코프입니다.
     * @param requiredRoles 선택적으로 확인할 역할입니다.
     * @returns 토큰이 유효한 경우 토큰 페이로드를 반환하는 Promise입니다.
     * @throws UnauthorizedException 토큰이 유효하지 않은 경우 발생합니다.
     */
    public async verifyToken(token: string): Promise<token.AccessTokenPayload>;
    public async verifyToken(token: string, requiredScopes: string[], requiredRoles: string[]): Promise<token.AccessTokenPayload>;
    public async verifyToken(token: string, requiredScopes?: string[], requiredRoles?: string[]): Promise<token.AccessTokenPayload> {
        if (!token) throw new UnauthorizedException('엑세스 토큰이 존재하지 않습니다.');

        const { payload } = await jwtVerify(
            token, createRemoteJWKSet(new URL(this.config.jwksUri)),
            { issuer: this.config.issuer }
        );

        const tokenPayload = payload as token.AccessTokenPayload;

        if (requiredScopes || requiredRoles) {
            this.shouldContainRequiredPrivileges(
                tokenPayload, requiredScopes, requiredRoles);
        }

        return tokenPayload;
    }

    /**
     * id token을 검증합니다.
     * @param token id token 문자열입니다.
     * @returns id token 페이로드입니다.
     */
    public async verifyIdToken(token: string): Promise<token.IdTokenPayload> {
        const { payload } = await jwtVerify(
            token,
            createRemoteJWKSet(new URL(this.config.jwksUri)),
            { issuer: this.config.issuer }
        );
        return payload as token.IdTokenPayload;
    }

    /**
     * 토큰 페이로드를 통해 필요한 스코프와 역할 등 추가적인 검사를 수행합니다.
     * @param payload 토큰 페이로드입니다.
     * @param requiredScopes 선택적으로 확인할 스코프입니다.
     * @param requiredRoles 선택적으로 확인할 역할입니다.
     */
    private shouldContainRequiredPrivileges(
        payload: token.AccessTokenPayload,
        requiredScopes?: string[],
        requiredRoles?: string[]
    ): void {
        const { userScopes, userRoles } = payload;
        const scopes = userScopes?.flat() ?? [];

        if (this.hasInsufficientScopes(requiredScopes, scopes)) {
            throw new UnauthorizedException(
                { code: 'auth.insufficient_scope', status: 403 },
                { cause: requiredScopes }
            );
        }

        if (this.hasInsufficientRoles(requiredRoles, userRoles)) {
            throw new UnauthorizedException(
                { code: 'auth.role_mismatch', status: 403 },
                { cause: requiredRoles }
            );
        }
    }

    private hasInsufficientScopes(requiredScopes: string[] | undefined, userScopes: string[]): boolean {
        return !!(requiredScopes && requiredScopes.length > 0 && !requiredScopes.every(scope => userScopes.includes(scope)));
    }

    private hasInsufficientRoles(requiredRoles: string[] | undefined, userRoles: string[]): boolean {
        return !!(requiredRoles && requiredRoles.length > 0 && !requiredRoles.some(role => userRoles.includes(role)));
    }
}
