/**
 * Logto M2M(Machine-to-Machine) 클라이언트
 * - Logto API의 M2M 인증 및 사용자/역할 관리 기능 제공
 * - NestJS DI 시스템에 등록됨
 *
 * @author
 */

import {
    Injectable,
    Global,
    LoggerService,
} from "@nestjs/common";

import {
    LogtoConfig,
    LogtoM2MConfig,
    GrantType,
} from "./config";
import {
    AccessToken,
    LogtoTokenVerifier,
} from "../token";
import {
    LogtoOAuthRESTTemplate,
    LogtoPasswordAlgorithm,
    LogtoPersonalAccessTokenResponse,
    LogtoRole,
    LogtoRoleResponse,
    LogtoUser,
    LogtoUserResponse,
    VerificationMethodType,
} from "./types";
import { p3Values, axiosAdapter } from "point3-common-tool";
import {
    UserMissingRequiredFieldsError,
    PersonalAccessTokenFetchError,
} from "../errors";

// DI 토큰
export const LogtoM2MClientToken = Symbol.for("LogtoM2MClient");

/**
 * LogtoM2MClient
 *
 * Logto M2M(Machine-to-Machine) 인증 및 사용자/역할 관리를 위한 클라이언트 서비스입니다.
 * NestJS DI 환경에서 사용되며, 서버 간 통신 및 자동화된 시스템에서 Logto API를 활용할 때 사용합니다.
 *
 * 주요 역할:
 *   - M2M 인증을 통한 AccessToken 발급 및 관리
 *   - 역할(Role) 생성, 조회, 사용자 역할 할당
 *   - 사용자(User) 생성, 조회, 수정, 정지/해제, 삭제 등 관리
 *   - 인증코드 발송 및 검증, 비밀번호 변경 등 부가 기능 제공
 *
 * 사용 예시:
 *   const client = new LogtoM2MClient(...);
 *   await client.fetchAccessToken();
 *   const roles = await client.getRoles();
 *   const userId = await client.createUser(user);
 *   await client.assignRoleToUser(userId, roleId);
 *   ...
 */
@Global()
@Injectable()
export class LogtoM2MClient {
    private logtoConfig: LogtoConfig;
    private accessToken?: AccessToken;

    // /oidc 엔드포인트용 REST 템플릿
    private readonly authRestTemplate: axiosAdapter.RESTTemplate;
    // /api 엔드포인트용 REST 템플릿
    private readonly apiRestTemplate: axiosAdapter.RESTTemplate;

    constructor(
        private readonly config: LogtoM2MConfig,
        private readonly tokenVerifier: LogtoTokenVerifier,
        private readonly logger: LoggerService,
    ) {
        // config 기반 Logto 설정
        this.logtoConfig = {
            endpoint: config.endpoint,
            appId: config.clientId,
            appSecret: config.clientSecret,
            scopes: config.scopes,
            resources: [config.resource],
            grantType: GrantType.ClientCredentials,
        };

        // 인증용 REST 템플릿 초기화
        this.authRestTemplate = new LogtoOAuthRESTTemplate(
            this.logger,
            this.logtoConfig.endpoint,
        );
        this.authRestTemplate.setBasic(this.logtoConfig.appId, this.logtoConfig.appSecret);

        // API용 REST 템플릿 초기화
        this.apiRestTemplate = new LogtoOAuthRESTTemplate(
            this.logger,
            config.apiUrl,
        );
    }

    // =========================
    // 1. 토큰 관리
    // =========================

    /**
     * AccessToken을 발급받아 저장 및 API 템플릿에 Bearer로 설정
     */
    async fetchAccessToken(): Promise<void> {
        const params = new URLSearchParams();
        params.set('grant_type', this.logtoConfig.grantType);
        params.set('scope', this.logtoConfig.scopes!.join(' '));
        params.set('resource', this.logtoConfig.resources!.join(' '));

        const response = await this.authRestTemplate.post<{
            access_token: string;
            expires_in: number;
        }>('/token', params.toString(), {
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        });

        const { access_token, expires_in } = response.data;
        const payload = await this.tokenVerifier.verifyToken(access_token);

        this.accessToken = new AccessToken(
            payload.sub,
            access_token,
            expires_in,
        );
        this.apiRestTemplate.setBearer(access_token);
    }

    /**
    * 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.authRestTemplate.post<
                {
                    access_token: string;
                }
            >(
                `${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();
        }
    }


    /**
     * 유효한 AccessToken 반환 (만료 시 자동 갱신)
     * @private
     */
    private async getAccessToken(): Promise<string> {
        if (!this.accessToken || this.accessToken.isExpired()) {
            await this.fetchAccessToken();
        }
        return this.accessToken!.token;
    }

