import { Hono } from 'hono';
import { randomUUID } from 'crypto';
import { query, withTransaction } from '../db.js';
import { getEnv } from '../env.js';
import { sessionAuth } from '../middleware/session.js';
import { licenseKeyAuth } from '../middleware/license-key.js';
import { signBalance, verifyBalance, validatePaymentIntentId } from '../balance-integrity.js';
import { getCheckoutCancelUrl } from '../public-url.js';
const checkout = new Hono();
/**
 * Build the Stripe checkout success_url pointing to the MCP server's callback route.
 * The MCP HTTP server handles the redirect, syncs the payment, then redirects to the dashboard.
 */
export function getCheckoutSuccessUrl() {
    const port = process.env.MCP_HTTP_PORT || '3366';
    return `http://127.0.0.1:${port}/api/checkout/callback?session_id={CHECKOUT_SESSION_ID}`;
}
/** Timeout for Worker fetch calls (15 seconds). */
const WORKER_TIMEOUT_MS = 15_000;
/**
 * Call the Cloudflare Worker checkout proxy.
 * Throws on network errors, timeouts, or non-2xx responses with full context.
 */
export async function callWorker(path, body) {
    const env = getEnv();
    const url = `${env.CHECKOUT_API_URL}${path}`;
    let response;
    try {
        response = await fetch(url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${env.WORKER_AUTH_SECRET}`,
            },
            body: JSON.stringify(body),
            signal: AbortSignal.timeout(WORKER_TIMEOUT_MS),
        });
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        throw new Error(`Checkout Worker unreachable at ${url}: ${message}`);
    }
    const text = await response.text();
    let data;
    try {
        data = JSON.parse(text);
    }
    catch {
        throw new Error(`Checkout Worker returned invalid JSON (status ${response.status}): ${text.slice(0, 200)}`);
    }
    if (!response.ok) {
        const errData = data;
        throw new Error(`Checkout Worker error (${response.status}): ${errData.error ?? text.slice(0, 200)}`);
    }
    return data;
}
/**
 * Credit balance directly — used by the internal/credit endpoint (Worker webhook callback).
 * The Worker has ALREADY verified the Stripe payment before calling this endpoint,
 * so we do NOT call back to the Worker (that would create a circular dependency).
 *
 * Steps:
 * 1. Validate payment_intent_id format
 * 2. Check idempotency (don't double-credit)
 * 3. Insert payment record
 * 4. Credit user's balance with HMAC signing
 */
export function creditBalance(userId, amountCents, paymentIntentId) {
    // Validate payment_intent_id format
    if (!validatePaymentIntentId(paymentIntentId)) {
        console.error(`[FATAL] Rejected payment with unknown payment_intent_id format: ${paymentIntentId}`);
        throw new Error(`Invalid payment_intent_id format: expected prefix pi_, charge_, or refund_ but got "${paymentIntentId.slice(0, 20)}"`);
    }
    // Idempotent: check if already recorded
    const existing = query('SELECT id FROM payments WHERE stripe_payment_intent_id = ?', [paymentIntentId]);
    if (existing.rows.length > 0) {
        const keyResult = query(`SELECT balance_cents FROM license_keys WHERE user_id = ? AND status = 'active'`, [userId]);
        return {
            payment_id: existing.rows[0].id,
            amount_cents: 0,
            balance_cents: keyResult.rows[0]?.balance_cents ?? 0,
            already_synced: true,
        };
    }
    const paymentId = randomUUID();
    const balanceCents = withTransaction(() => {
        query('INSERT INTO payments (id, user_id, amount_cents, stripe_payment_intent_id) VALUES (?, ?, ?, ?)', [paymentId, userId, amountCents, paymentIntentId]);
        // Credit balance and sign for all active keys belonging to this user
        const keyResults = query(`SELECT id, balance_cents, balance_hmac FROM license_keys WHERE user_id = ? AND status = 'active'`, [userId]);
        for (const key of keyResults.rows) {
            // Verify HMAC before crediting — reject tampered balances
            if (!verifyBalance(key.id, key.balance_cents, key.balance_hmac)) {
                console.error(`[FATAL] Balance integrity check failed for key ${key.id} during credit: HMAC mismatch. balance_cents=${key.balance_cents}`);
                throw new Error(`Balance integrity check failed for key ${key.id}: HMAC mismatch. Balance may have been tampered with.`);
            }
            const newBalance = key.balance_cents + amountCents;
            const hmac = signBalance(key.id, newBalance);
            query('UPDATE license_keys SET balance_cents = ?, balance_hmac = ? WHERE id = ?', [newBalance, hmac, key.id]);
        }
        return (keyResults.rows[0]?.balance_cents ?? 0) + amountCents;
    });
    return { payment_id: paymentId, amount_cents: amountCents, balance_cents: balanceCents };
}
/**
 * Sync a completed Stripe checkout session — used by dashboard sync flow and sync-by-key.
 * This flow DOES call the Worker to verify the payment via Stripe API, because the
 * user is clicking "sync" after returning from Stripe checkout (no prior verification).
 * Returns payment info or null if already synced, throws on error.
 */
export async function syncCheckoutPayment(userId, sessionId) {
    // Verify payment via Worker
    const session = await callWorker('/v1/checkout/verify', {
        session_id: sessionId,
    });
    if (session.payment_status !== 'paid') {
        throw new Error(`Payment not completed: ${session.payment_status}`);
    }
    if (!session.payment_intent_id) {
        throw new Error('No payment intent found');
    }
    // Delegate to creditBalance for the actual crediting logic
    return creditBalance(userId, session.amount_cents ?? 0, session.payment_intent_id);
}
// ── License-key authenticated endpoints (for MCP server proxy) ──────────────
// These MUST be defined BEFORE checkout.use('/*', sessionAuth) because Hono
// applies use() middleware to all subsequent routes.
// POST /v1/checkout/by-key — create a checkout session via license key
checkout.post('/by-key', licenseKeyAuth, async (c) => {
    try {
        let body;
        try {
            body = await c.req.json();
        }
        catch {
            return c.json({ error: 'Invalid or missing JSON body. Expected: { "amount_cents": number }' }, 400);
        }
        const amountCents = body.amount_cents;
        if (!amountCents || typeof amountCents !== 'number' || amountCents < 100) {
            return c.json({ error: 'amount_cents must be at least 100 ($1.00)' }, 400);
        }
        const userId = c.get('licenseKeyUserId');
        const keyHash = c.get('licenseKeyHash');
        const userResult = query('SELECT * FROM users WHERE id = ?', [userId]);
        if (userResult.rows.length === 0) {
            return c.json({ error: 'User not found' }, 404);
        }
        const user = userResult.rows[0];
        // No Stripe customer created upfront — user enters their real email on Stripe checkout page
        const session = await callWorker('/v1/checkout/create', {
            amount_cents: amountCents,
            machine_email: user.email,
            user_id: userId,
            license_key_hash: keyHash,
            success_url: getCheckoutSuccessUrl(),
            cancel_url: getCheckoutCancelUrl(c),
        });
        return c.json({ checkout_url: session.checkout_url, session_id: session.session_id });
    }
    catch (error) {
        console.error('[checkout/by-key] Failed to create checkout session:', {
            error: error instanceof Error ? error.message : String(error),
            stack: error instanceof Error ? error.stack : undefined,
        });
        const message = error instanceof Error ? error.message : 'Failed to create checkout session';
        return c.json({ error: message }, 500);
    }
});
// POST /v1/checkout/sync-by-key — sync payment from completed checkout via license key
checkout.post('/sync-by-key', licenseKeyAuth, async (c) => {
    try {
        let body;
        try {
            body = await c.req.json();
        }
        catch {
            return c.json({ error: 'Invalid or missing JSON body. Expected: { "session_id": string }' }, 400);
        }
        if (!body.session_id || typeof body.session_id !== 'string') {
            return c.json({ error: 'session_id is required' }, 400);
        }
        const userId = c.get('licenseKeyUserId');
        const result = await syncCheckoutPayment(userId, body.session_id);
        if (result.already_synced) {
            return c.json({ already_synced: true, balance_cents: result.balance_cents });
        }
        return c.json({
            payment_id: result.payment_id,
            amount_cents: result.amount_cents,
            balance_cents: result.balance_cents,
        });
    }
    catch (error) {
        console.error('[checkout/sync-by-key] Failed to sync payment:', {
            error: error instanceof Error ? error.message : String(error),
            stack: error instanceof Error ? error.stack : undefined,
        });
        const message = error instanceof Error ? error.message : 'Sync failed';
        return c.json({ error: message }, 400);
    }
});
// ── Internal webhook callback (no session/license auth — uses shared secret) ─
// POST /v1/checkout/internal/credit — webhook callback from checkout worker (Stripe payment confirmed)
checkout.post('/internal/credit', async (c) => {
    const webhookSecret = c.req.header('X-Webhook-Secret');
    const env = getEnv();
    if (!webhookSecret || webhookSecret !== env.WEBHOOK_SHARED_SECRET) {
        return c.json({ error: 'Unauthorized: invalid or missing X-Webhook-Secret' }, 401);
    }
    let body;
    try {
        body = await c.req.json();
    }
    catch {
        return c.json({ error: 'Invalid JSON body' }, 400);
    }
    if (!body.user_id || typeof body.user_id !== 'string') {
        return c.json({ error: 'user_id is required (string)' }, 400);
    }
    if (typeof body.amount_cents !== 'number' || body.amount_cents <= 0) {
        return c.json({ error: 'amount_cents must be a positive number' }, 400);
    }
    if (!body.session_id || typeof body.session_id !== 'string') {
        return c.json({ error: 'session_id is required (string)' }, 400);
    }
    if (!body.payment_intent_id || typeof body.payment_intent_id !== 'string') {
        return c.json({ error: 'payment_intent_id is required (string)' }, 400);
    }
    try {
        // Credit directly — the Worker already verified the Stripe payment before calling us.
        // Do NOT call syncCheckoutPayment() here as it calls back to the Worker (circular dependency).
        const result = creditBalance(body.user_id, body.amount_cents, body.payment_intent_id);
        if (result.already_synced) {
            return c.json({ credited: false, already_synced: true, balance_cents: result.balance_cents });
        }
        return c.json({ credited: true, payment_id: result.payment_id, amount_cents: result.amount_cents, balance_cents: result.balance_cents });
    }
    catch (error) {
        console.error('[internal/credit] Failed to credit balance:', {
            user_id: body.user_id,
            payment_intent_id: body.payment_intent_id,
            error: error instanceof Error ? error.message : String(error),
            stack: error instanceof Error ? error.stack : undefined,
        });
        const message = error instanceof Error ? error.message : 'Failed to credit balance';
        return c.json({ error: message }, 500);
    }
});
// ── Session-authenticated endpoints ─────────────────────────────────────────
// All routes below require session auth
checkout.use('/*', sessionAuth);
// POST /v1/checkout/session — create a checkout session
checkout.post('/session', async (c) => {
    try {
        let body;
        try {
            body = await c.req.json();
        }
        catch {
            return c.json({ error: 'Invalid or missing JSON body. Expected: { "amount_cents": number }' }, 400);
        }
        const amountCents = body.amount_cents;
        if (!amountCents || typeof amountCents !== 'number' || amountCents < 100) {
            return c.json({ error: 'amount_cents must be at least 100 ($1.00)' }, 400);
        }
        const userId = c.get('userId');
        const userResult = query('SELECT * FROM users WHERE id = ?', [userId]);
        if (userResult.rows.length === 0) {
            return c.json({ error: 'User not found' }, 404);
        }
        const user = userResult.rows[0];
        // Look up the user's active license key hash for central billing
        const keyResult = query(`SELECT key_hash FROM license_keys WHERE user_id = ? AND status = 'active' LIMIT 1`, [userId]);
        const licenseKeyHash = keyResult.rows[0]?.key_hash ?? '';
        // No Stripe customer created upfront — user enters their real email on Stripe checkout page
        const session = await callWorker('/v1/checkout/create', {
            amount_cents: amountCents,
            machine_email: user.email,
            user_id: userId,
            license_key_hash: licenseKeyHash,
            success_url: getCheckoutSuccessUrl(),
            cancel_url: getCheckoutCancelUrl(c),
        });
        return c.json({ checkout_url: session.checkout_url, session_id: session.session_id });
    }
    catch (error) {
        console.error('[checkout/session] Failed to create checkout session:', {
            error: error instanceof Error ? error.message : String(error),
            stack: error instanceof Error ? error.stack : undefined,
        });
        const message = error instanceof Error ? error.message : 'Failed to create checkout session';
        return c.json({ error: message }, 500);
    }
});
// POST /v1/checkout/sync — sync payment from completed checkout
checkout.post('/sync', async (c) => {
    try {
        let body;
        try {
            body = await c.req.json();
        }
        catch {
            return c.json({ error: 'Invalid or missing JSON body. Expected: { "session_id": string }' }, 400);
        }
        if (!body.session_id || typeof body.session_id !== 'string') {
            return c.json({ error: 'session_id is required' }, 400);
        }
        const userId = c.get('userId');
        const result = await syncCheckoutPayment(userId, body.session_id);
        if (result.already_synced) {
            return c.json({ already_synced: true, balance_cents: result.balance_cents });
        }
        return c.json({
            payment_id: result.payment_id,
            amount_cents: result.amount_cents,
            balance_cents: result.balance_cents,
        });
    }
    catch (error) {
        console.error('[checkout/sync] Failed to sync payment:', {
            error: error instanceof Error ? error.message : String(error),
            stack: error instanceof Error ? error.stack : undefined,
        });
        const message = error instanceof Error ? error.message : 'Sync failed';
        return c.json({ error: message }, 400);
    }
});
export default checkout;
//# sourceMappingURL=checkout.js.map