/**
 * AstroAgent host — the generic runtime that hosts pluggable agents.
 *
 * Provides the shared primitives every agent reuses (Claude spawn, build, git,
 * the PHP-CLI DB bridge, SSE job model, a route registry, and a scheduler) and
 * knows NOTHING agent-specific. Agent modules under ./agents/ call
 * `host.route(...)` / `host.everyMinutes(...)` and use `host.runClaudeJson` etc.
 *
 * Reached only through nginx (auth_request against the PHP admin session); this
 * process does not re-check auth. Node built-ins only — no dependencies.
 */

import { createServer } from "node:http";
import { spawn } from "node:child_process";
import { randomUUID } from "node:crypto";
import { readFileSync, writeFileSync, existsSync, rmSync, mkdirSync } from "node:fs";
import { dirname, resolve, join } from "node:path";
import { fileURLToPath } from "node:url";

const HERE = dirname(fileURLToPath(import.meta.url));      // agents/console
const REPO = resolve(HERE, "..", "..");                    // repo root
const APP = join(REPO, "app");
const PREVIEW_DIR = join(REPO, "public-preview");
const ENV_FILE = join(REPO, "agents", ".env");
const CLAUDE = "/usr/local/bin/claude";
const PHP = "/usr/bin/php";
const PORT = Number(process.env.ADMIN_PORT || 3011);
const AGENT_TOOLS = "Read Write Edit Glob Grep WebSearch";
const DEFAULT_MODEL = "claude-sonnet-4-6";
const GIT_SCOPE = ["app", "brand", "api/db", "api/cli"];   // paths the console may commit

// ---- env (.env) --------------------------------------------------------------
function loadEnv() {
  const out = {};
  if (existsSync(ENV_FILE)) {
    for (const line of readFileSync(ENV_FILE, "utf8").split("\n")) {
      const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
      if (m) out[m[1]] = m[2];
    }
  }
  return out;
}
function saveToken(token) {
  let s = existsSync(ENV_FILE) ? readFileSync(ENV_FILE, "utf8") : "";
  if (/^CLAUDE_CODE_OAUTH_TOKEN=.*$/m.test(s)) {
    s = s.replace(/^CLAUDE_CODE_OAUTH_TOKEN=.*$/m, `CLAUDE_CODE_OAUTH_TOKEN=${token}`);
  } else {
    s += `\nCLAUDE_CODE_OAUTH_TOKEN=${token}\n`;
  }
  writeFileSync(ENV_FILE, s, { mode: 0o600 });
}
function agentEnv() {
  const env = loadEnv();
  return {
    ...process.env,
    HOME: process.env.HOME || "/var/lib/carlos-arias-agent",
    PATH: "/usr/local/bin:/usr/bin:/bin",
    CLAUDE_CODE_OAUTH_TOKEN: env.CLAUDE_CODE_OAUTH_TOKEN || "",
  };
}

// ---- Claude spawn (the ONE place agents invoke claude) -----------------------
/** One-shot: `claude -p … --output-format json`. Resolves {ok, result}. */
function runClaudeJson({ prompt, tools = "", model }) {
  return new Promise((res) => {
    const child = spawn(CLAUDE, ["-p", prompt, "--output-format", "json", "--model", model || DEFAULT_MODEL, "--allowedTools", tools], { cwd: REPO, env: agentEnv() });
    let out = "";
    child.stdout.on("data", (d) => (out += d));
    child.on("close", () => {
      try { const env = JSON.parse(out); res({ ok: true, result: typeof env.result === "string" ? env.result : "" }); }
      catch { res({ ok: false, result: "" }); }
    });
    child.on("error", () => res({ ok: false, result: "" }));
  });
}
/** Streaming: `claude -p … --output-format stream-json`. Calls onMessage per NDJSON line; returns the child so the caller wires close/error. */
function runClaudeStream({ prompt, tools = AGENT_TOOLS, model, resume, onMessage, onStderr, onClose, onError }) {
  const args = ["-p", prompt, "--output-format", "stream-json", "--verbose", "--allowedTools", tools, "--model", model || DEFAULT_MODEL];
  if (resume) args.push("--resume", resume);
  const child = spawn(CLAUDE, args, { cwd: REPO, env: agentEnv() });
  let buf = "";
  child.stdout.on("data", (chunk) => {
    buf += chunk.toString();
    let nl;
    while ((nl = buf.indexOf("\n")) >= 0) {
      const line = buf.slice(0, nl).trim();
      buf = buf.slice(nl + 1);
      if (!line) continue;
      let msg; try { msg = JSON.parse(line); } catch { continue; }
      onMessage && onMessage(msg);
    }
  });
  child.stderr.on("data", (d) => onStderr && onStderr(d.toString()));
  child.on("close", (code) => onClose && onClose(code));
  child.on("error", (err) => onError && onError(err));
  return child;
}