    // =========================
    // 2. 역할(Role) 관리
    // =========================

    /**
     * 모든 역할 목록 조회
     */
    async getRoles(): Promise<LogtoRoleResponse[]> {
        await this.getAccessToken();
        const response = await this.apiRestTemplate.get<LogtoRoleResponse[]>('/roles');
        return response.data;
    }

    /**
     * 역할 이름으로 역할 조회
     * @param name 역할 이름
     */
    async getRoleByName(name: string): Promise<LogtoRoleResponse> {
        await this.getAccessToken();
        const params = new URLSearchParams();
        params.set('search.name', name);
        const response = await this.apiRestTemplate.get<LogtoRoleResponse[]>(
            `/roles?${params.toString()}`,
        );
        return response.data[0];
    }

    /**
     * 역할 생성 (이미 존재하면 기존 역할 반환)
     * @param role 역할 정보
     */
    async createRole(role: LogtoRole): Promise<LogtoRoleResponse> {
        await this.getAccessToken();
        const body = {
            name: role.name,
            description: role.description,
            type: role.type,
        };

        const response = await this.apiRestTemplate.post<LogtoRoleResponse>(
            '/roles',
            body,
        );

        if (response instanceof axiosAdapter.ValidationError) {
            if (response.code === 'role.name_in_use') {
                this.logger.error(
                    `이미 존재하는 역할: ${response.code}`,
                    this.constructor.name,
                );
                return this.getRoleByName(role.name);
            }
            throw response;
        }

        return response.data;
    }

    /**
     * 사용자에게 역할 할당
     * @param userId 사용자 ID
     * @param roleId 역할 ID
     */
    async assignRoleToUser(userId: string, roleId: string): Promise<void> {
        await this.getAccessToken();
        const body = { roleIds: [roleId] };
        await this.apiRestTemplate.post(`/users/${userId}/roles`, body);
        this.logger.log(
            `사용자에 역할 할당: ${userId}`,
            this.constructor.name,
        );
    }

    // =========================
    // 3. 사용자(User) 관리
    // =========================

    /**
     * 사용자 생성
     * @param user 사용자 정보
     * @returns 생성된 사용자 ID
     */
    async createUser(user: LogtoUser): Promise<string> {
        await this.getAccessToken();

        if (user.username && user.primaryEmail && user.password && user.name) {
            user.passwordAlgorithm = user.passwordAlgorithm ?? LogtoPasswordAlgorithm.Argon2i;
            const response = await this.apiRestTemplate.post<{ id: string }>('/users', user);
            return response.data.id;
        }

        this.logger.error(`필수 필드 누락`, this.constructor.name);
        throw new UserMissingRequiredFieldsError();
    }

    /**
     * 사용자 customData.clientId 정보 업데이트
     * @param userId 사용자 ID
     * @param clientId 고객사 ID
     */
    async updateUserClientInfo(
        userId: string,
        clientId?: string,
    ): Promise<void> {
        await this.getAccessToken();
        await this.apiRestTemplate.patch(`/users/${userId}`, {
            customData: { clientId },
        });
    }

    /**
     * 사용자 ID로 사용자 정보 조회
     * @param id 사용자 ID
     */
    async getUser(id: string): Promise<LogtoUserResponse> {
        await this.getAccessToken();
        const response = await this.apiRestTemplate.get<LogtoUserResponse>(`/users/${id}`);
        return response.data;
    }


    /**
     * username으로 사용자 단일 조회
     * @param username 사용자명
     */
    async getUserByUsername(username: string): Promise<LogtoUserResponse> {
        await this.getAccessToken();

        const params = new URLSearchParams();
        params.set('search.username', username);
        params.set('mode.username', 'exact');

        const response = await this.apiRestTemplate.get<LogtoUserResponse[]>(
            `/users?${params.toString()}`,
        );
        return response.data[0];
    }

    /**
     * 사용자 정지
     * @param userId 사용자 ID
     */
    async suspendUser(userId: string): Promise<LogtoUserResponse> {
        await this.getAccessToken();
        const response = await this.apiRestTemplate.patch<LogtoUserResponse>(
            `/users/${userId}/is-suspended`,
            { isSuspended: true },
        );
        return response.data;
    }

    /**
     * 사용자 삭제
     * @param userId 사용자 ID
     */
    async deleteUser(userId: string): Promise<void> {
        await this.getAccessToken();
        await this.apiRestTemplate.delete(`/users/${userId}`);
    }

