/**
 * Cookie picker route handler — HTTP + Playwright glue
 *
 * Handles all /cookie-picker/* routes. Imports from cookie-import-browser.ts
 * (decryption) and cookie-picker-ui.ts (HTML generation).
 *
 * Auth model (post-CVE fix):
 *   GET  /cookie-picker              → requires one-time code (?code=) or session cookie
 *   GET  /cookie-picker/browsers     → requires Bearer token or session cookie
 *   GET  /cookie-picker/domains      → requires Bearer token or session cookie
 *   POST /cookie-picker/import       → requires Bearer token or session cookie
 *   POST /cookie-picker/remove       → requires Bearer token or session cookie
 *   GET  /cookie-picker/imported     → requires Bearer token or session cookie
 *
 * The session cookie (gstack_picker) is isolated from the scoped token system.
 * It is NOT valid for /command. This prevents session cookie extraction from
 * re-enabling the auth token leak vulnerability.
 */

import * as crypto from 'crypto';
import type { BrowserManager } from './browser-manager';
import { findInstalledBrowsers, listProfiles, listDomains, importCookies, importCookiesViaCdp, hasV20Cookies, CookieImportError, type PlaywrightCookie } from './cookie-import-browser';
import { getCookiePickerHTML } from './cookie-picker-ui';

// ─── Auth State ─────────────────────────────────────────────────
// One-time codes for the cookie picker UI (code → expiry timestamp).
// Codes are generated by generatePickerCode() and consumed on first use.
const pendingCodes = new Map<string, number>();
const CODE_TTL_MS = 30_000; // 30 seconds

// Session cookies for authenticated picker access (session → expiry timestamp).
// Sessions are created after a valid code exchange and last 1 hour.
const validSessions = new Map<string, number>();
const SESSION_TTL_MS = 3_600_000; // 1 hour

/** Generate a one-time code for opening the cookie picker UI. */
export function generatePickerCode(): string {
  const code = crypto.randomUUID();
  pendingCodes.set(code, Date.now() + CODE_TTL_MS);
  return code;
}

/** Return true while the picker still has a live code or session. */
export function hasActivePicker(): boolean {
  const now = Date.now();

  for (const [code, expiry] of pendingCodes) {
    if (expiry > now) return true;
    pendingCodes.delete(code);
  }

  for (const [session, expiry] of validSessions) {
    if (expiry > now) return true;
    validSessions.delete(session);
  }

  return false;
}

/** Extract session ID from the gstack_picker cookie. */
function getSessionFromCookie(req: Request): string | null {
  const cookie = req.headers.get('cookie');
  if (!cookie) return null;
  const match = cookie.match(/gstack_picker=([^;]+)/);
  return match ? match[1] : null;
}

/** Check if a session cookie value is valid and not expired. */
function isValidSession(session: string): boolean {
  const expiry = validSessions.get(session);
  if (!expiry) return false;
  if (Date.now() > expiry) { validSessions.delete(session); return false; }
  return true;
}

// ─── Domain State ───────────────────────────────────────────────
// Tracks which domains were imported via the picker.
// /imported only returns cookies for domains in this Set.
// /remove clears from this Set.
const importedDomains = new Set<string>();
const importedCounts = new Map<string, number>();

// ─── JSON Helpers ───────────────────────────────────────────────

function corsOrigin(port: number): string {
  return `http://127.0.0.1:${port}`;
}

function jsonResponse(data: any, opts: { port: number; status?: number }): Response {
  return new Response(JSON.stringify(data), {
    status: opts.status ?? 200,
    headers: {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': corsOrigin(opts.port),
    },
  });
}

function errorResponse(message: string, code: string, opts: { port: number; status?: number; action?: string }): Response {
  return jsonResponse(
    { error: message, code, ...(opts.action ? { action: opts.action } : {}) },
    { port: opts.port, status: opts.status ?? 400 },
  );
}

// ─── Route Handler ──────────────────────────────────────────────

