// packages/host-cp/src/tasks-route.mjs
//
// B2.2: mount @olam/tasks-write-api's framework-neutral handlers under
// /api/tasks/*. host-cp owns the pg.Pool (per D-B-19 olam-local-PG-primary);
// wraps it via pgPoolExecutor (B2.1.1 adapter) and passes as HandlerDeps.pglite
// (duck-typed; PgExecutor's query/exec/transaction match PGlite's shape).
//
// Auth model: leverages host-cp's existing StartupToken bearer gate (Authorization:
// Bearer <token>). Per-request scopes + olamNodeId come from headers:
//   X-Olam-Node-Id:      UUID of the caller's olam node (sets RLS scope per D-B-23)
//   X-Olam-Session-Id:   UUID of the caller's session row (FK for task_claims)
//   X-Olam-Tasks-Scopes: comma-separated scope list (tasks-create,tasks-claim,
//                        tasks-state-update,tasks-query). Trust model: bearer
//                        token gates access; scope header lets the caller declare
//                        narrower intent.
//
// Deviation from B2.2 plan spec: spec called for JWT + auth-service integration;
// host-cp uses opaque tokens (StartupToken) and HTTP calls auth-service via HTTP.
// JWT scope encoding deferred to Phase D++ when multi-user auth lands; for v1,
// the existing bearer + per-request header model is sufficient (single-operator;
// 127.0.0.1:19000 only per host-cp threat model).

import pg from 'pg';

// Treat BIGINT (OID 20) as Number, not the default string. The tasks schema's
// `version` column is BIGINT but stays well within Number-safe range; without
// this parser pg returns the value as a string, and the task-store types
// declare it as `number`, letting a stray BigInt propagate (PGlite returns
// BigInt by default). JSON.stringify on BigInt throws — caused /api/tasks
// 500s with "Do not know how to serialize a BigInt" during the CLI E2E proof.
pg.types.setTypeParser(20, (v) => (v == null ? null : Number.parseInt(v, 10)));

let writeApi = null; // lazy-load tasks-write-api to keep cold-path light
let executor = null;
let pool = null;

const VALID_SCOPES = new Set(['tasks-create', 'tasks-claim', 'tasks-state-update', 'tasks-query']);
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

async function ensureWriteApi() {
  if (writeApi) return writeApi;
  // Dynamic import: tasks-write-api is built TS (ESM dist). Fail-loud if not
  // built — operator must `npm run build --workspace=@olam/tasks-write-api`
  // before host-cp starts.
  writeApi = await import('@olam/tasks-write-api');
  return writeApi;
}

function ensureExecutor() {
  if (executor) return executor;
  const connectionString = process.env.OLAM_LOCAL_PG_URL;
  if (!connectionString) {
    throw new Error(
      'tasks-route: OLAM_LOCAL_PG_URL not set. Bring up Docker PG: docker compose -f packages/infra/docker-compose.local-electric.yml up -d, then export OLAM_LOCAL_PG_URL=postgres://postgres:olam@localhost:54331/olam_tasks',
    );
  }
  pool = new pg.Pool({ connectionString, max: 8 });
  // Lazy require pgPoolExecutor from the same dynamic-imported module.
  // ensureWriteApi must have run first; tasks-route's dispatch order guarantees it.
  return writeApi.pgPoolExecutor(pool);
}

function parseAuth(req) {
  const olamNodeId = req.headers['x-olam-node-id'];
  const sessionId = req.headers['x-olam-session-id'];
  const scopesHeader = req.headers['x-olam-tasks-scopes'] ?? '';
  const scopes = String(scopesHeader)
    .split(',')
    .map((s) => s.trim())
    .filter((s) => VALID_SCOPES.has(s));
  if (!olamNodeId || !UUID_RE.test(String(olamNodeId))) return null;
  if (!sessionId || !UUID_RE.test(String(sessionId))) return null;
  if (scopes.length === 0) return null;
  return { olamNodeId: String(olamNodeId), sessionId: String(sessionId), scopes };
}

