/**
 * MCP gateway (ADR-057) — the host-side proxy that puts the owner's MCP
 * connections in front of task containers WITHOUT the credentials ever
 * entering them.
 *
 * Containers call `http://host.docker.internal:<port>/t/<token>/<slug>`; the
 * gateway resolves the per-task token to an ACL file written at session
 * ensure (which connections this task may use), attaches the connection's
 * Authorization header host-side (refreshing OAuth tokens as needed), and
 * streams the MCP traffic through (streamable HTTP + SSE). Every request
 * lands in an audit JSONL. Disconnecting a connection kills its secrets in
 * the host store, so the gateway 401s instantly — the kill switch.
 *
 * VERIFY-ON-MAC: OrbStack forwards host.docker.internal to loopback-bound
 * host services; if not, set UAI_MCP_GATEWAY_BIND=0.0.0.0 (the unguessable
 * per-task token stays the auth).
 */

import { appendFileSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
import { randomBytes, timingSafeEqual } from "node:crypto";
import { Readable } from "node:stream";
import { resolve } from "node:path";

import { env } from "./env";
import { dockerCli } from "./docker-exec";
import { authHeaderFor, getConnection } from "./mcp-connections";

export const MCP_GATEWAY_PORT = Number(process.env.UAI_MCP_GATEWAY_PORT ?? 5877);
const BIND = process.env.UAI_MCP_GATEWAY_BIND ?? "127.0.0.1";
/** MCP request bodies are small JSON-RPC frames; cap so audit parsing (and a
 *  hostile container) can't balloon host memory. Responses stream freely. */
const MAX_BODY_BYTES = 4 * 1024 * 1024;
const UPSTREAM_TIMEOUT_MS = 120_000;

export interface TaskMcpConnection {
  id: string;
  slug: string;
}

interface TaskAcl {
  token: string;
  connections: TaskMcpConnection[];
}

function aclDir(): string {
  return resolve(env.dataDir, "mcp-gateway");
}

function aclPath(taskId: string): string {
  return resolve(aclDir(), `${taskId}.json`);
}

function readAcl(taskId: string): TaskAcl | null {
  try {
    return JSON.parse(readFileSync(aclPath(taskId), "utf8")) as TaskAcl;
  } catch {
    return null;
  }
}

/**
 * Write/refresh the task's gateway ACL (the connection set snapshots the
 * cloud's latest ensure input). The token is minted once per task and
 * survives re-ensures so already-written .mcp.json files stay valid.
 */
export function ensureTaskGatewayAcl(
  taskId: string,
  connections: TaskMcpConnection[],
): TaskAcl {
  const existing = readAcl(taskId);
  const acl: TaskAcl = {
    token: existing?.token ?? `${taskId}.${randomBytes(24).toString("base64url")}`,
    connections,
  };
  mkdirSync(aclDir(), { recursive: true, mode: 0o700 });
  writeFileSync(aclPath(taskId), JSON.stringify(acl), { mode: 0o600 });
  return acl;
}

export function clearTaskGatewayAcl(taskId: string): void {
  try {
    rmSync(aclPath(taskId));
  } catch {
    // already gone
  }
}

function audit(entry: Record<string, unknown>): void {
  try {
    const dir = resolve(env.dataDir, "logs");
    mkdirSync(dir, { recursive: true });
    appendFileSync(
      resolve(dir, "mcp-audit.jsonl"),
      `${JSON.stringify({ ts: Date.now(), ...entry })}\n`,
    );
  } catch {
    // audit is best-effort
  }
}

function deny(res: ServerResponse, status: number, message: string): void {
  res.writeHead(status, { "content-type": "application/json" });
  res.end(JSON.stringify({ error: message }));
}

async function readBody(req: IncomingMessage): Promise<Buffer | null> {
  const chunks: Buffer[] = [];
  let size = 0;
  for await (const chunk of req) {
    const buf = chunk as Buffer;
    size += buf.length;
    if (size > MAX_BODY_BYTES) return null;
    chunks.push(buf);
  }
  return Buffer.concat(chunks);
}

/** JSON-RPC method (+ tool name for tools/call) out of a request body. */
function rpcSummary(body: Buffer): { method?: string; tool?: string } {
  try {
    const json = JSON.parse(body.toString("utf8")) as {
      method?: string;
      params?: { name?: string };
    };
    return {
      method: typeof json.method === "string" ? json.method : undefined,
      tool:
        json.method === "tools/call" && typeof json.params?.name === "string"
          ? json.params.name
          : undefined,
    };
  } catch {
    return {};
  }
}

const FORWARD_REQ_HEADERS = [
  "content-type",
  "accept",
  "mcp-session-id",
  "last-event-id",
  "mcp-protocol-version",
];
const FORWARD_RES_HEADERS = ["content-type", "mcp-session-id"];

async function handle(req: IncomingMessage, res: ServerResponse): Promise<void> {
  const match = /^\/t\/([^/]+)\/([^/?]+)\/?(?:\?.*)?$/.exec(req.url ?? "");
  if (!match) return deny(res, 404, "not found");
  const [, token, slug] = match as unknown as [string, string, string];

  const taskId = token.split(".")[0] ?? "";
  const acl = readAcl(taskId);
  const expected = acl ? Buffer.from(acl.token) : null;
  const provided = Buffer.from(token);
  if (
    !acl ||
    !expected ||
    expected.length !== provided.length ||
    !timingSafeEqual(expected, provided)
  ) {
    return deny(res, 401, "unknown task token");
  }
  const entry = acl.connections.find((c) => c.slug === slug);
  if (!entry) return deny(res, 404, `no connection "${slug}" for this task`);
  const conn = getConnection(entry.id);
  if (!conn || conn.status !== "connected") {
    // Disconnected since the task started — the kill switch answering.
    return deny(res, 401, `connection "${slug}" is no longer available`);
  }

  const body =
    req.method === "POST" || req.method === "PUT" ? await readBody(req) : null;
  if ((req.method === "POST" || req.method === "PUT") && body === null) {
    return deny(res, 413, "request body too large");
  }

  const headers: Record<string, string> = {};
  for (const name of FORWARD_REQ_HEADERS) {
    const v = req.headers[name];
    if (typeof v === "string") headers[name] = v;
  }

  const attempt = async (forceRefresh: boolean): Promise<Response> => {
    const auth = await authHeaderFor(conn, forceRefresh);
    if (auth) headers[auth.name] = auth.value;
    return fetch(conn.url, {
      method: req.method,
      headers,
      body: body ?? undefined,
      signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS),
    });
  };

  let upstream: Response;
  try {
    upstream = await attempt(false);
    if (upstream.status === 401 && conn.authKind === "oauth") {
      upstream = await attempt(true);
    }
  } catch (err) {
    audit({ taskId, slug, method: req.method, error: String(err) });
    return deny(res, 502, "upstream unreachable");
  }

  const summary = body ? rpcSummary(body) : {};
  audit({
    taskId,
    slug,
    method: req.method,
    rpc: summary.method,
    tool: summary.tool,
    status: upstream.status,
  });

  const resHeaders: Record<string, string> = {};
  for (const name of FORWARD_RES_HEADERS) {
    const v = upstream.headers.get(name);
    if (v) resHeaders[name] = v;
  }
  res.writeHead(upstream.status, resHeaders);
  if (upstream.body) {
    Readable.fromWeb(upstream.body as Parameters<typeof Readable.fromWeb>[0]).pipe(
      res,
    );
  } else {
    res.end();
  }
}

