/**
 * Capture fetch: GET /api/captures/:id on llama-swap host, decode base64,
 * persist trimmed copy (256KB cap app-enforced), render with shiki JSON.
 *
 * The 256KB cap is application-enforced in the fetch handler, not a DB constraint.
 * Total budget: 50MB default, configurable via CAPTURE_BUDGET_MB env var.
 */

import type { Sql } from '../db.js';

const MAX_CAPTURE_BYTES = 256 * 1024; // 256KB

export interface CaptureData {
  id: number;
  providerId: string;
  timestamp: string;
  model: string;
  requestHeaders: Record<string, string>;
  requestBody: string;
  responseHeaders: Record<string, string>;
  responseBody: string;
  durationMs: number;
  sizeBytes: number;
}

export interface CaptureFetchResult {
  ok: boolean;
  capture?: CaptureData;
  error?: string;
}

/**
 * Fetch a capture from a llama-swap host by its swap_entry_id.
 */
export async function fetchCapture(
  baseUrl: string,
  providerId: string,
  swapEntryId: number,
): Promise<CaptureFetchResult> {
  try {
    const res = await fetch(`${baseUrl}/api/captures/${swapEntryId}`, {
      signal: AbortSignal.timeout(10_000),
    });

    if (!res.ok) {
      if (res.status === 404) {
        return { ok: false, error: 'capture not found on host' };
      }
      return { ok: false, error: `fetch failed: ${res.status}` };
    }

    const raw = await res.json() as Record<string, unknown>;
    return { ok: true, capture: parseCapture(raw, providerId, swapEntryId) };
  } catch (err) {
    return { ok: false, error: (err as Error).message ?? String(err) };
  }
}

/**
 * Parse raw capture data from llama-swap into our structured format.
 * Trims to 256KB cap.
 */
export function parseCapture(
  raw: Record<string, unknown>,
  providerId: string,
  swapEntryId: number,
): CaptureData {
  const requestHeaders = (raw.request_headers ?? raw.headers ?? {}) as Record<string, string>;
  const responseHeaders = (raw.response_headers ?? {}) as Record<string, string>;

  let requestBody = '';
  let responseBody = '';

  // Decode base64 bodies if present
  const reqBodyRaw = raw.request_body as string | undefined;
  const respBodyRaw = raw.response_body as string | undefined;

  if (reqBodyRaw) {
    try {
      requestBody = Buffer.from(reqBodyRaw, 'base64').toString('utf8');
    } catch {
      requestBody = reqBodyRaw;
    }
  }

  if (respBodyRaw) {
    try {
      responseBody = Buffer.from(respBodyRaw, 'base64').toString('utf8');
    } catch {
      responseBody = respBodyRaw;
    }
  }

  // Enforce 256KB cap by trimming response body (largest component)
  const totalSize = requestBody.length + responseBody.length;
  if (totalSize > MAX_CAPTURE_BYTES) {
    const remaining = MAX_CAPTURE_BYTES - requestBody.length;
    responseBody = responseBody.slice(0, Math.max(0, Math.floor(remaining)));
    responseBody += '\n\n[truncated: capture exceeds 256KB cap]';
  }

  const sizeBytes = Buffer.byteLength(requestBody + responseBody);

  return {
    id: swapEntryId,
    providerId,
    timestamp: (raw.timestamp ?? raw.ts ?? new Date().toISOString()) as string,
    model: (raw.model ?? '') as string,
    requestHeaders,
    requestBody,
    responseHeaders,
    responseBody,
    durationMs: (raw.duration_ms ?? 0) as number,
    sizeBytes,
  };
}

/**
 * Persist a trimmed capture to the control_requests table.
 * Uses sql.json(value as never) per convention.
 */
export async function persistCapture(
  sql: Sql,
  capture: CaptureData,
): Promise<void> {
  // Pass the OBJECT to sql.json — wrapping a pre-stringified value stores a
  // JSON string in the JSONB column (the double-serialization gotcha).
  const captureObj = {
    requestHeaders: capture.requestHeaders,
    requestBody: capture.requestBody,
    responseHeaders: capture.responseHeaders,
    responseBody: capture.responseBody,
    durationMs: capture.durationMs,
  };

  await sql`
    INSERT INTO control_requests (provider_id, swap_entry_id, ts, model, capture)
    VALUES (${capture.providerId}, ${capture.id}, ${capture.timestamp}, ${capture.model}, ${sql.json(captureObj as never)})
    ON CONFLICT (provider_id, swap_entry_id, ts) DO UPDATE SET
      capture = EXCLUDED.capture
  `;
}
