// ChaseNewPanel — the stations PSK Reporter is hearing NEAR HERE that are new
// against the log.
//
// A DX cluster tells you what somebody chose to spot. This tells you what is
// actually being decoded in your own region, which is a different and often
// larger set: nobody spots the FT8 caller running ten watts from a rare square.
//
// The one thing an operator has to know, and the reason for the line at the
// bottom: PSK Reporter carries DIGITAL MODES ONLY. An empty panel means nothing
// new is being decoded on FT8/FT4/JS8 near here — not that the band is dead.
import { useEffect, useMemo, useState } from 'react';
import { Radar, Loader2, X } from 'lucide-react';
import { formatDistance } from '@/lib/units';
import { useI18n } from '@/lib/i18n';
import { chaseAllows } from '@/lib/spotDisplay';
import { markerColour } from '@/lib/spotMarkers';
import { cn } from '@/lib/utils';
import { GetChaseNewSpots, GetPSKReporterStatus } from '../../wailsjs/go/main/App';

export interface ChaseNewSpot {
  call: string;
  band: string;
  mode: string;
  freq_hz?: number;
  grid?: string;
  country?: string;
  cont?: string;
  dist_km?: number;
  bearing?: number;
  status?: string;
  new_pfx?: boolean;
  new_grid?: boolean;
  lotw?: boolean;
  at: string;
}

interface Props {
  // Tuning the rig to a row is the whole point — a station heard on 14.074 is
  // only useful if you can get there in one click.
  onPick?: (s: ChaseNewSpot) => void;
  onClose?: () => void;
}

// The categories, in priority order. A station can be several at once — a new
// band AND a new prefix — but a row shows ONE, the first that matches here.
//
// The order is by what would make an operator leave what they are doing: a new
// entity beats a new band on it, which beats a new mode, which beats a new slot;
// a prefix or a square is worth knowing but never worth interrupting a QSO for.
// Two badges on one line also made the list unreadable at a glance, which is the
// only thing this panel is for.
type Category = 'dxcc' | 'band' | 'mode' | 'slot' | 'pfx' | 'grid';

const CATEGORIES: Array<{ key: Category; labelKey: string; colour: string }> = [
  { key: 'dxcc', labelKey: 'clg2.newDxcc', colour: 'var(--danger)' },
  { key: 'band', labelKey: 'clg2.newBand', colour: 'var(--danger)' },
  { key: 'mode', labelKey: 'clg2.newMode', colour: 'var(--danger)' },
  { key: 'slot', labelKey: 'clg2.newSlot', colour: 'var(--danger)' },
  { key: 'pfx', labelKey: 'clg2.newPfx', colour: markerColour('new_pfx') },
  { key: 'grid', labelKey: 'clg2.newGrid', colour: markerColour('new_grid') },
];

// categoryOf is the single thing a row says about a station.
//
// A category the operator does not chase is not a category here either: with
// prefixes and squares switched off this panel showed rows whose only reason
// for being listed had been withdrawn everywhere else.
function categoryOf(s: ChaseNewSpot): Category | null {
  switch (s.status) {
    case 'new': return 'dxcc';
    case 'new-band-mode': return 'band';
    case 'new-band': return 'band';
    case 'new-mode': return 'mode';
    case 'new-slot': return 'slot';
  }
  if (s.new_pfx && chaseAllows('pfx')) return 'pfx';
  if (s.new_grid && chaseAllows('grid')) return 'grid';
  return null;
}

const FILTER_KEY = 'opslog.chaseNewFilters';

function allowedCategories(): Category[] {
  return CATEGORIES.filter((c) => chaseAllows(c.key)).map((c) => c.key);
}

function loadFilters(): Set<Category> {
  const allowed = allowedCategories();
  try {
    const raw = localStorage.getItem(FILTER_KEY);
    if (raw) {
      const list = JSON.parse(raw) as Category[];
      // A stored set holding NONE of the categories on offer hides the whole
      // panel, for ever, with nothing to say why — and that is exactly what a
      // preference written by an older build does once a category is renamed.
      // Treated as "no preference": a panel that shows nothing at every launch
      // is never what was meant, and the chips are one click away.
      if (Array.isArray(list) && list.some((k) => allowed.includes(k))) return new Set(list);
    }
  } catch { /* a corrupt preference is not worth a broken panel */ }
  return new Set(allowed);
}

