import fs from "node:fs/promises";
import JSON5 from "json5";
import type { Command } from "commander";

import type { ExecApprovalsAgent, ExecApprovalsFile } from "../infra/exec-approvals.js";
import { defaultRuntime } from "../runtime.js";
import { formatDocsLink } from "../terminal/links.js";
import { theme } from "../terminal/theme.js";
import { callGatewayFromCli } from "./gateway-rpc.js";
import { nodesCallOpts, resolveNodeId } from "./nodes-cli/rpc.js";
import type { NodesRpcOpts } from "./nodes-cli/types.js";

type ExecApprovalsSnapshot = {
  path: string;
  exists: boolean;
  hash: string;
  file: ExecApprovalsFile;
};

type ExecApprovalsCliOpts = NodesRpcOpts & {
  node?: string;
  file?: string;
  stdin?: boolean;
  agent?: string;
};

async function readStdin(): Promise<string> {
  const chunks: Buffer[] = [];
  for await (const chunk of process.stdin) {
    chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
  }
  return Buffer.concat(chunks).toString("utf8");
}

async function resolveTargetNodeId(opts: ExecApprovalsCliOpts): Promise<string | null> {
  const raw = opts.node?.trim() ?? "";
  if (!raw) return null;
  return await resolveNodeId(opts as NodesRpcOpts, raw);
}

async function loadSnapshot(
  opts: ExecApprovalsCliOpts,
  nodeId: string | null,
): Promise<ExecApprovalsSnapshot> {
  const method = nodeId ? "exec.approvals.node.get" : "exec.approvals.get";
  const params = nodeId ? { nodeId } : {};
  const snapshot = (await callGatewayFromCli(method, opts, params)) as ExecApprovalsSnapshot;
  return snapshot;
}

async function saveSnapshot(
  opts: ExecApprovalsCliOpts,
  nodeId: string | null,
  file: ExecApprovalsFile,
  baseHash: string,
): Promise<ExecApprovalsSnapshot> {
  const method = nodeId ? "exec.approvals.node.set" : "exec.approvals.set";
  const params = nodeId ? { nodeId, file, baseHash } : { file, baseHash };
  const snapshot = (await callGatewayFromCli(method, opts, params)) as ExecApprovalsSnapshot;
  return snapshot;
}

function resolveAgentKey(value?: string | null): string {
  const trimmed = value?.trim() ?? "";
  return trimmed ? trimmed : "default";
}

function normalizeAllowlistEntry(entry: { pattern?: string } | null): string | null {
  const pattern = entry?.pattern?.trim() ?? "";
  return pattern ? pattern : null;
}

function ensureAgent(file: ExecApprovalsFile, agentKey: string): ExecApprovalsAgent {
  const agents = file.agents ?? {};
  const entry = agents[agentKey] ?? {};
  file.agents = agents;
  return entry;
}

function isEmptyAgent(agent: ExecApprovalsAgent): boolean {
  const allowlist = Array.isArray(agent.allowlist) ? agent.allowlist : [];
  return (
    !agent.security &&
    !agent.ask &&
    !agent.askFallback &&
    agent.autoAllowSkills === undefined &&
    allowlist.length === 0
  );
}

