import * as i0 from '@angular/core';
import { InjectionToken, inject, PLATFORM_ID, signal, computed, Injectable, provideAppInitializer, makeEnvironmentProviders, APP_INITIALIZER, NgModule } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { StringUtilities } from '@sdcorejs/utils/fns';

const SD_AUTHOM_CONFIGURATION = new InjectionToken('sd-authom.configuration');

const STORAGE_KEY_STATE = 'authom_state';
const STORAGE_KEY_CODE_VERIFIER = 'authom_code_verifier';
const STORAGE_KEY_RETURN_TO = 'authom_return_to';
const DEFAULT_SCOPE = 'openid profile email';
const DEFAULT_REFRESH_THRESHOLD_SECONDS = 30;
const DEFAULT_AUTHORIZE_TIMEOUT_SECONDS = 5;
class SdAuthOmService {
    platformId = inject(PLATFORM_ID);
    isBrowser = isPlatformBrowser(this.platformId);
    accessToken = signal(null, ...(ngDevMode ? [{ debugName: "accessToken" }] : []));
    idTokenClaims = signal(null, ...(ngDevMode ? [{ debugName: "idTokenClaims" }] : []));
    isAuthenticated = computed(() => this.accessToken() !== null, ...(ngDevMode ? [{ debugName: "isAuthenticated" }] : []));
    config;
    refreshTimer = null;
    getAccessToken() {
        return this.accessToken();
    }
    base64UrlEncode(bytes) {
        let binary = '';
        for (let i = 0; i < bytes.length; i++)
            binary += String.fromCharCode(bytes[i]);
        return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
    }
    generateCodeVerifier() {
        const bytes = new Uint8Array(32);
        crypto.getRandomValues(bytes);
        return this.base64UrlEncode(bytes);
    }
    async generateCodeChallenge(verifier) {
        return StringUtilities.sha256(verifier);
    }
    buildAuthorizeUrl(params) {
        const search = new URLSearchParams({
            response_type: 'code',
            client_id: this.config.clientId,
            redirect_uri: params.redirectUri,
            state: params.state,
            code_challenge: params.codeChallenge,
            code_challenge_method: 'S256',
            scope: this.config.scope || DEFAULT_SCOPE,
        });
        if (this.config.audience)
            search.set('audience', this.config.audience);
        if (this.config.organization)
            search.set('organization', this.config.organization);
        if (params.prompt)
            search.set('prompt', params.prompt);
        return `https://${this.config.domain}/authorize?${search.toString()}`;
    }
    getDefaultRedirectUri() {
        return this.config.redirectUri || window.location.origin;
    }
    getSilentRedirectUri() {
        return this.config.silentRefreshRedirectUri || `${window.location.origin}/silent-authom.html`;
    }
    decodeJwtPayload(token) {
        try {
            const parts = token.split('.');
            if (parts.length !== 3)
                return null;
            const padded = parts[1].replace(/-/g, '+').replace(/_/g, '/');
            const json = atob(padded + '==='.slice((padded.length + 3) % 4));
            return JSON.parse(json);
        }
        catch {
            return null;
        }
    }
    async login(options) {
        if (!this.isBrowser)
            return;
        const state = crypto.randomUUID();
        const codeVerifier = this.generateCodeVerifier();
        const codeChallenge = await this.generateCodeChallenge(codeVerifier);
        const returnTo = options?.returnTo || (window.location.pathname + window.location.search);
        sessionStorage.setItem(STORAGE_KEY_STATE, state);
        sessionStorage.setItem(STORAGE_KEY_CODE_VERIFIER, codeVerifier);
        sessionStorage.setItem(STORAGE_KEY_RETURN_TO, returnTo);
        window.location.href = this.buildAuthorizeUrl({
            state,
            codeChallenge,
            redirectUri: this.getDefaultRedirectUri(),
        });
    }
    logout(options) {
        if (!this.isBrowser)
            return;
        this.accessToken.set(null);
        this.idTokenClaims.set(null);
        if (this.refreshTimer) {
            clearTimeout(this.refreshTimer);
            this.refreshTimer = null;
        }
        const returnTo = options?.returnTo || window.location.origin;
        const search = new URLSearchParams({
            client_id: this.config.clientId,
            returnTo,
        });
        window.location.href = `https://${this.config.domain}/v2/logout?${search.toString()}`;
    }
    async exchangeCode(code, codeVerifier, redirectUri) {
        try {
            const body = new URLSearchParams({
                grant_type: 'authorization_code',
                code,
                code_verifier: codeVerifier,
                redirect_uri: redirectUri,
                client_id: this.config.clientId,
            });
            const res = await fetch(`https://${this.config.domain}/oauth/token`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: body.toString(),
            });
            if (!res.ok)
                return false;
            const data = await res.json();
            if (!data.access_token)
                return false;
            this.accessToken.set(data.access_token);
            if (data.id_token) {
                this.idTokenClaims.set(this.decodeJwtPayload(data.id_token));
            }
            this.scheduleRefresh(data.access_token);
            return true;
        }
        catch {
            return false;
        }
    }
    scheduleRefresh(token) {
        if (this.refreshTimer) {
            clearTimeout(this.refreshTimer);
            this.refreshTimer = null;
        }
        const claims = this.decodeJwtPayload(token);
        const exp = claims?.['exp'];
        if (typeof exp !== 'number') {
            // @i18n-ignore — dev console warning
            console.warn('[SdAuthOmService] Token không có exp — bỏ qua auto-refresh');
            return;
        }
        const threshold = this.config.refreshThresholdSeconds ?? DEFAULT_REFRESH_THRESHOLD_SECONDS;
        const nowSeconds = Math.floor(Date.now() / 1000);
        const delayMs = Math.max(0, (exp - nowSeconds - threshold) * 1000);
        this.refreshTimer = setTimeout(() => {
            this.silentRefresh();
        }, delayMs);
    }
    silentRefresh() {
        if (!this.isBrowser)
            return Promise.resolve(false);
        return new Promise(async (resolve) => {
            const state = crypto.randomUUID();
            const codeVerifier = this.generateCodeVerifier();
            const codeChallenge = await this.generateCodeChallenge(codeVerifier);
            const redirectUri = this.getSilentRedirectUri();
            const timeoutMs = (this.config.authorizeTimeoutInSeconds ?? DEFAULT_AUTHORIZE_TIMEOUT_SECONDS) * 1000;
            const iframe = document.createElement('iframe');
            iframe.style.display = 'none';
            let done = false;
            const finish = (success) => {
                if (done)
                    return;
                done = true;
                clearTimeout(timer);
                iframe.remove();
                window.removeEventListener('message', onMessage);
                resolve(success);
            };
            const timer = setTimeout(() => finish(false), timeoutMs);
            const onMessage = async (event) => {
                if (event.origin !== window.location.origin)
                    return;
                const data = event.data;
                if (!data || typeof data !== 'object')
                    return;
                if (data.type === 'AUTHOM_SILENT_SUCCESS' && data.code && data.state === state) {
                    const ok = await this.exchangeCode(data.code, codeVerifier, redirectUri);
                    if (!ok) {
                        this.accessToken.set(null);
                        this.idTokenClaims.set(null);
                    }
                    finish(ok);
                }
                else if (data.type === 'AUTHOM_SILENT_ERROR') {
                    this.accessToken.set(null);
                    this.idTokenClaims.set(null);
                    finish(false);
                }
            };
            window.addEventListener('message', onMessage);
            iframe.src = this.buildAuthorizeUrl({
                state,
                codeChallenge,
                redirectUri,
                prompt: 'none',
            });
            document.body.appendChild(iframe);
        });
    }
    async handleCallback() {
        if (!this.isBrowser)
            return false;
        const params = new URLSearchParams(window.location.search);
        const code = params.get('code');
        const state = params.get('state');
        if (!code)
            return false;
        const savedState = sessionStorage.getItem(STORAGE_KEY_STATE);
        const codeVerifier = sessionStorage.getItem(STORAGE_KEY_CODE_VERIFIER);
        const returnTo = sessionStorage.getItem(STORAGE_KEY_RETURN_TO);
        sessionStorage.removeItem(STORAGE_KEY_STATE);
        sessionStorage.removeItem(STORAGE_KEY_CODE_VERIFIER);
        if (state !== savedState || !codeVerifier) {
            window.history.replaceState({}, '', window.location.pathname);
            sessionStorage.removeItem(STORAGE_KEY_RETURN_TO);
            return false;
        }
        const redirectUri = this.getDefaultRedirectUri();
        const ok = await this.exchangeCode(code, codeVerifier, redirectUri);
        if (returnTo) {
            sessionStorage.removeItem(STORAGE_KEY_RETURN_TO);
            window.history.replaceState({}, '', returnTo);
        }
        else {
            window.history.replaceState({}, '', window.location.pathname);
        }
        return ok;
    }
    async init(config) {
        this.config = config;
        if (!this.isBrowser)
            return false;
        const hasCode = new URLSearchParams(window.location.search).has('code');
        if (hasCode) {
            const ok = await this.handleCallback();
            if (ok)
                return true;
        }
        return this.silentRefresh();
    }
    static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: SdAuthOmService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
    static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: SdAuthOmService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: SdAuthOmService, decorators: [{
            type: Injectable,
            args: [{ providedIn: 'root' }]
        }] });

