/**
 * Dashboard Routes — Server-rendered HTML pages
 *
 * All HTML pages for ocrprovenance.com: landing, login, dashboard, account.
 * Uses cookie-based session auth for browser pages.
 *
 * @module routes/dashboard
 */
import { Hono } from 'hono';
import { getCookie, setCookie, deleteCookie } from 'hono/cookie';
import { randomUUID } from 'crypto';
import { query, withTransaction } from '../db.js';
import { getEnv } from '../env.js';
import { verifyToken, hashKey } from '../keys.js';
import { sendMagicLink } from './auth.js';
import { syncCheckoutPayment, callWorker, getCheckoutSuccessUrl } from './checkout.js';
import { getPublicBaseUrl, getCheckoutCancelUrl, isSecureRequest } from '../public-url.js';
import { getSpendingPeriodStart, getSpendingPeriodEnd } from '../spending.js';
import { signBalance } from '../balance-integrity.js';
const dashboard = new Hono();
// ═══════════════════════════════════════════════════════════════════════════════
// HTML LAYOUT
// ═══════════════════════════════════════════════════════════════════════════════
function layout(title, body, user) {
    const nav = user
        ? `<nav>
        <a href="/" class="logo">OCR Provenance</a>
        <div class="nav-right">
          <span class="nav-email">${escapeHtml(user.email)}</span>
          <a href="/dashboard">Dashboard</a>
          <a href="/dashboard/account">Account</a>
          <form method="POST" action="/dashboard/logout" style="display:inline">
            <button type="submit" class="btn-link">Sign out</button>
          </form>
        </div>
      </nav>`
        : `<nav>
        <a href="/" class="logo">OCR Provenance</a>
        <div class="nav-right">
          <a href="/login" class="btn btn-sm">Sign in</a>
        </div>
      </nav>`;
    return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${escapeHtml(title)} - OCR Provenance</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;color:#1a1a2e;background:#f8f9fa;line-height:1.6}
a{color:#4361ee;text-decoration:none}a:hover{text-decoration:underline}
nav{display:flex;align-items:center;justify-content:space-between;padding:1rem 2rem;background:#fff;border-bottom:1px solid #e9ecef;max-width:100%}
.logo{font-weight:700;font-size:1.1rem;color:#1a1a2e}
.nav-right{display:flex;align-items:center;gap:1rem;font-size:0.9rem}
.nav-email{color:#6c757d}
.container{max-width:800px;margin:0 auto;padding:2rem 1.5rem}
.wide{max-width:960px}
h1{font-size:1.8rem;margin-bottom:0.5rem}
h2{font-size:1.3rem;margin-bottom:0.75rem;color:#333}
.subtitle{color:#6c757d;margin-bottom:2rem}
.card{background:#fff;border:1px solid #e9ecef;border-radius:8px;padding:1.5rem;margin-bottom:1.5rem}
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1rem;margin-bottom:1.5rem}
.stat{background:#fff;border:1px solid #e9ecef;border-radius:8px;padding:1.25rem;text-align:center}
.stat-value{font-size:1.6rem;font-weight:700;color:#4361ee}
.stat-label{font-size:0.85rem;color:#6c757d;margin-top:0.25rem}
table{width:100%;border-collapse:collapse;font-size:0.9rem}
th{text-align:left;padding:0.6rem 0.75rem;border-bottom:2px solid #dee2e6;font-weight:600;color:#495057}
td{padding:0.6rem 0.75rem;border-bottom:1px solid #f0f0f0}
.badge{display:inline-block;padding:0.15rem 0.5rem;border-radius:10px;font-size:0.75rem;font-weight:600}
.badge-confirmed{background:#d4edda;color:#155724}
.badge-pending{background:#fff3cd;color:#856404}
.badge-refunded{background:#f8d7da;color:#721c24}
.badge-expired{background:#e2e3e5;color:#383d41}
.btn{display:inline-block;padding:0.6rem 1.2rem;border-radius:6px;font-weight:600;font-size:0.9rem;border:none;cursor:pointer;text-align:center}
.btn-primary{background:#4361ee;color:#fff}.btn-primary:hover{background:#3a56d4;text-decoration:none}
.btn-sm{padding:0.4rem 0.8rem;font-size:0.85rem}
.btn-outline{background:transparent;border:2px solid #4361ee;color:#4361ee}.btn-outline:hover{background:#4361ee;color:#fff;text-decoration:none}
.btn-danger{background:#dc3545;color:#fff}.btn-danger:hover{background:#c82333}
.btn-link{background:none;border:none;color:#4361ee;cursor:pointer;font-size:inherit;padding:0}
.btn-link:hover{text-decoration:underline}
.btn-group{display:flex;gap:0.75rem;flex-wrap:wrap}
.form-group{margin-bottom:1rem}
.form-group label{display:block;font-weight:600;margin-bottom:0.3rem;font-size:0.9rem}
.form-group input{width:100%;padding:0.6rem 0.8rem;border:1px solid #ced4da;border-radius:6px;font-size:0.95rem}
.form-group input:focus{outline:none;border-color:#4361ee;box-shadow:0 0 0 2px rgba(67,97,238,0.15)}
.alert{padding:1rem 1.25rem;border-radius:6px;margin-bottom:1.5rem;font-size:0.9rem}
.alert-success{background:#d4edda;color:#155724;border:1px solid #c3e6cb}
.alert-info{background:#d1ecf1;color:#0c5460;border:1px solid #bee5eb}
.alert-warning{background:#fff3cd;color:#856404;border:1px solid #ffeeba}
.alert-danger{background:#f8d7da;color:#721c24;border:1px solid #f5c6cb}
.hero{text-align:center;padding:4rem 1rem;background:linear-gradient(135deg,#1a1a2e 0%,#16213e 50%,#0f3460 100%);color:#fff;border-radius:12px;margin-bottom:2rem}
.hero h1{font-size:2.4rem;margin-bottom:0.5rem;color:#fff}
.hero .subtitle{color:rgba(255,255,255,0.8)}
.hero .btn-primary{background:#4cc9f0;color:#1a1a2e;font-size:1.05rem;padding:0.75rem 2rem}
.hero .btn-primary:hover{background:#3ab4d9}
.features{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:1.5rem;margin-bottom:2rem}
.feature{background:#fff;border:1px solid #e9ecef;border-radius:8px;padding:1.5rem}
.feature h3{margin-bottom:0.5rem}
.steps{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1.5rem;margin-bottom:2rem}
.step{text-align:center;padding:1.5rem}
.step-number{display:inline-block;width:2rem;height:2rem;border-radius:50%;background:#4361ee;color:#fff;font-weight:700;line-height:2rem;margin-bottom:0.75rem}
.pricing{text-align:center;padding:2rem;background:#fff;border:2px solid #4361ee;border-radius:12px;margin-bottom:2rem}
.pricing .price{font-size:2rem;font-weight:700;color:#4361ee}
code{background:#f0f0f0;padding:0.15rem 0.4rem;border-radius:4px;font-size:0.9em}
pre{background:#1a1a2e;color:#e9ecef;padding:1rem;border-radius:8px;overflow-x:auto;font-size:0.85rem;margin:1rem 0}
.text-muted{color:#6c757d}
.text-center{text-align:center}
.mt-1{margin-top:0.5rem}.mt-2{margin-top:1rem}.mb-2{margin-bottom:1rem}
footer{text-align:center;padding:2rem;color:#6c757d;font-size:0.85rem;border-top:1px solid #e9ecef;margin-top:3rem}
</style>
</head>
<body>
${nav}
<div class="container${title === 'Home' ? ' wide' : ''}">
${body}
</div>
<footer>OCR Provenance &copy; ${new Date().getFullYear()} &mdash; Document processing with full provenance tracking</footer>
</body>
</html>`;
}
function escapeHtml(str) {
    return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function statusBadge(status) {
    let cls;
    if (status === 'confirmed')
        cls = 'confirmed';
    else if (status === 'pending')
        cls = 'pending';
    else if (status === 'refunded')
        cls = 'refunded';
    else
        cls = 'expired';
    return `<span class="badge badge-${cls}">${escapeHtml(status)}</span>`;
}
// ═══════════════════════════════════════════════════════════════════════════════
// COOKIE SESSION MIDDLEWARE
// ═══════════════════════════════════════════════════════════════════════════════
async function dashboardSession(c, next) {
    const token = getCookie(c, 'session');
    if (!token) {
        return c.redirect('/login');
    }
    const result = query(`SELECT s.token, s.user_id, s.expires_at, u.email
     FROM sessions s JOIN users u ON s.user_id = u.id
     WHERE s.token = ? AND s.expires_at > datetime('now')`, [token]);
    if (result.rows.length === 0) {
        deleteCookie(c, 'session');
        return c.redirect('/login');
    }
    const session = result.rows[0];
    c.set('userId', session.user_id);
    c.set('userEmail', session.email);
    c.set('sessionToken', session.token);
    await next();
}
// ═══════════════════════════════════════════════════════════════════════════════
// PUBLIC PAGES
// ═══════════════════════════════════════════════════════════════════════════════
// GET / — Sales Landing Page
dashboard.get('/', (c) => {
    const html = layout('Home', `
    <div class="hero">
      <h1>OCR Provenance</h1>
      <p class="subtitle">Document processing with full provenance tracking. OCR, chunking, embeddings, image extraction, and VLM descriptions &mdash; all running locally on your GPU.</p>
      <div style="margin-top:1.5rem">
        <a href="/login" class="btn btn-primary" style="margin-right:0.75rem">Get Started</a>
        <a href="https://www.npmjs.com/package/ocr-provenance-mcp" class="btn btn-outline" style="color:#fff;border-color:rgba(255,255,255,0.5)">View on npm</a>
      </div>
    </div>

    <h2 class="text-center mb-2">What it does</h2>
    <div class="features">
      <div class="feature">
        <h3>OCR Processing</h3>
        <p>Extract text from PDFs, DOCX, XLSX with high accuracy using Marker-pdf. Full page-level quality scoring.</p>
      </div>
      <div class="feature">
        <h3>Smart Chunking</h3>
        <p>Semantic chunking with overlap, metadata preservation, and context-aware splitting for optimal retrieval.</p>
      </div>
      <div class="feature">
        <h3>Local Embeddings</h3>
        <p>Generate embeddings with nomic-embed-text-v1.5 on your GPU. 768-dimensional vectors with sqlite-vec search.</p>
      </div>
      <div class="feature">
        <h3>Image Extraction</h3>
        <p>Extract and catalog every image from your documents. VLM descriptions for visual content understanding.</p>
      </div>
      <div class="feature">
        <h3>Provenance Chains</h3>
        <p>Full chain-of-custody: document &rarr; OCR &rarr; chunks &rarr; embeddings. SHA-256 hashes, W3C PROV export.</p>
      </div>
      <div class="feature">
        <h3>MCP Native</h3>
        <p>149 MCP tools for Claude Code, Claude Desktop, and any MCP-compatible AI client. Full automation.</p>
      </div>
    </div>

    <h2 class="text-center mb-2">How it works</h2>
    <div class="steps">
      <div class="step">
        <div class="step-number">1</div>
        <h3>Install</h3>
        <pre>npm install -g ocr-provenance-mcp</pre>
      </div>
      <div class="step">
        <div class="step-number">2</div>
        <h3>Configure</h3>
        <p>Add to your Claude Code or Claude Desktop MCP config. Set your license key.</p>
      </div>
      <div class="step">
        <div class="step-number">3</div>
        <h3>Process</h3>
        <p>Ingest documents, search, extract, and analyze with full provenance tracking.</p>
      </div>
    </div>

    <div class="pricing">
      <div class="price">$0.03 / file</div>
      <p class="mt-1 text-muted">First 10 files free. No subscription required.</p>
      <p class="mt-1 text-muted">Pay as you go &mdash; add funds anytime from your dashboard.</p>
      <a href="/login" class="btn btn-primary mt-2" style="display:inline-block;margin-top:1rem">Get Started Free</a>
    </div>
  `);
    return c.html(html);
});
// GET /login — Login Page
dashboard.get('/login', (c) => {
    const error = c.req.query('error');
    const html = layout('Sign In', `
    <div style="max-width:400px;margin:3rem auto">
      <h1>Sign in</h1>
      <p class="subtitle">Enter your email to receive a magic link.</p>
      ${error ? `<div class="alert alert-danger">${escapeHtml(error)}</div>` : ''}
      <form method="POST" action="/login">
        <div class="form-group">
          <label for="email">Email</label>
          <input type="email" id="email" name="email" required placeholder="you@example.com" autofocus>
        </div>
        <button type="submit" class="btn btn-primary" style="width:100%">Send login link</button>
      </form>
    </div>
  `);
    return c.html(html);
});
// POST /login — Handle Login
dashboard.post('/login', async (c) => {
    const body = await c.req.parseBody();
    const email = typeof body.email === 'string' ? body.email.toLowerCase().trim() : '';
    if (!email) {
        return c.redirect('/login?error=Email+is+required');
    }
    const result = await sendMagicLink(email, getPublicBaseUrl(c));
    if (result.error) {
        return c.redirect(`/login?error=${encodeURIComponent(result.error)}`);
    }
    if (result.dev_token) {
        const verifyUrl = `${getPublicBaseUrl(c)}/auth/verify?token=${encodeURIComponent(result.dev_token)}`;
        const html = layout('Check Email', `
      <div style="max-width:500px;margin:3rem auto">
        <h1>Check your email</h1>
        <p class="subtitle">We sent a login link to <strong>${escapeHtml(email)}</strong></p>
        <div class="alert alert-info">
          <strong>Dev mode:</strong> Email sending is not configured.
          <a href="${escapeHtml(verifyUrl)}">Click here to sign in</a>.
        </div>
      </div>
    `);
        return c.html(html);
    }
    return c.redirect('/login/sent');
});
// GET /login/sent — Check Email Page
dashboard.get('/login/sent', (c) => {
    const html = layout('Check Email', `
    <div style="max-width:500px;margin:3rem auto;text-align:center">
      <h1>Check your email</h1>
      <p class="subtitle">We sent you a magic link. Click it to sign in.</p>
      <p class="text-muted">The link expires in 10 minutes. Check your spam folder if you don't see it.</p>
    </div>
  `);
    return c.html(html);
});
// GET /auth/verify — Verify Magic Link
dashboard.get('/auth/verify', async (c) => {
    const token = c.req.query('token');
    if (!token) {
        return c.redirect('/login?error=Missing+token');
    }
    const env = getEnv();
    const payload = verifyToken(token, env.SESSION_SECRET);
    if (!payload) {
        return c.redirect('/login?error=Invalid+or+expired+link');
    }
    // Verify user exists
    const userResult = query('SELECT id, email FROM users WHERE id = ?', [payload.userId]);
    if (userResult.rows.length === 0) {
        return c.redirect('/login?error=User+not+found');
    }
    const user = userResult.rows[0];
    // Ensure user has a license key
    const existingKey = query(`SELECT id FROM license_keys WHERE user_id = ? AND status = 'active'`, [user.id]);
    if (existingKey.rows.length === 0) {
        const rawKey = `lk_${randomUUID().replace(/-/g, '')}`;
        const hash = hashKey(rawKey);
        const prefix = rawKey.slice(0, 8);
        query('INSERT INTO license_keys (id, user_id, key_hash, key_prefix) VALUES (?, ?, ?, ?)', [randomUUID(), user.id, hash, prefix]);
    }
    // Create session (30-day expiry)
    const sessionToken = randomUUID();
    query(`INSERT INTO sessions (token, user_id, expires_at)
     VALUES (?, ?, datetime('now', '+30 days'))`, [sessionToken, user.id]);
    // Set HTTP-only cookie — detect HTTPS from the actual request, not the env var
    setCookie(c, 'session', sessionToken, {
        httpOnly: true,
        secure: isSecureRequest(c),
        sameSite: 'Lax',
        path: '/',
        maxAge: 30 * 24 * 60 * 60, // 30 days
    });
    return c.redirect('/dashboard');
});
// ═══════════════════════════════════════════════════════════════════════════════
// AUTHENTICATED PAGES
// ═══════════════════════════════════════════════════════════════════════════════
// GET /dashboard — Main Dashboard
dashboard.get('/dashboard', dashboardSession, async (c) => {
    const userId = c.get('userId');
    const userEmail = c.get('userEmail');
    // Handle checkout success callback
    const checkoutSuccess = c.req.query('checkout');
    const sessionId = c.req.query('session_id');
    let checkoutAlert = '';
    if (checkoutSuccess === 'success' && sessionId) {
        try {
            const result = await syncCheckoutPayment(userId, sessionId);
            if (result.already_synced) {
                checkoutAlert = '<div class="alert alert-info">Payment already recorded.</div>';
            }
            else {
                const dollars = (result.amount_cents / 100).toFixed(2);
                checkoutAlert = `<div class="alert alert-success">Payment of $${dollars} received! Your balance has been updated.</div>`;
            }
        }
        catch {
            checkoutAlert = `<div class="alert alert-warning">Could not sync payment. Please try refreshing.</div>`;
        }
    }
    else if (checkoutSuccess === 'cancelled') {
        checkoutAlert = '<div class="alert alert-info">Checkout was cancelled.</div>';
    }
    // Handle error redirects from checkout failures
    const errorParam = c.req.query('error');
    if (errorParam) {
        checkoutAlert = `<div class="alert alert-danger">${escapeHtml(errorParam)}</div>`;
    }
    // Fetch data
    const keyResult = query(`SELECT id, key_prefix, balance_cents, spending_limit_cents, spending_limit_reset_at, created_at FROM license_keys WHERE user_id = ? AND status = 'active'`, [userId]);
    const key = keyResult.rows[0];
    const balance = key?.balance_cents ?? 0;
    const filesRemaining = Math.floor(balance / 3);
    // Total confirmed charges
    let totalCharges = 0;
    if (key) {
        const countResult = query("SELECT COUNT(*) as c FROM charges WHERE license_key_id = ? AND status = 'confirmed'", [key.id]);
        totalCharges = countResult.rows[0].c;
    }
    // Spending data
    let spentThisMonth = 0;
    let spendingLimit = null;
    if (key) {
        spendingLimit = key.spending_limit_cents;
        const periodStart = getSpendingPeriodStart(key.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 >= ?`, [key.id, periodStart]);
        spentThisMonth = spentResult.rows[0].total;
    }
    // Recent charges
    let recentCharges = [];
    if (key) {
        const chargesResult = query('SELECT id, amount_cents, document_hash, status, file_type, created_at FROM charges WHERE license_key_id = ? ORDER BY created_at DESC LIMIT 20', [key.id]);
        recentCharges = chargesResult.rows;
    }
    // Recent payments
    const paymentsResult = query('SELECT id, amount_cents, stripe_payment_intent_id, created_at FROM payments WHERE user_id = ? ORDER BY created_at DESC LIMIT 20', [userId]);
    const recentPayments = paymentsResult.rows;
    // Charges table
    const chargesRows = recentCharges.length > 0
        ? recentCharges.map(ch => `
        <tr>
          <td>${escapeHtml(new Date(ch.created_at).toLocaleDateString())}</td>
          <td>${ch.file_type ? escapeHtml(ch.file_type) : '<span class="text-muted">-</span>'}</td>
          <td>$${(ch.amount_cents / 100).toFixed(2)}</td>
          <td>${statusBadge(ch.status)}</td>
        </tr>`).join('')
        : '<tr><td colspan="4" class="text-center text-muted">No charges yet</td></tr>';
    // Payments table
    const paymentsRows = recentPayments.length > 0
        ? recentPayments.map(p => `
        <tr>
          <td>${escapeHtml(new Date(p.created_at).toLocaleDateString())}</td>
          <td>$${(p.amount_cents / 100).toFixed(2)}</td>
          <td class="text-muted">${escapeHtml(p.stripe_payment_intent_id.slice(0, 20))}...</td>
        </tr>`).join('')
        : '<tr><td colspan="3" class="text-center text-muted">No payments yet</td></tr>';
    const html = layout('Dashboard', `
    <h1>Dashboard</h1>
    <p class="subtitle">Manage your OCR Provenance account</p>

    ${checkoutAlert}

    <div class="stats">
      <div class="stat">
        <div class="stat-value">$${(balance / 100).toFixed(2)}</div>
        <div class="stat-label">Balance</div>
      </div>
      <div class="stat">
        <div class="stat-value">${filesRemaining.toLocaleString()}</div>
        <div class="stat-label">Files Remaining</div>
      </div>
      <div class="stat">
        <div class="stat-value">${totalCharges.toLocaleString()}</div>
        <div class="stat-label">Files Processed</div>
      </div>
      ${spendingLimit !== null ? `
      <div class="stat">
        <div class="stat-value">$${(spentThisMonth / 100).toFixed(2)} / $${(spendingLimit / 100).toFixed(2)}</div>
        <div class="stat-label">This Month</div>
      </div>` : ''}
    </div>

    ${spendingLimit !== null && spendingLimit > 0 && spentThisMonth >= spendingLimit * 0.8 ? `
    <div class="alert ${spentThisMonth >= spendingLimit ? 'alert-danger' : 'alert-warning'}">
      ${spentThisMonth >= spendingLimit
        ? `You've reached your $${(spendingLimit / 100).toFixed(2)} monthly spending limit. <a href="/dashboard/account">Manage limit</a>.`
        : `You've used ${Math.round((spentThisMonth / spendingLimit) * 100)}% of your $${(spendingLimit / 100).toFixed(2)} monthly spending limit. <a href="/dashboard/account">Manage limit</a>.`}
    </div>` : ''}

    <div class="card">
      <h2>Add Funds</h2>
      <p class="text-muted mb-2">Each file costs $0.03 to process. Select an amount or enter a custom value.</p>
      <div class="btn-group" style="margin-bottom:1rem">
        <form method="POST" action="/dashboard/checkout"><input type="hidden" name="amount" value="500"><button type="submit" class="btn btn-primary">$5 <small>(166 files)</small></button></form>
        <form method="POST" action="/dashboard/checkout"><input type="hidden" name="amount" value="1000"><button type="submit" class="btn btn-primary">$10 <small>(333 files)</small></button></form>
        <form method="POST" action="/dashboard/checkout"><input type="hidden" name="amount" value="2500"><button type="submit" class="btn btn-primary">$25 <small>(833 files)</small></button></form>
        <form method="POST" action="/dashboard/checkout"><input type="hidden" name="amount" value="5000"><button type="submit" class="btn btn-primary">$50 <small>(1,666 files)</small></button></form>
      </div>
      <form method="POST" action="/dashboard/checkout" id="custom-amount-form" style="display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap">
        <input type="hidden" name="amount" id="custom-amount-cents" value="">
        <div style="display:flex;align-items:center;gap:0.25rem">
          <span style="font-weight:600;font-size:1.1rem">$</span>
          <input type="number" id="custom-amount-dollars" min="1" step="0.01" placeholder="Custom amount"
                 style="width:160px;padding:0.5rem 0.75rem;border:1px solid #ced4da;border-radius:6px;font-size:0.95rem">
        </div>
        <button type="submit" class="btn btn-outline">Add Custom Amount</button>
      </form>
      <script>
        document.getElementById('custom-amount-form').addEventListener('submit', function(e) {
          var dollars = parseFloat(document.getElementById('custom-amount-dollars').value);
          if (isNaN(dollars) || dollars < 1) {
            e.preventDefault();
            alert('Minimum amount is $1.00');
            return;
          }
          var cents = Math.round(dollars * 100);
          document.getElementById('custom-amount-cents').value = cents;
        });
      </script>
    </div>

    <div class="card">
      <h2>Recent Charges</h2>
      <table>
        <thead><tr><th>Date</th><th>File Type</th><th>Amount</th><th>Status</th></tr></thead>
        <tbody>${chargesRows}</tbody>
      </table>
    </div>

    <div class="card">
      <h2>Payment History</h2>
      <table>
        <thead><tr><th>Date</th><th>Amount</th><th>Stripe ID</th></tr></thead>
        <tbody>${paymentsRows}</tbody>
      </table>
    </div>
  `, { email: userEmail });
    return c.html(html);
});
// POST /dashboard/checkout — Create Checkout Session via Worker Proxy
dashboard.post('/dashboard/checkout', dashboardSession, async (c) => {
    const userId = c.get('userId');
    const body = await c.req.parseBody();
    const amountCents = parseInt(typeof body.amount === 'string' ? body.amount : '0', 10);
    if (!amountCents || amountCents < 100) {
        return c.redirect('/dashboard?error=' + encodeURIComponent('Amount must be at least $1.00'));
    }
    try {
        const userResult = query('SELECT * FROM users WHERE id = ?', [userId]);
        if (userResult.rows.length === 0) {
            return c.redirect('/dashboard?error=' + encodeURIComponent('User not found'));
        }
        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),
        });
        if (session.checkout_url) {
            return c.redirect(session.checkout_url);
        }
        return c.redirect('/dashboard?error=' + encodeURIComponent('Failed to create checkout session'));
    }
    catch (error) {
        console.error('[dashboard/checkout] Checkout failed:', {
            error: error instanceof Error ? error.message : String(error),
            stack: error instanceof Error ? error.stack : undefined,
            userId,
            amountCents,
        });
        return c.redirect('/dashboard?error=' + encodeURIComponent('Payment processing failed. Please try again.'));
    }
});
// GET /dashboard/account — Account Management
dashboard.get('/dashboard/account', dashboardSession, async (c) => {
    const userId = c.get('userId');
    const userEmail = c.get('userEmail');
    const userResult = query('SELECT * FROM users WHERE id = ?', [userId]);
    const user = userResult.rows[0];
    const keyResult = query(`SELECT id, key_prefix, balance_cents, spending_limit_cents, spending_limit_reset_at, status, created_at FROM license_keys WHERE user_id = ? AND status = 'active'`, [userId]);
    const key = keyResult.rows[0];
    // Spending data
    let acctSpent = 0;
    let acctSpendingResets = '';
    if (key) {
        const periodStart = getSpendingPeriodStart(key.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 >= ?`, [key.id, periodStart]);
        acctSpent = spentResult.rows[0].total;
        acctSpendingResets = getSpendingPeriodEnd(periodStart);
    }
    const acctLimit = key?.spending_limit_cents ?? null;
    const acctLimitDisplay = acctLimit === null ? 'No limit' : acctLimit === 0 ? 'Paused ($0.00)' : `$${(acctLimit / 100).toFixed(2)}/month`;
    const acctSpentDisplay = `$${(acctSpent / 100).toFixed(2)}`;
    const acctRemainingDisplay = acctLimit !== null && acctLimit > 0
        ? `$${(Math.max(0, acctLimit - acctSpent) / 100).toFixed(2)}`
        : 'Unlimited';
    const acctFilesUsed = Math.floor(acctSpent / 3);
    const successParam = c.req.query('success');
    const limitAlert = successParam === 'limit-set'
        ? '<div class="alert alert-success">Spending limit updated.</div>'
        : successParam === 'limit-reset'
            ? '<div class="alert alert-success">Monthly spending counter has been reset.</div>'
            : '';
    const html = layout('Account', `
    <h1>Account</h1>
    <p class="subtitle">Manage your account settings</p>

    ${limitAlert}

    <div class="card">
      <h2>Profile</h2>
      <table>
        <tr><td style="width:40%;font-weight:600">Email</td><td>${escapeHtml(user.email)}</td></tr>
        <tr><td style="font-weight:600">Member since</td><td>${escapeHtml(new Date(user.created_at).toLocaleDateString())}</td></tr>
        ${user.stripe_customer_id ? `<tr><td style="font-weight:600">Stripe Customer</td><td class="text-muted">${escapeHtml(user.stripe_customer_id)}</td></tr>` : ''}
      </table>
    </div>

    <div class="card">
      <h2>Spending Limit</h2>
      <table>
        <tr><td style="width:40%;font-weight:600">Current limit</td><td>${acctLimitDisplay}</td></tr>
        <tr><td style="font-weight:600">Spent this month</td><td>${acctSpentDisplay} (${acctFilesUsed} files)</td></tr>
        <tr><td style="font-weight:600">Remaining</td><td>${acctRemainingDisplay}</td></tr>
        ${acctSpendingResets ? `<tr><td style="font-weight:600">Resets</td><td>${escapeHtml(new Date(acctSpendingResets).toLocaleDateString())}</td></tr>` : ''}
      </table>

      <div style="margin-top:1.25rem">
        <p style="font-weight:600;font-size:0.9rem;margin-bottom:0.5rem">Set monthly limit</p>
        <div class="btn-group" style="margin-bottom:0.75rem">
          <form method="POST" action="/dashboard/account/spending-limit"><input type="hidden" name="limit_cents" value="1000"><button type="submit" class="btn btn-outline btn-sm">$10</button></form>
          <form method="POST" action="/dashboard/account/spending-limit"><input type="hidden" name="limit_cents" value="2500"><button type="submit" class="btn btn-outline btn-sm">$25</button></form>
          <form method="POST" action="/dashboard/account/spending-limit"><input type="hidden" name="limit_cents" value="5000"><button type="submit" class="btn btn-outline btn-sm">$50</button></form>
          <form method="POST" action="/dashboard/account/spending-limit"><input type="hidden" name="limit_cents" value="10000"><button type="submit" class="btn btn-outline btn-sm">$100</button></form>
          <form method="POST" action="/dashboard/account/spending-limit"><input type="hidden" name="limit_cents" value=""><button type="submit" class="btn btn-outline btn-sm">No limit</button></form>
        </div>
      </div>

      ${acctLimit !== null ? `
      <form method="POST" action="/dashboard/account/spending-limit/reset" style="margin-top:0.75rem"
            onsubmit="return confirm('Reset the spending counter? This restarts your monthly count from $0.00.')">
        <button type="submit" class="btn btn-sm btn-danger">Reset counter</button>
      </form>` : ''}
    </div>

    <div class="card">
      <h2>API Key</h2>
      ${key ? `
        <table>
          <tr><td style="width:40%;font-weight:600">Key prefix</td><td><code>${escapeHtml(key.key_prefix)}...</code></td></tr>
          <tr><td style="font-weight:600">Balance</td><td>$${(key.balance_cents / 100).toFixed(2)}</td></tr>
          <tr><td style="font-weight:600">Created</td><td>${escapeHtml(new Date(key.created_at).toLocaleDateString())}</td></tr>
        </table>
        <form method="POST" action="/dashboard/account/regenerate-key" style="margin-top:1rem"
              onsubmit="return confirm('Are you sure? This will revoke your current key. You will need to update your API key in all your clients.')">
          <button type="submit" class="btn btn-danger btn-sm">Regenerate Key</button>
        </form>
      ` : '<p class="text-muted">No active API key.</p>'}
    </div>
  `, { email: userEmail });
    return c.html(html);
});
// POST /dashboard/account/regenerate-key — Regenerate API Key
dashboard.post('/dashboard/account/regenerate-key', dashboardSession, async (c) => {
    const userId = c.get('userId');
    const userEmail = c.get('userEmail');
    const rawKey = `lk_${randomUUID().replace(/-/g, '')}`;
    const hash = hashKey(rawKey);
    const prefix = rawKey.slice(0, 8);
    const keyId = randomUUID();
    withTransaction(() => {
        // Sum balance and capture spending settings from all active keys before revoking
        const oldKeys = query(`SELECT balance_cents, spending_limit_cents, spending_limit_reset_at FROM license_keys WHERE user_id = ? AND status = 'active'`, [userId]);
        const totalBalance = oldKeys.rows.reduce((sum, k) => sum + k.balance_cents, 0);
        // Take spending limit settings from the first active key (if any)
        const spendingLimitCents = oldKeys.rows.length > 0 ? oldKeys.rows[0].spending_limit_cents : null;
        const spendingLimitResetAt = oldKeys.rows.length > 0 ? oldKeys.rows[0].spending_limit_reset_at : null;
        // Revoke existing active keys
        query(`UPDATE license_keys SET status = 'revoked' WHERE user_id = ? AND status = 'active'`, [userId]);
        // Generate new key with transferred balance, spending settings, and signed HMAC
        const regenHmac = signBalance(keyId, totalBalance);
        query('INSERT INTO license_keys (id, user_id, key_hash, key_prefix, balance_cents, spending_limit_cents, spending_limit_reset_at, balance_hmac) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [keyId, userId, hash, prefix, totalBalance, spendingLimitCents, spendingLimitResetAt, regenHmac]);
    });
    const html = layout('New API Key', `
    <div style="max-width:500px;margin:3rem auto">
      <h1>New API Key</h1>
      <div class="alert alert-warning">
        <strong>Important:</strong> This is the only time your key will be shown. Copy it now and store it securely.
      </div>
      <div class="card">
        <div class="form-group">
          <label>Your new API key</label>
          <input type="text" id="api-key" value="${escapeHtml(rawKey)}" readonly
                 style="font-family:monospace;background:#f8f9fa" onclick="this.select()">
        </div>
        <button class="btn btn-primary btn-sm" onclick="navigator.clipboard.writeText(document.getElementById('api-key').value).then(()=>this.textContent='Copied!')">
          Copy to clipboard
        </button>
      </div>
      <p class="mt-2">Update your environment variable:</p>
      <pre>export OCR_LICENSE_KEY="${escapeHtml(rawKey)}"</pre>
      <a href="/dashboard/account" class="btn btn-outline mt-2">Back to Account</a>
    </div>
  `, { email: userEmail });
    return c.html(html);
});
// POST /dashboard/account/spending-limit — Set spending limit
dashboard.post('/dashboard/account/spending-limit', dashboardSession, async (c) => {
    const userId = c.get('userId');
    const body = await c.req.parseBody();
    const rawLimit = typeof body.limit_cents === 'string' ? body.limit_cents.trim() : '';
    // Empty string or missing = remove limit (null)
    const limitCents = rawLimit === '' ? null : parseInt(rawLimit, 10);
    if (limitCents !== null) {
        if (!Number.isInteger(limitCents) || limitCents < 0) {
            return c.redirect('/dashboard/account?error=Invalid+limit');
        }
        if (limitCents > 0 && limitCents < 100) {
            return c.redirect('/dashboard/account?error=Minimum+limit+is+%241.00');
        }
        if (limitCents > 100000) {
            return c.redirect('/dashboard/account?error=Maximum+limit+is+%241%2C000.00');
        }
    }
    const keyResult = query(`SELECT id FROM license_keys WHERE user_id = ? AND status = 'active'`, [userId]);
    if (keyResult.rows.length === 0) {
        return c.redirect('/dashboard/account');
    }
    query('UPDATE license_keys SET spending_limit_cents = ? WHERE id = ?', [limitCents, keyResult.rows[0].id]);
    return c.redirect('/dashboard/account?success=limit-set');
});
// POST /dashboard/account/spending-limit/reset — Reset spending period
dashboard.post('/dashboard/account/spending-limit/reset', dashboardSession, async (c) => {
    const userId = c.get('userId');
    const keyResult = query(`SELECT id, spending_limit_cents FROM license_keys WHERE user_id = ? AND status = 'active'`, [userId]);
    if (keyResult.rows.length === 0) {
        return c.redirect('/dashboard/account');
    }
    if (keyResult.rows[0].spending_limit_cents === null) {
        return c.redirect('/dashboard/account');
    }
    query('UPDATE license_keys SET spending_limit_reset_at = ? WHERE id = ?', [new Date().toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, ''), keyResult.rows[0].id]);
    return c.redirect('/dashboard/account?success=limit-reset');
});
// POST /dashboard/logout — Logout
dashboard.post('/dashboard/logout', async (c) => {
    const token = getCookie(c, 'session');
    if (token) {
        query('DELETE FROM sessions WHERE token = ?', [token]);
    }
    deleteCookie(c, 'session', { path: '/' });
    return c.redirect('/');
});
export default dashboard;
//# sourceMappingURL=dashboard.js.map