import type { Express } from "express";
import { collectFirewallMetrics } from "../widgets/firewall-collector.js";
import { execElevated } from "./exec-elevated.js";
import { managerHandler, ManagerInputError } from "./route-helpers.js";
import {
  isValidPort,
  isValidIpProtocol,
  isValidFirewallTarget,
  type IpProtocol,
  type FirewallTarget,
} from "./validation.js";
import { detectPlatform } from "./platform.js";
import type { ManagerRoutesDeps } from "./types.js";

export interface FirewallRuleSpec {
  protocol: IpProtocol;
  port: number;
  target: FirewallTarget;
}

/**
 * Build an iptables add/delete for an INPUT rule. We only ever touch INPUT for a
 * specific dport with an explicit target, and never the chain policy, so an
 * existing ESTABLISHED/SSH rule is left intact.
 */
export function buildIptablesRuleCommand(
  op: "add" | "delete",
  spec: FirewallRuleSpec,
): string {
  const flag = op === "add" ? "-A" : "-D";
  return `iptables ${flag} INPUT -p ${spec.protocol} --dport ${spec.port} -j ${spec.target}`;
}

export function buildNftRuleCommand(
  op: "add" | "delete",
  spec: FirewallRuleSpec,
): string {
  // nftables uses the inet filter table's input chain by convention.
  const verb = op === "add" ? "add" : "delete";
  const action =
    spec.target.toLowerCase() === "reject"
      ? "reject"
      : spec.target.toLowerCase();
  return `nft ${verb} rule inet filter input ${spec.protocol} dport ${spec.port} ${action}`;
}

export function registerFirewallRoutes(
  app: Express,
  { validateHostId, runOnHost }: ManagerRoutesDeps,
): void {
  app.get(
    "/host-metrics/managers/firewall/:id",
    validateHostId,
    managerHandler(runOnHost, "connect", "firewall_read", async (client) => {
      return await collectFirewallMetrics(client);
    }),
  );

  app.post(
    "/host-metrics/managers/firewall/:id/rule",
    validateHostId,
    managerHandler(
      runOnHost,
      "connect",
      "firewall_rule",
      async (client, host, req) => {
        const { op, protocol, port, target } = req.body as {
          op?: "add" | "delete";
          protocol?: string;
          port?: number;
          target?: string;
        };
        if (op !== "add" && op !== "delete") {
          throw new ManagerInputError("Invalid op");
        }
        if (!isValidIpProtocol(protocol))
          throw new ManagerInputError("Invalid protocol");
        if (!isValidPort(port)) throw new ManagerInputError("Invalid port");
        if (!isValidFirewallTarget(target))
          throw new ManagerInputError("Invalid target");

        const spec: FirewallRuleSpec = {
          protocol,
          port: Number(port),
          target,
        };
        const fw = await collectFirewallMetrics(client);
        const cmd =
          fw.type === "nftables"
            ? buildNftRuleCommand(op, spec)
            : buildIptablesRuleCommand(op, spec);
        const result = await execElevated(client, cmd, host.sudoPassword, {
          forceSudo: true,
        });
        return {
          success: result.code === 0,
          output: result.stdout || result.stderr,
          backend: fw.type,
        };
      },
    ),
  );

  app.post(
    "/host-metrics/managers/firewall/:id/persist",
    validateHostId,
    managerHandler(
      runOnHost,
      "connect",
      "firewall_persist",
      async (client, host) => {
        const platform = await detectPlatform(client);
        // Best-effort persistence across common tools.
        const cmd =
          "(command -v netfilter-persistent >/dev/null 2>&1 && netfilter-persistent save) || " +
          "(command -v service >/dev/null 2>&1 && service iptables save) || " +
          "(command -v nft >/dev/null 2>&1 && nft list ruleset > /etc/nftables.conf) || true";
        const result = await execElevated(client, cmd, host.sudoPassword, {
          forceSudo: true,
        });
        return {
          success: result.code === 0,
          output: result.stdout || result.stderr,
          pkg: platform.pkg,
        };
      },
    ),
  );
}