const escapeRegExp = (str) => str.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
const matchGlob = (pattern, url) => {
    const regexBody = pattern.split('*').map(escapeRegExp).join('.*');
    return new RegExp(`^${regexBody}$`).test(url);
};
const SdAuthOmInterceptor = (req, next) => {
    const authom = inject(SdAuthOmService);
    const token = authom.getAccessToken();
    const config = authom.config;
    if (!token || !config)
        return next(req);
    const isSecure = config.secureRoutes?.some(pattern => matchGlob(pattern, req.url));
    if (!isSecure)
        return next(req);
    const authReq = req.clone({
        headers: req.headers.set('Authorization', `Bearer ${token}`),
    });
    return next(authReq);
};

function provideSdAuthOm(options) {
    const providers = [SdAuthOmService];
    if (options.useFactory) {
        providers.push({ provide: SD_AUTHOM_CONFIGURATION, useFactory: options.useFactory, deps: options.deps || [] });
    }
    else if (options.useClass) {
        providers.push({ provide: SD_AUTHOM_CONFIGURATION, useClass: options.useClass });
    }
    providers.push(provideAppInitializer(() => {
        const configLoader = inject(SD_AUTHOM_CONFIGURATION);
        const authom = inject(SdAuthOmService);
        return configLoader.loadTenantConfig().then((config) => authom.init(config));
    }));
    return makeEnvironmentProviders(providers);
}
class SdAuthOmModule {
    static forRoot(options) {
        return {
            ngModule: SdAuthOmModule,
            providers: [
                SdAuthOmService,
                ...(options.useFactory
                    ? [{ provide: SD_AUTHOM_CONFIGURATION, useFactory: options.useFactory, deps: options.deps || [] }]
                    : [{ provide: SD_AUTHOM_CONFIGURATION, useClass: options.useClass }]),
                {
                    provide: APP_INITIALIZER,
                    multi: true,
                    useFactory: () => {
                        const configLoader = inject(SD_AUTHOM_CONFIGURATION);
                        const authom = inject(SdAuthOmService);
                        return () => configLoader.loadTenantConfig().then((config) => authom.init(config));
                    },
                },
            ],
        };
    }
    static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: SdAuthOmModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
    static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.24", ngImport: i0, type: SdAuthOmModule });
    static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: SdAuthOmModule });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: SdAuthOmModule, decorators: [{
            type: NgModule,
            args: [{}]
        }] });

