import { AppMode, WorldRelation } from './generated/runtime/v1/auth.js';
import { ExternalPrincipalType } from './generated/runtime/v1/common.js';
import { AuthorizationPreset, PolicyMode } from './generated/runtime/v1/grant.js';
import { createRuntimeClient } from './core/client.js';
import { asNimiError, createNimiError } from './errors.js';
import { ReasonCode } from '../types/index.js';
const defaultTTLSeconds = 3600;
const refreshSkewMs = 30_000;
const runtimeProtectedScopeCatalogVersion = 'sdk-v2';
function normalizeText(value) {
    return typeof value === 'string' ? value.trim() : '';
}
function timestampFromDate(date) {
    const millis = date.getTime();
    return {
        seconds: String(Math.floor(millis / 1000)),
        nanos: (millis % 1000) * 1_000_000,
    };
}
function normalizeScopes(scopes) {
    return [...new Set(scopes.map((scope) => normalizeText(scope)).filter(Boolean))].sort();
}
function scopeKey(scopes) {
    return normalizeScopes(scopes).join('\n');
}
function isRetryableProtectedAccessError(error) {
    const normalized = asNimiError(error, {
        reasonCode: ReasonCode.RUNTIME_CALL_FAILED,
        actionHint: 'refresh_runtime_protected_access',
        source: 'runtime',
    });
    return normalized.reasonCode === ReasonCode.PRINCIPAL_UNAUTHORIZED
        || normalized.reasonCode === ReasonCode.APP_SCOPE_FORBIDDEN
        || normalized.reasonCode === ReasonCode.APP_TOKEN_EXPIRED
        || normalized.reasonCode === ReasonCode.APP_TOKEN_REVOKED
        || normalized.reasonCode === ReasonCode.APP_GRANT_INVALID;
}
export function createRuntimeProtectedScopeHelper(input) {
    const now = input.now ?? (() => new Date());
    const cache = new Map();
    const inflight = new Map();
    let registerPromise = null;
    let rawRuntimeClient = null;
    const resolveRuntimeAuthClient = () => {
        if (!input.runtime.transport) {
            return {
                auth: input.runtime.auth,
                appAuth: input.runtime.appAuth,
            };
        }
        if (!rawRuntimeClient) {
            rawRuntimeClient = createRuntimeClient({
                appId: input.runtime.appId,
                transport: input.runtime.transport,
            });
        }
        return rawRuntimeClient;
    };
    const ensureSubjectUserID = async () => {
        const subjectUserId = normalizeText(await input.getSubjectUserId());
        if (subjectUserId) {
            return subjectUserId;
        }
        throw createNimiError({
            message: 'runtime protected access requires subject user id',
            reasonCode: ReasonCode.SDK_RUNTIME_APP_AUTH_SUBJECT_USER_ID_REQUIRED,
            actionHint: 'provide_subject_user_id',
            source: 'sdk',
        });
    };
    const ensureRegistered = async () => {
        if (registerPromise) {
            return registerPromise;
        }
        registerPromise = (async () => {
            const response = await resolveRuntimeAuthClient().auth.registerApp({
                appId: input.runtime.appId,
                appInstanceId: `${input.runtime.appId}.runtime-protected-access`,
                deviceId: 'runtime-protected-access',
                appVersion: '1',
                capabilities: [],
                modeManifest: {
                    appMode: AppMode.FULL,
                    runtimeRequired: true,
                    realmRequired: true,
                    worldRelation: WorldRelation.NONE,
                },
            });
            if (!response.accepted) {
                throw createNimiError({
                    message: `runtime protected access registration rejected: ${String(response.reasonCode || 'unknown')}`,
                    reasonCode: ReasonCode.RUNTIME_CALL_FAILED,
                    actionHint: 'register_runtime_app_first',
                    source: 'runtime',
                });
            }
        })();
        try {
            await registerPromise;
        }
        catch (error) {
            registerPromise = null;
            throw error;
        }
    };
    const issueToken = async (scopes) => {
        const normalizedScopes = normalizeScopes(scopes);
        const key = normalizedScopes.join('\n');
        const cached = cache.get(key);
        const nowMs = now().getTime();
        if (cached && cached.expiresAtMs - nowMs > refreshSkewMs) {
            return cached;
        }
        const active = inflight.get(key);
        if (active) {
            return active;
        }
        const next = (async () => {
            await ensureRegistered();
            const issuedAt = now();
            const resourceSelectors = {
                conversationIds: [],
                messageIds: [],
                documentIds: [],
                labels: {},
            };
            const response = await resolveRuntimeAuthClient().appAuth.authorizeExternalPrincipal({
                domain: 'app-auth',
                appId: input.runtime.appId,
                externalPrincipalId: input.runtime.appId,
                externalPrincipalType: ExternalPrincipalType.APP,
                subjectUserId: await ensureSubjectUserID(),
                consentId: 'runtime-protected-access',
                consentVersion: 'v1',
                decisionAt: timestampFromDate(issuedAt),
                policyVersion: 'runtime-protected-access-v1',
                policyMode: PolicyMode.CUSTOM,
                preset: AuthorizationPreset.UNSPECIFIED,
                scopes: normalizedScopes,
                resourceSelectors,
                canDelegate: false,
                maxDelegationDepth: 0,
                ttlSeconds: defaultTTLSeconds,
                scopeCatalogVersion: runtimeProtectedScopeCatalogVersion,
                policyOverride: false,
            }, {
                metadata: {
                    domain: 'app-auth',
                },
            });
            const tokenId = normalizeText(response.tokenId);
            const secret = normalizeText(response.secret);
            if (!tokenId || !secret) {
                throw createNimiError({
                    message: 'runtime protected access token response missing token credentials',
                    reasonCode: ReasonCode.RUNTIME_CALL_FAILED,
                    actionHint: 'check_runtime_app_auth_response',
                    source: 'runtime',
                });
            }
            const expiresAtMs = response.expiresAt
                ? Number(response.expiresAt.seconds) * 1000 + Math.floor(Number(response.expiresAt.nanos || 0) / 1_000_000)
                : issuedAt.getTime() + defaultTTLSeconds * 1000;
            const issued = {
                token: { tokenId, secret },
                expiresAtMs,
            };
            cache.set(key, issued);
            return issued;
        })();
        inflight.set(key, next);
        try {
            return await next;
        }
        finally {
            inflight.delete(key);
        }
    };
    const getCallOptions = async (scopes, baseOptions) => {
        const normalizedScopes = normalizeScopes(scopes);
        if (normalizedScopes.length === 0) {
            return { ...(baseOptions || {}) };
        }
        const issued = await issueToken(normalizedScopes);
        return {
            ...(baseOptions || {}),
            protectedAccessToken: issued.token,
        };
    };
    return {
        async getCallOptions(scopes, baseOptions) {
            return getCallOptions(scopes, baseOptions);
        },
        invalidate(scopes) {
            const key = scopeKey(scopes);
            cache.delete(key);
            inflight.delete(key);
        },
        async withScopes(scopes, operation, baseOptions) {
            const normalizedScopes = normalizeScopes(scopes);
            const key = normalizedScopes.join('\n');
            try {
                return await operation(await getCallOptions(normalizedScopes, baseOptions));
            }
            catch (error) {
                if (!isRetryableProtectedAccessError(error)) {
                    throw error;
                }
                cache.delete(key);
                inflight.delete(key);
                return operation(await getCallOptions(normalizedScopes, baseOptions));
            }
        },
    };
}
//# sourceMappingURL=protected-access.js.map