export function ChaseNewPanel({ onPick, onClose }: Props) {
  const { t } = useI18n();
  const [spots, setSpots] = useState<ChaseNewSpot[]>([]);
  const [loaded, setLoaded] = useState(false);
  const [on, setOn] = useState<Set<Category>>(loadFilters);
  // The FEED, not the list: connected or not, how many reports it has taken,
  // and what it is filtered on. Without it an empty panel says nothing about
  // whether anything is arriving at all — which is the first question.
  const [feed, setFeed] = useState<any>(null);

  // Polled rather than pushed: the feed can deliver several a second under an
  // opening, and an event per row would be a redraw per row for a list nobody
  // reads that fast.
  useEffect(() => {
    let alive = true;
    const tick = async () => {
      try {
        const r = ((await GetChaseNewSpots()) ?? []) as ChaseNewSpot[];
        if (alive) { setSpots(r); setLoaded(true); }
        const st = await GetPSKReporterStatus();
        if (alive) setFeed(st);
      } catch { /* the feed may not be up yet */ }
    };
    tick();
    const id = window.setInterval(tick, 5000);
    return () => { alive = false; window.clearInterval(id); };
  }, []);

  function toggle(k: Category) {
    setOn((prev) => {
      const next = new Set(prev);
      if (next.has(k)) next.delete(k); else next.add(k);
      try { localStorage.setItem(FILTER_KEY, JSON.stringify([...next])); } catch { /* not worth failing over */ }
      return next;
    });
  }

  const shown = useMemo(() => {
    return spots.filter((s) => {
      const c = categoryOf(s);
      return c !== null && on.has(c);
    });
  }, [spots, on]);

  return (
    <div className="flex h-full flex-col rounded-md border border-border bg-card overflow-hidden">
      <div className="flex items-center gap-2 border-b border-border px-2 py-1.5">
        <Radar className="size-3.5 shrink-0 text-primary" />
        <span className="shrink-0 text-xs font-semibold">{t('chn.title')}</span>

        {/* Filters, in the same order and colours as the badges they hide. */}
        <div className="flex flex-1 flex-wrap items-center gap-1">
          {CATEGORIES.filter((c) => chaseAllows(c.key)).map((c) => (
            <button
              key={c.key}
              type="button"
              onClick={() => toggle(c.key)}
              title={t('chn.filterHint')}
              className={cn(
                'rounded border px-1 py-px text-[9px] font-bold uppercase tracking-wide transition-colors',
                on.has(c.key) ? 'border-transparent' : 'border-border text-muted-foreground opacity-50',
              )}
              style={on.has(c.key) ? { color: c.colour, borderColor: c.colour } : undefined}
            >
              {t(c.labelKey).replace(/^(NEW|NOUV)\s+/i, '')}
            </button>
          ))}
        </div>

        {/* Both numbers: what is on screen, and what was heard. They differ
            exactly when a category is switched off, which is the one case an
            operator reads this panel as broken. */}
        <span className="shrink-0 text-[10px] text-muted-foreground">
          {loaded ? (shown.length === spots.length
            ? t('chn.count', { n: spots.length })
            : t('chn.countOf', { n: shown.length, total: spots.length })) : ''}
        </span>
        {onClose && (
          <button
            type="button"
            onClick={onClose}
            title={t('chn.close')}
            className="shrink-0 rounded p-0.5 text-muted-foreground hover:bg-muted hover:text-foreground"
          >
            <X className="size-3.5" />
          </button>
        )}
      </div>

      <div className="flex-1 overflow-y-auto">
        {!loaded ? (
          <div className="flex items-center justify-center gap-2 p-3 text-[11px] text-muted-foreground">
            <Loader2 className="size-3 animate-spin" /> {t('chn.loading')}
          </div>
        ) : shown.length === 0 ? (
          <p className="p-3 text-[11px] text-muted-foreground leading-relaxed">
            {spots.length === 0 ? t('chn.empty') : t('chn.allFiltered')}
          </p>
        ) : (
          <div className="divide-y divide-border/60">
            {shown.map((s, i) => {
              const c = categoryOf(s);
              const def = CATEGORIES.find((x) => x.key === c);
              return (
                <button
                  key={`${s.call}-${s.band}-${s.mode}-${i}`}
                  type="button"
                  onClick={() => onPick?.(s)}
                  className="flex w-full items-center gap-1.5 px-2 py-1 text-left text-[11px] hover:bg-accent/40"
                  title={[
                    s.country,
                    s.grid,
                    s.dist_km ? formatDistance(s.dist_km) : '',
                    s.freq_hz ? `${(s.freq_hz / 1000).toFixed(1)} kHz` : '',
                  ].filter(Boolean).join(' · ')}
                >
                  <span className="w-[84px] shrink-0 truncate font-mono font-bold">{s.call}</span>
                  <span className="w-9 shrink-0 font-mono text-muted-foreground">{s.band}</span>
                  <span className="w-10 shrink-0 truncate text-muted-foreground">{s.mode}</span>
                  {/* No frequency column: clicking the row tunes the rig to it,
                      so the number was a thing to read and never to use. It is
                      still carried on the spot — the click is what needs it —
                      and shows in the row's tooltip for anyone who wants it.
                      The country takes the space it freed. */}
                  <span className="min-w-0 flex-1 truncate text-muted-foreground">{s.country ?? ''}</span>
                  {def && (
                    <span className="shrink-0 text-[9px] font-bold" style={{ color: def.colour }}>
                      {t(def.labelKey)}
                    </span>
                  )}
                  {s.lotw && <span className="shrink-0 text-[9px] text-info" title="LoTW">L</span>}
                </button>
              );
            })}
          </div>
        )}
      </div>

      {/* The radius comes from the FEED, not from a sentence: it was written
          into this line as "~300 km" and stayed 300 while the setting said
          1000, which is the panel telling the operator their change did not
          take when it had. */}
      <p className="border-t border-border px-2 py-1 text-[10px] text-muted-foreground">
        {t('chn.heardWithin', { km: feed?.near_km || 300 })}
        {feed && (feed.running
          ? <span className="text-success"> · {t('chn.feedOn', { n: feed.received ?? 0, sq: feed.squares ?? 0 })}</span>
          : <span className="text-warning"> · {feed.last_err ? t('chn.feedErr', { e: feed.last_err }) : t('chn.feedOff')}</span>)}
      </p>
    </div>
  );
}