/**
 * Module `authom` — OAuth 2.0 + PKCE authentication client cho AuthOM (Auth0-based).
 *
 * Cách dùng (standalone):
 * ```ts
 * import { provideHttpClient, withInterceptors } from '@angular/common/http';
 * import { provideSdAuthOm, SdAuthOmInterceptor } from '@sdcorejs/angular/modules/authom';
 *
 * export const appConfig: ApplicationConfig = {
 *   providers: [
 *     provideHttpClient(withInterceptors([SdAuthOmInterceptor])),
 *     provideSdAuthOm({
 *       useFactory: () => ({
 *         loadTenantConfig: () => Promise.resolve({
 *           domain: 'login.example.com',
 *           clientId: 'YOUR_CLIENT_ID',
 *           audience: 'https://api.example.com',
 *           organization: 'org_xxx',
 *           scope: 'openid profile email offline_access',
 *           secureRoutes: ['https://api.example.com/*'],
 *         }),
 *       }),
 *     }),
 *   ],
 * };
 * ```
 *
 * Yêu cầu setup thêm:
 * 1. Copy file `silent-authom.html` từ source module này vào thư mục `public/` của app.
 * 2. App phải chạy trên HTTPS (hoặc localhost) — Web Crypto API yêu cầu secure context.
 */

/**
 * Generated bundle index. Do not edit.
 */

export { SD_AUTHOM_CONFIGURATION, SdAuthOmInterceptor, SdAuthOmModule, SdAuthOmService, matchGlob, provideSdAuthOm };
//# sourceMappingURL=sdcorejs-angular-modules-authom.mjs.map
