#!/usr/bin/env bun
// od-incidents — CLI over the collector's incident-dispatch API.
//
//   od-incidents list [--type T] [--state S]           table: id, state, type, priority, title
//   od-incidents show <id>                              full record incl. brief + metadata
//   od-incidents search <query>                         title/description substring match
//   od-incidents resolve <id> --artifact <ref> [--summary <line>]
//
// Base URL / auth mirror apps/web/src/pages/api/collector/[...path].ts and
// collector/src/paths.ts. Base URL resolution (no server env to inherit, unlike the
// web proxy, so this CLI reads the same config.toml the collector itself reads its
// port from): COLLECTOR_URL env > port key in ${OVERDECK_CONFIG_DIR:-~/.config/overdeck}/config.toml
// (loopback, that file's own bindHost) > http://127.0.0.1:4980. Token: COLLECTOR_TOKEN
// env > ${OVERDECK_CONFIG_DIR:-~/.config/overdeck}/token, sent as `Authorization: Bearer <token>`.
//
// The resolve subcommand targets `POST /actions/incident.resolve` with the
// action-verb-gateway body shape `{ args: { incidentId, artifact, summary? } }`
// (collector/src/actions.ts ALLOWED_ACTION_VERBS + RequestBodySchema).

import { readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { spawnSync } from "node:child_process";

const DEFAULT_COLLECTOR_URL = "http://127.0.0.1:4980";

function configDir() {
  return process.env.OVERDECK_CONFIG_DIR ?? join(homedir(), ".config", "overdeck");
}

function collectorBaseUrl() {
  if (process.env.COLLECTOR_URL) return process.env.COLLECTOR_URL;
  try {
    const raw = readFileSync(join(configDir(), "config.toml"), "utf8");
    const config = Bun.TOML.parse(raw);
    if (config.port) return `http://${config.bindHost || "127.0.0.1"}:${config.port}`;
  } catch {
    // no config.toml (or unreadable/unparsable) — fall through to the code default
  }
  return DEFAULT_COLLECTOR_URL;
}

function resolveToken() {
  if (process.env.COLLECTOR_TOKEN) return process.env.COLLECTOR_TOKEN;
  try {
    return readFileSync(join(configDir(), "token"), "utf8").trim();
  } catch {
    return "";
  }
}

class CollectorRequestError extends Error {
  constructor(status, body) {
    super(`collector request failed: ${status}`);
    this.status = status;
    this.body = body;
  }
}

async function collectorFetch(path, init = {}) {
  const url = new URL(path, collectorBaseUrl());
  const headers = new Headers(init.headers ?? {});
  headers.set("authorization", `Bearer ${resolveToken()}`);
  if (init.body !== undefined) headers.set("content-type", "application/json");
  let res;
  try {
    res = await fetch(url, { ...init, headers });
  } catch (cause) {
    throw new Error(`could not reach collector at ${url.origin}: ${cause.message ?? cause}`);
  }
  const text = await res.text();
  if (!res.ok) throw new CollectorRequestError(res.status, text);
  return text.length ? JSON.parse(text) : null;
}

function usage(message) {
  const err = new Error(
    message ??
      "usage: od-incidents <list [--type T] [--state S]|show <id>|search <query>|resolve <id> --artifact <ref> [--summary <line>]>"
  );
  err.usage = true;
  throw err;
}

function parseFlags(args, valueFlags) {
  const flags = {};
  const positional = [];
  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    if (arg.startsWith("--")) {
      const name = arg.slice(2);
      if (!valueFlags.has(name)) usage(`unknown flag --${name}`);
      const value = args[++i];
      if (value === undefined) usage(`--${name} requires a value`);
      flags[name] = value;
    } else {
      positional.push(arg);
    }
  }
  return { flags, positional };
}

// Incident.type is not yet on the server's Incident interface (collector/src/incidents/
// incident-service.ts) — the spec's overdeck.incident_type metadata key is a sibling
// lane's addition, not yet landed. Read whichever shape shows up so `list --type`
// keeps working once it does, instead of silently matching nothing.
function incidentType(inc) {
  return inc.type ?? inc.metadata?.["overdeck.incident_type"] ?? null;
}

function printTable(rows, columns) {
  const widths = columns.map((c) =>
    Math.max(c.length, ...rows.map((r) => String(r[c] ?? "").length))
  );
  const line = (cells) => cells.map((c, i) => String(c).padEnd(widths[i])).join("  ");
  console.log(line(columns));
  for (const row of rows) console.log(line(columns.map((c) => row[c] ?? "")));
}

