import { Hono } from 'hono';
import { randomUUID } from 'crypto';
import { query } from '../db.js';
import { licenseKeyAuth } from '../middleware/license-key.js';
import { sessionAuth } from '../middleware/session.js';
import { getSpendingPeriodStart, getSpendingPeriodEnd } from '../spending.js';
import { signBalance, verifyBalance, validatePaymentIntentId } from '../balance-integrity.js';
import { callWorker } from './checkout.js';
/** Server-side cost — single source of truth. Clients MUST use this value. */
const COST_PER_FILE_CENTS = 3;
/**
 * Sync local balance from D1 (source of truth).
 * Called before returning balance to ensure the dashboard shows the current state
 * (including refunds issued by admin via Stripe Dashboard).
 *
 * If D1 is unreachable (network error), logs the error and returns false.
 * The local balance is a cache — D1 charge authorization is what actually controls processing.
 */
export async function syncBalanceFromD1(licenseKeyId, keyHash) {
    let d1Data;
    try {
        d1Data = await callWorker('/v1/balance/sync', {
            key_hash: keyHash,
        });
    }
    catch (err) {
        console.error(`[d1-sync] Failed to sync balance from D1 for key=${licenseKeyId.slice(0, 8)}...: ${err instanceof Error ? err.message : String(err)}`);
        return { synced: false };
    }
    if (!d1Data.registered) {
        return { synced: true, d1Balance: 0 };
    }
    // Update local balance to match D1 (D1 is the source of truth)
    const localKey = query('SELECT id, balance_cents, balance_hmac FROM license_keys WHERE id = ?', [licenseKeyId]);
    if (localKey.rows.length === 0) {
        return { synced: false };
    }
    const localBalance = localKey.rows[0].balance_cents;
    const d1Balance = d1Data.balance_cents;
    if (d1Balance !== localBalance) {
        const hmac = signBalance(licenseKeyId, d1Balance);
        query('UPDATE license_keys SET balance_cents = ?, balance_hmac = ? WHERE id = ?', [d1Balance, hmac, licenseKeyId]);
        console.error(`[d1-sync] Balance synced for key=${licenseKeyId.slice(0, 8)}...: local=${localBalance} -> d1=${d1Balance}`);
    }
    // Store real email from D1 if available
    if (d1Data.email) {
        const userRow = query('SELECT user_id FROM license_keys WHERE id = ?', [licenseKeyId]);
        if (userRow.rows.length > 0) {
            query('UPDATE users SET real_email = ? WHERE id = ?', [d1Data.email, userRow.rows[0].user_id]);
        }
        // Also persist to /data/.account_email for wrapper to sync
        try {
            const { writeFileSync } = await import('fs');
            const emailFile = process.env.OCR_PROVENANCE_DATABASES_PATH
                ? `${process.env.OCR_PROVENANCE_DATABASES_PATH}/.account_email`
                : '/data/.account_email';
            writeFileSync(emailFile, d1Data.email, { mode: 0o600 });
        }
        catch (err) {
            console.error(`[d1-sync] Failed to persist email to file: ${err instanceof Error ? err.message : String(err)}`);
        }
    }
    // Record any new refunds in local payment history
    const userId = query('SELECT user_id FROM license_keys WHERE id = ?', [licenseKeyId]);
    if (userId.rows.length > 0) {
        for (const refund of d1Data.refunds) {
            const refundPaymentId = `refund_${refund.stripe_refund_id}`;
            // Validate the payment ID format (refund_ prefix is allowed)
            if (!validatePaymentIntentId(refundPaymentId)) {
                console.error(`[d1-sync] Skipping refund with invalid ID format: ${refundPaymentId}`);
                continue;
            }
            // Idempotency: skip if already recorded
            const existing = query('SELECT id FROM payments WHERE stripe_payment_intent_id = ?', [refundPaymentId]);
            if (existing.rows.length > 0)
                continue;
            // Record as negative payment for audit trail
            query('INSERT INTO payments (id, user_id, amount_cents, stripe_payment_intent_id) VALUES (?, ?, ?, ?)', [randomUUID(), userId.rows[0].user_id, -refund.amount_cents, refundPaymentId]);
            console.error(`[d1-sync] Recorded refund: ${refund.stripe_refund_id} for -${refund.amount_cents} cents`);
        }
    }
    return { synced: true, d1Balance };
}
const balance = new Hono();
// GET /v1/balance — returns balance, cost per file, and spending info.
// Syncs from D1 first to pick up any admin-issued Stripe refunds.
balance.get('/balance', licenseKeyAuth, async (c) => {
    const licenseKeyId = c.get('licenseKeyId');
    const licenseKeyHash = c.get('licenseKeyHash');
    // Sync balance from D1 before returning — picks up refunds issued via Stripe Dashboard
    await syncBalanceFromD1(licenseKeyId, licenseKeyHash);
    const result = query('SELECT id, balance_cents, balance_hmac, spending_limit_cents, spending_limit_reset_at FROM license_keys WHERE id = ?', [licenseKeyId]);
    if (result.rows.length === 0) {
        return c.json({ error: 'License key not found' }, 404);
    }
    const row = result.rows[0];
    // Verify balance integrity — fail fast on tampered or missing HMAC
    if (!verifyBalance(row.id, row.balance_cents, row.balance_hmac)) {
        console.error(`[FATAL] Balance integrity check failed for key ${row.id}: HMAC mismatch. balance_cents=${row.balance_cents}`);
        return c.json({ error: `Balance integrity check failed for key ${row.id}: HMAC mismatch. Balance may have been tampered with.` }, 403);
    }
    const balanceCents = row.balance_cents;
    // Compute spending this month
    const periodStart = getSpendingPeriodStart(row.spending_limit_reset_at);
    const spentResult = query(`SELECT COALESCE(SUM(amount_cents), 0) as total
     FROM charges
     WHERE license_key_id = ? AND status IN ('pending', 'confirmed') AND created_at >= ?`, [licenseKeyId, periodStart]);
    const spent = spentResult.rows[0].total;
    return c.json({
        balance_cents: balanceCents,
        cost_per_file_cents: COST_PER_FILE_CENTS,
        files_remaining: Math.floor(balanceCents / COST_PER_FILE_CENTS),
        spending_limit_cents: row.spending_limit_cents,
        spent_this_month_cents: spent,
        spending_limit_resets_at: row.spending_limit_cents !== null ? getSpendingPeriodEnd(periodStart) : null,
    });
});
// PUT /v1/spending-limit — set spending limit via license-key auth (used by MCP server)
balance.put('/spending-limit', licenseKeyAuth, async (c) => {
    const licenseKeyId = c.get('licenseKeyId');
    const body = await c.req.json();
    const limitCents = body.limit_cents;
    // Validation: must be null, 0, or positive integer
    if (limitCents !== null && limitCents !== undefined) {
        if (!Number.isInteger(limitCents) || limitCents < 0) {
            return c.json({ error: 'limit_cents must be null, 0, or a positive integer' }, 400);
        }
        if (limitCents > 0 && limitCents < 100) {
            return c.json({ error: 'Minimum non-zero spending limit is 100 ($1.00)' }, 400);
        }
        if (limitCents > 100000) {
            return c.json({ error: 'Maximum spending limit is 100000 ($1,000.00)' }, 400);
        }
    }
    const newLimit = limitCents === undefined ? null : limitCents;
    query('UPDATE license_keys SET spending_limit_cents = ? WHERE id = ?', [newLimit, licenseKeyId]);
    const limitDisplay = newLimit === null
        ? 'No limit'
        : `$${(newLimit / 100).toFixed(2)}/month`;
    return c.json({
        spending_limit_cents: newLimit,
        limit_display: limitDisplay,
    });
});
// ---------------------------------------------------------------------------
// Email-based balance recovery
// ---------------------------------------------------------------------------
/**
 * Consolidate balances from all old keys associated with an email into the current key.
 * Calls the Cloudflare Worker's /v1/balance/consolidate-by-email endpoint.
 */
