/**
 * Monthly Verify Scheduler — SDK consumer of PR-C (verify-monthly endpoint).
 *
 * Responsibilities:
 *   1. On first AGENT_TOOL_OBSERVED event of each calendar month per agent,
 *      call POST https://license.my-cc.io/api/v1/license/verify-monthly with
 *      the agent's service JWT (Bearer).
 *   2. Cache the response { valid, licensedPacks, paidThrough,
 *      gracePeriodUntil?, cacheUntil } until `cacheUntil` (start of next
 *      calendar month UTC).
 *   3. Expose getLicensedPacks() so LicensePackLoader can pre-check packs
 *      before hitting the PR-D endpoint.
 *   4. Expose licenseStatus() for diagnostic logging.
 *   5. On verify failure: use last cached state. Never throw, never block.
 *
 * Implements MonthlyVerifyCache interface consumed by LicensePackLoader.
 *
 * Architectural invariants honoured:
 *   Invariant 2  (advisory, never authoritative): failure falls back to cache.
 *   Invariant 10 (push-receive-only): SDK calls out to our endpoint.
 *                We never push into customer infrastructure.
 *
 * Integration gap closed: PR-C endpoint exists + tested but had no SDK caller.
 * This module is that caller.
 *
 * @connexum/ai-governance  Task #32 — SDK consumer PR-C
 */
/* ------------------------------------------------------------------ */
/*  Default production implementation                                  */
/* ------------------------------------------------------------------ */
/**
 * Production postVerifyMonthly — calls the real endpoint.
 * 10-second timeout. Never throws (returns synthetic 503 on error).
 */
export async function defaultPostVerifyMonthly(licenseServerUrl, agentJwt) {
    const url = `${licenseServerUrl.replace(/\/$/, '')}/api/v1/license/verify-monthly`;
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 10_000);
    try {
        const response = await fetch(url, {
            method: 'POST',
            headers: {
                'Authorization': `Bearer ${agentJwt}`,
                'Content-Type': 'application/json',
            },
            signal: controller.signal,
        });
        let body = null;
        try {
            body = await response.json();
        }
        catch {
            body = null;
        }
        return { status: response.status, body };
    }
    catch (err) {
        return {
            status: 503,
            body: {
                error: 'network_error',
                message: err instanceof Error ? err.message : String(err),
            },
        };
    }
    finally {
        clearTimeout(timeoutId);
    }
}
/**
 * Calendar-month key in UTC.
 * Format: "YYYY-MM"
 *
 * Exported for test use — allows tests to verify month-key logic.
 */
export function calendarMonthKey(date = new Date()) {
    return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;
}
/**
 * MonthlyVerifyScheduler — wraps PR-C verify-monthly with calendar-month
 * deduplication and an in-process response cache.
 *
 * Usage:
 *
 * ```ts
 * const scheduler = new MonthlyVerifyScheduler({
 *   licenseServerUrl: 'https://license.my-cc.io',
 *   agentJwt: serviceToken,
 * });
 *
 * // Wire into the SelfRegistrationHook sink:
 * hook.sink = (event) => {
 *   if (event.kind === 'AGENT_TOOL_OBSERVED') {
 *     void scheduler.onToolObserved(event.agentId);
 *   }
 * };
 *
 * // LicensePackLoader reads from the scheduler:
 * const loader = new LicensePackLoader({ ..., monthlyVerifyCache: scheduler });
 * ```
 */