    /**
     * 사용자 역할 삭제
     * @param userId 사용자 ID
     * @param roleId 역할 ID
     */
    async deleteUserRole(userId: string, roleId: string): Promise<void> {
        await this.getAccessToken();
        await this.apiRestTemplate.delete(`/roles/${roleId}/users/${userId}`);
    }

    /**
     * 사용자 정지 해제
     * @param userId 사용자 ID
     */
    async unsuspendUser(userId: string): Promise<LogtoUserResponse> {
        await this.getAccessToken();
        const response = await this.apiRestTemplate.patch<LogtoUserResponse>(
            `/users/${userId}/is-suspended`,
            { isSuspended: false },
        );
        return response.data;
    }

    /**
     * 인증코드 발송 (이메일/휴대폰)
     * @param identifier 이메일 또는 휴대폰
     */
    async sendVerificationCode(
        identifier: p3Values.PhoneNumber | p3Values.Email,
    ): Promise<void> {
        await this.getAccessToken();

        // VerificationMethodType.email/phone은 클래스(static)로 정의되어 있음
        const method =
            identifier instanceof VerificationMethodType.email
                ? "email"
                : "phone";

        await this.apiRestTemplate.post('/verification-codes', {
            [method]: identifier.toString(),
        });
    }

    /**
     * 인증코드 검증
     * @param identifier 이메일 또는 휴대폰
     * @param code 인증코드
     */
    async verifyCode(
        identifier: p3Values.PhoneNumber | p3Values.Email,
        code: string,
    ): Promise<void> {
        await this.getAccessToken();

        const method =
            identifier instanceof VerificationMethodType.email
                ? 'email'
                : 'phone';

        await this.apiRestTemplate.post(`/verification-codes/verify`, {
            [method]: identifier.toString(),
            verificationCode: code,
        });
    }

    /**
     * 사용자 비밀번호 변경
     * @param userId 사용자 ID
     * @param password 새 비밀번호
     */
    async updateUserPassword(userId: string, password: string): Promise<LogtoUserResponse> {
        await this.getAccessToken();
        const response = await this.apiRestTemplate.patch<LogtoUserResponse>(
            `/users/${userId}/password`,
            { password },
        );
        return response.data;
    }
    /**
     * 사용자 Personal Access Token 발급
     * @param userId 사용자 ID
     * @param name 발급할 토큰의 고유 이름
     * @param expiresIn 만료 시간(초). 지정하지 않으면 만료되지 않음
     */
    async addPersonalAccessToken(
        userId: string,
        name: string,
        expiresIn?: number
    ): Promise<LogtoPersonalAccessTokenResponse> {
        await this.getAccessToken();

        const body: Record<string, any> = { name };
        if (expiresIn !== undefined && expiresIn !== null) {
            // expiresIn is in seconds, expiresAt requires epoch time in milliseconds
            body.expiresAt = Date.now() + expiresIn * 1000;
        }

        const response = await this.apiRestTemplate.post<LogtoPersonalAccessTokenResponse>(
            `/users/${userId}/personal-access-tokens`,
            body
        );
        return response.data;
    }

    /**
     * 사용자 Personal Access Token 삭제
     * @param userId 사용자 ID
     * @param name 삭제할 토큰 이름
     */
    async deletePersonalAccessToken(userId: string, name: string): Promise<void> {
        await this.getAccessToken();
        await this.apiRestTemplate.delete(
            `/users/${userId}/personal-access-tokens/${name}`
        );
    }

    /**
     * 사용자의 모든 Personal Access Token 조회
     * @param userId 사용자 ID
     */
    async getPersonalAccessTokens(userId: string): Promise<LogtoPersonalAccessTokenResponse[]> {
        await this.getAccessToken();
        const response = await this.apiRestTemplate.get<LogtoPersonalAccessTokenResponse[]>(
            `/users/${userId}/personal-access-tokens`
        );
        return response.data;
    }
}

/**
 * 국가번호와 휴대폰번호를 합쳐 국제전화번호 형태로 반환
 * @param countryCode 국가번호 (예: '82')
 * @param phoneNumber 휴대폰번호 (예: '01012345678')
 * @returns 국제전화번호 (예: '821012345678')
 */
export function generatePhoneNumberWithCountryCode(countryCode: string, phoneNumber: string): string {
    if (phoneNumber.startsWith('0')) {
        phoneNumber = phoneNumber.slice(1);
    }
    return `${countryCode}${phoneNumber}`;
}