export function registerExecApprovalsCli(program: Command) {
  const approvals = program
    .command("approvals")
    .alias("exec-approvals")
    .description("Manage exec approvals (gateway or node host)")
    .addHelpText(
      "after",
      () =>
        `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/approvals", "docs.clawd.bot/cli/approvals")}\n`,
    );

  const getCmd = approvals
    .command("get")
    .description("Fetch exec approvals snapshot")
    .option("--node <node>", "Target node id/name/IP (defaults to gateway)")
    .action(async (opts: ExecApprovalsCliOpts) => {
      const nodeId = await resolveTargetNodeId(opts);
      const snapshot = await loadSnapshot(opts, nodeId);
      const payload = opts.json ? JSON.stringify(snapshot) : JSON.stringify(snapshot, null, 2);
      defaultRuntime.log(payload);
    });
  nodesCallOpts(getCmd);

  const setCmd = approvals
    .command("set")
    .description("Replace exec approvals with a JSON file")
    .option("--node <node>", "Target node id/name/IP (defaults to gateway)")
    .option("--file <path>", "Path to JSON file to upload")
    .option("--stdin", "Read JSON from stdin", false)
    .action(async (opts: ExecApprovalsCliOpts) => {
      if (!opts.file && !opts.stdin) {
        defaultRuntime.error("Provide --file or --stdin.");
        defaultRuntime.exit(1);
        return;
      }
      if (opts.file && opts.stdin) {
        defaultRuntime.error("Use either --file or --stdin (not both).");
        defaultRuntime.exit(1);
        return;
      }
      const nodeId = await resolveTargetNodeId(opts);
      const snapshot = await loadSnapshot(opts, nodeId);
      if (!snapshot.hash) {
        defaultRuntime.error("Exec approvals hash missing; reload and retry.");
        defaultRuntime.exit(1);
        return;
      }
      const raw = opts.stdin ? await readStdin() : await fs.readFile(String(opts.file), "utf8");
      let file: ExecApprovalsFile;
      try {
        file = JSON5.parse(raw) as ExecApprovalsFile;
      } catch (err) {
        defaultRuntime.error(`Failed to parse approvals JSON: ${String(err)}`);
        defaultRuntime.exit(1);
        return;
      }
      file.version = 1;
      const next = await saveSnapshot(opts, nodeId, file, snapshot.hash);
      const payload = opts.json ? JSON.stringify(next) : JSON.stringify(next, null, 2);
      defaultRuntime.log(payload);
    });
  nodesCallOpts(setCmd);

  const allowlist = approvals.command("allowlist").description("Edit the per-agent allowlist");

  const allowlistAdd = allowlist
    .command("add <pattern>")
    .description("Add a glob pattern to an allowlist")
    .option("--node <node>", "Target node id/name/IP (defaults to gateway)")
    .option("--agent <id>", 'Agent id (defaults to "default")')
    .action(async (pattern: string, opts: ExecApprovalsCliOpts) => {
      const trimmed = pattern.trim();
      if (!trimmed) {
        defaultRuntime.error("Pattern required.");
        defaultRuntime.exit(1);
        return;
      }
      const nodeId = await resolveTargetNodeId(opts);
      const snapshot = await loadSnapshot(opts, nodeId);
      if (!snapshot.hash) {
        defaultRuntime.error("Exec approvals hash missing; reload and retry.");
        defaultRuntime.exit(1);
        return;
      }
      const file = snapshot.file ?? { version: 1 };
      file.version = 1;
      const agentKey = resolveAgentKey(opts.agent);
      const agent = ensureAgent(file, agentKey);
      const allowlistEntries = Array.isArray(agent.allowlist) ? agent.allowlist : [];
      if (allowlistEntries.some((entry) => normalizeAllowlistEntry(entry) === trimmed)) {
        defaultRuntime.log("Already allowlisted.");
        return;
      }
      allowlistEntries.push({ pattern: trimmed, lastUsedAt: Date.now() });
      agent.allowlist = allowlistEntries;
      file.agents = { ...file.agents, [agentKey]: agent };
      const next = await saveSnapshot(opts, nodeId, file, snapshot.hash);
      const payload = opts.json ? JSON.stringify(next) : JSON.stringify(next, null, 2);
      defaultRuntime.log(payload);
    });
  nodesCallOpts(allowlistAdd);

  const allowlistRemove = allowlist
    .command("remove <pattern>")
    .description("Remove a glob pattern from an allowlist")
    .option("--node <node>", "Target node id/name/IP (defaults to gateway)")
    .option("--agent <id>", 'Agent id (defaults to "default")')
    .action(async (pattern: string, opts: ExecApprovalsCliOpts) => {
      const trimmed = pattern.trim();
      if (!trimmed) {
        defaultRuntime.error("Pattern required.");
        defaultRuntime.exit(1);
        return;
      }
      const nodeId = await resolveTargetNodeId(opts);
      const snapshot = await loadSnapshot(opts, nodeId);
      if (!snapshot.hash) {
        defaultRuntime.error("Exec approvals hash missing; reload and retry.");
        defaultRuntime.exit(1);
        return;
      }
      const file = snapshot.file ?? { version: 1 };
      file.version = 1;
      const agentKey = resolveAgentKey(opts.agent);
      const agent = ensureAgent(file, agentKey);
      const allowlistEntries = Array.isArray(agent.allowlist) ? agent.allowlist : [];
      const nextEntries = allowlistEntries.filter(
        (entry) => normalizeAllowlistEntry(entry) !== trimmed,
      );
      if (nextEntries.length === allowlistEntries.length) {
        defaultRuntime.log("Pattern not found.");
        return;
      }
      if (nextEntries.length === 0) {
        delete agent.allowlist;
      } else {
        agent.allowlist = nextEntries;
      }
      if (isEmptyAgent(agent)) {
        const agents = { ...file.agents };
        delete agents[agentKey];
        file.agents = Object.keys(agents).length > 0 ? agents : undefined;
      } else {
        file.agents = { ...file.agents, [agentKey]: agent };
      }
      const next = await saveSnapshot(opts, nodeId, file, snapshot.hash);
      const payload = opts.json ? JSON.stringify(next) : JSON.stringify(next, null, 2);
      defaultRuntime.log(payload);
    });
  nodesCallOpts(allowlistRemove);
}
