// ABOUTME: Curated security news/advisory retrieval for trusted sources like CISA, NVD, OWASP, and CVE.
// ABOUTME: Registers a security_news tool that returns trust-ranked, freshness-aware advisory data.

import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import { Text } from "@mariozechner/pi-tui";

const SOURCE_IDS = ["cisa", "owasp", "nvd", "cve"] as const;
type SourceId = typeof SOURCE_IDS[number];

type SecurityNewsAction = "sources" | "latest" | "search" | "cve_lookup";

interface SecuritySource {
  id: SourceId;
  name: string;
  tier: 1 | 2;
  trustScore: number;
  category: string;
  description: string;
  homepage: string;
  fetchLatest?: (query?: string) => Promise<SecurityNewsItem[]>;
  lookupCve?: (cveId: string) => Promise<SecurityNewsItem[]>;
}

interface SecurityNewsItem {
  title: string;
  summary: string;
  url: string;
  source: SourceId;
  sourceName: string;
  category: string;
  publishedAt?: string;
  trustScore: number;
  tags: string[];
  cveIds?: string[];
}

const CISA_KEV_URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json";
const NVD_API_URL = "https://services.nvd.nist.gov/rest/json/cves/2.0";
const OWASP_NEWS_URL = "https://owasp.org/www-project-top-ten/";
const CVE_API_URL = "https://cveawg.mitre.org/api/cve/";

function normalizeText(value: unknown): string {
  return typeof value === "string" ? value.trim() : "";
}

function safeArray<T>(value: unknown): T[] {
  return Array.isArray(value) ? value as T[] : [];
}

function containsQuery(item: SecurityNewsItem, query?: string): boolean {
  if (!query) return true;
  const haystack = [item.title, item.summary, item.tags.join(" "), ...(item.cveIds || [])].join(" ").toLowerCase();
  return query.toLowerCase().split(/\s+/).filter(Boolean).every((term) => haystack.includes(term));
}

