import { LicenseCache } from './cache.js';
import { LicenseClient } from './client.js';
export class LicenseGate {
    cache;
    client;
    config;
    constructor(config, storagePath) {
        this.config = config;
        this.cache = new LicenseCache(storagePath);
        this.client = new LicenseClient(config.serverUrl, config.licenseKey || '');
    }
    async charge(documentHash, metadata) {
        // No license key -> block processing, require payment first
        if (!this.config.licenseKey) {
            throw new Error('No license key configured. A paid license is required to process files. ' +
                'Add funds and get your license key at https://ocrprovenance.com/dashboard');
        }
        // Step 1: Local charge (deducts from local license server)
        const result = await this.client.charge(documentHash, this.config.machineId, metadata);
        if (!result) {
            // Server unreachable -> hard failure, no offline grace
            throw new Error(`License server unreachable at ${this.config.serverUrl}. Cannot process files without an active license server. ` +
                'Ensure the license server is running and OCR_LICENSE_SERVER is correctly configured.');
        }
        // Step 2: Central verification (deducts from Cloudflare D1 — source of truth)
        // D1 charge routes authenticate via the license_key in the request body
        // (no Bearer token needed — the license key IS the credential)
        const centralUrl = process.env.CHECKOUT_API_URL;
        if (centralUrl) {
            try {
                const centralRes = await fetch(`${centralUrl}/v1/charge/authorize`, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({
                        license_key: this.config.licenseKey,
                        amount_cents: result.cost_cents,
                        document_hash: documentHash,
                        charge_id: result.charge_id,
                    }),
                    signal: AbortSignal.timeout(10000),
                });
                if (centralRes.status === 402) {
                    // Central says insufficient balance — refund local charge and throw
                    await this.client.refund(result.charge_id);
                    const data = await centralRes.json().catch(() => ({}));
                    const { InsufficientBalanceError } = await import('./client.js');
                    throw new InsufficientBalanceError(data.balance_cents ?? 0, data.cost_cents ?? result.cost_cents);
                }
                if (!centralRes.ok) {
                    // Central API error — refund local and fail fast
                    await this.client.refund(result.charge_id);
                    const errText = await centralRes.text().catch(() => '');
                    throw new Error(`Central billing verification failed (${centralRes.status}): ${errText}`);
                }
                // Store the server-signed balance token (signed by Worker's WORKER_AUTH_SECRET,
                // which NEVER exists locally). This token proves the balance was authorized by D1.
                const authData = await centralRes.json().catch(() => ({}));
                if (authData.balance_token) {
                    this.cache.setBalanceToken(authData.balance_token);
                }
                console.error(`[LICENSE] Charge ${result.charge_id} authorized by central billing API (token: ${authData.balance_token ? 'issued' : 'none'})`);
            }
            catch (error) {
                // If it's already an InsufficientBalanceError, re-throw
                if (error instanceof Error && error.name === 'InsufficientBalanceError')
                    throw error;
                // If central API returned a non-ok status, re-throw
                if (error instanceof Error && error.message.includes('Central billing verification failed'))
                    throw error;
                // Network error — refund local charge and fail fast (no offline grace)
                await this.client.refund(result.charge_id);
                throw new Error(`Central billing API unreachable at ${centralUrl}. Cannot process files without central verification. ` +
                    `Error: ${error instanceof Error ? error.message : String(error)}`);
            }
        }
        this.cache.setCachedBalance(result.balance_cents, result.cost_cents);
        return result.charge_id;
    }
    async confirm(chargeId) {
        const balance = await this.client.confirm(chargeId);
        if (balance != null) {
            const cached = this.cache.getCachedBalance();
            this.cache.setCachedBalance(balance, cached.cost_per_file_cents);
        }
    }
    async refund(chargeId) {
        const balance = await this.client.refund(chargeId);
        if (balance != null) {
            const cached = this.cache.getCachedBalance();
            this.cache.setCachedBalance(balance, cached.cost_per_file_cents);
        }
        // Notify central API of refund (best-effort — local refund already done)
        const centralUrl = process.env.CHECKOUT_API_URL;
        if (centralUrl && this.config.licenseKey) {
            try {
                await fetch(`${centralUrl}/v1/charge/refund`, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({
                        license_key: this.config.licenseKey,
                        charge_id: chargeId,
                    }),
                    signal: AbortSignal.timeout(5000),
                });
            }
            catch {
                console.error(`[LICENSE] WARN: Failed to notify central API of refund for charge ${chargeId}`);
            }
        }
    }
    getStatus() {
        if (!this.config.licenseKey) {
            return { balance_cents: 0, cost_per_file_cents: 0, files_remaining: 0 };
        }
        const cached = this.cache.getCachedBalance();
        const costPerFile = cached.cost_per_file_cents;
        return {
            balance_cents: cached.balance_cents,
            cost_per_file_cents: costPerFile,
            files_remaining: costPerFile > 0 ? Math.floor(cached.balance_cents / costPerFile) : 0,
        };
    }
    async getStatusLive() {
        // No license key -> direct user to dashboard
        if (!this.config.licenseKey) {
            return {
                balance_cents: 0,
                cost_per_file_cents: 0,
                files_remaining: 0,
                server_reachable: false,
                warning: 'No license key configured. Get a license key at https://ocrprovenance.com/dashboard to process files.',
            };
        }
        // Fetch live balance from license server.
        // client.balance() returns null for network/timeout — falls through to cached data below.
        // It re-throws InsufficientBalanceError, SpendingLimitError, and auth errors —
        // those must propagate to the caller, not be caught here.
        const balanceInfo = await this.client.balance();
        if (balanceInfo) {
            // Live data available — update cache and return fresh values
            const costPerFile = balanceInfo.cost_per_file_cents;
            this.cache.setCachedBalance(balanceInfo.balance_cents, costPerFile);
            return {
                balance_cents: balanceInfo.balance_cents,
                cost_per_file_cents: costPerFile,
                files_remaining: costPerFile > 0 ? Math.floor(balanceInfo.balance_cents / costPerFile) : 0,
                server_reachable: true,
                spending: {
                    spending_limit_cents: balanceInfo.spending_limit_cents ?? null,
                    spent_this_month_cents: balanceInfo.spent_this_month_cents ?? 0,
                    spending_limit_resets_at: balanceInfo.spending_limit_resets_at ?? null,
                },
            };
        }
        // Server unreachable — return cached data with explicit warning
        const cached = this.cache.getCachedBalance();
        const costPerFile = cached.cost_per_file_cents;
        console.error('[LICENSE] License server unreachable. Returning cached balance:', cached.balance_cents, 'cents. Server URL:', this.config.serverUrl);
        return {
            balance_cents: cached.balance_cents,
            cost_per_file_cents: costPerFile,
            files_remaining: costPerFile > 0 ? Math.floor(cached.balance_cents / costPerFile) : 0,
            server_reachable: false,
            warning: `License server unreachable (${this.config.serverUrl}). Showing cached balance which may be stale.`,
        };
    }
    async syncPending() {
        if (!this.config.licenseKey)
            return;
        const pending = this.cache.getPendingCharges();
        if (pending.length === 0)
            return;
        // Only sync confirmed charges; unconfirmed ones are local failures -- remove without syncing
        const confirmed = pending.filter(p => p.confirmed === 1);
        const unconfirmed = pending.filter(p => p.confirmed !== 1);
        for (const p of unconfirmed)
            this.cache.removePendingCharge(p.id);
        if (confirmed.length === 0)
            return;
        const result = await this.client.sync(confirmed);
        if (result != null) {
            this.cache.setCachedBalance(result.balance_cents, this.cache.getCachedBalance().cost_per_file_cents);
            // Only remove charges that were accepted (server processes in order,
            // so the first N are accepted and the rest are rejected due to insufficient funds)
            const acceptedCount = confirmed.length - result.rejected;
            for (let i = 0; i < acceptedCount; i++) {
                this.cache.removePendingCharge(confirmed[i].id);
            }
        }
    }
    getPendingInfo() {
        const count = this.cache.getPendingCount();
        const cached = this.cache.getCachedBalance();
        return { pending_count: count, pending_amount_cents: count * cached.cost_per_file_cents };
    }
    hasLicenseKey() {
        return !!this.config.licenseKey;
    }
    getServerUrl() {
        return this.config.serverUrl;
    }
    getCostCents() {
        return this.cache.getCachedBalance().cost_per_file_cents;
    }
    async getSpendingInfo() {
        if (!this.config.licenseKey)
            return null;
        const info = await this.client.balance();
        if (!info)
            return null;
        // Update cached balance while we're at it
        this.cache.setCachedBalance(info.balance_cents, info.cost_per_file_cents);
        return {
            spending_limit_cents: info.spending_limit_cents ?? null,
            spent_this_month_cents: info.spent_this_month_cents ?? 0,
            spending_limit_resets_at: info.spending_limit_resets_at ?? null,
        };
    }
    async setSpendingLimit(limitCents) {
        if (!this.config.licenseKey)
            return null;
        return this.client.setSpendingLimit(limitCents);
    }
    close() {
        this.cache.close();
    }
    /**
     * Pre-seed the local cache with balance for integration tests.
     * Uses REAL SQLite cache — not a mock.
     */
    seedCacheForTests(balanceCents) {
        this.cache.setCachedBalance(balanceCents, 3);
    }
}
//# sourceMappingURL=gate.js.map