/**
 * pi-web-chat extension
 *
 * - `pi --web`  → start UI server daemon and exit (no TUI)
 * - `/web`      → start | stop | status | <port> [--host <addr>|--lan] inside a normal pi session
 */
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { execFileSync, spawn } from "node:child_process";
import {
  existsSync,
  mkdirSync,
  openSync,
  readFileSync,
  unlinkSync,
  writeFileSync,
} from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
const SERVER = join(ROOT, "dist", "index.js");
const STATE_DIR = join(homedir(), ".pi", "web-chat");
const PID_FILE = join(STATE_DIR, "pi-web-chat.pid");
const PORT_FILE = join(STATE_DIR, "pi-web-chat.port");
const HOST_FILE = join(STATE_DIR, "pi-web-chat.host");
const LOG_FILE = join(STATE_DIR, "pi-web-chat.log");
const DEFAULT_PORT = "3141";
const DEFAULT_HOST = "127.0.0.1";
const LAN_HOST = "0.0.0.0";

type StartResult =
  | { ok: true; port: string; host: string; already: boolean; pid: number }
  | { ok: false; error: string };

function readPid(): number | null {
  try {
    if (!existsSync(PID_FILE)) return null;
    const pid = Number(readFileSync(PID_FILE, "utf8").trim());
    if (!Number.isFinite(pid) || pid <= 0) return null;
    process.kill(pid, 0); // throws if not running
    return pid;
  } catch {
    try {
      unlinkSync(PID_FILE);
    } catch {
      /* ignore */
    }
    return null;
  }
}

function readPort(): string {
  try {
    if (existsSync(PORT_FILE)) {
      const p = readFileSync(PORT_FILE, "utf8").trim();
      if (p) return p;
    }
  } catch {
    /* ignore */
  }
  return process.env.PORT ?? DEFAULT_PORT;
}

function readHost(): string {
  try {
    if (existsSync(HOST_FILE)) {
      const h = readFileSync(HOST_FILE, "utf8").trim();
      if (h) return h;
    }
  } catch {
    /* ignore */
  }
  return process.env.HOST ?? DEFAULT_HOST;
}

function isLoopbackHost(host: string): boolean {
  return (
    host === "127.0.0.1" ||
    host === "localhost" ||
    host === "::1" ||
    host === "[::1]"
  );
}

function urlFor(port: string, host = readHost()): string {
  const displayHost =
    host === "0.0.0.0" || host === "::" || host === "[::]" ? "localhost" : host;
  const needsBrackets = displayHost.includes(":") && !displayHost.startsWith("[");
  const formatted = needsBrackets ? `[${displayHost}]` : displayHost;
  return `http://${formatted}:${port}`;
}

function describeServer(port: string, host: string, pid: number): string {
  const bindNote = isLoopbackHost(host) ? "" : ` (bind ${host})`;
  return `${urlFor(port, host)}${bindNote} (pid ${pid})`;
}

function startServer(port: string, host: string): StartResult {
  if (!existsSync(SERVER)) {
    return {
      ok: false,
      error:
        "build missing (dist/index.js). Rebuild the package (`npm run build`) or reinstall pi-web-chat.",
    };
  }

  const existing = readPid();
  if (existing !== null) {
    return {
      ok: true,
      port: readPort(),
      host: readHost(),
      already: true,
      pid: existing,
    };
  }

  mkdirSync(STATE_DIR, { recursive: true });
  const logFd = openSync(LOG_FILE, "a");

  const child = spawn(process.execPath, [SERVER], {
    cwd: ROOT,
    env: { ...process.env, PORT: port, HOST: host },
    detached: true,
    stdio: ["ignore", logFd, logFd],
  });
  child.unref();

  if (!child.pid) {
    return { ok: false, error: "failed to spawn server process" };
  }

  writeFileSync(PID_FILE, `${child.pid}\n`, "utf8");
  writeFileSync(PORT_FILE, `${port}\n`, "utf8");
  writeFileSync(HOST_FILE, `${host}\n`, "utf8");
  return { ok: true, port, host, already: false, pid: child.pid };
}

