/**
 * CrowdSec MCP Server
 *
 * Wraps a subset of the Local API (LAPI). Requires a bouncer-type API key
 * generated by `cscli bouncers add <name>` inside the container — see the
 * skill for the first-run walk-through.
 *
 * Tool surface is intentionally read-only + one narrow admin action
 * (delete_decision) for MVP. More writes (add_decision, inventory
 * management) are future work; for now ops uses `cscli` directly inside
 * the container for those.
 */

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

const LAPI_URL = () =>
  (process.env.CROWDSEC_LAPI_URL || "http://127.0.0.1:8091").replace(/\/+$/, "");
const API_KEY = () => process.env.CROWDSEC_API_KEY || "";
const REQUEST_TIMEOUT_MS = 10_000;

function requireKey() {
  const key = API_KEY();
  if (!key) {
    throw new Error(
      "CROWDSEC_API_KEY is not set. Generate one with:\n" +
        "  docker exec crow-crowdsec cscli bouncers add crow-mcp\n" +
        "Copy the printed key into .env as CROWDSEC_API_KEY and restart the gateway.",
    );
  }
  return key;
}

async function lapiFetch(path, options = {}) {
  const key = requireKey();
  const ctl = new AbortController();
  const t = setTimeout(() => ctl.abort(), REQUEST_TIMEOUT_MS);
  try {
    const res = await fetch(`${LAPI_URL()}${path}`, {
      ...options,
      signal: ctl.signal,
      headers: {
        "X-Api-Key": key,
        "Content-Type": "application/json",
        ...options.headers,
      },
    });
    const text = await res.text();
    if (res.status === 401 || res.status === 403) {
      throw new Error(
        "LAPI authentication failed — the bouncer API key in CROWDSEC_API_KEY is rejected. " +
          "Regenerate with `docker exec crow-crowdsec cscli bouncers delete crow-mcp && " +
          "cscli bouncers add crow-mcp`.",
      );
    }
    if (!res.ok) throw new Error(`LAPI ${res.status}: ${text.slice(0, 400)}`);
    if (!text) return [];
    try { return JSON.parse(text); } catch { return { raw: text }; }
  } catch (err) {
    if (err.name === "AbortError") throw new Error(`Request timed out: ${path}`);
    if (err.cause?.code === "ECONNREFUSED" || err.message?.includes("ECONNREFUSED")) {
      throw new Error(`Cannot reach CrowdSec LAPI at ${LAPI_URL()} — is the container running?`);
    }
    throw err;
  } finally {
    clearTimeout(t);
  }
}

function formatDate(ts) {
  if (!ts) return null;
  try { return new Date(ts).toISOString(); } catch { return ts; }
}

export function createCrowdsecServer(options = {}) {
  const server = new McpServer(
    { name: "crow-crowdsec", version: "1.0.0" },
    { instructions: options.instructions },
  );

  // --- crowdsec_status ---
  server.tool(
    "crowdsec_status",
    "LAPI reachability, active decisions (bans in force), and alert counts",
    {},
    async () => {
      try {
        const [decisions, alerts] = await Promise.all([
          lapiFetch("/v1/decisions").catch(() => []),
          lapiFetch("/v1/alerts?since=24h").catch(() => []),
        ]);
        const active = Array.isArray(decisions) ? decisions.length : 0;
        const alerts24h = Array.isArray(alerts) ? alerts.length : 0;
        return {
          content: [{
            type: "text",
            text: JSON.stringify({
              lapi_url: LAPI_URL(),
              api_key_configured: !!API_KEY(),
              active_decisions: active,
              alerts_last_24h: alerts24h,
            }, null, 2),
          }],
        };
      } catch (err) {
        return { content: [{ type: "text", text: `Error: ${err.message}` }] };
      }
    }
  );

  // --- crowdsec_alerts ---
  server.tool(
    "crowdsec_alerts",
    "Recent CrowdSec alerts. Each alert represents one detected attack pattern (triggered scenario).",
    {
      since: z.enum(["1h", "24h", "7d"]).optional().default("24h").describe("Time window"),
      limit: z.number().int().min(1).max(200).optional().default(50).describe("Max alerts to return"),
    },
    async ({ since, limit }) => {
      try {
        const data = await lapiFetch(`/v1/alerts?since=${since}&limit=${limit}`);
        const rows = (Array.isArray(data) ? data : []).map((a) => ({
          id: a.id,
          source_ip: a.source?.ip,
          source_scope: a.source?.scope,
          source_as: a.source?.as_name,
          source_cn: a.source?.cn,
          scenario: a.scenario,
          message: a.message,
          events_count: a.events_count,
          decisions: (a.decisions || []).map((d) => ({
            type: d.type,
            scope: d.scope,
            value: d.value,
            duration: d.duration,
            origin: d.origin,
          })),
          created_at: formatDate(a.created_at),
        }));
        return {
          content: [{
            type: "text",
            text: rows.length
              ? `${rows.length} alert(s) in ${since}:\n${JSON.stringify(rows, null, 2)}`
              : `No alerts in the last ${since}.`,
          }],
        };
      } catch (err) {
        return { content: [{ type: "text", text: `Error: ${err.message}` }] };
      }
    }
  );

  // --- crowdsec_decisions ---
  server.tool(
    "crowdsec_decisions",
    "Active decisions (bans currently enforced by any bouncer listening on the LAPI).",
    {
      scope: z.enum(["Ip", "Range", "Country", "AS", "Username"]).optional().describe("Filter by scope"),
      origin: z.string().max(100).optional().describe("Filter by origin (crowdsec, cscli, lists, etc.)"),
    },
    async ({ scope, origin }) => {
      try {
        const params = new URLSearchParams();
        if (scope) params.set("scopes", scope);
        if (origin) params.set("origins", origin);
        const data = await lapiFetch(`/v1/decisions${params.toString() ? "?" + params : ""}`);
        const rows = (Array.isArray(data) ? data : []).map((d) => ({
          id: d.id,
          scope: d.scope,
          value: d.value,
          type: d.type,
          duration: d.duration,
          origin: d.origin,
          scenario: d.scenario,
          until: formatDate(d.until),
        }));
        return {
          content: [{
            type: "text",
            text: rows.length
              ? `${rows.length} active decision(s):\n${JSON.stringify(rows, null, 2)}`
              : "No active decisions.",
          }],
        };
      } catch (err) {
        return { content: [{ type: "text", text: `Error: ${err.message}` }] };
      }
    }
  );

  // --- crowdsec_delete_decision ---
  server.tool(
    "crowdsec_delete_decision",
    "Lift a specific ban by its decision ID. Destructive — the target IP/range will no longer be blocked by any bouncer. Use this to undo a false-positive ban.",
    {
      id: z.number().int().positive().describe("Decision ID from crowdsec_decisions"),
      confirm: z.literal("yes").describe('Must be "yes" to confirm'),
    },
    async ({ id }) => {
      try {
        await lapiFetch(`/v1/decisions/${id}`, { method: "DELETE" });
        return {
          content: [{
            type: "text",
            text: `Decision ${id} deleted. Any bouncer polling the LAPI will stop enforcing this ban on its next sync (within seconds to minutes depending on bouncer refresh interval).`,
          }],
        };
      } catch (err) {
        return { content: [{ type: "text", text: `Error: ${err.message}` }] };
      }
    }
  );

  return server;
}
