import { Injectable, Global, LoggerService } from "@nestjs/common";
import axios, { AxiosResponse } from "axios";
import { GrantType, LogtoConfig, LogtoOAuthConfig } from "./config";
import { axiosAdapter, p3Values } from "point3-common-tool";
import {
    TokenRevocationFailedError,
    AuthorizationCodeTokenFetchError,
    SignInUriGenerationError,
    SignOutUriGenerationError,
    PersonalAccessTokenFetchError,
} from "../errors";
import { LogtoLoggerServiceToken, LogtoOAuthRESTTemplate } from "./types";

const Gulid = p3Values.Gulid;

/** DI 토큰 */
export const OAuthClientToken = "OAuthClient";

/**
 * OAuthClient
 *
 * Logto OAuth 인증을 위한 클라이언트 서비스입니다.
 * 로그인/로그아웃 URI 생성, 토큰 발급 및 해지 등 OAuth 인증 플로우의 핵심 기능을 제공합니다.
 * NestJS DI 환경에서 사용되며, Logto와의 통합 인증 처리를 담당합니다.
 *
 * 주요 역할:
 *   - 로그인/로그아웃 URI 생성
 *   - 인증 코드로 액세스 토큰 및 ID 토큰 발급
 *   - 토큰 해지(로그아웃)
 *   - Logto OAuth 관련 예외 및 로깅 처리
 *
 * 사용 예시:
 *   const client = new OAuthClient(...);
 *   const { uri, state } = client.getSignInURI(SignInType.Admin);
 *   const tokens = await client.fetchTokenByAuthorizationCode(code);
 *   await client.revokeToken(tokens.accessToken);
 *
 */
@Global()
@Injectable()
export class OAuthClient {
    /** Logto 설정 정보 */
    private logtoConfig: LogtoConfig;
    /** OAuth REST 템플릿 */
    private logtoRestTemplate: axiosAdapter.RESTTemplate;
    /** 상태값 prefix (CSRF 방지용) */
    static readonly prefix: string = "signin";

    /**
     * 생성자
     * @param config OAuth 설정
     * @param logger 로거 서비스
     */
    constructor(
        private readonly config: LogtoOAuthConfig,
        private readonly logger: LoggerService
    ) {
        // Logto 설정 초기화
        this.logtoConfig = {
            endpoint: config.endpoint,
            appId: config.clientId,
            appSecret: config.clientSecret,
            resources: config.resources,
            scopes: config.scopes,
            prompt: config.prompt,
            redirectUri: config.redirectUri,
            grantType: GrantType.AuthorizationCode,
        };

        // REST 템플릿 및 Basic Auth 설정
        this.logtoRestTemplate = new LogtoOAuthRESTTemplate(
            logger,
            this.logtoConfig.endpoint
        );
        this.logtoRestTemplate.setBasic(
            this.logtoConfig.appId!,
            this.logtoConfig.appSecret!
        );
    }

    /**
     * 로그인 URI 생성
     * @param signInType 로그인 타입 (Admin | Dashboard)
     * @returns { uri, state } 로그인 URI와 상태값
     */
    public getSignInURI(
        signInType: SignInType
    ): { uri: string; state: string } {
        try {
            let uri: URL;

            // 대시보드 로그인일 경우 별도 URI, 실패시 기본 URI로 폴백
            if (signInType === SignInType.Dashboard) {
                if (this.config.dashboardSignInUri) {
                    uri = new URL(`${this.config.dashboardSignInUri}/auth`);
                } else {
                    this.logger.warn(
                        "대시보드 로그인 URI 설정을 찾을 수 없어 기본 URI를 사용합니다.",
                        this.constructor.name
                    );
                    uri = new URL(`${this.config.signInUri}/auth`);
                }
            } else {
                uri = new URL(`${this.config.signInUri}/auth`);
            }

            // 상태값 생성 (CSRF 방지)
            const state = Gulid.create(OAuthClient.prefix);

            // OAuth 필수 파라미터 설정
            uri.searchParams.set("redirect_uri", this.logtoConfig.redirectUri!);
            uri.searchParams.set("response_type", "code");
            uri.searchParams.set("scope", this.logtoConfig.scopes!.join(" "));
            uri.searchParams.set("prompt", this.logtoConfig.prompt!);
            uri.searchParams.set("client_id", this.logtoConfig.appId!);
            uri.searchParams.set("resource", this.logtoConfig.resources!.join(" "));
            uri.searchParams.set("state", state.toString());

            return { uri: uri.toString(), state: state.toString() };
        } catch (error) {
            throw new SignInUriGenerationError(signInType);
        }
    }