export class MonthlyVerifyScheduler {
    licenseServerUrl;
    agentJwt;
    deps;
    /** Map of agentId → month key of last verify call ("YYYY-MM"). */
    lastVerifiedMonth = new Map();
    /** Cached verify response (shared across all agents in this process). */
    cachedResponse = null;
    /** ISO 8601 timestamp of last successful verify. */
    lastVerifiedAt = null;
    /** In-flight promise guard — prevents duplicate concurrent calls. */
    _inFlightVerify = null;
    constructor(options) {
        this.licenseServerUrl = options.licenseServerUrl;
        this.agentJwt = options.agentJwt;
        this.deps = {
            postVerifyMonthly: options.deps?.postVerifyMonthly ?? defaultPostVerifyMonthly,
        };
    }
    /* ------------------------------------------------------------------
     * MonthlyVerifyCache interface (consumed by LicensePackLoader)
     * ------------------------------------------------------------------ */
    /**
     * Return cached licensedPacks array, or null when the cache is cold.
     * Called synchronously by LicensePackLoader before any network attempt.
     */
    getLicensedPacks() {
        if (!this.cachedResponse)
            return null;
        // Honour cacheUntil — treat stale cache as cold so we re-verify.
        if (new Date(this.cachedResponse.cacheUntil).getTime() <= Date.now())
            return null;
        return this.cachedResponse.licensedPacks;
    }
    /**
     * Diagnostic snapshot of the current license status.
     * Never throws.
     */
    licenseStatus() {
        return {
            valid: this.cachedResponse?.valid ?? null,
            licensedPacks: this.cachedResponse?.licensedPacks ?? null,
            paidThrough: this.cachedResponse?.paidThrough ?? null,
            gracePeriodUntil: this.cachedResponse?.gracePeriodUntil ?? null,
            cacheUntil: this.cachedResponse?.cacheUntil ?? null,
            lastVerifiedAt: this.lastVerifiedAt,
        };
    }
    /* ------------------------------------------------------------------
     * Event trigger
     * ------------------------------------------------------------------ */
    /**
     * Call on first AGENT_TOOL_OBSERVED event of each calendar month.
     * Triggers verify-monthly if this agent has not yet verified this month.
     *
     * Safe to call from every AGENT_TOOL_OBSERVED event — internally
     * deduplicates by calendar month per agent and prevents concurrent calls.
     *
     * Never throws (Invariant 2: fails-soft).
     *
     * @param agentId - The agentId from the SelfRegistrationEvent.
     */
    async onToolObserved(agentId) {
        const currentMonth = calendarMonthKey();
        const lastMonth = this.lastVerifiedMonth.get(agentId);
        // Already verified this month for this agent — no-op.
        if (lastMonth === currentMonth)
            return;
        // Mark the month immediately so concurrent observations from the same
        // agent don't trigger duplicate calls.
        this.lastVerifiedMonth.set(agentId, currentMonth);
        // Deduplicate concurrent verify calls across multiple agents firing
        // in the same process at approximately the same time.
        if (this._inFlightVerify) {
            await this._inFlightVerify;
            return;
        }
        this._inFlightVerify = this._runVerify();
        try {
            await this._inFlightVerify;
        }
        finally {
            this._inFlightVerify = null;
        }
    }
    /* ------------------------------------------------------------------
     * Internal verify call
     * ------------------------------------------------------------------ */
    async _runVerify() {
        let result;
        try {
            result = await this.deps.postVerifyMonthly(this.licenseServerUrl, this.agentJwt);
        }
        catch {
            // Defensive: postVerifyMonthly should never throw.
            // On unexpected error, fall back to cached state (Invariant 2).
            return;
        }
        if (result.status === 200) {
            const body = result.body;
            if (body &&
                typeof body.valid === 'boolean' &&
                Array.isArray(body.licensedPacks) &&
                typeof body.paidThrough === 'string' &&
                typeof body.cacheUntil === 'string') {
                this.cachedResponse = {
                    valid: body.valid,
                    licensedPacks: body.licensedPacks,
                    paidThrough: body.paidThrough,
                    gracePeriodUntil: typeof body.gracePeriodUntil === 'string' ? body.gracePeriodUntil : undefined,
                    cacheUntil: body.cacheUntil,
                };
                this.lastVerifiedAt = new Date().toISOString();
            }
            else {
                // Malformed 200 body — treat as transient failure, keep cached state.
                console.warn('[MonthlyVerifyScheduler] verify-monthly returned 200 but body is malformed — keeping cached state');
            }
            return;
        }
        // Non-200: transient server error or license problem.
        // Invariant 2: use last cached state; never block the agent.
        if (result.status !== 200) {
            const errorBody = result.body;
            const errorMsg = (typeof errorBody?.error === 'string' ? errorBody.error : null) ??
                `HTTP ${result.status}`;
            console.warn(`[MonthlyVerifyScheduler] verify-monthly failed (${errorMsg}) — ` +
                `using cached state (valid=${this.cachedResponse?.valid ?? 'null'})`);
        }
    }
    /* ------------------------------------------------------------------
     * Test helpers
     * ------------------------------------------------------------------ */
    /** Reset all state. Call between tests. */
    __resetForTest() {
        this.lastVerifiedMonth.clear();
        this.cachedResponse = null;
        this.lastVerifiedAt = null;
        this._inFlightVerify = null;
    }
    /**
     * Inject a cached response directly (for test setup without calling onToolObserved).
     * This simulates a state where verify-monthly has already run.
     */
    __setCacheForTest(response, verifiedAt = new Date().toISOString()) {
        this.cachedResponse = response;
        this.lastVerifiedAt = verifiedAt;
    }
}
//# sourceMappingURL=monthly-verify-scheduler.js.map