function clearStateFiles(): void {
  for (const file of [PID_FILE, PORT_FILE, HOST_FILE]) {
    try {
      unlinkSync(file);
    } catch {
      /* ignore */
    }
  }
}

function sleepSync(ms: number): void {
  const clamped = Math.max(1, Math.min(ms, 5_000));
  try {
    execFileSync(
      process.execPath,
      [
        "-e",
        `Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,${clamped})`,
      ],
      { stdio: "ignore", timeout: clamped + 1_000 },
    );
  } catch {
    /* ignore timeout / platform quirks */
  }
}

function stopServer(opts: { waitMs?: number } = {}): {
  stopped: boolean;
  pid?: number;
} {
  const pid = readPid();
  if (pid === null) {
    clearStateFiles();
    return { stopped: false };
  }

  try {
    process.kill(pid, "SIGTERM");
  } catch {
    /* already dead */
  }

  const waitMs = opts.waitMs ?? 0;
  if (waitMs > 0) {
    const deadline = Date.now() + waitMs;
    while (Date.now() < deadline && isProcessAlive(pid)) {
      sleepSync(50);
    }
    if (isProcessAlive(pid)) {
      try {
        process.kill(pid, "SIGKILL");
      } catch {
        /* ignore */
      }
      const killDeadline = Date.now() + 1_000;
      while (Date.now() < killDeadline && isProcessAlive(pid)) {
        sleepSync(50);
      }
    }
  }

  clearStateFiles();
  return { stopped: true, pid };
}

function openBrowser(url: string): void {
  try {
    const cmd =
      process.platform === "darwin"
        ? "open"
        : process.platform === "win32"
          ? "cmd"
          : "xdg-open";
    const args =
      process.platform === "win32" ? ["/c", "start", "", url] : [url];
    spawn(cmd, args, { detached: true, stdio: "ignore" }).unref();
  } catch {
    /* optional */
  }
}

function isProcessAlive(pid: number): boolean {
  try {
    process.kill(pid, 0);
    return true;
  } catch {
    return false;
  }
}

/** Host used for local readiness probes (wildcard binds → loopback). */
function probeHost(host: string): string {
  if (host === "0.0.0.0" || host === "::" || host === "[::]" || host === "localhost") {
    return "127.0.0.1";
  }
  if (host.startsWith("[") && host.endsWith("]")) return host.slice(1, -1);
  return host;
}

/**
 * Block until the daemon serves /api/health (or the process dies / timeout).
 * Implemented via a short-lived Node child so the extension factory can stay
 * synchronous and exit before the pi TUI starts.
 */
function waitForServerReady(
  port: string,
  host: string,
  pid: number,
  timeoutMs = 45_000,
): { ok: true } | { ok: false; error: string } {
  const probe = probeHost(host);
  // Inline waiter keeps the package free of extra bin files.
  const waiter = `
const port = process.argv[1];
const host = process.argv[2];
const pid = Number(process.argv[3]);
const timeoutMs = Number(process.argv[4]);
const healthUrl = "http://" + (host.includes(":") ? "[" + host + "]" : host) + ":" + port + "/api/health";
const started = Date.now();
function alive() {
  try { process.kill(pid, 0); return true; } catch { return false; }
}
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
(async () => {
  let last = "not ready";
  while (Date.now() - started < timeoutMs) {
    if (!alive()) {
      process.stderr.write("dead");
      process.exit(2);
    }
    try {
      const res = await fetch(healthUrl, { signal: AbortSignal.timeout(1000) });
      if (res.ok) process.exit(0);
      last = "HTTP " + res.status;
    } catch (err) {
      last = err && err.message ? err.message : String(err);
    }
    await sleep(150);
  }
  process.stderr.write(last);
  process.exit(1);
})();
`;

  try {
    execFileSync(
      process.execPath,
      ["--input-type=module", "-e", waiter, port, probe, String(pid), String(timeoutMs)],
      {
        encoding: "utf8",
        stdio: ["ignore", "ignore", "pipe"],
        timeout: timeoutMs + 5_000,
      },
    );
    return { ok: true };
  } catch (err) {
    const e = err as {
      status?: number | null;
      stderr?: string;
      signal?: string | null;
      message?: string;
    };
    if (e.status === 2 || !isProcessAlive(pid)) {
      clearStateFiles();
      return {
        ok: false,
        error: `server process exited before becoming ready (see ${LOG_FILE})`,
      };
    }
    const detail = (e.stderr ?? "").trim() || e.message || "unknown error";
    return {
      ok: false,
      error: `server did not become ready within ${Math.round(timeoutMs / 1000)}s (${detail})`,
    };
  }
}