    /**
     * 로그아웃 URI 생성
     * @returns 로그아웃 URI
     */
    public async getSignOutURI(): Promise<string> {
        try {
            const uri = new URL(`${this.config.signInUri}/session/end`);

            // 로그아웃 후 리다이렉트 URI 및 클라이언트 ID 설정
            uri.searchParams.set("redirect_uri", this.logtoConfig.redirectUri!);
            uri.searchParams.set("client_id", this.logtoConfig.appId!);
            return uri.toString();
        } catch (error) {
            throw new SignOutUriGenerationError();
        }
    }

    /**
     * 인증 코드로 액세스 토큰 및 ID 토큰 발급
     * @param code OAuth 인증 코드
     * @returns { accessToken, idToken } 액세스 토큰과 ID 토큰
     */
    public async fetchTokenByAuthorizationCode(
        code: string
    ): Promise<{ accessToken: string; idToken: string }> {
        try {
            // 토큰 요청 파라미터 설정
            const parameters = new URLSearchParams();
            parameters.set("code", code);
            parameters.set("grant_type", this.logtoConfig.grantType);
            parameters.set("redirect_uri", this.logtoConfig.redirectUri!);
            parameters.set("resource", this.logtoConfig.resources!.join(" "));
            parameters.set("scope", this.logtoConfig.scopes!.join(" "));

            // 토큰 엔드포인트 호출
            const response = await this.logtoRestTemplate.post<TokenResponse>(
                `${this.logtoConfig.endpoint}/token`,
                parameters.toString()
            );
            return {
                accessToken: response.data.access_token,
                idToken: response.data.id_token,
            };
        } catch (error) {
            throw new AuthorizationCodeTokenFetchError(code);
        }
    }
    /**
     * PAT 토큰을 이용해 AccessToken 발급
     * @param pat Personal Access Token
     * @returns { accessToken } 액세스 토큰
     */
    public async fetchAccessTokenByPAT(pat: string): Promise<{ accessToken: string }> {
        try {
            const parameters = new URLSearchParams();
            parameters.set("client_id", this.logtoConfig.appId!);
            parameters.set("grant_type", 'urn:ietf:params:oauth:grant-type:token-exchange');
            parameters.set("resource", this.logtoConfig.resources!.join(" "));
            parameters.set("scope", this.logtoConfig.scopes!.join(" "));
            parameters.set("subject_token", pat);
            parameters.set("subject_token_type", 'urn:logto:token-type:personal_access_token');

            const response = await this.logtoRestTemplate.post<TokenResponse>(
                `${this.logtoConfig.endpoint}/token`,
                parameters.toString(),
                {
                    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                }
            );

            return {
                accessToken: response.data.access_token,
            };
        } catch (error) {
            this.logger.error(`PAT를 이용한 AccessToken 발급 실패: ${error.message}`, error.stack, this.constructor.name);
            throw new PersonalAccessTokenFetchError();
        }
    }


    /**
     * 토큰 해지
     * @param token 해지할 토큰
     */
    public async revokeToken(token: string): Promise<void> {
        try {
            const response: AxiosResponse = await axios.post(
                `${this.logtoConfig.endpoint}/token/revoke`,
                new URLSearchParams({
                    token: token,
                    client_id: this.logtoConfig.appId!,
                }).toString(),
                {
                    headers: { "Content-Type": "application/x-www-form-urlencoded" },
                }
            );

            if (response.status === 200) return;

            throw new TokenRevocationFailedError();
        } catch (error) {
            throw new TokenRevocationFailedError();
        }
    }
}

/**
 * 로그인 타입 열거형
 * - Admin: 관리자 로그인
 * - Dashboard: 대시보드 로그인
 */
export enum SignInType {
    Admin = "admin",
    Dashboard = "dashboard",
}

/**
 * 토큰 응답 타입
 */
type TokenResponse = {
    access_token: string; // 액세스 토큰
    refresh_token?: string; // 리프레시 토큰 (선택)
    id_token: string; // ID 토큰
    scope: string; // 부여된 스코프
    expires_in: number; // 토큰 만료 시간(초)
};