export async function handleCookiePickerRoute(
  url: URL,
  req: Request,
  bm: BrowserManager,
  authToken?: string,
): Promise<Response> {
  const pathname = url.pathname;
  const port = parseInt(url.port, 10) || 9400;

  // CORS preflight
  if (req.method === 'OPTIONS') {
    return new Response(null, {
      status: 204,
      headers: {
        'Access-Control-Allow-Origin': corsOrigin(port),
        'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
        'Access-Control-Allow-Headers': 'Content-Type, Authorization',
      },
    });
  }

  try {
    // GET /cookie-picker — serve the picker UI (requires code or session cookie)
    if (pathname === '/cookie-picker' && req.method === 'GET') {
      const code = url.searchParams.get('code');

      // Code exchange: validate one-time code, set session cookie, redirect
      if (code) {
        const expiry = pendingCodes.get(code);
        if (!expiry || Date.now() > expiry) {
          pendingCodes.delete(code);
          return new Response('Invalid or expired code. Re-run cookie-import-browser.', {
            status: 403,
            headers: { 'Content-Type': 'text/plain' },
          });
        }
        pendingCodes.delete(code); // one-time use
        const session = crypto.randomUUID();
        validSessions.set(session, Date.now() + SESSION_TTL_MS);
        return new Response(null, {
          status: 302,
          headers: {
            'Location': '/cookie-picker',
            'Set-Cookie': `gstack_picker=${session}; HttpOnly; SameSite=Strict; Path=/cookie-picker; Max-Age=3600`,
            'Cache-Control': 'no-store',
          },
        });
      }

      // Session cookie: serve HTML (no auth token inlined)
      const session = getSessionFromCookie(req);
      if (session && isValidSession(session)) {
        const html = getCookiePickerHTML(port);
        return new Response(html, {
          status: 200,
          headers: { 'Content-Type': 'text/html; charset=utf-8' },
        });
      }

      // No code, no session: reject
      return new Response('Access denied. Open the cookie picker from gstack.', {
        status: 403,
        headers: { 'Content-Type': 'text/plain' },
      });
    }

    // ─── Auth gate: all data/action routes below require Bearer token or session cookie ───
    const authHeader = req.headers.get('authorization');
    const sessionId = getSessionFromCookie(req);
    const hasBearer = !!authToken && !!authHeader && authHeader === `Bearer ${authToken}`;
    const hasSession = sessionId !== null && isValidSession(sessionId);
    if (!hasBearer && !hasSession) {
      return new Response(JSON.stringify({ error: 'Unauthorized' }), {
        status: 401,
        headers: { 'Content-Type': 'application/json' },
      });
    }

    // GET /cookie-picker/browsers — list installed browsers
    if (pathname === '/cookie-picker/browsers' && req.method === 'GET') {
      const browsers = findInstalledBrowsers();
      return jsonResponse({
        browsers: browsers.map(b => ({
          name: b.name,
          aliases: b.aliases,
        })),
      }, { port });
    }

    // GET /cookie-picker/profiles?browser=<name> — list profiles for a browser
    if (pathname === '/cookie-picker/profiles' && req.method === 'GET') {
      const browserName = url.searchParams.get('browser');
      if (!browserName) {
        return errorResponse("Missing 'browser' parameter", 'missing_param', { port });
      }
      const profiles = listProfiles(browserName);
      return jsonResponse({ profiles }, { port });
    }

    // GET /cookie-picker/domains?browser=<name>&profile=<profile> — list domains + counts
    if (pathname === '/cookie-picker/domains' && req.method === 'GET') {
      const browserName = url.searchParams.get('browser');
      if (!browserName) {
        return errorResponse("Missing 'browser' parameter", 'missing_param', { port });
      }
      const profile = url.searchParams.get('profile') || 'Default';
      const result = listDomains(browserName, profile);
      return jsonResponse({
        browser: result.browser,
        domains: result.domains,
      }, { port });
    }

    // POST /cookie-picker/import — decrypt + import to Playwright session
    if (pathname === '/cookie-picker/import' && req.method === 'POST') {
      let body: any;
      try {
        body = await req.json();
      } catch {
        return errorResponse('Invalid JSON body', 'bad_request', { port });
      }

      const { browser, domains, profile } = body;
      if (!browser) return errorResponse("Missing 'browser' field", 'missing_param', { port });
      if (!domains || !Array.isArray(domains) || domains.length === 0) {
        return errorResponse("Missing or empty 'domains' array", 'missing_param', { port });
      }

      // Decrypt cookies from the browser DB
      const selectedProfile = profile || 'Default';
      let result = await importCookies(browser, domains, selectedProfile);

      // If all cookies failed and v20 encryption is detected, try CDP extraction
      if (result.cookies.length === 0 && result.failed > 0 && hasV20Cookies(browser, selectedProfile)) {
        console.log(`[cookie-picker] v20 App-Bound Encryption detected, trying CDP extraction...`);
        try {
          result = await importCookiesViaCdp(browser, domains, selectedProfile);
        } catch (cdpErr: any) {
          console.log(`[cookie-picker] CDP fallback failed: ${cdpErr.message}`);
          return jsonResponse({
            imported: 0,
            failed: result.failed,
            domainCounts: {},
            message: `Cookies use App-Bound Encryption (v20). Close ${browser}, retry, or use /connect-chrome to browse with your real browser directly.`,
            code: 'v20_encryption',
          }, { port });
        }
      }

      if (result.cookies.length === 0) {
        return jsonResponse({
          imported: 0,
          failed: result.failed,
          domainCounts: {},
          message: result.failed > 0
            ? `All ${result.failed} cookies failed to decrypt`
            : 'No cookies found for the specified domains',
        }, { port });
      }

      // Add to Playwright context
      const page = bm.getActiveSession().getPage();
      await page.context().addCookies(result.cookies);

      // Track what was imported
      for (const domain of Object.keys(result.domainCounts)) {
        importedDomains.add(domain);
        importedCounts.set(domain, (importedCounts.get(domain) || 0) + result.domainCounts[domain]);
      }

      console.log(`[cookie-picker] Imported ${result.count} cookies for ${Object.keys(result.domainCounts).length} domains`);

      return jsonResponse({
        imported: result.count,
        failed: result.failed,
        domainCounts: result.domainCounts,
      }, { port });
    }

    // POST /cookie-picker/remove — clear cookies for domains
    if (pathname === '/cookie-picker/remove' && req.method === 'POST') {
      let body: any;
      try {
        body = await req.json();
      } catch {
        return errorResponse('Invalid JSON body', 'bad_request', { port });
      }

      const { domains } = body;
      if (!domains || !Array.isArray(domains) || domains.length === 0) {
        return errorResponse("Missing or empty 'domains' array", 'missing_param', { port });
      }

      const page = bm.getActiveSession().getPage();
      const context = page.context();
      for (const domain of domains) {
        await context.clearCookies({ domain });
        importedDomains.delete(domain);
        importedCounts.delete(domain);
      }

      console.log(`[cookie-picker] Removed cookies for ${domains.length} domains`);

      return jsonResponse({
        removed: domains.length,
        domains,
      }, { port });
    }

    // GET /cookie-picker/imported — currently imported domains + counts
    if (pathname === '/cookie-picker/imported' && req.method === 'GET') {
      const entries: Array<{ domain: string; count: number }> = [];
      for (const domain of importedDomains) {
        entries.push({ domain, count: importedCounts.get(domain) || 0 });
      }
      entries.sort((a, b) => b.count - a.count);

      return jsonResponse({
        domains: entries,
        totalDomains: entries.length,
        totalCookies: entries.reduce((sum, e) => sum + e.count, 0),
      }, { port });
    }

    return new Response('Not found', { status: 404 });
  } catch (err: any) {
    if (err instanceof CookieImportError) {
      return errorResponse(err.message, err.code, { port, status: 400, action: err.action });
    }
    console.error(`[cookie-picker] Error: ${err.message}`);
    return errorResponse(err.message || 'Internal error', 'internal_error', { port, status: 500 });
  }
}