async function readBody(req) {
  if (req.method === 'GET' || req.method === 'HEAD') return {};
  return new Promise((resolve, reject) => {
    let raw = '';
    req.on('data', (chunk) => (raw += chunk));
    req.on('end', () => {
      if (!raw) return resolve({});
      try {
        resolve(JSON.parse(raw));
      } catch {
        resolve({ __invalid: true });
      }
    });
    req.on('error', reject);
  });
}

function sendEnvelope(res, status, envelope) {
  res.statusCode = status;
  res.setHeader('Content-Type', 'application/json');
  // BigInt-safe serialization. @olam/tasks's task-store wraps `version`
  // (and any future BIGINT fields) in BigInt() during row→Task mapping;
  // default JSON.stringify throws on BigInt. The values stay safely
  // within Number range (version starts at 0, increments per mutation),
  // so emitting as a JSON number is lossless for any realistic load.
  res.end(JSON.stringify(envelope, (_key, value) =>
    typeof value === 'bigint' ? Number(value) : value,
  ));
}

/**
 * Dispatch a /api/tasks/* request. Returns true if handled; false if route
 * doesn't match (caller continues to next dispatcher in server.mjs).
 */
export async function dispatchTasksRoute(req, res, url) {
  const pathname = url.pathname;
  if (!pathname.startsWith('/api/tasks')) return false;

  // Lazy initialise on first request (avoids boot-time crash when PG not up).
  let api;
  try {
    api = await ensureWriteApi();
  } catch (e) {
    sendEnvelope(res, 500, { success: false, data: null, error: `tasks-write-api unbuilt: ${e.message}` });
    return true;
  }
  let exec;
  try {
    exec = ensureExecutor();
  } catch (e) {
    sendEnvelope(res, 503, { success: false, data: null, error: e.message });
    return true;
  }

  const auth = parseAuth(req);
  if (!auth) {
    sendEnvelope(res, 401, {
      success: false,
      data: null,
      error: 'Missing or malformed X-Olam-Node-Id / X-Olam-Session-Id / X-Olam-Tasks-Scopes headers',
    });
    return true;
  }

  const body = await readBody(req);
  if (body && body.__invalid) {
    sendEnvelope(res, 400, { success: false, data: null, error: 'Invalid JSON body' });
    return true;
  }

  // Route matching — minimal pattern (host-cp's existing if-ladder style).
  const segments = pathname.split('/').filter(Boolean); // ['api','tasks',...]
  const ctx = { auth, params: {}, query: Object.fromEntries(url.searchParams) };
  const deps = { pglite: exec };

  try {
    let response;
    if (segments.length === 2 && req.method === 'POST') {
      response = await api.createHandler({ ...ctx, body }, deps);
    } else if (segments.length === 2 && req.method === 'GET') {
      response = await api.queryHandler({ ...ctx, body }, deps);
    } else if (segments.length === 3 && segments[2] === 'claim' && req.method === 'POST') {
      response = await api.claimHandler({ ...ctx, body }, deps);
    } else if (segments.length === 3 && segments[2] === 'distill' && req.method === 'GET') {
      response = await api.distillHandler({ ...ctx, body }, deps);
    } else if (segments.length === 4 && segments[3] === 'heartbeat' && req.method === 'POST') {
      ctx.params.id = segments[2];
      response = await api.heartbeatHandler({ ...ctx, body }, deps);
    } else if (segments.length === 4 && segments[3] === 'complete' && req.method === 'POST') {
      ctx.params.id = segments[2];
      response = await api.completeHandler({ ...ctx, body }, deps);
    } else if (segments.length === 4 && segments[3] === 'update' && req.method === 'POST') {
      ctx.params.id = segments[2];
      response = await api.updateHandler({ ...ctx, body }, deps);
    } else {
      sendEnvelope(res, 404, { success: false, data: null, error: `Unknown /api/tasks route: ${req.method} ${pathname}` });
      return true;
    }

    sendEnvelope(res, response.status, response.envelope);
    return true;
  } catch (e) {
    console.error('[tasks-route] handler error:', e);
    sendEnvelope(res, 500, { success: false, data: null, error: e?.message ?? 'internal error' });
    return true;
  }
}

// Test surface — for unit tests to reset module state between cases.
export function _resetForTests() {
  writeApi = null;
  executor = null;
  if (pool) pool.end().catch(() => undefined);
  pool = null;
}