/** Start the gateway listener. Idempotent-ish: call once from main. */
export function startMcpGateway(): void {
  const server = createServer((req, res) => {
    void handle(req, res).catch(() => deny(res, 500, "gateway error"));
  });
  server.on("error", (err) => {
    console.warn(`[mcp-gateway] listener error: ${err.message}`);
  });
  server.listen(MCP_GATEWAY_PORT, BIND, () => {
    console.log(`[mcp-gateway] listening on ${BIND}:${MCP_GATEWAY_PORT}`);
  });
}

// --- task container wiring (ADR-057 task-up writers) -------------------------

/** Idempotent node -e merge of entries into /workspace/.mcp.json. */
const MERGE_MCP_JSON = `
const fs = require("fs");
const p = "/workspace/.mcp.json";
let j = {};
try { j = JSON.parse(fs.readFileSync(p, "utf8")); } catch {}
j.mcpServers = j.mcpServers || {};
let changed = false;
for (const [k, v] of Object.entries(JSON.parse(process.argv[1]))) {
  if (JSON.stringify(j.mcpServers[k]) !== JSON.stringify(v)) { j.mcpServers[k] = v; changed = true; }
}
if (changed) fs.writeFileSync(p, JSON.stringify(j, null, 2) + "\\n");
`.trim();

function shellQuote(value: string): string {
  return `'${value.replace(/'/g, `'\\''`)}'`;
}

/**
 * Write the task's MCP configs inside the container: gateway-URL entries per
 * connection for Claude (/workspace/.mcp.json, merged — coexists with the
 * ADR-053 browser server) and mcp-remote shims for Codex (config.toml,
 * append-once per slug). Safe to re-run every ensure.
 */
export async function setupMcpTaskConfig(
  taskId: string,
  containerName: string,
  connections: TaskMcpConnection[],
  hasCodex: boolean,
): Promise<void> {
  if (connections.length === 0) return;
  try {
    const acl = ensureTaskGatewayAcl(taskId, connections);
    const urlFor = (slug: string): string =>
      `http://host.docker.internal:${MCP_GATEWAY_PORT}/t/${acl.token}/${slug}`;

    const claudeEntries: Record<string, unknown> = {};
    for (const c of connections) {
      claudeEntries[c.slug] = { type: "http", url: urlFor(c.slug) };
    }
    const steps = [
      "mkdir -p /workspace/.claude",
      `[ -f /workspace/.claude/settings.json ] || printf '%s\\n' ${shellQuote(
        JSON.stringify({ enableAllProjectMcpServers: true }, null, 2),
      )} > /workspace/.claude/settings.json`,
      `node -e ${shellQuote(MERGE_MCP_JSON)} ${shellQuote(JSON.stringify(claudeEntries))}`,
      ...(hasCodex
        ? connections.map(
            (c) =>
              `grep -q "mcp_servers.${c.slug}]" /home/node/.codex/config.toml 2>/dev/null || printf '%s' ${shellQuote(
                `\n[mcp_servers.${c.slug}]\ncommand = "npx"\nargs = ["-y", "mcp-remote", "${urlFor(c.slug)}", "--allow-http"]\n`,
              )} >> /home/node/.codex/config.toml`,
          )
        : []),
    ].join(" && ");
    const result = await dockerCli(
      ["exec", containerName, "sh", "-lc", steps],
      { timeoutMs: 20_000 },
    );
    if (result.status !== 0) {
      console.warn(
        `[mcp-gateway] task ${taskId}: config write failed: ${result.stderr.slice(0, 300)}`,
      );
    }
  } catch (err) {
    console.warn(
      `[mcp-gateway] task ${taskId}: setup failed: ${err instanceof Error ? err.message : err}`,
    );
  }
}