export async function consolidateByEmail(email, licenseKeyId, keyHash, keyPrefix) {
    const result = await callWorker('/v1/balance/consolidate-by-email', {
        email,
        new_key_hash: keyHash,
        new_key_prefix: keyPrefix,
    });
    if (result.consolidated_balance_cents > 0) {
        // Update local balance to reflect consolidated amount
        const currentKey = query('SELECT id, balance_cents, balance_hmac FROM license_keys WHERE id = ?', [licenseKeyId]);
        if (currentKey.rows.length > 0) {
            const newBalance = currentKey.rows[0].balance_cents + result.consolidated_balance_cents;
            const hmac = signBalance(licenseKeyId, newBalance);
            query('UPDATE license_keys SET balance_cents = ?, balance_hmac = ? WHERE id = ?', [newBalance, hmac, licenseKeyId]);
            console.error(`[consolidate] Recovered ${result.consolidated_balance_cents} cents for email=${email} from ${result.keys_consolidated} old key(s)`);
        }
    }
    return { consolidated: true, amount: result.consolidated_balance_cents };
}
// POST /v1/account/recover-by-email — recover balance from old keys via email.
// SECURITY: Uses sessionAuth (magic link) — the user MUST prove email ownership
// by clicking a magic link sent to their email. The email is read from the
// authenticated session, NOT from user input. This prevents anyone from
// claiming another person's balance by guessing their email.
balance.post('/account/recover-by-email', sessionAuth, async (c) => {
    const userId = c.get('userId');
    const authenticatedEmail = c.get('userEmail');
    if (!authenticatedEmail || !authenticatedEmail.includes('@')) {
        return c.json({ error: 'Session has no valid email. Re-authenticate via magic link.' }, 400);
    }
    // Find the active license key for this user
    const keyRow = query("SELECT id, key_hash, key_prefix FROM license_keys WHERE user_id = ? AND status = 'active' ORDER BY created_at DESC LIMIT 1", [userId]);
    if (keyRow.rows.length === 0) {
        return c.json({ error: 'No active license key found for this account' }, 404);
    }
    const { id: licenseKeyId, key_hash: licenseKeyHash, key_prefix: keyPrefix } = keyRow.rows[0];
    // Also check if the user has a real_email stored — use it for consolidation
    // because the machine-email (auto-xxx@machine...) won't match D1 customers.
    // The authenticated session email may be the machine-email if they logged in
    // via dev token. Use real_email from users table if available, fall back to session email.
    const userRow = query('SELECT real_email FROM users WHERE id = ?', [userId]);
    const recoveryEmail = userRow.rows[0]?.real_email || authenticatedEmail;
    const result = await consolidateByEmail(recoveryEmail, licenseKeyId, licenseKeyHash, keyPrefix);
    // Now sync from D1 to get the authoritative balance
    await syncBalanceFromD1(licenseKeyId, licenseKeyHash);
    const updatedKey = query('SELECT balance_cents FROM license_keys WHERE id = ?', [licenseKeyId]);
    return c.json({
        recovered: result.amount > 0,
        recovered_cents: result.amount,
        current_balance_cents: updatedKey.rows[0]?.balance_cents ?? 0,
        email: recoveryEmail,
    });
});
export default balance;
//# sourceMappingURL=balance.js.map