function dedupeItems(items: SecurityNewsItem[]): SecurityNewsItem[] {
  const seen = new Set<string>();
  return items.filter((item) => {
    const key = `${item.source}:${item.url}:${(item.cveIds || []).join(",")}`;
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
}

function extractCveIds(...values: string[]): string[] {
  const matches = new Set<string>();
  for (const value of values) {
    const found = value.match(/CVE-\d{4}-\d{4,7}/gi) || [];
    for (const id of found) matches.add(id.toUpperCase());
  }
  return [...matches];
}

async function fetchJson(url: string): Promise<any> {
  const resp = await fetch(url, {
    headers: {
      "User-Agent": "pi-agent-security-news/1.0",
      "Accept": "application/json, text/plain;q=0.9, */*;q=0.8",
    },
  });
  if (!resp.ok) {
    throw new Error(`Fetch failed (${resp.status}) for ${url}`);
  }
  return resp.json();
}

async function fetchText(url: string): Promise<string> {
  const resp = await fetch(url, {
    headers: {
      "User-Agent": "pi-agent-security-news/1.0",
      "Accept": "text/html, text/plain;q=0.9, */*;q=0.8",
    },
  });
  if (!resp.ok) {
    throw new Error(`Fetch failed (${resp.status}) for ${url}`);
  }
  return resp.text();
}

async function fetchCisaKev(query?: string): Promise<SecurityNewsItem[]> {
  const data = await fetchJson(CISA_KEV_URL);
  const vulns = safeArray<any>(data?.vulnerabilities).slice(0, 50);
  return vulns
    .map((item) => {
      const cveId = normalizeText(item.cveID).toUpperCase();
      const title = `${cveId} — ${normalizeText(item.vulnerabilityName) || "Known Exploited Vulnerability"}`;
      const summary = [
        normalizeText(item.vendorProject),
        normalizeText(item.product),
        normalizeText(item.shortDescription),
        normalizeText(item.requiredAction) ? `Required action: ${normalizeText(item.requiredAction)}` : "",
      ].filter(Boolean).join(" | ");
      return {
        title,
        summary,
        url: "https://www.cisa.gov/known-exploited-vulnerabilities-catalog",
        source: "cisa" as const,
        sourceName: "CISA KEV",
        category: "known-exploited-vulnerability",
        publishedAt: normalizeText(item.dateAdded),
        trustScore: 10,
        tags: ["cisa", "kev", "vulnerability", "advisory"],
        cveIds: cveId ? [cveId] : [],
      } satisfies SecurityNewsItem;
    })
    .filter((item) => containsQuery(item, query));
}

async function fetchNvdLatest(query?: string): Promise<SecurityNewsItem[]> {
  const data = await fetchJson(`${NVD_API_URL}?resultsPerPage=20`);
  const vulns = safeArray<any>(data?.vulnerabilities);
  return vulns.map((entry) => {
    const cve = entry?.cve || {};
    const cveId = normalizeText(cve.id).toUpperCase();
    const descriptions = safeArray<any>(cve.descriptions);
    const desc = descriptions.find((d) => d?.lang === "en")?.value || descriptions[0]?.value || "";
    return {
      title: `${cveId} — ${desc.slice(0, 120) || "NVD Advisory"}`,
      summary: normalizeText(desc),
      url: cveId ? `https://nvd.nist.gov/vuln/detail/${cveId}` : "https://nvd.nist.gov/",
      source: "nvd" as const,
      sourceName: "NVD",
      category: "cve",
      publishedAt: normalizeText(cve.published),
      trustScore: 10,
      tags: ["nvd", "cve", "vulnerability"],
      cveIds: cveId ? [cveId] : [],
    } satisfies SecurityNewsItem;
  }).filter((item) => containsQuery(item, query));
}

async function fetchNvdByCve(cveId: string): Promise<SecurityNewsItem[]> {
  const data = await fetchJson(`${NVD_API_URL}?cveId=${encodeURIComponent(cveId)}`);
  const vulns = safeArray<any>(data?.vulnerabilities);
  return vulns.map((entry) => {
    const cve = entry?.cve || {};
    const descriptions = safeArray<any>(cve.descriptions);
    const desc = descriptions.find((d) => d?.lang === "en")?.value || descriptions[0]?.value || "";
    return {
      title: `${cveId.toUpperCase()} — ${desc.slice(0, 120) || "NVD Advisory"}`,
      summary: normalizeText(desc),
      url: `https://nvd.nist.gov/vuln/detail/${cveId.toUpperCase()}`,
      source: "nvd" as const,
      sourceName: "NVD",
      category: "cve",
      publishedAt: normalizeText(cve.published),
      trustScore: 10,
      tags: ["nvd", "cve", "vulnerability"],
      cveIds: [cveId.toUpperCase()],
    } satisfies SecurityNewsItem;
  });
}

async function fetchCveById(cveId: string): Promise<SecurityNewsItem[]> {
  const data = await fetchJson(`${CVE_API_URL}${encodeURIComponent(cveId)}`);
  const title = normalizeText(data?.cveMetadata?.cveId || cveId.toUpperCase());
  const descriptions = safeArray<any>(data?.containers?.cna?.descriptions);
  const desc = descriptions.find((d) => d?.lang === "en")?.value || descriptions[0]?.value || "";
  return [{
    title: `${title} — ${desc.slice(0, 120) || "CVE Record"}`,
    summary: normalizeText(desc),
    url: `https://www.cve.org/CVERecord?id=${title}`,
    source: "cve",
    sourceName: "CVE / MITRE",
    category: "cve-record",
    publishedAt: normalizeText(data?.cveMetadata?.datePublished),
    trustScore: 9,
    tags: ["cve", "mitre", "vulnerability"],
    cveIds: [title],
  }];
}

async function fetchOwaspLatest(query?: string): Promise<SecurityNewsItem[]> {
  const html = await fetchText(OWASP_NEWS_URL);
  const text = html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
  const item: SecurityNewsItem = {
    title: "OWASP Top 10 Web Application Security Risks",
    summary: text.slice(0, 500),
    url: OWASP_NEWS_URL,
    source: "owasp",
    sourceName: "OWASP",
    category: "owasp-guidance",
    trustScore: 8,
    tags: ["owasp", "web-security", "guidance", ...extractCveIds(text)],
    cveIds: extractCveIds(text),
  };
  return containsQuery(item, query) ? [item] : [];
}

const SOURCES: SecuritySource[] = [
  {
    id: "cisa",
    name: "CISA KEV",
    tier: 1,
    trustScore: 10,
    category: "government",
    description: "Known Exploited Vulnerabilities catalog from CISA.",
    homepage: "https://www.cisa.gov/known-exploited-vulnerabilities-catalog",
    fetchLatest: fetchCisaKev,
  },
  {
    id: "nvd",
    name: "NVD",
    tier: 1,
    trustScore: 10,
    category: "government",
    description: "National Vulnerability Database CVE feed and API.",
    homepage: "https://nvd.nist.gov/",
    fetchLatest: fetchNvdLatest,
    lookupCve: fetchNvdByCve,
  },
  {
    id: "owasp",
    name: "OWASP",
    tier: 2,
    trustScore: 8,
    category: "non-profit",
    description: "OWASP guidance and project advisories relevant to application and network security.",
    homepage: OWASP_NEWS_URL,
    fetchLatest: fetchOwaspLatest,
  },
  {
    id: "cve",
    name: "CVE / MITRE",
    tier: 2,
    trustScore: 9,
    category: "non-profit",
    description: "Canonical CVE record service operated by MITRE/CVE program.",
    homepage: "https://www.cve.org/",
    lookupCve: fetchCveById,
  },
];

function formatItem(item: SecurityNewsItem): string {
  const lines = [
    `- ${item.title}`,
    `  Source: ${item.sourceName} | Trust: ${item.trustScore}/10 | Category: ${item.category}`,
    item.publishedAt ? `  Published: ${item.publishedAt}` : "",
    item.cveIds?.length ? `  CVEs: ${item.cveIds.join(", ")}` : "",
    `  URL: ${item.url}`,
    `  Summary: ${item.summary}`,
  ].filter(Boolean);
  return lines.join("\n");
}

function formatSource(source: SecuritySource): string {
  return `- ${source.name} (${source.id}) — Tier ${source.tier}, Trust ${source.trustScore}/10\n  ${source.description}\n  ${source.homepage}`;
}

export default function (pi: ExtensionAPI) {
  pi.registerTool({
    name: "security_news",
    label: "Security News",
    description: "Curated security news and advisory retrieval from trusted sources such as CISA, NVD, OWASP, and CVE. Supports source listing, latest advisories, filtered search, and CVE lookup.",
    parameters: Type.Object({
      action: Type.String({ description: "Action to perform: sources, latest, search, cve_lookup" }),
      query: Type.Optional(Type.String({ description: "Optional search filter for latest/search actions" })),
      source: Type.Optional(Type.String({ description: "Optional source filter: cisa, owasp, nvd, cve" })),
      cve_id: Type.Optional(Type.String({ description: "Specific CVE ID for cve_lookup action" })),
      limit: Type.Optional(Type.Number({ description: "Maximum number of results to return (default 10)" })),
    }),
    async execute(_toolCallId, params) {
      const action = normalizeText((params as any).action) as SecurityNewsAction;
      const query = normalizeText((params as any).query) || undefined;
      const sourceId = normalizeText((params as any).source) as SourceId | "";
      const cveId = normalizeText((params as any).cve_id).toUpperCase();
      const limit = typeof (params as any).limit === "number" ? Math.max(1, Math.min(25, (params as any).limit)) : 10;

      if (!["sources", "latest", "search", "cve_lookup"].includes(action)) {
        return { content: [{ type: "text" as const, text: `Unknown action: ${action}` }], details: { error: "invalid_action" } };
      }

      if (action === "sources") {
        const text = ["Trusted security news/advisory sources:", "", ...SOURCES.map(formatSource)].join("\n");
        return { content: [{ type: "text" as const, text }], details: { action, count: SOURCES.length } };
      }

      const selectedSources = sourceId ? SOURCES.filter((s) => s.id === sourceId) : SOURCES;
      if (sourceId && selectedSources.length === 0) {
        return { content: [{ type: "text" as const, text: `Unknown source: ${sourceId}` }], details: { error: "invalid_source" } };
      }

      try {
        let items: SecurityNewsItem[] = [];
        if (action === "cve_lookup") {
          if (!/^CVE-\d{4}-\d{4,7}$/i.test(cveId)) {
            return { content: [{ type: "text" as const, text: "cve_lookup requires a valid CVE ID like CVE-2024-12345." }], details: { error: "invalid_cve" } };
          }
          for (const source of selectedSources.filter((s) => s.lookupCve)) {
            items.push(...await source.lookupCve!(cveId));
          }
        } else {
          for (const source of selectedSources.filter((s) => s.fetchLatest)) {
            items.push(...await source.fetchLatest!(query));
          }
        }

        items = dedupeItems(items)
          .filter((item) => action !== "search" || containsQuery(item, query))
          .sort((a, b) => b.trustScore - a.trustScore)
          .slice(0, limit);

        if (items.length === 0) {
          return {
            content: [{ type: "text" as const, text: "No trusted security news results matched the request." }],
            details: { action, count: 0 },
          };
        }

        const heading = action === "cve_lookup"
          ? `Trusted advisory results for ${cveId}:`
          : action === "search"
            ? `Trusted security news results for \"${query || ""}\":`
            : "Latest trusted security advisories:";

        const text = [heading, "", ...items.map(formatItem)].join("\n\n");
        return {
          content: [{ type: "text" as const, text }],
          details: { action, count: items.length, items },
        };
      } catch (error: any) {
        return {
          content: [{ type: "text" as const, text: `security_news failed: ${error.message}` }],
          details: { action, error: error.message },
        };
      }
    },
    renderCall(args, theme) {
      const p = args as any;
      const label = `${p.action || "security_news"}${p.source ? `:${p.source}` : ""}`;
      return new Text(theme.fg("toolTitle", theme.bold("security_news ")) + theme.fg("accent", label), 0, 0);
    },
    renderResult(result, _options, theme) {
      const details = result.details as any;
      if (details?.error) return new Text(theme.fg("error", `security_news error: ${details.error}`), 0, 0);
      return new Text(theme.fg("success", `security_news ${details?.count ?? 0} result(s)`), 0, 0);
    },
  });
}
