// PSKReporterPanel — can the station I am about to call actually hear me?
//
// The decodes list to the left says who is transmitting. It cannot say anything
// about the other direction, and on FT8 that is the whole question: the DX's
// pileup is invisible from here, and a station whose region is not open to
// yours will not hear you however many times you call.
//
// Every number here comes from PSK Reporter — reports uploaded by ordinary
// stations saying "I decoded X" — over a five-minute window. Nothing is
// inferred and nothing is remembered: when the window empties the panel says it
// does not know, which is the honest answer and the reason each block also says
// what it is measuring.
//
// The backend (internal/pskrtgt) does the analysis; this draws it and polls.
import { useEffect, useMemo, useState } from 'react';
import { Activity, ChevronRight } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useI18n } from '@/lib/i18n';
import { GetPSKAnalysis, SetPSKTarget } from '../../wailsjs/go/main/App';

export type PSKEntry = {
  call: string;
  grid?: string;
  snr: number;
  offset_hz: number;
  age_sec: number;
};

export type PSKAnalysis = {
  target?: string;
  mode?: string;
  enabled: boolean;
  online: boolean;
  spots: number;
  he_me: boolean;
  he_me_seconds: number;
  he_me_snr: number;
  he_me_offset_hz: number;
  target_uploads: boolean;
  target_grid?: string;
  near_him_count: number;
  near_him_top?: PSKEntry[];
  from_my_area_count: number;
  from_my_area_top?: PSKEntry[];
  path_open: boolean;
  heard_by_count: number;
  heard_near_me: number;
  heard_near_me_top?: PSKEntry[];
  decoded_by_count: number;
  decoded_by_top?: PSKEntry[];
  decoded_by_calls?: string[];
  pileup_count: number;
  dial_hz: number;
  ceiling_hz: number;
  decodes_in_window: number;
  bins?: { offset_hz: number; count: number; avg_snr: number }[];
  suggested_offset: number;
};

interface Props {
  // The station to analyse and the mode it was heard on. Set by clicking a
  // decode, or by whoever the digital application says it is calling.
  target: string;
  mode?: string;
  // The operator's own dial, which is what turns a report's frequency into an
  // audio offset. Without it the passband block has nothing to say.
  dialHz?: number;
  // The local decodes, for "callers you hear": stations WE are decoding that
  // are calling the same DX. That is the competition measured at this end,
  // which no amount of PSK Reporter data can show.
  callers: number;
  callerCalls?: string[];
  onCollapse: () => void;
}

// The passband strip: 60 Hz bins, drawn from 200 Hz to 4000 Hz. The bin edges
// have to match the backend's alignment exactly — it keys them on multiples of
// 60 from zero, so a strip starting at 200 would ask for edges that never
// exist and draw an empty histogram over a busy passband.
const LO = 200, HI = 4000, STEP = 60;
const FIRST_EDGE = Math.floor(LO / STEP) * STEP;
const COLS = Math.floor((HI - FIRST_EDGE) / STEP);