type DaemonAction = "start" | "stop" | "status" | "restart";

type ParsedWebArgs = {
  action: DaemonAction;
  port: string;
  host: string;
  /** True when user explicitly set port (so restart can keep previous otherwise). */
  portExplicit: boolean;
  /** True when user explicitly set host / --lan. */
  hostExplicit: boolean;
};

function defaultPort(): string {
  return process.env.PORT ?? DEFAULT_PORT;
}

function defaultHost(): string {
  return process.env.HOST ?? DEFAULT_HOST;
}

/** Parse port/host tokens shared by `pi --web ...` and `/web ...`. */
function parseWebOptions(
  tokens: string[],
  defaults: { port: string; host: string } = {
    port: defaultPort(),
    host: defaultHost(),
  },
): ParsedWebArgs | { error: string } {
  let action: DaemonAction = "start";
  let port = defaults.port;
  let host = defaults.host;
  let portExplicit = false;
  let hostExplicit = false;
  let sawAction = false;

  for (let i = 0; i < tokens.length; i++) {
    const token = tokens[i]!;

    if (
      token === "stop" ||
      token === "status" ||
      token === "restart" ||
      token === "--restart"
    ) {
      if (sawAction) return { error: `unexpected extra action '${token}'` };
      action = token === "--restart" ? "restart" : token;
      sawAction = true;
      continue;
    }

    if (token === "--lan" || token === "--public" || token === "lan" || token === "public") {
      host = LAN_HOST;
      hostExplicit = true;
      continue;
    }

    if (token === "--host" || token === "-H" || token === "host") {
      const value = tokens[++i];
      if (!value || value.startsWith("-")) {
        return { error: `${token} requires an address (e.g. 0.0.0.0)` };
      }
      host = value;
      hostExplicit = true;
      continue;
    }

    if (token.startsWith("--host=")) {
      const value = token.slice("--host=".length);
      if (!value) return { error: "--host requires an address (e.g. 0.0.0.0)" };
      host = value;
      hostExplicit = true;
      continue;
    }

    if (token.startsWith("-H=")) {
      const value = token.slice("-H=".length);
      if (!value) return { error: "-H requires an address (e.g. 0.0.0.0)" };
      host = value;
      hostExplicit = true;
      continue;
    }

    if (/^\d+$/.test(token)) {
      port = token;
      portExplicit = true;
      continue;
    }

    return {
      error: `unknown argument '${token}' (use port, --host <addr>, --lan, stop, status, restart)`,
    };
  }

  return { action, port, host, portExplicit, hostExplicit };
}

/** Parse `pi --web [stop|status|port] [--host <addr>|--lan]` from raw argv. */
function parseWebDaemonArgs(argv: string[] = process.argv): {
  enabled: boolean;
} & (ParsedWebArgs | { error: string }) {
  const idx = argv.findIndex((arg) => arg === "--web" || arg.startsWith("--web="));
  if (idx === -1) {
    return {
      enabled: false,
      action: "start",
      port: defaultPort(),
      host: defaultHost(),
    };
  }

  const tokens: string[] = [];

  // Allow --host/--lan/--restart before --web; later tokens (after --web) override.
  for (let i = 0; i < idx; i++) {
    const arg = argv[i]!;
    if (arg === "--lan" || arg === "--public" || arg === "--restart") {
      tokens.push(arg);
      continue;
    }
    if (arg === "--host" || arg === "-H") {
      const value = argv[i + 1];
      if (value && !value.startsWith("-")) {
        tokens.push(arg, value);
        i++;
      } else {
        tokens.push(arg);
      }
      continue;
    }
    if (arg.startsWith("--host=") || arg.startsWith("-H=")) {
      tokens.push(arg);
    }
  }

  const eq = argv[idx]!.startsWith("--web=") ? argv[idx]!.slice("--web=".length) : undefined;
  if (eq !== undefined && eq.length > 0) tokens.push(eq);

  for (let i = idx + 1; i < argv.length; i++) {
    const arg = argv[i]!;
    // Stop at next top-level pi flag, but keep our own web flags.
    if (
      arg.startsWith("-") &&
      arg !== "--lan" &&
      arg !== "--public" &&
      arg !== "--restart" &&
      arg !== "--host" &&
      arg !== "-H" &&
      !arg.startsWith("--host=") &&
      !arg.startsWith("-H=")
    ) {
      break;
    }
    tokens.push(arg);
  }

  const parsed = parseWebOptions(tokens);
  if ("error" in parsed) return { enabled: true, error: parsed.error };
  return { enabled: true, ...parsed };
}

