import { DynamicModule, LoggerService, Provider, Type } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
    LogtoLoggerServiceToken,
    LogtoLoginSessionToken,
    LogtoM2MClientToken,
    OAuthClient,
    OAuthClientToken,
    LogtoLoginSession,
    LogtoM2MClient,
    Prompt,
} from './client';
import { LogtoTokenVerifier, LogtoTokenVerifierToken } from './token';
import { LogtoTokenGuard, LogtoTokenGuardToken } from './stateless';

/** LogtoModule 옵션 주입 토큰 */
export const LOGTO_MODULE_OPTIONS = Symbol('LOGTO_MODULE_OPTIONS');

/** 로거 설정 */
export interface LogtoLoggerOptions {
    /** 로거 모듈 (forRoot에서만 필요) */
    module?: Type<any>;
    /** 로거 서비스의 Injection Token */
    token: Symbol | string;
}

/** forRoot 옵션 */
export interface LogtoModuleOptions {
    /** 모듈을 전역으로 설정할지 여부 */
    global?: boolean;
    /** 클라이언트 기능 활성화 (OAuthClient, LogtoM2MClient, LogtoLoginSession) */
    enableClient?: boolean;
    /** 로거 설정 */
    logger: LogtoLoggerOptions & { module: Type<any> };
}

/** forRootAsync useFactory 반환 타입 */
export interface LogtoModuleFactoryOptions {
    /** 클라이언트 기능 활성화 */
    enableClient?: boolean;
}

/** forRootAsync 옵션 */
export interface LogtoModuleAsyncOptions {
    /** 모듈을 전역으로 설정할지 여부 */
    global?: boolean;
    /** 의존 모듈 목록 (로거 모듈 포함) */
    imports?: Type<any>[];
    /** 로거 서비스의 Injection Token */
    loggerToken: Symbol | string;
    /** 옵션 팩토리 함수 */
    useFactory: (...args: any[]) => LogtoModuleFactoryOptions | Promise<LogtoModuleFactoryOptions>;
    /** 팩토리 함수에 주입할 의존성 */
    inject?: any[];
}

/**
 * LogtoModule
 *
 * Logto 인증 및 토큰 검증, 클라이언트 기능을 NestJS 모듈로 제공합니다.
 *
 * - **Stateless 모드 (enableClient=false)**:
 *   API 서버와 같이 토큰 검증만 필요한 경우 사용합니다.
 *   `@LogtoProtected()` 가드와 `LogtoTokenVerifier`만 활성화됩니다.
 *
 * - **Stateful 모드 (enableClient=true)**:
 *   로그인/로그아웃 처리가 필요한 웹 애플리케이션이나,
 *   M2M 통신으로 Logto의 User/Role 관리 API를 사용해야 할 경우에 사용합니다.
 *   `OAuthClient`, `LogtoM2MClient` 등 모든 클라이언트 기능이 활성화됩니다.
 */
