import { type ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
import { SystemSheet, SheetSection } from '@/components/ui/system-sheet';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Button } from '@/components/ui/button';
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table';
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
  ShieldOff,
  ShieldCheck,
  ExternalLink,
  ChevronLeft,
  ChevronRight,
  RefreshCw,
  Download,
  Loader2,
  Check,
  GitCompare,
} from 'lucide-react';
import { Combobox } from '@/components/ui/combobox';
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { ScanComparisonSheet } from './ScanComparisonSheet';
import { fetchAllScanVulnerabilities } from './VulnerabilityScanSheet.export';
import { apiFetch } from '@/lib/api';
import { toast } from '@/components/ui/toast-store';
import { cn } from '@/lib/utils';
import { cveUrl } from '@/lib/cveUrl';
import { SEVERITY_ROW_TINT } from '@/lib/severityStyles';
import { formatTimeAgo } from '@/lib/relativeTime';
import { formatPolicyReasons } from '@/lib/policyReasons';
import type {
  VulnerabilityScan,
  VulnerabilityDetail,
  VulnSeverity,
  SecretFinding,
  MisconfigFinding,
  ScanDetailTab,
  TriageStatus,
} from '@/types/security';
import { TRIAGE_STATUS_OPTIONS, TRIAGE_JUSTIFICATION_OPTIONS, TRIAGE_STATUS_HINT, openVexRequiresJustification, justificationForStatus, triageStatusLabel } from '@/lib/triage';

interface VulnerabilityScanSheetProps {
  scanId: number | null;
  onClose: () => void;
  onRescan?: (imageRef: string) => void;
  canGenerateSbom?: boolean;
  canExportSarif?: boolean;
  canCompare?: boolean;
  canManageSuppressions?: boolean;
  /**
   * Tab to open on first load. Defaults to 'vulns' (with the existing
   * auto-switch to a populated tab when the scan has no vulnerabilities).
   * Callers that open the sheet from a secret/misconfig context pass the
   * matching tab so it lands there even when the scan also has CVEs.
   */
  initialTab?: FindingTab;
  /**
   * When set (from a posture reason with drivers), the vuln list is filtered
   * to these CVE/GHSA ids and a driving / monitoring findings banner shows.
   */
  driverVulnerabilityIds?: string[];
  /**
   * action = Action-needed driver set; monitoring = waiting/uncertain review set.
   * Defaults to action when omitted.
   */
  driverFilterMode?: 'action' | 'monitoring';
  /** Full driver count before cap (for truncated title). */
  driverCount?: number;
  driversTruncated?: boolean;
}

interface SuppressDialogState {
  cveId: string;
  pkgName: string;
  imagePattern: string;
  reason: string;
  expiresInDays: string;
  status: TriageStatus;
  justification: string;
}

interface AckDialogState {
  ruleId: string;
  stackPattern: string;
  reason: string;
  expiresInDays: string;
}

type SeverityFilter = 'ALL' | VulnSeverity;
// Single source of truth lives in types/security as ScanDetailTab; alias here so
// the initialTab prop is provably the same type its callers (SecurityView) hold.
type FindingTab = ScanDetailTab;

const PAGE_SIZE = 25;

const SEVERITY_CLASSES: Record<VulnSeverity, string> = {
  CRITICAL: 'text-destructive border-destructive/40 bg-destructive/10',
  HIGH: 'text-warning border-warning/40 bg-warning/10',
  MEDIUM: 'text-warning border-warning/40 bg-warning/10',
  LOW: 'text-stat-subtitle border-border bg-muted/30',
  UNKNOWN: 'text-stat-subtitle border-border bg-muted/20',
};

function SeverityChip({ severity }: { severity: VulnSeverity }) {
  return (
    <span
      className={cn(
        'inline-flex items-center gap-1 rounded border px-1.5 py-0.5 text-[10px] font-mono tabular-nums uppercase tracking-[0.18em] shadow-card-bevel',
        SEVERITY_CLASSES[severity],
      )}
    >
      {severity}
    </span>
  );
}

const EVIDENCE_TAG_CLASSES = {
  danger: 'text-destructive border-destructive/40 bg-destructive/10',
  warn: 'text-warning border-warning/40 bg-warning/10',
  muted: 'text-stat-subtitle border-border bg-muted/30',
  neutral: 'text-stat-value border-border bg-muted/20',
} as const;

function EvidenceTag({ tone, children }: { tone: keyof typeof EVIDENCE_TAG_CLASSES; children: ReactNode }) {
  return (
    <span className={cn('inline-flex items-center rounded border px-1.5 py-px text-[9px] font-mono uppercase tracking-[0.1em]', EVIDENCE_TAG_CLASSES[tone])}>
      {children}
    </span>
  );
}

/**
 * Small, independently-verifiable evidence atoms per finding. Severity is one
 * signal among several, not the only one: these surface exploit intel (KEV,
 * EPSS), vendor status, and the CVSS score so an operator can tell scary from
 * exploitable without an invented composite priority number.
 */
function EvidenceTags({ d }: { d: VulnerabilityDetail }) {
  const tags: ReactNode[] = [];
  if (d.kev) tags.push(<EvidenceTag key="kev" tone="danger">KEV</EvidenceTag>);
  if (typeof d.epss_score === 'number') {
    tags.push(
      <EvidenceTag key="epss" tone={d.epss_score >= 0.1 ? 'warn' : 'muted'}>
        EPSS {Math.round(d.epss_score * 100)}%
      </EvidenceTag>,
    );
  }
  if (d.status === 'will_not_fix' || d.status === 'end_of_life') {
    tags.push(<EvidenceTag key="wontfix" tone="muted">{"Won't fix"}</EvidenceTag>);
  }
  if (typeof d.cvss_score === 'number') {
    tags.push(<EvidenceTag key="cvss" tone="neutral">CVSS {d.cvss_score}</EvidenceTag>);
  }
  if (d.triage_status) {
    tags.push(
      <EvidenceTag key="triage" tone={d.triage_status === 'affected' || d.triage_status === 'needs_review' ? 'warn' : 'muted'}>
        {triageStatusLabel(d.triage_status)}
      </EvidenceTag>,
    );
  }
  if (tags.length === 0) return null;
  return <span className="mt-1 flex flex-wrap items-center gap-1">{tags}</span>;
}

function isActiveTriageStatus(status: TriageStatus | undefined): boolean {
  return status === 'affected' || status === 'needs_review';
}