/** Resolve port/host for start/restart, keeping previous bind on bare restart. */
function resolveLaunchTarget(parsed: ParsedWebArgs): { port: string; host: string } {
  const running = readPid() !== null;
  const prevPort = running ? readPort() : null;
  const prevHost = running ? readHost() : null;
  return {
    port: parsed.portExplicit ? parsed.port : (prevPort ?? parsed.port),
    host: parsed.hostExplicit ? parsed.host : (prevHost ?? parsed.host),
  };
}

function launchDaemon(
  port: string,
  host: string,
  opts: { openBrowser: boolean; verb: "started" | "restarted" },
): void {
  const result = startServer(port, host);
  if (!result.ok) {
    console.error(`pi-web-chat: ${result.error}`);
    process.exit(1);
  }

  const url = urlFor(result.port, result.host);
  const summary = describeServer(result.port, result.host, result.pid);

  if (result.already) {
    console.log(`pi-web-chat already running — ${summary}`);
    if (opts.openBrowser) openBrowser(url);
    process.exit(0);
  }

  process.stdout.write(`pi-web-chat starting (pid ${result.pid})…`);
  const ready = waitForServerReady(result.port, result.host, result.pid);
  if (!ready.ok) {
    process.stdout.write("\n");
    console.error(`pi-web-chat: ${ready.error}`);
    process.exit(1);
  }
  process.stdout.write(" ready\n");

  console.log(`pi-web-chat ${opts.verb} — ${summary}`);
  if (!isLoopbackHost(result.host)) {
    console.log(
      "  warning: bound beyond loopback with no app auth — use only on trusted networks",
    );
  }
  console.log(`  stop:    pi --web stop`);
  console.log(`  restart: pi --web restart`);
  console.log(`  status:  pi --web status`);
  console.log(`  logs:    ${LOG_FILE}`);
  if (opts.openBrowser) openBrowser(url);
}

function runDaemonAndExit(): void {
  const parsed = parseWebDaemonArgs();
  if ("error" in parsed) {
    console.error(`pi-web-chat: ${parsed.error}`);
    process.exit(1);
  }

  const { action } = parsed;

  if (action === "stop") {
    const { stopped, pid } = stopServer({ waitMs: 3_000 });
    console.log(stopped ? `pi-web-chat stopped (pid ${pid})` : "pi-web-chat is not running");
    process.exit(0);
  }

  if (action === "status") {
    const pid = readPid();
    if (pid === null) {
      console.log("pi-web-chat is not running");
      process.exit(1);
    }
    console.log(`pi-web-chat running — ${describeServer(readPort(), readHost(), pid)}`);
    process.exit(0);
  }

  if (action === "restart") {
    const { port, host } = resolveLaunchTarget(parsed);
    const { stopped, pid } = stopServer({ waitMs: 5_000 });
    if (stopped) {
      console.log(`pi-web-chat stopped (pid ${pid})`);
    }
    launchDaemon(port, host, { openBrowser: true, verb: "restarted" });
    process.exit(0);
  }

  // start
  const { port, host } = resolveLaunchTarget(parsed);
  launchDaemon(port, host, { openBrowser: true, verb: "started" });
  // Detached server keeps running; do not open pi TUI.
  process.exit(0);
}