export class LogtoModule {
    /**
     * forRoot
     *
     * LogtoModule을 초기화합니다. (동기 설정)
     *
     * @param options - 모듈 설정 옵션
     * @returns DynamicModule
     *
     * @example
     * // Stateless 모드: 토큰 검증만 필요한 경우
     * LogtoModule.forRoot({
     *   global: true,
     *   logger: { module: WinstonLoggerModule, token: 'LOGGER' },
     * })
     *
     * @example
     * // Stateful 모드: 클라이언트 기능이 필요한 경우
     * LogtoModule.forRoot({
     *   global: true,
     *   enableClient: true,
     *   logger: { module: WinstonLoggerModule, token: 'LOGGER' },
     * })
     */
    static forRoot(options: LogtoModuleOptions): DynamicModule {
        const { global = false, enableClient = false, logger } = options;

        const baseProviders: Provider[] = [
            {
                provide: LogtoLoggerServiceToken,
                useExisting: logger.token,
            },
            {
                provide: LogtoTokenVerifierToken,
                useFactory: (configService: ConfigService) => {
                    return new LogtoTokenVerifier({
                        jwksUri: configService.getOrThrow<string>('LOGTO_JWKS_URI'),
                        issuer: configService.getOrThrow<string>('LOGTO_AUTH_ISSUER'),
                    });
                },
                inject: [ConfigService],
            },
            {
                provide: LogtoTokenGuardToken,
                useClass: LogtoTokenGuard,
            },
        ];

        const clientProviders: Provider[] = enableClient
            ? [
                {
                    provide: OAuthClientToken,
                    useFactory: (configService: ConfigService, loggerService: LoggerService) => {
                        return new OAuthClient(
                            {
                                endpoint: configService.getOrThrow<string>('LOGTO_AUTH_ENDPOINT'),
                                clientId: configService.getOrThrow<string>('LOGTO_CLIENT_ID'),
                                clientSecret: configService.getOrThrow<string>('LOGTO_CLIENT_SECRET'),
                                resources: [configService.getOrThrow<string>('LOGTO_RESOURCES')],
                                scopes: configService.getOrThrow<string>('LOGTO_SCOPES').split(','),
                                prompt: configService.getOrThrow<string>('LOGTO_PROMPT') as Prompt,
                                redirectUri: configService.getOrThrow<string>('LOGTO_REDIRECT_URI'),
                                signInUri: configService.getOrThrow<string>('LOGTO_SIGN_IN_URI'),
                                dashboardSignInUri: configService.getOrThrow<string>('LOGTO_DASHBOARD_SIGN_IN_URI'),
                            },
                            loggerService,
                        );
                    },
                    inject: [ConfigService, LogtoLoggerServiceToken],
                },
                {
                    provide: LogtoLoginSessionToken,
                    useFactory: (
                        configService: ConfigService,
                        loggerService: LoggerService,
                        oauthClient: OAuthClient,
                    ) => {
                        return new LogtoLoginSession(
                            configService.getOrThrow<string>('LOGTO_M2M_API_URL'),
                            loggerService,
                            oauthClient,
                        );
                    },
                    inject: [ConfigService, LogtoLoggerServiceToken, OAuthClientToken],
                },
                {
                    provide: LogtoM2MClientToken,
                    useFactory: (
                        configService: ConfigService,
                        tokenVerifier: LogtoTokenVerifier,
                        loggerService: LoggerService,
                    ) => {
                        return new LogtoM2MClient(
                            {
                                endpoint: configService.getOrThrow<string>('LOGTO_AUTH_ENDPOINT'),
                                clientId: configService.getOrThrow<string>('LOGTO_M2M_CLIENT_ID'),
                                clientSecret: configService.getOrThrow<string>('LOGTO_M2M_CLIENT_SECRET'),
                                resource: configService.getOrThrow<string>('LOGTO_M2M_RESOURCE'),
                                apiUrl: configService.getOrThrow<string>('LOGTO_M2M_API_URL'),
                                scopes: ['all'],
                            },
                            tokenVerifier,
                            loggerService,
                        );
                    },
                    inject: [ConfigService, LogtoTokenVerifierToken, LogtoLoggerServiceToken],
                },
            ]
            : [];

        const providers = [...baseProviders, ...clientProviders];

        return {
            module: LogtoModule,
            global,
            imports: [logger.module],
            providers,
            exports: providers,
        };
    }

