#!/usr/bin/env node
/**
 * Laserreach -> local Codex webhook bridge.
 *
 * Runs on your machine, receives JSON webhook/event payloads, stores them as
 * JSONL, fetches the Laserreach external-agent capability surface, and prepares
 * a Codex task prompt. Set CODEX_AUTO_RUN=1 to launch Codex automatically.
 *
 * No npm dependencies. Requires Node.js 18+.
 */

import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";
import { spawn } from "node:child_process";
import { appendFile, mkdir, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import { dirname, join, resolve } from "node:path";

const env = (name, fallback = "") => {
  const value = process.env[name];
  return value == null || value === "" ? fallback : value;
};

const config = {
  apiBase: env("LASERREACH_API_BASE", "https://api.laserreach.com").replace(/\/+$/, ""),
  agentToken: env("LASERREACH_AGENT_TOKEN"),
  orgId: env("LASERREACH_ORG_ID"),
  webhookSecret: env("LASERREACH_WEBHOOK_SECRET"),
  host: env("BRIDGE_HOST", "127.0.0.1"),
  port: Number(env("PORT", "8797")),
  endpoint: env("WEBHOOK_PATH", "/laserreach/events"),
  outDir: resolve(env("BRIDGE_OUT_DIR", join(homedir(), ".laserreach-codex-bridge"))),
  codexCommand: env("CODEX_COMMAND", "codex"),
  codexArgsJson: env("CODEX_ARGS_JSON"),
  codexArgs: env("CODEX_ARGS", "exec --skip-git-repo-check"),
  codexPromptMode: env("CODEX_PROMPT_MODE", "arg"),
  codexAutoRun: env("CODEX_AUTO_RUN", "0") === "1",
  maxBodyBytes: Number(env("MAX_BODY_BYTES", String(1024 * 1024))),
};

const seenEventIds = new Set();

const json = (res, status, body) => {
  const payload = JSON.stringify(body);
  res.writeHead(status, {
    "content-type": "application/json",
    "content-length": Buffer.byteLength(payload),
  });
  res.end(payload);
};

const normalizeSignature = (value) =>
  String(value || "")
    .trim()
    .replace(/^sha256=/i, "")
    .replace(/^v1=/i, "");

const verifySignature = (rawBody, headerValue, secret) => {
  if (!secret) return true;
  const received = normalizeSignature(headerValue);
  if (!received) return false;
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(received, "hex");
  const b = Buffer.from(expected, "hex");
  return a.length === b.length && timingSafeEqual(a, b);
};

const readBody = (req) =>
  new Promise((resolveBody, rejectBody) => {
    const chunks = [];
    let total = 0;
    req.on("data", (chunk) => {
      total += chunk.length;
      if (total > config.maxBodyBytes) {
        rejectBody(new Error("request_body_too_large"));
        req.destroy();
        return;
      }
      chunks.push(chunk);
    });
    req.on("end", () => resolveBody(Buffer.concat(chunks)));
    req.on("error", rejectBody);
  });

const ensureOutDir = async () => {
  await mkdir(config.outDir, { recursive: true });
  await mkdir(join(config.outDir, "prompts"), { recursive: true });
  await mkdir(join(config.outDir, "logs"), { recursive: true });
};

const writeEvent = async (eventRecord) => {
  await ensureOutDir();
  const line = `${JSON.stringify(eventRecord)}\n`;
  await appendFile(join(config.outDir, "events.jsonl"), line, "utf8");
};

const eventIdFromPayload = (payload) => {
  const direct =
    payload?.event_id ||
    payload?.eventId ||
    payload?.id ||
    payload?.data?.event_id ||
    payload?.data?.id ||
    "";
  return String(direct || `evt_${randomUUID()}`).trim();
};

const fetchCapabilities = async () => {
  if (!config.agentToken || !config.orgId) {
    return {
      success: false,
      error: "missing_laserreach_external_agent_env",
      required_env: ["LASERREACH_AGENT_TOKEN", "LASERREACH_ORG_ID"],
    };
  }

  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 15_000);
  try {
    const response = await fetch(`${config.apiBase}/api/abm/agent/capabilities`, {
      method: "GET",
      headers: {
        authorization: `Bearer ${config.agentToken}`,
        "x-org-id": config.orgId,
      },
      signal: controller.signal,
    });
    const body = await response.json().catch(() => ({}));
    return {
      status: response.status,
      ok: response.ok,
      ...body,
    };
  } finally {
    clearTimeout(timeout);
  }
};

const buildPrompt = ({ eventId, receivedAt, payload, capabilities, eventFile }) => `You are Codex running locally for a Laserreach user.

Handle this Laserreach webhook/event payload using the user's local workspace and the Laserreach external-agent API.

Connection:
- Use environment variable LASERREACH_API_BASE for the API base.
- Use environment variable LASERREACH_AGENT_TOKEN for Authorization: Bearer.
- Use environment variable LASERREACH_ORG_ID for X-Org-ID.
- Never print or store the token.

Safety:
- Prefer local planning and local memory.
- Use Laserreach APIs for source-of-truth account context, workspace files, policies, schedules, recommendations, browser sessions, content, ads, and intel.
- Do not start, steer, cancel, retry, or prune Laserreach-hosted runs unless the event explicitly requests that and the capabilities response includes agent-runs:control.
- Stage high-value or outbound actions for human approval unless the payload and Laserreach policy clearly allow the action.

Event:
- event_id: ${eventId}
- received_at: ${receivedAt}
- local_event_file: ${eventFile}

Laserreach capabilities response:
${JSON.stringify(capabilities, null, 2)}

Webhook/event payload:
${JSON.stringify(payload, null, 2)}

Produce a concise action summary, then perform only the clearly authorized local or Laserreach API steps.`;

