"use client";

import { useMemo, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { api, vulnerabilities, type FindingState, type Severity, type VulnFinding } from "@/lib/api";
import { useAuth } from "@/components/AuthProvider";
import { Button, Card, Pagination, usePagination } from "@/components/ui";
import { AcceptDialog } from "@/components/vulnerabilities/AcceptDialog";
import { DBFreshness } from "@/components/vulnerabilities/DBFreshness";
import { PackageRow } from "@/components/vulnerabilities/PackageRow";
import { SEVERITY_ORDER, SeverityBadge } from "@/components/vulnerabilities/SeverityVisuals";
import { groupByPackage } from "@/lib/vulnPackages";

/*
 * The fleet vulnerability board.
 *
 * Grouped by package, defaulting to open findings, with database freshness
 * always on screen. The three things this page must never do: imply freshness
 * it does not have, present an unsupported distribution as clean, or make one
 * upgrade look like several problems.
 *
 * The API groups by CVE; the rollup to packages happens here, in
 * `lib/vulnPackages`, because a finding is still per-CVE everywhere it is
 * stored, accepted or remediated.
 */

const STATES: FindingState[] = ["open", "accepted", "fixed"];

/*
 * The fix filter. "Unfixable" is not a synonym for "ignorable": those findings
 * are the ones whose action is to remove the package, disable the service or
 * move off an end-of-life release, and they are invisible in a list sorted for
 * patching. Splitting them is what lets the patchable list be worked top to
 * bottom without them quietly disappearing.
 */
const FIX_FILTERS: { key: string; label: string; hasFix: boolean | undefined }[] = [
    { key: "all", label: "All", hasFix: undefined },
    { key: "fixable", label: "Fix available", hasFix: true },
    { key: "nofix", label: "No fix", hasFix: false },
];

export default function VulnerabilitiesPage() {
    const { isAdmin } = useAuth();
    const qc = useQueryClient();

    const [state, setState] = useState<FindingState>("open");
    const [severity, setSeverity] = useState<Severity | "">("");
    const [fixFilter, setFixFilter] = useState("all");
    const [accepting, setAccepting] = useState<VulnFinding | null>(null);

    const hasFix = FIX_FILTERS.find((f) => f.key === fixFilter)?.hasFix;

    const groups = useQuery({
        queryKey: ["vulnerabilities", state, severity, fixFilter],
        queryFn: () => vulnerabilities.list({ state, severity: severity || undefined, hasFix }),
    });

    const summary = useQuery({
        queryKey: ["vulnerabilities", "summary"],
        queryFn: () => vulnerabilities.summary(),
    });

    const servers = useQuery({ queryKey: ["servers"], queryFn: () => api.listServers() });

    const packages = useMemo(() => groupByPackage(groups.data ?? []), [groups.data]);

    // Each package row carries its own findings and servers underneath it, so
    // the cost of a full fleet's board is well past the row count alone.
    const paged = usePagination(packages, 25);

    const serverName = useMemo(() => {
        const byId = new Map((servers.data ?? []).map((s) => [s.server_id, s.hostname]));
        // Falls back to the raw id rather than an empty cell: an unnamed row is
        // worse than an ugly one.
        return (id: string) => byId.get(id) ?? id;
    }, [servers.data]);

    const invalidate = () => {
        qc.invalidateQueries({ queryKey: ["vulnerabilities"] });
    };

    const rescan = useMutation({
        mutationFn: () => vulnerabilities.rescan(),
        onSuccess: invalidate,
    });

    const accept = useMutation({
        mutationFn: ({ id, reason, until }: { id: string; reason: string; until: string }) => vulnerabilities.accept(id, reason, until),
        onSuccess: () => {
            setAccepting(null);
            invalidate();
        },
    });

    const unaccept = useMutation({
        mutationFn: (id: string) => vulnerabilities.unaccept(id),
        onSuccess: invalidate,
    });

    const applyUpdates = useMutation({
        mutationFn: (serverId: string) => api.applyUpdates(serverId),
    });

    const counts = summary.data?.counts ?? {};
    const total = SEVERITY_ORDER.reduce((n, s) => n + (counts[s] ?? 0), 0);

    return (
        <div className="p-4 sm:p-6 lg:p-8">
            <div className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
                <div>
                    <h1 className="text-2xl font-bold text-text-primary">Vulnerabilities</h1>
                    <p className="mt-1 text-sm text-text-secondary">
                        {total} open finding{total !== 1 ? "s" : ""} · installed packages matched against distribution security advisories
                    </p>
                </div>
                {isAdmin && (
                    <Button variant="secondary" loading={rescan.isPending} onClick={() => rescan.mutate()}>
                        Rescan
                    </Button>
                )}
            </div>

            <div className="mb-4">
                <DBFreshness summary={summary.data} />
            </div>

            {/* Severity counts double as the filter. They are always the whole
                fleet's open counts, never the filtered view's, so switching
                state cannot make the fleet look better than it is. */}
            <div className="mb-4 flex flex-wrap gap-2 rounded-lg border border-border bg-surface px-4 py-3 sm:px-5">
                {SEVERITY_ORDER.map((s) => (
                    <button
                        key={s}
                        onClick={() => {
                            setSeverity(severity === s ? "" : s);
                            paged.reset();
                        }}
                        aria-pressed={severity === s}
                        className={`flex items-center gap-2 rounded-lg border px-2.5 py-1.5 text-left transition-colors ${severity === s ? "border-accent bg-surface-2" : "border-transparent hover:bg-surface-2"}`}
                    >
                        <SeverityBadge severity={s} />
                        <span className="font-mono text-lg font-semibold tabular-nums text-text-primary">{counts[s] ?? 0}</span>
                    </button>
                ))}
            </div>

            <div className="mb-4 flex flex-wrap items-center gap-2">
                {STATES.map((s) => (
                    <button
                        key={s}
                        onClick={() => {
                            setState(s);
                            paged.reset();
                        }}
                        aria-pressed={state === s}
                        className={`rounded-lg border px-3 py-1.5 text-sm capitalize transition-colors ${state === s ? "border-accent text-accent" : "border-border text-text-secondary hover:text-text-primary"}`}
                    >
                        {s}
                    </button>
                ))}

                <span aria-hidden className="mx-1 h-5 w-px bg-border" />

                {FIX_FILTERS.map((f) => (
                    <button
                        key={f.key}
                        onClick={() => {
                            setFixFilter(f.key);
                            paged.reset();
                        }}
                        aria-pressed={fixFilter === f.key}
                        className={`rounded-lg border px-3 py-1.5 text-sm transition-colors ${fixFilter === f.key ? "border-accent text-accent" : "border-border text-text-secondary hover:text-text-primary"}`}
                    >
                        {f.label}
                    </button>
                ))}
            </div>

            {groups.error && <div className="mb-4 rounded-lg border border-danger/30 bg-danger/10 px-3 py-2 text-sm text-danger">{(groups.error as Error).message}</div>}

            <Card padding={false}>
                {groups.isLoading ? (
                    <div className="flex items-center justify-center py-20">
                        <div className="h-8 w-8 animate-spin rounded-full border-2 border-border border-t-accent" />
                    </div>
                ) : packages.length > 0 ? (
                    <>
                        {paged.slice.map((g) => (
                            <PackageRow key={g.package_name} group={g} serverName={serverName} canAct={isAdmin} onAccept={setAccepting} onUnaccept={(f) => unaccept.mutate(f.id)} onApplyUpdates={(serverId) => applyUpdates.mutate(serverId)} applying={applyUpdates.isPending ? (applyUpdates.variables as string) : undefined} />
                        ))}
                        <Pagination page={paged.page} pageCount={paged.pageCount} size={paged.size} total={paged.total} onPage={paged.setPage} onSize={paged.setSize} unit="packages" />
                    </>
                ) : (
                    <div className="px-6 py-14 text-center">
                        <p className="text-[15px] font-semibold text-text-primary">
                            No {state} findings{severity ? ` at ${severity} severity` : ""}
                            {hasFix === true ? " with a fix available" : hasFix === false ? " without a vendor fix" : ""}.
                        </p>
                        {/* Named explicitly, because "no findings" under a filter
                            the reader has forgotten setting reads as a clean
                            fleet — the one claim this page must never make by
                            accident. */}
                        <p className="mx-auto mt-2 max-w-[52ch] text-sm text-text-secondary">
                            {hasFix !== undefined
                                ? "This is a filtered view. Switch to All to see every finding in this state."
                                : "Servers report their packages hourly. A server whose distribution has no advisory feed is reported as unsupported on its own page rather than counted as clean here."}
                        </p>
                    </div>
                )}
            </Card>

            {accepting && <AcceptDialog finding={accepting} serverName={serverName(accepting.server_id)} pending={accept.isPending} onClose={() => setAccepting(null)} onAccept={(reason, until) => accept.mutate({ id: accepting.id, reason, until })} />}
        </div>
    );
}
