// A conservative human workflow gate, independent of Pi sandbox/OS security.
import { createConnection } from "node:net";
import { join } from "node:path";
import { homedir } from "node:os";

export function should_request_approval(toolName: string, args: any): boolean {
  if (["read", "grep", "search", "ls", "list", "find"].includes(toolName)) return false;
  if (["bash", "shell", "exec"].includes(toolName)) {
    const command = args?.command ?? args?.cmd;
    // Intentionally a tiny positive grammar, not a mutation blacklist. Shell
    // operators, substitution, scripts and all unfamiliar commands require a human.
    if (typeof command === "string") {
      if (/^\s*(pwd|whoami|uname(?: -[amnrsv]+)?)\s*$/.test(command)) return false;
      if (/^\s*git (status(?: --short)?|diff(?: --stat| --name-only)?|log(?: --oneline)?|branch --show-current)\s*$/.test(command)) return false;
      if (/^\s*(ls(?: -[lah]+)?|cat|head|tail|wc)(?: [a-zA-Z0-9_./-]+)*\s*$/.test(command)) return false;
    }
  }
  return true; // write/edit/apply_patch, deploy, delete and unknown tools
}

export class ApprovalChannel {
  private sockets = new Set<ReturnType<typeof createConnection>>();
  private stopped = false;
  private timeoutMs: number;
  private path: string;
  constructor(timeoutMs = 5000, path = join(
    process.env.DEVICE_LAB_CONTROL_DIR ?? join(homedir(), ".config", "esp32-device-gateway", "runtime"), "approvals.sock")) { this.timeoutMs = timeoutMs; this.path = path; }

  async request(request: object): Promise<boolean> {
    return (await this.requestDecision(request)) === "APPROVED";
  }
  async requestDecision(request: object): Promise<"APPROVED" | "REJECTED" | "CANCELLED"> {
    if (this.stopped) return "CANCELLED";
    return new Promise(resolve => {
      const socket = createConnection(this.path);
      this.sockets.add(socket);
      let buffer = "", finished = false;
      const finish = (state: "APPROVED" | "REJECTED" | "CANCELLED" = "CANCELLED") => {
        if (finished) return;
        finished = true;
        clearTimeout(timer);
        this.sockets.delete(socket);
        socket.destroy();
        resolve(state);
      };
      // Only connect/register is bounded. Accepted requests await a human indefinitely.
      const timer = setTimeout(() => finish(), this.timeoutMs);
      socket.on("error", () => finish());
      socket.on("close", () => finish());
      socket.on("connect", () => socket.write(JSON.stringify(request) + "\n"));
      socket.on("data", data => {
        buffer += data.toString();
        if (buffer.length > 8192) { finish(); return; }
        while (buffer.includes("\n")) {
          const newline = buffer.indexOf("\n");
          const line = buffer.slice(0, newline); buffer = buffer.slice(newline + 1);
          try {
            const value = JSON.parse(line);
            if (value.state === "PENDING" && typeof value.approval_id === "string") {
              clearTimeout(timer);
              continue;
            }
            finish(typeof value.approval_id !== "string" ? "CANCELLED" :
              value.decision === "approve" && value.state === "APPROVED" ? "APPROVED" :
              value.decision === "reject" && value.state === "REJECTED" ? "REJECTED" : "CANCELLED");
          } catch { finish(); }
        }
      });
    });
  }
  close() {
    this.stopped = true;
    for (const socket of this.sockets) socket.destroy();
  }
}