// ---- build / git / php-cli ---------------------------------------------------
function build(extraEnv) {
  return new Promise((res) => {
    const child = spawn("npm", ["run", "build"], { cwd: APP, env: { ...agentEnv(), ...extraEnv } });
    let tail = "";
    const grab = (d) => { tail = (tail + d.toString()).slice(-4000); };
    child.stdout.on("data", grab);
    child.stderr.on("data", grab);
    child.on("close", (code) => res({ ok: code === 0, tail }));
    child.on("error", () => res({ ok: false, tail: "build failed to start" }));
  });
}
function git(args) {
  return new Promise((res) => {
    const child = spawn("git", args, { cwd: REPO, env: agentEnv() });
    let tail = "";
    child.stdout.on("data", (d) => (tail += d));
    child.stderr.on("data", (d) => (tail += d));
    child.on("close", (code) => res({ ok: code === 0, tail: tail.toString() }));
    child.on("error", () => res({ ok: false, tail: "" }));
  });
}
function phpCli(args) {
  return new Promise((res) => {
    const child = spawn(PHP, args, { cwd: REPO, env: agentEnv() });
    let out = "", err = "";
    child.stdout.on("data", (d) => (out += d));
    child.stderr.on("data", (d) => (err += d));
    child.on("close", () => res({ out, err }));
    child.on("error", () => res({ out: "", err: "php spawn failed" }));
  });
}

// ---- HTTP helpers ------------------------------------------------------------
function json(res, status, obj) {
  res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
  res.end(JSON.stringify(obj));
}
function readBody(req) {
  return new Promise((res) => {
    let b = "";
    req.on("data", (c) => (b += c));
    req.on("end", () => { try { res(b ? JSON.parse(b) : {}); } catch { res({}); } });
  });
}

// ---- SSE job model -----------------------------------------------------------
const jobs = new Map();
function newJob(page) {
  const conversationId = randomUUID();
  const job = {
    conversationId,
    jobId: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`,
    sessionId: null,
    page: page || "/",
    events: [],
    clients: new Set(),
    done: false,
  };
  jobs.set(conversationId, job);
  return job;
}
function emit(job, ev) {
  job.events.push(ev);
  const line = `data: ${JSON.stringify(ev)}\n\n`;
  for (const res of job.clients) { try { res.write(line); } catch {} }
}

// ---- the host singleton ------------------------------------------------------
const _routes = [];
const _schedules = [];

export const host = {
  // paths / constants (Phase 2: source from config)
  REPO, APP, PREVIEW_DIR, CLAUDE, PHP, PORT, AGENT_TOOLS, DEFAULT_MODEL, gitScope: GIT_SCOPE,
  // node fs re-exports agents need
  rmSync, join,
  // shared single-flight lock (mutable by reference across agent modules)
  state: { busy: false },
  // env + claude + ops + http + SSE
  loadEnv, saveToken, agentEnv,
  runClaudeJson, runClaudeStream,
  build, git, phpCli,
  json, readBody,
  jobs, newJob, emit,

  /** Register an HTTP route. method "ANY" matches any verb. */
  route(method, path, handler) { _routes.push({ method, path, handler }); },

  /** Run fn every `min` minutes, plus one pulse `bootDelayMs` after listen(). */
  everyMinutes(min, fn, { bootDelayMs = 90_000 } = {}) {
    if (min > 0) _schedules.push({ min, fn, bootDelayMs });
  },

  /** Start the server + scheduled tasks. */
  listen() {
    const server = createServer(async (req, res) => {
      const url = new URL(req.url, "http://localhost");
      const path = url.pathname.replace(/^\/devconsole/, "") || "/";
      const method = req.method || "GET";
      try {
        const route = _routes.find((r) => r.path === path && (r.method === "ANY" || r.method === method));
        if (!route) return json(res, 404, { error: "not found" });
        await route.handler(req, res, { url, path, method });
      } catch (err) { json(res, 500, { error: err.message }); }
    });
    if (!existsSync(PREVIEW_DIR)) mkdirSync(PREVIEW_DIR, { recursive: true });
    server.listen(PORT, "127.0.0.1", () => console.log(`[console] runner on 127.0.0.1:${PORT}, repo ${REPO}`));
    for (const s of _schedules) {
      setTimeout(s.fn, s.bootDelayMs);
      setInterval(s.fn, s.min * 60_000);
    }
  },
};

// ---- core routes (token/health — framework-level, agent-independent) ---------
host.route("ANY", "/ping", (req, res) => {
  const env = loadEnv();
  json(res, 200, { authed: true, hasToken: Boolean(env.CLAUDE_CODE_OAUTH_TOKEN) });
});
host.route("POST", "/auth", async (req, res) => {
  const { token } = await readBody(req);
  if (!token || !/^sk-ant-/.test(token)) return json(res, 400, { error: "That doesn't look like a Claude token (expected sk-ant-…)." });
  saveToken(token.trim());
  json(res, 200, { ok: true });
});
host.route("POST", "/logout", (req, res) => json(res, 200, { ok: true })); // admin session cleared by PHP