function boundedLimit(value) {
  if (value === undefined) return 50;
  if (!/^\d+$/.test(value)) usage("--limit must be an integer between 1 and 100");
  const limit = Number(value);
  if (limit < 1 || limit > 100) usage("--limit must be between 1 and 100");
  return limit;
}

async function fetchAllIncidents(params, pageSize) {
  const incidents = [];
  let cursor;
  let highWater;
  for (;;) {
    params.set("limit", String(pageSize));
    if (cursor === undefined) params.delete("cursor"); else params.set("cursor", String(cursor));
    if (highWater === undefined) params.delete("highWater"); else params.set("highWater", String(highWater));
    const data = await collectorFetch(`/incidents?${params}`);
    incidents.push(...(data.incidents ?? data));
    if (!data.page || data.page.exhausted) return incidents;
    if (!Number.isSafeInteger(data.page.nextCursor) || !Number.isSafeInteger(data.page.highWater)) {
      throw new Error("collector returned invalid incident pagination metadata");
    }
    cursor = data.page.nextCursor;
    highWater = data.page.highWater;
  }
}

async function cmdList(args) {
  const { flags } = parseFlags(args, new Set(["type", "state", "limit"]));
  const limit = boundedLimit(flags.limit);
  const params = new URLSearchParams();
  params.set("scope", "all");
  params.set("limit", String(limit));
  const data = await fetchAllIncidents(params, limit);
  let incidents = data;
  if (flags.type) incidents = incidents.filter((inc) => incidentType(inc) === flags.type);
  if (flags.state) incidents = incidents.filter((inc) => inc.state === flags.state);
  incidents = incidents.slice(0, limit);
  printTable(
    incidents.map((inc) => ({
      id: inc.id,
      state: inc.state,
      type: incidentType(inc) ?? "-",
      priority: inc.priority ?? "-",
      title: inc.title,
    })),
    ["id", "state", "type", "priority", "title"]
  );
}

async function cmdShow(args) {
  const [id] = args;
  if (!id) usage("show requires <id>");
  const incident = await collectorFetch(`/incidents/${encodeURIComponent(id)}`);
  console.log(JSON.stringify(incident, null, 2));
}

async function cmdSearch(args) {
  const { flags, positional } = parseFlags(args, new Set(["limit"]));
  const query = positional.join(" ");
  if (!query) usage("search requires <query>");
  const limit = boundedLimit(flags.limit);
  const params = new URLSearchParams({ scope: "all", query, limit: String(limit) });
  const incidents = (await fetchAllIncidents(params, limit)).slice(0, limit);
  printTable(
    incidents.map((inc) => ({
      id: inc.id,
      state: inc.state,
      type: incidentType(inc) ?? "-",
      priority: inc.priority ?? "-",
      title: inc.title,
    })),
    ["id", "state", "type", "priority", "title"]
  );
}

async function cmdResolve(args) {
  const { flags, positional } = parseFlags(args, new Set(["artifact", "summary"]));
  const [id] = positional;
  if (!id) usage("resolve requires <id>");
  if (!flags.artifact || flags.artifact.trim() === "") {
    const err = new Error(
      "resolve requires --artifact <ref>: resolution must be a checkable artifact, not a claim"
    );
    err.exitCode = 2;
    throw err;
  }
  const resolveArgs = { incidentId: id, artifact: flags.artifact };
  if (flags.summary) resolveArgs.summary = flags.summary;
  const result = await collectorFetch(`/actions/incident.resolve`, {
    method: "POST",
    body: JSON.stringify({ args: resolveArgs }),
  });
  spawnSync("od-live-report-refresh", { stdio: "inherit" });
  console.log(JSON.stringify(result, null, 2));
}

async function main() {
  const [cmd, ...rest] = process.argv.slice(2);
  if (!cmd) usage();
  switch (cmd) {
    case "list":
      return cmdList(rest);
    case "show":
      return cmdShow(rest);
    case "search":
      return cmdSearch(rest);
    case "resolve":
      return cmdResolve(rest);
    default:
      usage(`unknown subcommand: ${cmd}`);
  }
}

main().catch((err) => {
  if (err instanceof CollectorRequestError) {
    console.error(`collector error: HTTP ${err.status}`);
    console.error(err.body);
    process.exit(1);
  }
  console.error(err.message ?? String(err));
  process.exit(err.exitCode ?? (err.usage ? 2 : 1));
});