export function VulnerabilityScanSheet({
  scanId,
  onClose,
  onRescan,
  canGenerateSbom = false,
  canExportSarif = false,
  canCompare = false,
  canManageSuppressions: canManageSuppressionsProp = false,
  initialTab,
  driverVulnerabilityIds,
  driverFilterMode = 'action',
  driverCount,
  driversTruncated = false,
}: VulnerabilityScanSheetProps) {
  const [isReplica, setIsReplica] = useState(false);
  useEffect(() => {
    // Reset on every probe so a stale `true` from a previous replica view
    // does not survive switching to a control instance with the sheet kept
    // mounted by its parent. Defense in depth: if the probe never resolves
    // the UI stays permissive and the backend blockIfReplica guard runs.
    setIsReplica(false);
    if (!canManageSuppressionsProp || scanId == null) return;
    let cancelled = false;
    (async () => {
      try {
        const res = await apiFetch('/fleet/role', { localOnly: true });
        if (cancelled || !res.ok) return;
        const data = await res.json();
        if (!cancelled) setIsReplica(data?.role === 'replica');
      } catch (err) {
        console.warn('Failed to probe fleet role for replica gate:', err);
      }
    })();
    return () => { cancelled = true; };
  }, [canManageSuppressionsProp, scanId]);
  const canManageSuppressions = canManageSuppressionsProp && !isReplica;
  const [scan, setScan] = useState<VulnerabilityScan | null>(null);
  const [details, setDetails] = useState<VulnerabilityDetail[]>([]);
  const [totalDetails, setTotalDetails] = useState(0);
  const [secrets, setSecrets] = useState<SecretFinding[]>([]);
  const [misconfigs, setMisconfigs] = useState<MisconfigFinding[]>([]);
  const [loading, setLoading] = useState(false);
  const [severityFilter, setSeverityFilter] = useState<SeverityFilter>('ALL');
  const [page, setPage] = useState(0);
  const [secretsPage, setSecretsPage] = useState(0);
  const [misconfigsPage, setMisconfigsPage] = useState(0);
  const [tab, setTab] = useState<FindingTab>('vulns');
  const [downloadingSbom, setDownloadingSbom] = useState(false);
  const [compareOpen, setCompareOpen] = useState(false);
  const [compareOptions, setCompareOptions] = useState<VulnerabilityScan[]>([]);
  const [compareLoading, setCompareLoading] = useState(false);
  const [compareBaselineId, setCompareBaselineId] = useState<number | null>(null);
  const [suppressForm, setSuppressForm] = useState<SuppressDialogState | null>(null);
  const [savingSuppression, setSavingSuppression] = useState(false);
  const [ackForm, setAckForm] = useState<AckDialogState | null>(null);
  const [savingAck, setSavingAck] = useState(false);
  const [exportingCsv, setExportingCsv] = useState(false);

  const DETAIL_FETCH_LIMIT = 500;

  const load = useCallback(async () => {
    if (scanId == null) return;
    setLoading(true);
    try {
      // The vulnerability list is fetched in full (paging past the per-request
      // cap), not a single capped page, so severity filtering, inspection, and
      // suppression reach every finding rather than only the first page.
      const [scanRes, allVulns, secretsRes, misconfigsRes] = await Promise.all([
        apiFetch(`/security/scans/${scanId}`),
        fetchAllScanVulnerabilities(scanId),
        apiFetch(`/security/scans/${scanId}/secrets?limit=${DETAIL_FETCH_LIMIT}`),
        apiFetch(`/security/scans/${scanId}/misconfigs?limit=${DETAIL_FETCH_LIMIT}`),
      ]);
      if (!scanRes.ok) throw new Error('Failed to fetch scan');
      const scanData = (await scanRes.json()) as VulnerabilityScan;
      const secretsData = secretsRes.ok ? await secretsRes.json() : { items: [] };
      const misconfigsData = misconfigsRes.ok ? await misconfigsRes.json() : { items: [] };
      setScan(scanData);
      setDetails(allVulns);
      setTotalDetails(allVulns.length);
      setSecrets(Array.isArray(secretsData.items) ? secretsData.items : []);
      setMisconfigs(Array.isArray(misconfigsData.items) ? misconfigsData.items : []);
      setPage(0);
      setSecretsPage(0);
      setMisconfigsPage(0);
      if (initialTab) {
        // Caller asked to land on a specific tab (e.g. opened from the
        // Secrets or Compose-risks list), which wins over the default.
        setTab(initialTab);
      } else if ((scanData.total_vulnerabilities ?? 0) === 0) {
        if ((scanData.misconfig_count ?? 0) > 0) setTab('misconfigs');
        else if ((scanData.secret_count ?? 0) > 0) setTab('secrets');
        else setTab('vulns');
      } else {
        setTab('vulns');
      }
    } catch (err) {
      toast.error((err as Error)?.message || 'Failed to load scan');
    } finally {
      setLoading(false);
    }
  }, [scanId, initialTab]);

  useEffect(() => {
    setCompareOpen(false);
    setCompareOptions([]);
    setCompareBaselineId(null);
    if (scanId != null) {
      load();
    } else {
      setScan(null);
      setDetails([]);
      setTotalDetails(0);
      setSecrets([]);
      setMisconfigs([]);
      setSeverityFilter('ALL');
      setPage(0);
      setSecretsPage(0);
      setMisconfigsPage(0);
      setTab('vulns');
    }
  }, [scanId, load]);

  const filtered = useMemo(() => {
    let rows = details;
    if (driverVulnerabilityIds && driverVulnerabilityIds.length > 0) {
      const allow = new Set(driverVulnerabilityIds);
      rows = rows.filter((d) => allow.has(d.vulnerability_id));
    }
    if (severityFilter === 'ALL') return rows;
    return rows.filter((d) => d.severity === severityFilter);
  }, [details, severityFilter, driverVulnerabilityIds]);

  const drivingFindings = (driverVulnerabilityIds?.length ?? 0) > 0;

  const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
  const safePage = Math.min(page, totalPages - 1);
  const pageItems = filtered.slice(safePage * PAGE_SIZE, (safePage + 1) * PAGE_SIZE);
  const needsPagination = filtered.length > PAGE_SIZE;

  const secretsTotalPages = Math.max(1, Math.ceil(secrets.length / PAGE_SIZE));
  const secretsSafePage = Math.min(secretsPage, secretsTotalPages - 1);
  const secretsPageItems = secrets.slice(
    secretsSafePage * PAGE_SIZE,
    (secretsSafePage + 1) * PAGE_SIZE,
  );
  const secretsNeedsPagination = secrets.length > PAGE_SIZE;

  const misconfigsTotalPages = Math.max(1, Math.ceil(misconfigs.length / PAGE_SIZE));
  const misconfigsSafePage = Math.min(misconfigsPage, misconfigsTotalPages - 1);
  const misconfigsPageItems = misconfigs.slice(
    misconfigsSafePage * PAGE_SIZE,
    (misconfigsSafePage + 1) * PAGE_SIZE,
  );
  const misconfigsNeedsPagination = misconfigs.length > PAGE_SIZE;

  const downloadSbom = useCallback(
    async (format: 'spdx-json' | 'cyclonedx') => {
      if (!scan) return;
      setDownloadingSbom(true);
      try {
        const res = await apiFetch('/security/sbom', {
          method: 'POST',
          body: JSON.stringify({ imageRef: scan.image_ref, format }),
        });
        if (!res.ok) {
          const body = await res.json().catch(() => ({}));
          throw new Error(body?.error || 'Failed to generate SBOM');
        }
        const blob = await res.blob();
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = `${scan.image_ref.replace(/[^a-z0-9]+/gi, '_')}-sbom-${format}.json`;
        document.body.appendChild(a);
        a.click();
        a.remove();
        URL.revokeObjectURL(url);
        toast.success('SBOM downloaded');
      } catch (err) {
        toast.error((err as Error)?.message || 'SBOM generation failed');
      } finally {
        setDownloadingSbom(false);
      }
    },
    [scan],
  );

  const openCompareMenu = useCallback(async () => {
    if (!scan) return;
    setCompareOpen((o) => !o);
    if (compareOptions.length > 0 || compareLoading) return;
    setCompareLoading(true);
    try {
      const res = await apiFetch(
        `/security/scans?imageRef=${encodeURIComponent(scan.image_ref)}&limit=25`,
      );
      if (!res.ok) throw new Error('Failed to load scan history');
      const body = await res.json();
      const items: VulnerabilityScan[] = Array.isArray(body?.items) ? body.items : [];
      setCompareOptions(
        items.filter((s) => s.id !== scan.id && s.status === 'completed'),
      );
    } catch (err) {
      toast.error((err as Error)?.message || 'Could not load scan history');
    } finally {
      setCompareLoading(false);
    }
  }, [scan, compareOptions.length, compareLoading]);

  const openSuppressDialog = useCallback((d: VulnerabilityDetail) => {
    setSuppressForm({
      cveId: d.vulnerability_id,
      pkgName: d.pkg_name,
      imagePattern: scan?.image_ref ?? '',
      reason: '',
      expiresInDays: '',
      status: 'accepted',
      justification: '',
    });
  }, [scan?.image_ref]);

  const submitSuppression = useCallback(async () => {
    if (!suppressForm) return;
    const reason = suppressForm.reason.trim();
    if (!reason) {
      toast.error('A reason is required.');
      return;
    }
    const justification = suppressForm.justification.trim();
    if (openVexRequiresJustification(suppressForm.status) && !justification) {
      toast.error('An OpenVEX justification is required for this triage decision.');
      return;
    }
    const days = suppressForm.expiresInDays.trim();
    let expiresAt: number | null = null;
    if (days) {
      const n = Number(days);
      if (!Number.isFinite(n) || n <= 0) {
        toast.error('Expiry must be a positive number of days or blank.');
        return;
      }
      expiresAt = Date.now() + n * 24 * 60 * 60 * 1000;
    }
    setSavingSuppression(true);
    try {
      const res = await apiFetch('/security/suppressions', {
        method: 'POST',
        localOnly: true,
        body: JSON.stringify({
          cve_id: suppressForm.cveId,
          pkg_name: suppressForm.pkgName || null,
          image_pattern: suppressForm.imagePattern.trim() || null,
          reason,
          expires_at: expiresAt,
          status: suppressForm.status,
          justification: openVexRequiresJustification(suppressForm.status) ? (justification || null) : null,
        }),
      });
      if (!res.ok) {
        const body = await res.json().catch(() => ({}));
        throw new Error(body?.error || 'Failed to create suppression');
      }
      toast.success('Suppression created');
      setSuppressForm(null);
      await load();
    } catch (err) {
      toast.error((err as Error)?.message || 'Failed to create suppression');
    } finally {
      setSavingSuppression(false);
    }
  }, [suppressForm, load]);

  const openAckDialog = useCallback((m: MisconfigFinding) => {
    // Prefill stack_pattern with the exact stack name when this scan is a
    // stack-scoped config scan. Operators can broaden in the dialog.
    const stackPattern = scan?.stack_context ?? '';
    setAckForm({
      ruleId: m.rule_id,
      stackPattern,
      reason: '',
      expiresInDays: '',
    });
  }, [scan]);

  const submitAcknowledgement = useCallback(async () => {
    if (!ackForm) return;
    const reason = ackForm.reason.trim();
    if (!reason) {
      toast.error('A reason is required.');
      return;
    }
    const days = ackForm.expiresInDays.trim();
    let expiresAt: number | null = null;
    if (days) {
      const n = Number(days);
      if (!Number.isFinite(n) || n <= 0) {
        toast.error('Expiry must be a positive number of days or blank.');
        return;
      }
      expiresAt = Date.now() + n * 24 * 60 * 60 * 1000;
    }
    setSavingAck(true);
    try {
      const res = await apiFetch('/security/misconfig-acks', {
        method: 'POST',
        localOnly: true,
        body: JSON.stringify({
          rule_id: ackForm.ruleId,
          stack_pattern: ackForm.stackPattern.trim() || null,
          reason,
          expires_at: expiresAt,
        }),
      });
      if (!res.ok) {
        const body = await res.json().catch(() => ({}));
        throw new Error(body?.error || 'Failed to create acknowledgement');
      }
      toast.success('Acknowledgement created');
      setAckForm(null);
      await load();
    } catch (err) {
      toast.error((err as Error)?.message || 'Failed to create acknowledgement');
    } finally {
      setSavingAck(false);
    }
  }, [ackForm, load]);

  const exportCsv = useCallback(async () => {
    if (!scan || details.length === 0) return;
    setExportingCsv(true);
    try {
      // The table renders a capped page; the CSV is the complete-list recovery
      // path the in-sheet notice promises, so fetch every row when the loaded
      // set is short of the total. Otherwise reuse what is already in memory.
      const rows =
        details.length < totalDetails
          ? await fetchAllScanVulnerabilities(scan.id)
          : details;
      const escape = (v: string) => `"${v.replace(/"/g, '""')}"`;
      const csv =
        'CVE,Package,Severity,Installed,Fixed,URL\n' +
        rows
          .map((d) =>
            [
              escape(d.vulnerability_id),
              escape(d.pkg_name),
              escape(d.severity),
              escape(d.installed_version),
              escape(d.fixed_version ?? ''),
              escape(d.primary_url ?? ''),
            ].join(','),
          )
          .join('\n');
      const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }));
      const a = document.createElement('a');
      a.href = url;
      a.download = `${scan.image_ref.replace(/[^a-z0-9]+/gi, '_')}-vulnerabilities.csv`;
      document.body.appendChild(a);
      a.click();
      a.remove();
      URL.revokeObjectURL(url);
      toast.success(`Exported ${rows.length} ${rows.length === 1 ? 'vulnerability' : 'vulnerabilities'}`);
    } catch (err) {
      toast.error((err as Error)?.message || 'CSV export failed');
    } finally {
      setExportingCsv(false);
    }
  }, [scan, details, totalDetails]);

  const exportSarif = useCallback(async () => {
    if (!scan) return;
    try {
      const res = await apiFetch(`/security/scans/${scan.id}/sarif`);
      if (!res.ok) {
        const body = await res.json().catch(() => ({}));
        throw new Error(body?.error || 'Failed to generate SARIF');
      }
      const blob = await res.blob();
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = `${scan.image_ref.replace(/[^a-z0-9]+/gi, '_')}.sarif.json`;
      document.body.appendChild(a);
      a.click();
      a.remove();
      URL.revokeObjectURL(url);
      toast.success('SARIF downloaded');
    } catch (err) {
      const error = err as { message?: string; error?: string; data?: { error?: string } };
      toast.error(error?.message || error?.error || error?.data?.error || 'SARIF export failed');
    }
  }, [scan]);

  const meta = scan ? (
    <span className="inline-flex flex-wrap items-center gap-2">
      {scan.total_vulnerabilities} vulns · {scan.fixable_count} fixable · {scan.triggered_by}
      {scan.publicly_exposed === true && (
        <EvidenceTag tone="warn">Network exposed</EvidenceTag>
      )}
    </span>
  ) : (loading ? 'Loading…' : 'No scan');

  const footerContext = scan
    ? `Scanned ${formatTimeAgo(new Date(scan.scanned_at).getTime())}`
    : undefined;

  const secondaryActions = scan ? [
    ...(canCompare ? [{
      label: 'Compare',
      icon: compareLoading ? Loader2 : GitCompare,
      onClick: openCompareMenu,
      disabled: compareLoading,
    }] : []),
    ...(details.length > 0 ? [{
      label: 'CSV',
      icon: exportingCsv ? Loader2 : Download,
      onClick: () => { void exportCsv(); },
      disabled: exportingCsv,
    }] : []),
    ...(canExportSarif && scan.status === 'completed' ? [{
      label: 'SARIF',
      icon: Download,
      onClick: () => { void exportSarif(); },
    }] : []),
  ] : undefined;

  return (
    <>
      <SystemSheet
        open={scanId != null}
        onOpenChange={(open) => !open && onClose()}
        crumb={['Security', 'Scans', scan?.image_ref ?? '…']}
        name={scan?.image_ref ?? 'Loading…'}
        meta={meta}
        primaryAction={onRescan && scan ? {
          label: 'Re-scan',
          icon: RefreshCw,
          onClick: () => onRescan(scan.image_ref),
          disabled: scan.status === 'in_progress',
        } : undefined}
        secondaryActions={secondaryActions}
        tabs={scan ? [
          { id: 'vulns', label: 'Vulnerabilities', count: totalDetails },
          { id: 'secrets', label: 'Secrets', count: scan.secret_count ?? secrets.length },
          { id: 'misconfigs', label: 'Misconfigs', count: scan.misconfig_count ?? misconfigs.length },
        ] : undefined}
        activeTab={tab}
        onTabChange={(id) => setTab(id as FindingTab)}
        footerContext={footerContext}
        size="lg"
        noScroll
      >
        {loading && !scan && (
          <div className="flex items-center justify-center py-12">
            <Loader2 className="w-6 h-6 animate-spin text-muted-foreground" strokeWidth={1.5} />
          </div>
        )}

        {scan && (
          // noScroll skips SystemSheet's default px-6 py-5 wrapper, so supply it
          // here; SheetSection's -mx-6 bleed depends on this px-6. The column lets
          // the active finding section flex to fill the sheet (single scroll box).
          <div className="flex min-h-0 flex-1 flex-col px-6 py-5">
            <SheetSection title="Summary" className="shrink-0">
              {scan.policy_evaluation?.violated && (
                <div
                  role="alert"
                  className="relative rounded border border-destructive/40 bg-destructive/10 px-3 py-2 pl-4 mb-3 shadow-card-bevel"
                >
                  <span
                    aria-hidden="true"
                    className="absolute left-0 top-0 bottom-0 w-[3px] bg-destructive/80 rounded-l"
                  />
                  <div className="font-mono text-[10px] uppercase tracking-[0.18em] text-destructive">
                    Policy violation
                  </div>
                  <div className="text-sm text-stat-value mt-0.5">
                    This scan violates{' '}
                    <span className="font-mono">{scan.policy_evaluation.policyName}</span>
                    {scan.policy_evaluation.reasons.length > 0 ? (
                      <>: matched {formatPolicyReasons(scan.policy_evaluation.reasons, scan.policy_evaluation.maxSeverity)}.</>
                    ) : (
                      <>.</>
                    )}
                  </div>
                </div>
              )}
              <div className="flex flex-wrap gap-2 mb-3">
                {scan.critical_count > 0 && (
                  <span className={cn('rounded border px-2 py-1 text-xs font-mono tabular-nums uppercase tracking-[0.18em] shadow-card-bevel', SEVERITY_CLASSES.CRITICAL)}>
                    {scan.critical_count} CRITICAL
                  </span>
                )}
                {scan.high_count > 0 && (
                  <span className={cn('rounded border px-2 py-1 text-xs font-mono tabular-nums uppercase tracking-[0.18em] shadow-card-bevel', SEVERITY_CLASSES.HIGH)}>
                    {scan.high_count} HIGH
                  </span>
                )}
                {scan.medium_count > 0 && (
                  <span className={cn('rounded border px-2 py-1 text-xs font-mono tabular-nums uppercase tracking-[0.18em] shadow-card-bevel', SEVERITY_CLASSES.MEDIUM)}>
                    {scan.medium_count} MEDIUM
                  </span>
                )}
                {scan.low_count > 0 && (
                  <span className={cn('rounded border px-2 py-1 text-xs font-mono tabular-nums uppercase tracking-[0.18em] shadow-card-bevel', SEVERITY_CLASSES.LOW)}>
                    {scan.low_count} LOW
                  </span>
                )}
                {scan.total_vulnerabilities === 0 && (
                  <span className="rounded border border-success/40 bg-success/10 text-success px-2 py-1 text-xs font-mono tabular-nums uppercase tracking-[0.18em] shadow-card-bevel">
                    No vulnerabilities
                  </span>
                )}
              </div>

              <div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-xs">
                <div>
                  <div className="font-mono text-[10px] uppercase tracking-[0.18em] text-stat-subtitle">Total</div>
                  <div className="font-mono tabular-nums text-stat-value">{scan.total_vulnerabilities}</div>
                </div>
                <div>
                  <div className="font-mono text-[10px] uppercase tracking-[0.18em] text-stat-subtitle">Fixable</div>
                  <div className="font-mono tabular-nums text-success">{scan.fixable_count}</div>
                </div>
                <div>
                  <div className="font-mono text-[10px] uppercase tracking-[0.18em] text-stat-subtitle">Triggered</div>
                  <div className="font-mono tabular-nums text-stat-value">{scan.triggered_by}</div>
                </div>
                <div>
                  <div className="font-mono text-[10px] uppercase tracking-[0.18em] text-stat-subtitle">Scanned</div>
                  <div className="font-mono tabular-nums text-stat-value">
                    {new Date(scan.scanned_at).toLocaleString()}
                  </div>
                </div>
              </div>

              {canGenerateSbom && (
                <div className="flex flex-wrap items-center gap-2 mt-3">
                  <DropdownMenu>
                    <DropdownMenuTrigger asChild>
                      <Button variant="outline" size="sm" disabled={downloadingSbom}>
                        {downloadingSbom ? (
                          <Loader2 className="w-3.5 h-3.5 mr-1.5 animate-spin" strokeWidth={1.5} />
                        ) : (
                          <Download className="w-3.5 h-3.5 mr-1.5" strokeWidth={1.5} />
                        )}
                        SBOM
                      </Button>
                    </DropdownMenuTrigger>
                    <DropdownMenuContent align="end">
                      <DropdownMenuItem onClick={() => downloadSbom('spdx-json')}>
                        SPDX JSON
                      </DropdownMenuItem>
                      <DropdownMenuItem onClick={() => downloadSbom('cyclonedx')}>
                        CycloneDX
                      </DropdownMenuItem>
                    </DropdownMenuContent>
                  </DropdownMenu>
                </div>
              )}

              {compareOpen && canCompare && (
                <div className="pt-3 space-y-2">
                  {compareOptions.length === 0 && !compareLoading ? (
                    <div className="rounded border border-border bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
                      No other completed scans for this image yet. Run a second scan to enable comparison.
                    </div>
                  ) : (
                    <>
                      <div className="font-mono text-[10px] uppercase tracking-[0.18em] text-stat-subtitle">
                        Compare against
                      </div>
                      <Combobox
                        options={compareOptions.map((s) => ({
                          value: String(s.id),
                          label: `${new Date(s.scanned_at).toLocaleString()} - ${s.total_vulnerabilities} findings (${s.triggered_by})`,
                        }))}
                        value={compareBaselineId != null ? String(compareBaselineId) : ''}
                        onValueChange={(v) => setCompareBaselineId(v ? Number(v) : null)}
                        placeholder="Choose a baseline scan..."
                        searchPlaceholder="Search by date..."
                      />
                    </>
                  )}
                </div>
              )}
            </SheetSection>

            {tab === 'vulns' && (
              <SheetSection
                title={
                  drivingFindings
                    ? (() => {
                        const n = filtered.length;
                        const total = driverCount ?? n;
                        const truncated = driversTruncated && total > n;
                        if (driverFilterMode === 'monitoring') {
                          return truncated
                            ? `Findings under Monitoring · showing ${n} of ${total}`
                            : `Findings under Monitoring · ${n} finding${n === 1 ? '' : 's'}`;
                        }
                        return truncated
                          ? `Driving current Security action · showing ${n} of ${total}`
                          : `Driving current Security action · ${n} finding${n === 1 ? '' : 's'}`;
                      })()
                    : `Vulnerabilities · ${totalDetails}`
                }
                className="flex min-h-0 flex-1 flex-col"
              >
                {drivingFindings ? (
                  <p className="text-xs text-stat-subtitle mb-3">
                    {driverFilterMode === 'monitoring'
                      ? 'Showing findings under Monitoring for this image. Remediating or triaging these findings can change Security posture.'
                      : 'Showing the findings that currently drive Action needed for this image. Remediating or triaging these findings updates Security posture.'}
                  </p>
                ) : null}
                <div className="flex items-center gap-1 flex-wrap mb-3">
                  {(['ALL', 'CRITICAL', 'HIGH', 'MEDIUM', 'LOW'] as SeverityFilter[]).map((s) => (
                    <Button
                      key={s}
                      variant={severityFilter === s ? 'default' : 'ghost'}
                      size="sm"
                      className="h-7 text-xs px-2.5"
                      onClick={() => {
                        setSeverityFilter(s);
                        setPage(0);
                      }}
                    >
                      {s}
                    </Button>
                  ))}
                  {needsPagination && (
                    <div className="flex items-center gap-1 ml-auto">
                      <Button
                        variant="ghost"
                        size="icon"
                        className="h-6 w-6"
                        onClick={() => setPage(Math.max(0, safePage - 1))}
                        disabled={safePage === 0}
                      >
                        <ChevronLeft className="w-4 h-4" strokeWidth={1.5} />
                      </Button>
                      <span className="text-xs font-mono tabular-nums text-stat-subtitle min-w-[3rem] text-center">
                        {safePage + 1} / {totalPages}
                      </span>
                      <Button
                        variant="ghost"
                        size="icon"
                        className="h-6 w-6"
                        onClick={() => setPage(Math.min(totalPages - 1, safePage + 1))}
                        disabled={safePage >= totalPages - 1}
                      >
                        <ChevronRight className="w-4 h-4" strokeWidth={1.5} />
                      </Button>
                    </div>
                  )}
                </div>

                <ScrollArea block className="flex-1 min-h-0">
                  {pageItems.length === 0 ? (
                    <div className="text-center text-sm text-muted-foreground py-12">
                      {details.length === 0
                        ? 'No vulnerabilities found.'
                        : 'No vulnerabilities match the selected filter.'}
                    </div>
                  ) : (
                    <Table className="max-md:min-w-[720px]">
                      <TableHeader>
                        <TableRow>
                          <TableHead className="w-[180px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">CVE</TableHead>
                          <TableHead className="text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Package</TableHead>
                          <TableHead className="w-[100px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Severity</TableHead>
                          <TableHead className="w-[110px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Installed</TableHead>
                          <TableHead className="w-[110px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Fixed</TableHead>
                          {canManageSuppressions && <TableHead className="w-[40px]" />}
                        </TableRow>
                      </TableHeader>
                      <TableBody>
                        {pageItems.map((d) => {
                          const href = cveUrl(d.vulnerability_id, d.primary_url);
                          const dimmed = Boolean(d.suppressed) && !isActiveTriageStatus(d.triage_status);
                          return (
                            <TableRow
                              key={d.id}
                              className={cn(SEVERITY_ROW_TINT[d.severity], dimmed && 'opacity-60')}
                            >
                              <TableCell className="font-mono text-xs tabular-nums align-top">
                                <span className="flex flex-col">
                                  <span className="inline-flex items-center gap-1.5">
                                    {d.suppressed && (
                                      <ShieldOff
                                        className="w-3 h-3 text-muted-foreground"
                                        strokeWidth={1.5}
                                        aria-label="Suppressed"
                                      />
                                    )}
                                    {href ? (
                                      <a
                                        href={href}
                                        target="_blank"
                                        rel="noreferrer noopener"
                                        className="inline-flex items-center gap-1 hover:underline"
                                      >
                                        {d.vulnerability_id}
                                        <ExternalLink className="w-3 h-3" strokeWidth={1.5} />
                                      </a>
                                    ) : (
                                      d.vulnerability_id
                                    )}
                                  </span>
                                  <EvidenceTags d={d} />
                                </span>
                              </TableCell>
                              <TableCell
                                className="font-mono text-xs truncate max-w-[180px]"
                                title={d.suppression_reason || d.pkg_name}
                              >
                                {d.pkg_name}
                              </TableCell>
                              <TableCell>
                                <SeverityChip severity={d.severity} />
                              </TableCell>
                              <TableCell className="font-mono text-xs">{d.installed_version}</TableCell>
                              <TableCell className="font-mono text-xs">
                                {d.fixed_version ? (
                                  <span className="inline-flex items-center gap-1 text-success">
                                    <Check className="w-3 h-3" strokeWidth={1.5} />
                                    {d.fixed_version}
                                  </span>
                                ) : (
                                  <span className="text-muted-foreground">-</span>
                                )}
                              </TableCell>
                              {canManageSuppressions && (
                                <TableCell>
                                  {!d.suppressed && (
                                    <Button
                                      variant="ghost"
                                      size="icon"
                                      className="h-7 w-7 text-muted-foreground/70 hover:text-foreground"
                                      title="Triage finding"
                                      aria-label="Triage finding"
                                      onClick={() => openSuppressDialog(d)}
                                    >
                                      <ShieldOff className="w-3.5 h-3.5" strokeWidth={1.5} />
                                    </Button>
                                  )}
                                </TableCell>
                              )}
                            </TableRow>
                          );
                        })}
                      </TableBody>
                    </Table>
                  )}
                </ScrollArea>
              </SheetSection>
            )}

            {tab === 'secrets' && (
              <SheetSection title={`Secrets · ${scan.secret_count ?? secrets.length}`} className="flex min-h-0 flex-1 flex-col">
                {secretsNeedsPagination && (
                  <div className="flex items-center gap-1 mb-3">
                    <div className="flex items-center gap-1 ml-auto">
                      <Button
                        variant="ghost"
                        size="icon"
                        className="h-6 w-6"
                        onClick={() => setSecretsPage(Math.max(0, secretsSafePage - 1))}
                        disabled={secretsSafePage === 0}
                      >
                        <ChevronLeft className="w-4 h-4" strokeWidth={1.5} />
                      </Button>
                      <span className="text-xs font-mono tabular-nums text-stat-subtitle min-w-[3rem] text-center">
                        {secretsSafePage + 1} / {secretsTotalPages}
                      </span>
                      <Button
                        variant="ghost"
                        size="icon"
                        className="h-6 w-6"
                        onClick={() =>
                          setSecretsPage(Math.min(secretsTotalPages - 1, secretsSafePage + 1))
                        }
                        disabled={secretsSafePage >= secretsTotalPages - 1}
                      >
                        <ChevronRight className="w-4 h-4" strokeWidth={1.5} />
                      </Button>
                    </div>
                  </div>
                )}
                <ScrollArea block className="flex-1 min-h-0">
                  {secrets.length === 0 ? (
                    <div className="text-center text-sm text-muted-foreground py-12">
                      No secrets detected.
                    </div>
                  ) : (
                    <Table className="max-md:min-w-[720px]">
                      <TableHeader>
                        <TableRow>
                          <TableHead className="w-[100px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Severity</TableHead>
                          <TableHead className="w-[160px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Rule</TableHead>
                          <TableHead className="text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Title</TableHead>
                          <TableHead className="w-[260px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Target</TableHead>
                        </TableRow>
                      </TableHeader>
                      <TableBody>
                        {secretsPageItems.map((s) => (
                          <TableRow key={s.id} className={SEVERITY_ROW_TINT[s.severity]}>
                            <TableCell>
                              <SeverityChip severity={s.severity} />
                            </TableCell>
                            <TableCell className="font-mono text-xs truncate max-w-[160px]" title={s.rule_id}>
                              {s.rule_id}
                            </TableCell>
                            <TableCell className="text-xs">
                              <div className="truncate max-w-[320px]" title={s.title ?? undefined}>
                                {s.title || <span className="text-muted-foreground">-</span>}
                              </div>
                              {s.match_excerpt && (
                                <div
                                  className="font-mono text-[11px] text-muted-foreground truncate max-w-[320px]"
                                  title={s.match_excerpt}
                                >
                                  {s.match_excerpt}
                                </div>
                              )}
                            </TableCell>
                            <TableCell className="font-mono text-xs truncate max-w-[260px]" title={s.target}>
                              {s.target}
                              {s.start_line != null && (
                                <span className="text-muted-foreground">
                                  :{s.start_line}
                                  {s.end_line != null && s.end_line !== s.start_line
                                    ? `-${s.end_line}`
                                    : ''}
                                </span>
                              )}
                            </TableCell>
                          </TableRow>
                        ))}
                      </TableBody>
                    </Table>
                  )}
                </ScrollArea>
              </SheetSection>
            )}

            {tab === 'misconfigs' && (
              <SheetSection title={`Misconfigs · ${scan.misconfig_count ?? misconfigs.length}`} className="flex min-h-0 flex-1 flex-col">
                {misconfigsNeedsPagination && (
                  <div className="flex items-center gap-1 mb-3">
                    <div className="flex items-center gap-1 ml-auto">
                      <Button
                        variant="ghost"
                        size="icon"
                        className="h-6 w-6"
                        onClick={() => setMisconfigsPage(Math.max(0, misconfigsSafePage - 1))}
                        disabled={misconfigsSafePage === 0}
                      >
                        <ChevronLeft className="w-4 h-4" strokeWidth={1.5} />
                      </Button>
                      <span className="text-xs font-mono tabular-nums text-stat-subtitle min-w-[3rem] text-center">
                        {misconfigsSafePage + 1} / {misconfigsTotalPages}
                      </span>
                      <Button
                        variant="ghost"
                        size="icon"
                        className="h-6 w-6"
                        onClick={() =>
                          setMisconfigsPage(
                            Math.min(misconfigsTotalPages - 1, misconfigsSafePage + 1),
                          )
                        }
                        disabled={misconfigsSafePage >= misconfigsTotalPages - 1}
                      >
                        <ChevronRight className="w-4 h-4" strokeWidth={1.5} />
                      </Button>
                    </div>
                  </div>
                )}
                <ScrollArea block className="flex-1 min-h-0">
                  {misconfigs.length === 0 ? (
                    <div className="text-center text-sm text-muted-foreground py-12">
                      No misconfigurations detected.
                    </div>
                  ) : (
                    <Table className="max-md:min-w-[720px]">
                      <TableHeader>
                        <TableRow>
                          <TableHead className="w-[100px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Severity</TableHead>
                          <TableHead className="w-[140px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Check</TableHead>
                          <TableHead className="text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Title</TableHead>
                          <TableHead className="w-[200px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Target</TableHead>
                          <TableHead className="w-[220px] text-[10px] uppercase tracking-[0.18em] font-mono text-stat-subtitle">Fix</TableHead>
                          {canManageSuppressions && <TableHead className="w-[40px]" />}
                        </TableRow>
                      </TableHeader>
                      <TableBody>
                        {misconfigsPageItems.map((m) => (
                          <TableRow
                            key={m.id}
                            className={cn(
                              SEVERITY_ROW_TINT[m.severity],
                              m.acknowledged && 'opacity-60',
                            )}
                            title={
                              m.acknowledged
                                ? `Acknowledged: ${m.acknowledgement_reason ?? '(no reason)'}`
                                : undefined
                            }
                          >
                            <TableCell>
                              <SeverityChip severity={m.severity} />
                            </TableCell>
                            <TableCell
                              className="font-mono text-xs truncate max-w-[140px]"
                              title={m.check_id ?? m.rule_id}
                            >
                              {m.check_id || m.rule_id}
                            </TableCell>
                            <TableCell className={cn('text-xs', m.acknowledged && 'line-through')}>
                              <div className="truncate max-w-[320px]" title={m.title ?? undefined}>
                                {m.primary_url ? (
                                  <a
                                    href={m.primary_url}
                                    target="_blank"
                                    rel="noreferrer noopener"
                                    className="inline-flex items-center gap-1 hover:underline"
                                  >
                                    {m.title || m.rule_id}
                                    <ExternalLink className="w-3 h-3" strokeWidth={1.5} />
                                  </a>
                                ) : (
                                  m.title || m.rule_id
                                )}
                              </div>
                              {m.message && (
                                <div
                                  className="text-[11px] text-muted-foreground truncate max-w-[320px]"
                                  title={m.message}
                                >
                                  {m.message}
                                </div>
                              )}
                            </TableCell>
                            <TableCell className="font-mono text-xs truncate max-w-[200px]" title={m.target}>
                              {m.target}
                            </TableCell>
                            <TableCell className="text-xs truncate max-w-[220px]" title={m.resolution ?? undefined}>
                              {m.resolution || <span className="text-muted-foreground">-</span>}
                            </TableCell>
                            {canManageSuppressions && (
                              <TableCell>
                                {!m.acknowledged && (
                                  <Button
                                    variant="ghost"
                                    size="icon"
                                    className="h-7 w-7"
                                    onClick={() => openAckDialog(m)}
                                    title="Acknowledge this misconfiguration"
                                  >
                                    <ShieldCheck className="w-3.5 h-3.5" strokeWidth={1.5} />
                                  </Button>
                                )}
                              </TableCell>
                            )}
                          </TableRow>
                        ))}
                      </TableBody>
                    </Table>
                  )}
                </ScrollArea>
              </SheetSection>
            )}
          </div>
        )}
      </SystemSheet>

      <ScanComparisonSheet
        baselineScanId={compareBaselineId}
        currentScanId={compareBaselineId != null ? scanId : null}
        onClose={() => setCompareBaselineId(null)}
      />

      <Dialog
        open={suppressForm !== null}
        onOpenChange={(open) => !open && setSuppressForm(null)}
      >
        <DialogContent className="sm:max-w-md">
          <DialogHeader>
            <DialogTitle>Triage finding</DialogTitle>
            <DialogDescription className="sr-only">
              Record a triage decision for this finding so it stops or continues driving security posture.
            </DialogDescription>
          </DialogHeader>
          {suppressForm && (
            <div className="space-y-4 py-2">
              <div className="grid grid-cols-2 gap-3">
                <div className="space-y-1">
                  <Label className="text-xs uppercase tracking-wide text-stat-subtitle">CVE</Label>
                  <div className="font-mono text-sm">{suppressForm.cveId}</div>
                </div>
                <div className="space-y-1">
                  <Label className="text-xs uppercase tracking-wide text-stat-subtitle">Package</Label>
                  <div className="font-mono text-sm truncate" title={suppressForm.pkgName}>
                    {suppressForm.pkgName || '-'}
                  </div>
                </div>
              </div>
              <div className="space-y-2">
                <Label htmlFor="suppress-pattern">Image pattern (optional)</Label>
                <Input
                  id="suppress-pattern"
                  placeholder="e.g. registry.internal/* (leave blank for all images)"
                  value={suppressForm.imagePattern}
                  onChange={(e) =>
                    setSuppressForm((f) => (f ? { ...f, imagePattern: e.target.value } : f))
                  }
                />
                <p className="text-xs text-muted-foreground">
                  Defaults to this image. Clear or broaden the pattern to apply more widely; leave blank for every image.
                </p>
              </div>
              <div className="space-y-2">
                <Label htmlFor="suppress-status">Triage decision</Label>
                <Select
                  value={suppressForm.status}
                  onValueChange={(v) => {
                    const status = v as TriageStatus;
                    setSuppressForm((f) =>
                      f ? { ...f, status, justification: justificationForStatus(status, f.justification) } : f,
                    );
                  }}
                >
                  <SelectTrigger id="suppress-status" aria-label="Triage decision" className="w-full">
                    <SelectValue />
                  </SelectTrigger>
                  <SelectContent>
                    {TRIAGE_STATUS_OPTIONS.map((o) => (
                      <SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>
                    ))}
                  </SelectContent>
                </Select>
                <p className="text-xs text-muted-foreground">
                  {TRIAGE_STATUS_HINT}
                </p>
              </div>
              {openVexRequiresJustification(suppressForm.status) && (
                <div className="space-y-2">
                  <Label htmlFor="suppress-justification">OpenVEX justification</Label>
                  <Select
                    value={suppressForm.justification}
                    onValueChange={(v) =>
                      setSuppressForm((f) => (f ? { ...f, justification: v } : f))
                    }
                  >
                    <SelectTrigger id="suppress-justification" aria-label="OpenVEX justification" className="w-full">
                      <SelectValue placeholder="Select a justification" />
                    </SelectTrigger>
                    <SelectContent>
                      {TRIAGE_JUSTIFICATION_OPTIONS.map((o) => (
                        <SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>
                      ))}
                    </SelectContent>
                  </Select>
                </div>
              )}
              <div className="space-y-2">
                <Label htmlFor="suppress-reason">Reason</Label>
                <textarea
                  id="suppress-reason"
                  className="flex min-h-[72px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
                  placeholder="Why is this CVE safe to accept?"
                  value={suppressForm.reason}
                  onChange={(e) =>
                    setSuppressForm((f) => (f ? { ...f, reason: e.target.value } : f))
                  }
                />
              </div>
              <div className="space-y-2">
                <Label htmlFor="suppress-expiry">Expires in (days, optional)</Label>
                <Input
                  id="suppress-expiry"
                  type="number"
                  min="1"
                  placeholder="Leave blank for no expiry"
                  value={suppressForm.expiresInDays}
                  onChange={(e) =>
                    setSuppressForm((f) => (f ? { ...f, expiresInDays: e.target.value } : f))
                  }
                />
              </div>
            </div>
          )}
          <DialogFooter>
            <Button variant="outline" onClick={() => setSuppressForm(null)} disabled={savingSuppression}>
              Cancel
            </Button>
            <Button onClick={submitSuppression} disabled={savingSuppression}>
              {savingSuppression ? 'Saving...' : 'Save'}
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>

      <Dialog
        open={ackForm !== null}
        onOpenChange={(open) => !open && setAckForm(null)}
      >
        <DialogContent className="sm:max-w-md">
          <DialogHeader>
            <DialogTitle>Acknowledge misconfiguration</DialogTitle>
            <DialogDescription className="sr-only">
              Accept this misconfiguration as known-benign so it stops triggering alerts across the fleet.
            </DialogDescription>
          </DialogHeader>
          {ackForm && (
            <div className="space-y-4 py-2">
              <div className="space-y-1">
                <Label className="text-xs uppercase tracking-wide text-stat-subtitle">Rule</Label>
                <div className="font-mono text-sm">{ackForm.ruleId}</div>
              </div>
              <div className="space-y-2">
                <Label htmlFor="ack-pattern">Stack pattern (optional)</Label>
                <Input
                  id="ack-pattern"
                  placeholder="e.g. traefik or web-* (leave blank for all stacks)"
                  value={ackForm.stackPattern}
                  onChange={(e) =>
                    setAckForm((f) => (f ? { ...f, stackPattern: e.target.value } : f))
                  }
                />
                <p className="text-xs text-muted-foreground">
                  Glob pattern matched against the stack name. Prefilled with the current stack so this ack stays narrowest by default.
                </p>
              </div>
              <div className="space-y-2">
                <Label htmlFor="ack-reason">Reason</Label>
                <textarea
                  id="ack-reason"
                  className="flex min-h-[72px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
                  placeholder="Why is this misconfiguration safe to accept?"
                  value={ackForm.reason}
                  onChange={(e) =>
                    setAckForm((f) => (f ? { ...f, reason: e.target.value } : f))
                  }
                />
              </div>
              <div className="space-y-2">
                <Label htmlFor="ack-expiry">Expires in (days, optional)</Label>
                <Input
                  id="ack-expiry"
                  type="number"
                  min="1"
                  placeholder="Leave blank for no expiry"
                  value={ackForm.expiresInDays}
                  onChange={(e) =>
                    setAckForm((f) => (f ? { ...f, expiresInDays: e.target.value } : f))
                  }
                />
              </div>
            </div>
          )}
          <DialogFooter>
            <Button variant="outline" onClick={() => setAckForm(null)} disabled={savingAck}>
              Cancel
            </Button>
            <Button onClick={submitAcknowledgement} disabled={savingAck}>
              {savingAck ? 'Saving...' : 'Acknowledge'}
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </>
  );
}

export { SeverityChip };
export type { VulnerabilityScanSheetProps };