    /**
     * forRootAsync
     *
     * LogtoModule을 초기화합니다. (비동기 설정, ConfigService 활용 가능)
     *
     * @param options - 비동기 모듈 설정 옵션
     * @returns DynamicModule
     *
     * @example
     * LogtoModule.forRootAsync({
     *   global: true,
     *   imports: [WinstonLoggerModule],
     *   loggerToken: 'LOGGER',
     *   useFactory: (configService: ConfigService) => ({
     *     enableClient: configService.get('LOGTO_CLIENT') === 'true',
     *   }),
     *   inject: [ConfigService],
     * })
     */
    static forRootAsync(options: LogtoModuleAsyncOptions): DynamicModule {
        const { global = false, imports = [], loggerToken, useFactory, inject = [] } = options;

        const asyncOptionsProvider: Provider = {
            provide: LOGTO_MODULE_OPTIONS,
            useFactory,
            inject,
        };

        const baseProviders: Provider[] = [
            asyncOptionsProvider,
            {
                provide: LogtoLoggerServiceToken,
                useExisting: loggerToken,
            },
            {
                provide: LogtoTokenVerifierToken,
                useFactory: (configService: ConfigService) => {
                    return new LogtoTokenVerifier({
                        jwksUri: configService.getOrThrow<string>('LOGTO_JWKS_URI'),
                        issuer: configService.getOrThrow<string>('LOGTO_AUTH_ISSUER'),
                    });
                },
                inject: [ConfigService],
            },
            {
                provide: LogtoTokenGuardToken,
                useClass: LogtoTokenGuard,
            },
        ];

        const clientProviders: Provider[] = [
            {
                provide: OAuthClientToken,
                useFactory: (
                    opts: LogtoModuleFactoryOptions,
                    configService: ConfigService,
                    loggerService: LoggerService,
                ) => {
                    if (opts.enableClient) {
                        return new OAuthClient(
                            {
                                endpoint: configService.getOrThrow<string>('LOGTO_AUTH_ENDPOINT'),
                                clientId: configService.getOrThrow<string>('LOGTO_CLIENT_ID'),
                                clientSecret: configService.getOrThrow<string>('LOGTO_CLIENT_SECRET'),
                                resources: [configService.getOrThrow<string>('LOGTO_RESOURCES')],
                                scopes: configService.getOrThrow<string>('LOGTO_SCOPES').split(','),
                                prompt: configService.getOrThrow<string>('LOGTO_PROMPT') as Prompt,
                                redirectUri: configService.getOrThrow<string>('LOGTO_REDIRECT_URI'),
                                signInUri: configService.getOrThrow<string>('LOGTO_SIGN_IN_URI'),
                                dashboardSignInUri: configService.getOrThrow<string>('LOGTO_DASHBOARD_SIGN_IN_URI'),
                            },
                            loggerService,
                        );
                    }
                    return null;
                },
                inject: [LOGTO_MODULE_OPTIONS, ConfigService, LogtoLoggerServiceToken],
            },
            {
                provide: LogtoLoginSessionToken,
                useFactory: (
                    opts: LogtoModuleFactoryOptions,
                    configService: ConfigService,
                    loggerService: LoggerService,
                    oauthClient: OAuthClient,
                ) => {
                    if (opts.enableClient) {
                        return new LogtoLoginSession(
                            configService.getOrThrow<string>('LOGTO_M2M_API_URL'),
                            loggerService,
                            oauthClient,
                        );
                    }
                    return null;
                },
                inject: [LOGTO_MODULE_OPTIONS, ConfigService, LogtoLoggerServiceToken, OAuthClientToken],
            },
            {
                provide: LogtoM2MClientToken,
                useFactory: (
                    opts: LogtoModuleFactoryOptions,
                    configService: ConfigService,
                    tokenVerifier: LogtoTokenVerifier,
                    loggerService: LoggerService,
                ) => {
                    if (opts.enableClient) {
                        return new LogtoM2MClient(
                            {
                                endpoint: configService.getOrThrow<string>('LOGTO_AUTH_ENDPOINT'),
                                clientId: configService.getOrThrow<string>('LOGTO_M2M_CLIENT_ID'),
                                clientSecret: configService.getOrThrow<string>('LOGTO_M2M_CLIENT_SECRET'),
                                resource: configService.getOrThrow<string>('LOGTO_M2M_RESOURCE'),
                                apiUrl: configService.getOrThrow<string>('LOGTO_M2M_API_URL'),
                                scopes: ['all'],
                            },
                            tokenVerifier,
                            loggerService,
                        );
                    }
                    return null;
                },
                inject: [LOGTO_MODULE_OPTIONS, ConfigService, LogtoTokenVerifierToken, LogtoLoggerServiceToken],
            },
        ];

        const providers = [...baseProviders, ...clientProviders];

        return {
            module: LogtoModule,
            global,
            imports: [...imports],
            providers,
            exports: providers,
        };
    }
}
