import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
  AllCommunityModule, ModuleRegistry,
  type ColDef, type ColumnState, type GridReadyEvent, type RowClickedEvent,
} from 'ag-grid-community';
import { hamlogGridTheme } from '@/lib/gridTheme';
import { AgGridReact } from 'ag-grid-react';
import { Columns3, FilterX } from 'lucide-react';
import {
  Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { cleanSpotter, inferSpotMode, spotStatusKey } from '@/lib/spot';
import { loadLocal, loadRemote, saveState, seedLocal } from '@/lib/gridPrefs';
import { useI18n } from '@/lib/i18n';

type TFn = (key: string, vars?: Record<string, string | number>) => string;

ModuleRegistry.registerModules([AllCommunityModule]);

const hamlogTheme = hamlogGridTheme;

export type ClusterSpot = {
  source_id: number;
  source_name: string;
  spotter: string;
  dx_call: string;
  freq_khz: number;
  freq_hz: number;
  band?: string;
  comment?: string;
  locator?: string;
  time_utc?: string;
  country?: string;
  continent?: string;
  cqz?: number;
  ituz?: number;
  distance_km?: number;
  sp_deg?: number;
  lp_deg?: number;
  received_at: string;
  raw: string;
  repeats?: number;
  pota_ref?: string;
  pota_name?: string;
};

export type SpotStatusEntry = {
  status?: string;
  country?: string;
  continent?: string;
  worked_call?: boolean;
  new_county?: boolean;
  new_pota?: boolean;
};

type Props = {
  rows: ClusterSpot[];
  spotStatus: Record<string, SpotStatusEntry>;
  onSpotClick?: (s: ClusterSpot) => void;
};

const COL_STATE_KEY = 'hamlog.clusterColState.v1';

// Extracts the prefix from a callsign — drops portable suffixes (/P, /MM
// etc.), keeps a slashed prefix (HB0/DL2SBY → HB0), and trims the trailing
// digits after the last letter group (DL2SBY → DL2).
function fmtPfx(call: string): string {
  if (!call) return '';
  const c = call.trim().toUpperCase();
  const base = c.includes('/') ? c.split('/')[0] : c;
  // If "base" is a callsign rather than a bare prefix (like DL2SBY), cut
  // at the last digit to get DL2.
  let lastDigit = -1;
  for (let i = 0; i < base.length; i++) {
    if (base[i] >= '0' && base[i] <= '9') lastDigit = i;
  }
  return lastDigit >= 0 ? base.slice(0, lastDigit + 1) : base;
}

// Renders an ISO timestamp (RFC3339 with nanoseconds) as a compact UTC
// "YYYY-MM-DD HH:MM:SS" string — matches the rest of the app's date style.
function fmtDateTimeUTC(s: any): string {
  if (!s) return '';
  const d = new Date(s);
  if (isNaN(d.getTime())) return String(s);
  const p = (n: number) => String(n).padStart(2, '0');
  return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())} ${p(d.getUTCHours())}:${p(d.getUTCMinutes())}:${p(d.getUTCSeconds())}`;
}

type ColEntry = ColDef<ClusterSpot> & { group: string; label: string; defaultVisible?: boolean };

// statusFor resolves the precomputed spot status (new / new-band / new-slot /
// worked-call) for an ag-Grid cell's row.
function statusFor(p: any): SpotStatusEntry | undefined {
  return p?.context?.spotStatus?.[
    spotStatusKey(p.data?.dx_call, p.data?.band ?? '', p.data?.comment ?? '', p.data?.freq_hz)
  ];
}

// statusBadge maps a resolved spot status to a short labelled badge for the
// Status column, using the same colours as the per-cell fills (NEW DXCC =
// call cell, NEW BAND = band cell, NEW SLOT = mode cell). Returns null when
// there's nothing notable to show.
type Badge = { text: string; fg: string; bg: string; bd: string };

// tok builds a badge from a semantic status token (success/warning/caution/
// danger/info/neutral) so the colours adapt to every theme via CSS variables,
// instead of the hard-coded light-theme pastels that looked wrong in dark mode.
function tok(name: string, text: string): Badge {
  if (name === 'neutral') return { text, fg: 'var(--muted-foreground)', bg: 'var(--muted)', bd: 'var(--border)' };
  return { text, fg: `var(--${name}-muted-foreground)`, bg: `var(--${name}-muted)`, bd: `var(--${name}-border)` };
}

// cellChip wraps a Band/Mode cell value in a small rounded pill (same look as the
// Status badges) when the slot is notable, instead of flooding the whole cell with
// a heavy muted fill that turned into an ugly olive block on dark themes. `name` is
// null → plain text, no pill.
function cellChip(value: any, name: string | null): any {
  const txt = value === undefined || value === null || value === '' ? '' : String(value);
  if (!name) return txt || <span style={{ color: 'var(--muted-foreground)', fontSize: 10 }}>—</span>;
  // Inherit the column's font size (no fixed 9px / height) so a pill around a
  // callsign stays the same size as the plain callsigns next to it — just tinted
  // and rounded, not shrunk.
  return (
    <span style={{
      display: 'inline-flex', alignItems: 'center', lineHeight: 1.1,
      backgroundColor: `var(--${name}-muted)`, color: `var(--${name}-muted-foreground)`,
      border: `1px solid var(--${name}-border)`, fontWeight: 700,
      padding: '1px 6px', borderRadius: 999, whiteSpace: 'nowrap',
    }}>{txt}</span>
  );
}

function statusBadge(t: TFn, s: SpotStatusEntry | undefined): Badge | null {
  switch (s?.status) {
    case 'new':      return tok('danger',  t('clg2.newDxcc'));
    case 'new-band': return tok('warning', t('clg2.newBand'));
    case 'new-mode': return tok('caution', t('clg2.newMode'));
    case 'new-slot': return tok('caution', t('clg2.newSlot'));
    default:         return s?.worked_call ? tok('neutral', t('clg2.wkdCall')) : null;
  }
}

const makeColCatalog = (t: TFn): ColEntry[] => [
  {
    group: 'Spot', label: t('clg2.c.time'),  colId: 'time',
    headerName: t('clg2.c.time'), field: 'time_utc' as any, width: 80, cellClass: 'font-mono',
    defaultVisible: true,
    sort: 'desc',
    cellStyle: { color: 'var(--muted-foreground)' },
  },
  {
    group: 'Spot', label: t('clg2.c.call'),  colId: 'call',
    headerName: t('clg2.c.call'), field: 'dx_call' as any, width: 120,
    defaultVisible: true,
    cellClass: 'font-mono',
    // NEW DXCC → a danger pill around the call, consistent with the NEW BAND /
    // NEW MODE pills (same look, same tokens). Worked-call stays blue bold text;
    // a plain spot inherits the theme's normal text colour so callsigns blend in
    // with the rest of the row instead of always shouting a colour.
    cellRenderer: (p: any) => {
      const s = statusFor(p);
      if (s?.status === 'new') return cellChip(p.value, 'danger');
      const color = s?.worked_call ? 'var(--info)' : undefined;
      return <span style={{ color, fontWeight: 700 }}>{p.value ?? ''}</span>;
    },
    tooltipValueGetter: (p: any) => {
      const s = statusFor(p);
      return s?.status === 'new' ? t('clg2.tipNewDxcc', { country: s?.country ?? '' }) : s?.worked_call ? t('clg2.tipWorkedCall') : undefined;
    },
  },
  {
    group: 'Spot', label: t('clg2.c.status'), colId: 'status',
    headerName: t('clg2.c.status'), width: 120, sortable: true,
    defaultVisible: true,
    // Spells out the slot status as a text badge so NEW SLOT (and the others)
    // is obvious at the row level, not just a single coloured cell. NEW COUNTY
    // and NEW POTA are orthogonal, so they stack as extra badges.
    valueGetter: (p: any) => {
      const s = statusFor(p);
      const parts: string[] = [];
      if (s?.status === 'new') parts.push(t('clg2.newDxcc'));
      else if (s?.status === 'new-band') parts.push(t('clg2.newBand'));
      else if (s?.status === 'new-mode') parts.push(t('clg2.newMode'));
      else if (s?.status === 'new-slot') parts.push(t('clg2.newSlot'));
      else if (s?.worked_call) parts.push(t('clg2.wkdCall'));
      if (s?.new_county) parts.push(t('clg2.newCounty'));
      if (s?.new_pota) parts.push(t('clg2.newPota'));
      return parts.join(' ');
    },
    cellRenderer: (p: any) => {
      const s = statusFor(p);
      const badges: Badge[] = [];
      const b = statusBadge(t, s);
      if (b) badges.push(b);
      if (s?.new_county) badges.push(tok('info', t('clg2.newCounty')));
      if (s?.new_pota) badges.push(tok('success', t('clg2.newPota')));
      if (badges.length === 0) return <span style={{ color: 'var(--muted-foreground)', fontSize: 10 }}>—</span>;
      return (
        <span style={{ display: 'flex', height: '100%', flexWrap: 'wrap', gap: 2, alignItems: 'center' }}>
          {badges.map((bd, i) => (
            <span key={i} style={{
              display: 'inline-flex', alignItems: 'center', height: 15, lineHeight: 1,
              backgroundColor: bd.bg, color: bd.fg, border: `1px solid ${bd.bd}`,
              fontWeight: 700, fontSize: 9,
              padding: '0 5px', borderRadius: 999, letterSpacing: 0.3, whiteSpace: 'nowrap',
            }}>{bd.text}</span>
          ))}
        </span>
      );
    },
    tooltipValueGetter: (p: any) => {
      const s = statusFor(p);
      if (s?.status === 'new') return t('clg2.tipNewDxcc', { country: s?.country ?? '' });
      if (s?.status === 'new-band') return t('clg2.tipNewBand');
      if (s?.status === 'new-slot') return t('clg2.tipNewSlotBand');
      if (s?.worked_call) return t('clg2.tipWorkedCall');
      return undefined;
    },
  },
  {
    group: 'Spot', label: t('clg2.c.pota'), colId: 'pota',
    headerName: t('clg2.c.pota'), field: 'pota_ref' as any, width: 92, cellClass: 'font-mono',
    defaultVisible: true,
    cellStyle: { color: 'var(--success)' },
    tooltipValueGetter: (p: any) => (p.data?.pota_name ? t('clg2.tipPota', { name: p.data.pota_name }) : undefined),
  },
  {
    group: 'Spot', label: t('clg2.c.freq'), colId: 'freq',
    headerName: t('clg2.c.freq'), field: 'freq_khz' as any, width: 95, type: 'rightAligned', cellClass: 'font-mono',
    defaultVisible: true,
    valueFormatter: (p) => typeof p.value === 'number' ? p.value.toFixed(1) : '',
    comparator: (a, b) => (a ?? 0) - (b ?? 0),
  },
  {
    group: 'Spot', label: t('clg2.c.band'), colId: 'band',
    headerName: t('clg2.c.band'), field: 'band' as any, width: 75,
    defaultVisible: true,
    cellClass: 'font-mono',
    // NEW BAND for this entity → small warning pill around the band text.
    cellRenderer: (p: any) => cellChip(p.value, statusFor(p)?.status === 'new-band' ? 'warning' : null),
    tooltipValueGetter: (p: any) => (statusFor(p)?.status === 'new-band' ? t('clg2.tipNewBand') : undefined),
  },
  {
    group: 'Spot', label: t('clg2.c.mode'), colId: 'mode',
    headerName: t('clg2.c.mode'), colSpan: undefined, width: 80,
    defaultVisible: true,
    cellClass: 'font-mono',
    valueGetter: (p: any) => p.data ? inferSpotMode(p.data.comment ?? '', p.data.freq_hz) : '',
    // Only NEW MODE pills the mode cell — there the mode itself is genuinely new
    // for the entity. NEW SLOT means band AND mode were each worked before (just
    // not together), so highlighting the mode cell would wrongly imply "CW is new";
    // that case is signalled by the Status badge alone.
    cellRenderer: (p: any) => cellChip(p.value, statusFor(p)?.status === 'new-mode' ? 'caution' : null),
    tooltipValueGetter: (p: any) => {
      const st = statusFor(p)?.status;
      if (st === 'new-mode') return t('clg2.tipNewMode');
      if (st === 'new-slot') return t('clg2.tipNewSlot');
      return undefined;
    },
  },
  {
    group: 'Spot', label: t('clg2.c.pfx'), colId: 'pfx',
    headerName: t('clg2.c.pfx'), width: 60, cellClass: 'font-mono',
    valueGetter: (p: any) => fmtPfx(p.data?.dx_call ?? ''),
    cellStyle: { color: 'var(--muted-foreground)' },
  },
  {
    group: 'Geo', label: t('clg2.c.cqz'), colId: 'cqz',
    headerName: t('clg2.h.cqz'), field: 'cqz' as any, width: 60, type: 'rightAligned', cellClass: 'font-mono',
    valueFormatter: (p) => p.value ? String(p.value) : '',
  },
  {
    group: 'Geo', label: t('clg2.c.ituz'), colId: 'ituz',
    headerName: t('clg2.h.ituz'), field: 'ituz' as any, width: 60, type: 'rightAligned', cellClass: 'font-mono',
    valueFormatter: (p) => p.value ? String(p.value) : '',
  },
  {
    group: 'Geo', label: t('clg2.c.distance_km'), colId: 'distance_km',
    headerName: t('clg2.h.distance_km'), field: 'distance_km' as any, width: 80, type: 'rightAligned', cellClass: 'font-mono',
    valueFormatter: (p) => p.value ? String(p.value) : '',
    comparator: (a, b) => (a ?? 0) - (b ?? 0),
  },
  {
    group: 'Geo', label: t('clg2.c.sp_deg'), colId: 'sp_deg',
    headerName: t('clg2.h.sp_deg'), field: 'sp_deg' as any, width: 60, type: 'rightAligned', cellClass: 'font-mono',
    valueFormatter: (p) => (p.value || p.value === 0) ? String(p.value) : '',
    comparator: (a, b) => (a ?? 0) - (b ?? 0),
  },
  {
    group: 'Geo', label: t('clg2.c.lp_deg'), colId: 'lp_deg',
    headerName: t('clg2.h.lp_deg'), field: 'lp_deg' as any, width: 60, type: 'rightAligned', cellClass: 'font-mono',
    valueFormatter: (p) => (p.value || p.value === 0) ? String(p.value) : '',
    comparator: (a, b) => (a ?? 0) - (b ?? 0),
  },
  {
    group: 'Spot', label: t('clg2.c.country'), colId: 'country',
    headerName: t('clg2.c.country'), width: 140,
    defaultVisible: true,
    valueGetter: (p: any) => p.data?.country ?? p.context?.spotStatus?.[
      spotStatusKey(p.data?.dx_call, p.data?.band ?? '', p.data?.comment ?? '', p.data?.freq_hz)
    ]?.country ?? '',
    cellStyle: { color: 'var(--muted-foreground)' },
  },
  {
    group: 'Spot', label: t('clg2.c.continent'), colId: 'continent',
    headerName: t('clg2.h.continent'), width: 60, cellClass: 'font-mono',
    defaultVisible: true,
    valueGetter: (p: any) => p.data?.continent ?? p.context?.spotStatus?.[
      spotStatusKey(p.data?.dx_call, p.data?.band ?? '', p.data?.comment ?? '', p.data?.freq_hz)
    ]?.continent ?? '',
    cellStyle: { color: 'var(--muted-foreground)', fontSize: 10 },
  },
  {
    group: 'Spot', label: t('clg2.c.spotter'), colId: 'spotter',
    headerName: t('clg2.c.spotter'), field: 'spotter' as any, width: 100, cellClass: 'font-mono',
    defaultVisible: true,
    valueFormatter: (p) => cleanSpotter(p.value ?? ''),
    cellStyle: { color: 'var(--muted-foreground)' },
  },
  {
    group: 'Spot', label: t('clg2.c.source'), colId: 'source',
    headerName: t('clg2.c.source'), field: 'source_name' as any, width: 100,
    defaultVisible: true,
    cellStyle: { color: 'var(--muted-foreground)', fontSize: 10 },
  },
  {
    group: 'Spot', label: t('clg2.c.locator'), colId: 'locator',
    headerName: t('clg2.h.locator'), field: 'locator' as any, width: 80, cellClass: 'font-mono',
    cellStyle: { color: 'var(--muted-foreground)' },
  },
  {
    group: 'Spot', label: t('clg2.c.comment'), colId: 'comment',
    headerName: t('clg2.c.comment'), field: 'comment' as any, flex: 1, minWidth: 160,
    defaultVisible: true,
    cellStyle: { color: 'var(--muted-foreground)' },
  },
  {
    group: 'Spot', label: t('clg2.c.received_at'), colId: 'received_at',
    headerName: t('clg2.h.received_at'), field: 'received_at' as any, width: 160, cellClass: 'font-mono',
    valueFormatter: (p) => fmtDateTimeUTC(p.value),
  },
  {
    group: 'Spot', label: t('clg2.c.raw'), colId: 'raw',
    headerName: t('clg2.c.raw'), field: 'raw' as any, width: 300, cellClass: 'font-mono',
  },
];

const GROUP_ORDER = ['Spot', 'Geo'];

const CLG_GRP_KEYS: Record<string, string> = { Spot: 'clg2.grpSpot', Geo: 'clg2.grpGeo' };
const groupLabel = (t: TFn, g: string): string => t(CLG_GRP_KEYS[g] ?? g);

export function ClusterGrid({ rows, spotStatus, onSpotClick }: Props) {
  const { t } = useI18n();
  const gridRef = useRef<any>(null);
  const [pickerOpen, setPickerOpen] = useState(false);

  // Localized column catalog — rebuilt when the language changes.
  const COL_CATALOG = useMemo(() => makeColCatalog(t), [t]);

  const columnDefs = useMemo<ColDef<ClusterSpot>[]>(() => COL_CATALOG.map((c) => {
    const { group: _g, label: _l, defaultVisible, ...rest } = c;
    return { ...rest, hide: !defaultVisible };
  }), [COL_CATALOG]);

  const defaultColDef = useMemo<ColDef>(() => ({
    sortable: true, resizable: true, filter: true, suppressMovable: false,
  }), []);

  // Pass spotStatus through AG Grid's context so cell renderers can look up
  // per-cell highlight without re-rendering the whole grid when the map
  // updates. We refresh cells whose values depend on it after each prop
  // change below.
  const context = useMemo(() => ({ spotStatus }), [spotStatus]);

  // Spot statuses arrive asynchronously (~after the rows render). The Call/Band/
  // Mode cellStyles depend on them but their cell VALUE doesn't change, so ag-grid
  // won't re-render those cells on its own — force a refresh so e.g. a worked call
  // turns blue once its status loads.
  useEffect(() => {
    gridRef.current?.api?.refreshCells({ force: true });
  }, [spotStatus]);

  function onGridReady(e: GridReadyEvent) {
    const local = loadLocal(COL_STATE_KEY);
    if (local) e.api.applyColumnState({ state: local as ColumnState[], applyOrder: true });
    loadRemote(COL_STATE_KEY).then((remote) => {
      if (remote && !local) {
        e.api.applyColumnState({ state: remote as ColumnState[], applyOrder: true });
        seedLocal(COL_STATE_KEY, remote);
      }
    });
  }
  const saveColumnState = useCallback(() => {
    const state = gridRef.current?.api?.getColumnState();
    if (state) saveState(COL_STATE_KEY, state);
  }, []);

  function handleRowClicked(e: RowClickedEvent<ClusterSpot>) {
    if (e.data && onSpotClick) onSpotClick(e.data);
  }

  function isColVisible(colId: string): boolean {
    const col = gridRef.current?.api?.getColumn(colId);
    return col ? col.isVisible() : !!COL_CATALOG.find((c) => c.colId === colId)?.defaultVisible;
  }
  function setColVisible(colId: string, visible: boolean) {
    const api = gridRef.current?.api;
    if (!api) return;
    api.setColumnsVisible([colId], visible);
    saveColumnState();
  }
  function showAll(group?: string) {
    const api = gridRef.current?.api;
    if (!api) return;
    const ids = COL_CATALOG.filter((c) => !group || c.group === group).map((c) => c.colId!);
    api.setColumnsVisible(ids, true);
    saveColumnState();
  }
  function hideAll(group?: string) {
    const api = gridRef.current?.api;
    if (!api) return;
    const ids = COL_CATALOG.filter((c) => !group || c.group === group).map((c) => c.colId!);
    api.setColumnsVisible(ids, false);
    saveColumnState();
  }
  function resetDefaults() {
    const api = gridRef.current?.api;
    if (!api) return;
    const visible = COL_CATALOG.filter((c) => c.defaultVisible).map((c) => c.colId!);
    const hidden = COL_CATALOG.filter((c) => !c.defaultVisible).map((c) => c.colId!);
    api.setColumnsVisible(visible, true);
    api.setColumnsVisible(hidden, false);
    saveColumnState();
  }

  return (
    <>
      <div className="flex items-center justify-end gap-2 px-2.5 py-1 border-b border-border/60 bg-muted/20">
        <Button variant="ghost" size="sm" className="h-7 text-[11px]" onClick={() => gridRef.current?.api?.setFilterModel(null)}
          title={t('clg2.clearFiltersTitle')}>
          <FilterX className="size-3.5" /> {t('clg2.clearFilters')}
        </Button>
        <Button variant="ghost" size="sm" className="h-7 text-[11px]" onClick={() => setPickerOpen(true)}>
          <Columns3 className="size-3.5" /> {t('clg2.columns')}
        </Button>
      </div>
      <div style={{ flex: 1, minHeight: 0, position: 'relative' }}>
        <div style={{ position: 'absolute', inset: 0 }}>
          <AgGridReact<ClusterSpot>
            ref={gridRef}
            theme={hamlogTheme}
            rowData={rows}
            columnDefs={columnDefs}
            defaultColDef={defaultColDef}
            context={context}
            onGridReady={onGridReady}
            onColumnResized={saveColumnState}
            onColumnMoved={saveColumnState}
            onColumnPinned={saveColumnState}
            onColumnVisible={saveColumnState}
            onSortChanged={saveColumnState}
            onRowClicked={handleRowClicked}
            animateRows={false}
            suppressCellFocus
            getRowId={(p) => `${(p.data as any).received_at}-${(p.data as any).dx_call}-${(p.data as any).source_id}`}
          />
        </div>
      </div>

      <Dialog open={pickerOpen} onOpenChange={setPickerOpen}>
        <DialogContent className="max-w-md">
          <DialogHeader>
            <DialogTitle>{t('clg2.pickerTitle')}</DialogTitle>
            <DialogDescription>
              {t('clg2.pickerDesc')}
            </DialogDescription>
          </DialogHeader>
          <div className="max-h-[60vh] overflow-y-auto py-2">
            {GROUP_ORDER.map((group) => {
              const cols = COL_CATALOG.filter((c) => c.group === group);
              if (cols.length === 0) return null;
              return (
                <div key={group} className="rounded-md border border-border p-2.5 mb-2">
                  <div className="flex items-center justify-between mb-2 pb-1.5 border-b border-border/60">
                    <span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">{groupLabel(t, group)}</span>
                    <div className="flex gap-0.5">
                      <button className="text-[10px] text-primary hover:underline px-1" onClick={() => showAll(group)}>{t('clg2.all')}</button>
                      <button className="text-[10px] text-muted-foreground hover:underline px-1" onClick={() => hideAll(group)}>{t('clg2.none')}</button>
                    </div>
                  </div>
                  <div className="grid grid-cols-2 gap-1">
                    {cols.map((c) => (
                      <label key={c.colId} className="flex items-center gap-2 text-xs cursor-pointer hover:bg-accent/30 rounded px-1 py-0.5">
                        <Checkbox
                          checked={isColVisible(c.colId!)}
                          onCheckedChange={(v) => setColVisible(c.colId!, !!v)}
                        />
                        {c.label}
                      </label>
                    ))}
                  </div>
                </div>
              );
            })}
          </div>
          <DialogFooter>
            <Button variant="ghost" size="sm" onClick={resetDefaults}>{t('clg2.resetDefaults')}</Button>
            <Button size="sm" onClick={() => setPickerOpen(false)}>{t('clg2.done')}</Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </>
  );
}