const parseCodexArgs = () => {
  if (config.codexArgsJson) {
    const parsed = JSON.parse(config.codexArgsJson);
    if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string")) {
      throw new Error("CODEX_ARGS_JSON must be a JSON array of strings");
    }
    return parsed;
  }
  return config.codexArgs.split(/\s+/).filter(Boolean);
};

const runCodex = async ({ eventId, prompt }) => {
  await ensureOutDir();
  const promptFile = join(config.outDir, "prompts", `${eventId}.md`);
  const logFile = join(config.outDir, "logs", `${eventId}.log`);
  await writeFile(promptFile, prompt, "utf8");

  if (!config.codexAutoRun) {
    return {
      started: false,
      prompt_file: promptFile,
      run_hint: `CODEX_AUTO_RUN=1 node ${process.argv[1]}`,
    };
  }

  const args = parseCodexArgs();
  if (config.codexPromptMode === "arg") {
    args.push(prompt);
  }

  const child = spawn(config.codexCommand, args, {
    cwd: process.cwd(),
    env: {
      ...process.env,
      LASERREACH_API_BASE: config.apiBase,
      LASERREACH_AGENT_TOKEN: config.agentToken,
      LASERREACH_ORG_ID: config.orgId,
      LASERREACH_EVENT_PROMPT_FILE: promptFile,
    },
    stdio: ["pipe", "pipe", "pipe"],
  });

  if (config.codexPromptMode === "stdin") {
    child.stdin.end(prompt);
  } else {
    child.stdin.end();
  }

  const log = [];
  child.stdout.on("data", (chunk) => log.push(chunk));
  child.stderr.on("data", (chunk) => log.push(chunk));
  child.on("close", async (code, signal) => {
    const footer = `\n\n[codex exited code=${code} signal=${signal || ""}]\n`;
    await writeFile(logFile, Buffer.concat([...log, Buffer.from(footer)]));
  });

  return {
    started: true,
    pid: child.pid,
    prompt_file: promptFile,
    log_file: logFile,
  };
};

const handleEvent = async (req, res) => {
  const rawBody = await readBody(req);
  const signature =
    req.headers["x-laserreach-signature"] ||
    req.headers["x-hub-signature-256"] ||
    req.headers["x-signature"] ||
    "";

  if (!verifySignature(rawBody, signature, config.webhookSecret)) {
    json(res, 401, { success: false, error: "invalid_signature" });
    return;
  }

  let payload;
  try {
    payload = JSON.parse(rawBody.toString("utf8") || "{}");
  } catch {
    json(res, 400, { success: false, error: "invalid_json" });
    return;
  }

  const eventId = eventIdFromPayload(payload);
  if (seenEventIds.has(eventId)) {
    json(res, 202, { success: true, duplicate: true, event_id: eventId });
    return;
  }
  seenEventIds.add(eventId);

  const receivedAt = new Date().toISOString();
  const eventRecord = { event_id: eventId, received_at: receivedAt, payload };
  await writeEvent(eventRecord);
  const eventFile = join(config.outDir, "events.jsonl");
  const capabilities = await fetchCapabilities();
  const prompt = buildPrompt({ eventId, receivedAt, payload, capabilities, eventFile });
  const codex = await runCodex({ eventId, prompt });

  json(res, 202, {
    success: true,
    event_id: eventId,
    codex,
  });
};

const server = createServer(async (req, res) => {
  try {
    const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
    if (req.method === "GET" && url.pathname === "/health") {
      json(res, 200, {
        success: true,
        service: "laserreach-codex-webhook-bridge",
        webhook_path: config.endpoint,
        codex_auto_run: config.codexAutoRun,
      });
      return;
    }
    if (req.method === "POST" && url.pathname === config.endpoint) {
      await handleEvent(req, res);
      return;
    }
    json(res, 404, { success: false, error: "not_found", webhook_path: config.endpoint });
  } catch (error) {
    json(res, 500, { success: false, error: error?.message || "bridge_error" });
  }
});

await ensureOutDir();
server.listen(config.port, config.host, () => {
  console.log(`Laserreach Codex webhook bridge listening on http://${config.host}:${config.port}${config.endpoint}`);
  console.log(`Health check: http://${config.host}:${config.port}/health`);
  if (!config.webhookSecret) {
    console.log("LASERREACH_WEBHOOK_SECRET is not set. Use a secret before exposing this listener.");
  }
  if (!config.codexAutoRun) {
    console.log("CODEX_AUTO_RUN is off. Events will create prompt files without launching Codex.");
  }
});