export function PSKReporterPanel({ target, mode, dialHz, callers, callerCalls, onCollapse }: Props) {
  const { t } = useI18n();
  const [a, setA] = useState<PSKAnalysis | null>(null);

  // One second, matching the panel's own claim about how fresh it is. The call
  // is a snapshot of an in-memory window — no query and no network of its own.
  useEffect(() => {
    let stop = false;
    const tick = () => {
      GetPSKAnalysis().then((r) => { if (!stop) setA(r as unknown as PSKAnalysis); }).catch(() => {});
    };
    tick();
    const id = window.setInterval(tick, 1000);
    return () => { stop = true; window.clearInterval(id); };
  }, []);

  // The target is re-asserted rather than sent once. The backend treats an
  // unchanged callsign as a no-op, and this way a broker that dropped while
  // nobody was looking comes back on its own instead of leaving a panel that
  // is permanently, silently empty.
  useEffect(() => {
    SetPSKTarget(target ?? '', mode ?? '', dialHz ?? 0).catch(() => {});
    if (!target) return;
    const id = window.setInterval(() => {
      SetPSKTarget(target, mode ?? '', dialHz ?? 0).catch(() => {});
    }, 15000);
    return () => window.clearInterval(id);
  }, [target, mode, dialHz]);

  const bins = a?.bins ?? [];
  const maxCount = useMemo(() => bins.reduce((m, b) => Math.max(m, b.count || 0), 0) || 1, [bins]);
  const byOffset = useMemo(() => {
    const m = new Map<number, { count: number; avg_snr: number }>();
    for (const b of bins) m.set(b.offset_hz, b);
    return m;
  }, [bins]);
  const columns = useMemo(() => {
    const out: { edge: number; count: number; snr: number | null }[] = [];
    for (let i = 0; i < COLS; i++) {
      const edge = FIRST_EDGE + i * STEP;
      const b = byOffset.get(edge);
      out.push({ edge, count: b?.count ?? 0, snr: b?.avg_snr ?? null });
    }
    return out;
  }, [byOffset]);

  // Confirmed pileup: a station we hear calling this DX that the DX has also
  // decoded. Two independent pieces of evidence, so it is the one number here
  // that is not a proxy for anything.
  const confirmed = useMemo(() => {
    const heard = new Set((a?.decoded_by_calls ?? []).map((c) => c.toUpperCase()));
    return (callerCalls ?? []).filter((c) => heard.has(c.toUpperCase())).length;
  }, [a?.decoded_by_calls, callerCalls]);

  const snr = (v: number) => `${v > 0 ? '+' : ''}${v}`;

  const Tile = ({ label, value, foot, tone, title }: {
    label: string; value: number | string; foot: string; tone: string; title?: string;
  }) => (
    <div className="px-2 py-1.5 rounded-md bg-muted/40 border border-border/60" title={title}>
      <div className="text-[9px] uppercase tracking-wide text-muted-foreground">{label}</div>
      <div className={cn('text-lg font-bold leading-tight', tone)}>{value}</div>
      <div className="text-[10px] text-muted-foreground">{foot}</div>
    </div>
  );

  return (
    <div className="w-[340px] shrink-0 flex flex-col min-h-0 border-l border-border bg-card">
      {/* Header: what is being watched, and whether the feed is actually up. A
          panel full of zeros means one of two very different things. */}
      <div className="flex items-center gap-2 px-2.5 py-2 border-b border-border shrink-0">
        <Activity className="size-4 text-primary shrink-0" />
        <span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
          {t('psk.title')}
        </span>
        {target && <span className="text-xs font-mono text-foreground truncate">→ {target}</span>}
        <span className="ml-auto flex items-center gap-2 text-[10px] shrink-0">
          {a?.target && a.spots > 0 && (
            <span className="text-muted-foreground" title={t('psk.spotsTip')}>{t('psk.spots', { n: a.spots })}</span>
          )}
          {a?.enabled === false
            ? <span className="text-muted-foreground">{t('psk.off')}</span>
            : a?.online
              ? <span className="text-success">● {t('psk.online')}</span>
              : <span className="text-muted-foreground">○ {t('psk.offline')}</span>}
          <button type="button" onClick={onCollapse} title={t('psk.hide')}
            className="p-0.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground">
            <ChevronRight className="size-4" />
          </button>
        </span>
      </div>

      <div className="flex-1 min-h-0 overflow-y-auto px-2.5 py-2 space-y-3">
        {a?.enabled === false ? (
          <p className="text-xs text-muted-foreground italic">{t('psk.enableHint')}</p>
        ) : !target ? (
          <p className="text-xs text-muted-foreground italic">{t('psk.pickHint')}</p>
        ) : (
          <>
            {/* ── The answer ──────────────────────────────────────────── */}
            <div className="flex items-center gap-3">
              <div className={cn('shrink-0 size-8 rounded-full border flex items-center justify-center text-base',
                a?.he_me ? 'bg-success/20 border-success/50 text-success'
                  : a?.path_open ? 'bg-warning/20 border-warning/50 text-warning'
                    : 'bg-muted border-border text-muted-foreground')}>
                {a?.he_me ? '✓' : a?.path_open ? '≈' : '·'}
              </div>
              <div className="min-w-0">
                {a?.he_me ? (
                  <>
                    <div className="text-sm font-semibold text-success">{t('psk.heardYou', { s: a.he_me_seconds })}</div>
                    <div className="text-[11px] font-mono text-muted-foreground">
                      {snr(a.he_me_snr)} dB{a.he_me_offset_hz > 0 ? ` @ +${a.he_me_offset_hz} Hz` : ''}
                    </div>
                  </>
                ) : a?.path_open ? (
                  <>
                    <div className="text-sm font-semibold text-warning">{t('psk.pathOpen')}</div>
                    <div className="text-[11px] text-muted-foreground">{t('psk.pathOpenSub', { n: a.from_my_area_count })}</div>
                  </>
                ) : (
                  <>
                    <div className="text-sm font-semibold text-muted-foreground">{t('psk.notYet')}</div>
                    <div className="text-[11px] text-muted-foreground">{t('psk.notYetSub')}</div>
                  </>
                )}
              </div>
            </div>

            {/* Your signal reported next to him. Only when he has not decoded
                you himself — that is strictly stronger evidence, and two
                banners saying the same thing differently is noise. */}
            {!a?.he_me && (a?.near_him_count ?? 0) > 0 && a?.target_grid && (
              <div className="px-2 py-1.5 rounded-md bg-info/10 border border-info/30">
                <div className="flex items-baseline justify-between gap-2 mb-0.5">
                  <span className="text-[10px] uppercase tracking-wide font-semibold text-info">
                    ✓ {t('psk.nearHim', { g: a.target_grid })}
                  </span>
                  <span className="text-[10px] text-muted-foreground">{t('psk.nRx', { n: a.near_him_count })}</span>
                </div>
                <div className="flex flex-wrap gap-x-2 font-mono text-[11px]">
                  {(a.near_him_top ?? []).map((h) => (
                    <span key={h.call} className="text-info" title={`${h.call} ${h.grid ?? ''} · ${h.age_sec}s`}>
                      {h.call} <span className="text-muted-foreground">{snr(h.snr)}</span>
                    </span>
                  ))}
                </div>
              </div>
            )}

            {/* ── The four numbers ────────────────────────────────────── */}
            <div className="grid grid-cols-2 gap-1.5">
              <Tile label={t('psk.tFromArea')} value={a?.from_my_area_count ?? 0} foot={t('psk.tFromAreaFoot')}
                tone="text-success"
                title={(a?.from_my_area_top ?? []).map((h) => `${h.call} (${h.grid ?? '?'}) ${snr(h.snr)}`).join(' · ')} />
              <Tile label={t('psk.tPileup')} value={a?.pileup_count ?? 0} foot={t('psk.tPileupFoot')}
                tone="text-primary" title={t('psk.tPileupTip')} />
              <Tile label={t('psk.tHeardNear')} value={a?.heard_near_me ?? 0} foot={t('psk.tHeardNearFoot')}
                tone="text-info"
                title={t('psk.tHeardNearTip', { n: a?.heard_by_count ?? 0 })} />
              <Tile label={t('psk.tCallers')} value={confirmed > 0 ? `${callers} (${confirmed})` : callers}
                foot={confirmed > 0 ? t('psk.tCallersFootConf') : t('psk.tCallersFoot')}
                tone="text-warning" title={t('psk.tCallersTip')} />
            </div>

            {/* The one thing that turns an empty panel from a verdict into a
                missing measurement. */}
            {a?.target_uploads ? (
              <div className="text-[11px] text-success">✓ {t('psk.uploads')}</div>
            ) : (
              <div className="px-2 py-1.5 rounded-md bg-warning/10 border border-warning/30 text-[11px] text-warning">
                ⚠ {t('psk.noUploads', { c: target })}
              </div>
            )}

            {/* ── Who near you he is hearing ──────────────────────────── */}
            <div>
              <div className="text-[10px] uppercase tracking-wide text-muted-foreground mb-0.5">{t('psk.fromAreaList')}</div>
              {(a?.from_my_area_top ?? []).length > 0 ? (
                <div className="flex flex-col gap-0.5 font-mono text-[11px]">
                  {(a?.from_my_area_top ?? []).slice(0, 4).map((h) => (
                    <div key={h.call} className="flex items-baseline gap-2 truncate"
                      title={t('psk.rowTip', { c: h.call, g: h.grid ?? '?', s: h.age_sec, d: snr(h.snr) })}>
                      <span className="font-semibold text-foreground w-20 truncate">{h.call}</span>
                      <span className="text-muted-foreground w-12">({(h.grid ?? '?').slice(0, 4)})</span>
                      <span className="text-success w-14">{snr(h.snr)} dB</span>
                      {h.offset_hz > 0 && h.offset_hz < 10000 && (
                        <span className="ml-auto text-muted-foreground whitespace-nowrap">@ +{h.offset_hz} Hz</span>
                      )}
                    </div>
                  ))}
                </div>
              ) : (
                <div className="text-[11px] text-muted-foreground italic">{t('psk.fromAreaEmpty')}</div>
              )}
            </div>

            {/* ── His passband ────────────────────────────────────────── */}
            <div>
              <div className="flex items-baseline justify-between gap-2 mb-1">
                <span className="text-[10px] uppercase tracking-wide text-muted-foreground">{t('psk.passband')}</span>
                <span className="text-[10px] font-mono text-muted-foreground">
                  {(a?.ceiling_hz ?? 0) > 0
                    ? t('psk.ceiling', { hz: a!.ceiling_hz, n: a!.decodes_in_window })
                    : (a?.decodes_in_window ?? 0) > 0 ? t('psk.noDial') : t('psk.noDecodes')}
                </span>
              </div>
              <div className="relative flex items-end gap-px h-10 rounded bg-muted/40 px-1 py-0.5 overflow-hidden">
                {columns.map((c) => {
                  const ratio = c.count / maxCount;
                  return (
                    <div key={c.edge}
                      className={cn('flex-1 min-w-0 rounded-sm',
                        c.count === 0 ? 'bg-border'
                          : ratio > 0.66 ? 'bg-primary'
                            : ratio > 0.33 ? 'bg-primary/70' : 'bg-primary/40')}
                      style={{ height: `${Math.max(2, Math.round(ratio * 36))}px` }}
                      title={`${c.edge}-${c.edge + STEP} Hz · ${c.count}${c.snr !== null ? ` @ ${c.snr.toFixed(0)} dB` : ''}`} />
                  );
                })}
                {(a?.suggested_offset ?? 0) > 0 && (
                  <div className="absolute top-0 bottom-0 w-0.5 bg-success pointer-events-none"
                    style={{ left: `${((a!.suggested_offset - LO) / (HI - LO)) * 100}%`, boxShadow: '0 0 4px currentColor' }}
                    title={t('psk.tryOffset', { hz: a!.suggested_offset })} />
                )}
              </div>
              <div className="relative h-3 mt-0.5 text-[9px] font-mono text-muted-foreground">
                {[1000, 2000, 3000, 4000].map((hz) => (
                  <span key={hz} className="absolute whitespace-nowrap"
                    style={{ left: `${((hz - LO) / (HI - LO)) * 100}%`, transform: `translateX(${hz === HI ? '-100%' : '-50%'})` }}>
                    {hz}
                  </span>
                ))}
              </div>
              {(a?.suggested_offset ?? 0) > 0 && (
                <div className="text-center text-[11px] font-mono text-success mt-0.5">
                  🎯 {t('psk.tryOffset', { hz: a!.suggested_offset })}
                </div>
              )}
            </div>
          </>
        )}
      </div>
    </div>
  );
}