export default function (pi: ExtensionAPI) {
  pi.registerFlag("web", {
    description:
      "Start pi-web-chat UI in background and exit (no TUI). Options: [port] [--host <addr>|--lan] [stop|status|restart]",
    type: "boolean",
    default: false,
  });

  pi.registerFlag("host", {
    description:
      "Bind address for pi-web-chat when used with --web (default 127.0.0.1; use 0.0.0.0 for LAN)",
    type: "string",
  });

  pi.registerFlag("lan", {
    description: "With --web, bind pi-web-chat to 0.0.0.0 (LAN access; no app auth)",
    type: "boolean",
    default: false,
  });

  pi.registerFlag("restart", {
    description: "With --web, stop the existing daemon (if any) and start a fresh one",
    type: "boolean",
    default: false,
  });

  // Handle --web as early as possible during extension load, before TUI starts.
  // getFlag() is not populated yet at factory time, so read argv directly.
  // waitForServerReady is synchronous so we exit only after the daemon is up.
  if (parseWebDaemonArgs().enabled) {
    runDaemonAndExit();
  }

  pi.registerCommand("web", {
    description:
      "pi-web-chat UI: /web [port] [--host <addr>|--lan] | /web stop | /web status | /web restart",
    handler: async (args, ctx) => {
      const tokens = args.trim() ? args.trim().split(/\s+/) : [];
      const parsed = parseWebOptions(tokens);
      if ("error" in parsed) {
        ctx.ui.notify(`pi-web-chat: ${parsed.error}`, "error");
        return;
      }

      const { action } = parsed;

      if (action === "stop") {
        const { stopped, pid } = stopServer({ waitMs: 3_000 });
        ctx.ui.notify(
          stopped ? `pi-web-chat stopped (pid ${pid})` : "pi-web-chat is not running",
          "info",
        );
        return;
      }

      if (action === "status") {
        const pid = readPid();
        if (pid === null) {
          ctx.ui.notify("pi-web-chat is not running", "info");
          return;
        }
        ctx.ui.notify(
          `pi-web-chat running — ${describeServer(readPort(), readHost(), pid)}`,
          "info",
        );
        return;
      }

      if (action === "restart") {
        const { port, host } = resolveLaunchTarget(parsed);
        const { stopped, pid } = stopServer({ waitMs: 5_000 });
        if (stopped) {
          ctx.ui.notify(`pi-web-chat stopped (pid ${pid})`, "info");
        }
        const result = startServer(port, host);
        if (!result.ok) {
          ctx.ui.notify(`pi-web-chat: ${result.error}`, "error");
          return;
        }
        ctx.ui.notify(`pi-web-chat starting (pid ${result.pid})…`, "info");
        const ready = waitForServerReady(result.port, result.host, result.pid);
        if (!ready.ok) {
          ctx.ui.notify(`pi-web-chat: ${ready.error}`, "error");
          return;
        }
        const summary = describeServer(result.port, result.host, result.pid);
        const warning = !isLoopbackHost(result.host)
          ? " — warning: non-loopback bind, no app auth"
          : "";
        ctx.ui.notify(`pi-web-chat restarted — ${summary}${warning}`, "info");
        return;
      }

      // start
      const { port, host } = resolveLaunchTarget(parsed);
      const result = startServer(port, host);
      if (!result.ok) {
        ctx.ui.notify(`pi-web-chat: ${result.error}`, "error");
        return;
      }

      if (!result.already) {
        ctx.ui.notify(`pi-web-chat starting (pid ${result.pid})…`, "info");
        const ready = waitForServerReady(result.port, result.host, result.pid);
        if (!ready.ok) {
          ctx.ui.notify(`pi-web-chat: ${ready.error}`, "error");
          return;
        }
      }

      const summary = describeServer(result.port, result.host, result.pid);
      const warning =
        !result.already && !isLoopbackHost(result.host)
          ? " — warning: non-loopback bind, no app auth"
          : "";
      ctx.ui.notify(
        result.already
          ? `pi-web-chat already running — ${summary}`
          : `pi-web-chat started — ${summary}${warning}`,
        "info",
      );
    },
  });
}
