import { useEffect, useMemo, useRef, useState } from 'react';
import { Plus, Trash2, Save, FolderOpen, X } from 'lucide-react';
import { writeUiPref } from '@/lib/uiPref';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { useI18n } from '@/lib/i18n';

// FilterBuilder — Log4OM-style advanced filter for the QSO list. The operator
// adds field/operator/value conditions, joins them with AND or OR, and can
// save/recall named presets. Closing the dialog applies the filter.

export type FilterOp =
  | 'eq' | 'ne' | 'gt' | 'lt' | 'ge' | 'le'
  | 'contains' | 'startswith' | 'endswith' | 'empty' | 'notempty'
  | 'in' | 'notin';

export interface FilterCondition { field: string; op: FilterOp; value: string }
export interface QueryFilter {
  quick_callsign?: string;
  conditions: FilterCondition[];
  match: 'AND' | 'OR';
  limit?: number;
  offset?: number;
}

// Curated field catalog. `value` MUST match a column in the backend whitelist
// (qso.FilterableFields); `type` only drives which operators/value input we show.
// 'date' is an ISO timestamp column (qso_date); 'adifdate' is an ADIF YYYYMMDD
// string (the confirmation dates) — picked with a calendar, stored 8-digit.
type FieldType = 'text' | 'number' | 'date' | 'adifdate';
const FIELDS: { value: string; label: string; type: FieldType }[] = [
  { value: 'callsign', label: 'fltb.fCallsign', type: 'text' },
  { value: 'qso_date', label: 'fltb.fDate', type: 'date' },
  // When the record entered the logbook — the way to isolate what an import added.
  { value: 'created_at', label: 'fltb.fCreated', type: 'date' },
  { value: 'qso_date_off', label: 'fltb.fEndDate', type: 'date' },
  { value: 'band', label: 'fltb.fBand', type: 'text' },
  { value: 'band_rx', label: 'fltb.fRxBand', type: 'text' },
  { value: 'mode', label: 'fltb.fMode', type: 'text' },
  { value: 'submode', label: 'fltb.fSubmode', type: 'text' },
  { value: 'freq_hz', label: 'fltb.fFreq', type: 'number' },
  { value: 'freq_rx_hz', label: 'fltb.fRxFreq', type: 'number' },
  { value: 'rst_sent', label: 'fltb.fRstSent', type: 'text' },
  { value: 'rst_rcvd', label: 'fltb.fRstRcvd', type: 'text' },
  { value: 'name', label: 'fltb.fName', type: 'text' },
  { value: 'qth', label: 'fltb.fQth', type: 'text' },
  { value: 'address', label: 'fltb.fAddress', type: 'text' },
  { value: 'email', label: 'fltb.fEmail', type: 'text' },
  { value: 'grid', label: 'fltb.fGrid', type: 'text' },
  { value: 'country', label: 'fltb.fCountry', type: 'text' },
  { value: 'state', label: 'fltb.fState', type: 'text' },
  { value: 'cnty', label: 'fltb.fCounty', type: 'text' },
  { value: 'dxcc', label: 'fltb.fDxcc', type: 'number' },
  { value: 'cont', label: 'fltb.fContinent', type: 'text' },
  { value: 'cqz', label: 'fltb.fCqz', type: 'number' },
  { value: 'ituz', label: 'fltb.fItuz', type: 'number' },
  { value: 'iota', label: 'fltb.fIota', type: 'text' },
  { value: 'sota_ref', label: 'fltb.fSota', type: 'text' },
  { value: 'pota_ref', label: 'fltb.fPota', type: 'text' },
  { value: 'wwff_ref', label: 'fltb.fWwff', type: 'text' },
  { value: 'rig', label: 'fltb.fRig', type: 'text' },
  { value: 'ant', label: 'fltb.fAntenna', type: 'text' },
  // Same naming as the bulk editor: each entry says whether it is a STATUS or a
  // DATE and in which direction, grouped by channel. "Upload" was wrong for half
  // of them — a paper QSL is not uploaded.
  { value: 'qsl_sent', label: 'fltb.fQslSent', type: 'text' },
  { value: 'qsl_sent_date', label: 'fltb.fQslSentDate', type: 'adifdate' },
  { value: 'qsl_rcvd', label: 'fltb.fQslRcvd', type: 'text' },
  { value: 'qsl_rcvd_date', label: 'fltb.fQslRcvdDate', type: 'adifdate' },
  { value: 'qsl_via', label: 'fltb.fQslVia', type: 'text' },
  { value: 'qsl_sent_via', label: 'fltb.fQslSentVia', type: 'text' },
  { value: 'qsl_rcvd_via', label: 'fltb.fQslRcvdVia', type: 'text' },
  { value: 'lotw_sent', label: 'fltb.fLotwSent', type: 'text' },
  { value: 'lotw_sent_date', label: 'fltb.fLotwSentDate', type: 'adifdate' },
  { value: 'lotw_rcvd', label: 'fltb.fLotwRcvd', type: 'text' },
  { value: 'lotw_rcvd_date', label: 'fltb.fLotwRcvdDate', type: 'adifdate' },
  { value: 'eqsl_sent', label: 'fltb.fEqslSent', type: 'text' },
  { value: 'eqsl_sent_date', label: 'fltb.fEqslSentDate', type: 'adifdate' },
  { value: 'eqsl_rcvd', label: 'fltb.fEqslRcvd', type: 'text' },
  { value: 'eqsl_rcvd_date', label: 'fltb.fEqslRcvdDate', type: 'adifdate' },
  { value: 'qrzcom_qso_upload_status', label: 'fltb.fQrzSent', type: 'text' },
  { value: 'qrzcom_qso_upload_date', label: 'fltb.fQrzSentDate', type: 'adifdate' },
  { value: 'qrzcom_qso_download_status', label: 'fltb.fQrzRcvd', type: 'text' },
  { value: 'qrzcom_qso_download_date', label: 'fltb.fQrzRcvdDate', type: 'adifdate' },
  { value: 'clublog_qso_upload_status', label: 'fltb.fClublogSent', type: 'text' },
  { value: 'clublog_qso_upload_date', label: 'fltb.fClublogSentDate', type: 'adifdate' },
  { value: 'clublog_qso_download_status', label: 'fltb.fClublogRcvd', type: 'text' },
  { value: 'clublog_qso_download_date', label: 'fltb.fClublogRcvdDate', type: 'adifdate' },
  { value: 'hrdlog_qso_upload_status', label: 'fltb.fHrdlogSent', type: 'text' },
  { value: 'hrdlog_qso_upload_date', label: 'fltb.fHrdlogSentDate', type: 'adifdate' },
  // HAMLOG.online: no promoted column, filtered through extras_json (see
  // qso.filterableExtras).
  { value: 'hamlog_sent', label: 'fltb.fHamlogSent', type: 'text' },
  { value: 'hamlog_sent_date', label: 'fltb.fHamlogSentDate', type: 'adifdate' },
  { value: 'hamlog_rcvd', label: 'fltb.fHamlogRcvd', type: 'text' },
  { value: 'hamlog_rcvd_date', label: 'fltb.fHamlogRcvdDate', type: 'adifdate' },
  { value: 'hamqth_sent', label: 'fltb.fHamqthSent', type: 'text' },
  { value: 'hamqth_sent_date', label: 'fltb.fHamqthSentDate', type: 'adifdate' },
  { value: 'contest_id', label: 'fltb.fContestId', type: 'text' },
  { value: 'srx', label: 'fltb.fSerialRcvd', type: 'number' },
  { value: 'stx', label: 'fltb.fSerialSent', type: 'number' },
  { value: 'prop_mode', label: 'fltb.fPropMode', type: 'text' },
  { value: 'sat_name', label: 'fltb.fSatellite', type: 'text' },
  { value: 'station_callsign', label: 'fltb.fMyCallsign', type: 'text' },
  { value: 'operator', label: 'fltb.fOperator', type: 'text' },
  { value: 'owner_callsign', label: 'fltb.fOwnerCallsign', type: 'text' },
  { value: 'my_grid', label: 'fltb.fMyGrid', type: 'text' },
  { value: 'my_country', label: 'fltb.fMyCountry', type: 'text' },
  { value: 'my_state', label: 'fltb.fMyState', type: 'text' },
  { value: 'my_cnty', label: 'fltb.fMyCounty', type: 'text' },
  { value: 'my_iota', label: 'fltb.fMyIota', type: 'text' },
  { value: 'my_sota_ref', label: 'fltb.fMySota', type: 'text' },
  { value: 'my_pota_ref', label: 'fltb.fMyPota', type: 'text' },
  { value: 'my_wwff_ref', label: 'fltb.fMyWwff', type: 'text' },
  { value: 'my_street', label: 'fltb.fMyStreet', type: 'text' },
  { value: 'my_city', label: 'fltb.fMyCity', type: 'text' },
  { value: 'my_postal_code', label: 'fltb.fMyPostal', type: 'text' },
  { value: 'my_rig', label: 'fltb.fMyRig', type: 'text' },
  { value: 'my_antenna', label: 'fltb.fMyAntenna', type: 'text' },
  { value: 'tx_pwr', label: 'fltb.fTxPower', type: 'number' },
  { value: 'comment', label: 'fltb.fComment', type: 'text' },
  { value: 'notes', label: 'fltb.fNotes', type: 'text' },
];

const OPS: { value: FilterOp; label: string }[] = [
  { value: 'eq', label: 'fltb.opEq' },
  { value: 'ne', label: 'fltb.opNe' },
  { value: 'contains', label: 'fltb.opContains' },
  { value: 'startswith', label: 'fltb.opStartsWith' },
  { value: 'endswith', label: 'fltb.opEndsWith' },
  { value: 'gt', label: 'fltb.opGt' },
  { value: 'lt', label: 'fltb.opLt' },
  { value: 'ge', label: 'fltb.opGe' },
  { value: 'le', label: 'fltb.opLe' },
  { value: 'empty', label: 'fltb.opEmpty' },
  { value: 'notempty', label: 'fltb.opNotEmpty' },
  // 'in' is what makes a real question askable. Every condition is joined by ONE
  // AND or OR, so "2 m or 70 cm, in FT8, since January" had no expression: AND
  // killed the two bands, OR let every FT8 QSO through. The OR lives inside the
  // condition instead, and the rest keeps ANDing.
  { value: 'in', label: 'fltb.opIn' },
  { value: 'notin', label: 'fltb.opNotIn' },
];

const TEXT_OPS: FilterOp[] = ['contains', 'startswith', 'endswith', 'eq', 'ne', 'in', 'notin', 'empty', 'notempty'];
const NUM_OPS: FilterOp[] = ['eq', 'ne', 'gt', 'lt', 'ge', 'le', 'empty', 'notempty'];

function opsFor(field: string): { value: FilterOp; label: string }[] {
  const t = FIELDS.find((f) => f.value === field)?.type ?? 'text';
  // 'adifdate' is stored as a string but sorts chronologically, so it takes the
  // comparison operators (before/after), not the text ones.
  const allow = t === 'text' ? TEXT_OPS : NUM_OPS;
  return OPS.filter((o) => allow.includes(o.value));
}

const PRESETS_KEY = 'hamlog.filterPresets';
function loadPresets(): Record<string, QueryFilter> {
  try { return JSON.parse(localStorage.getItem(PRESETS_KEY) || '{}'); } catch { return {}; }
}
function savePresets(p: Record<string, QueryFilter>) {
  // Write-through to the portable DB so presets travel with the data/ folder.
  writeUiPref(PRESETS_KEY, JSON.stringify(p));
}

interface Props {
  open: boolean;
  initial: QueryFilter;
  onApply: (f: QueryFilter) => void; // applies and closes
  onClose: () => void;
}

export function FilterBuilder({ open, initial, onApply, onClose }: Props) {
  const { t } = useI18n();
  // Field dropdown sorted alphabetically by translated label (the FIELDS array is
  // grouped by category for maintenance; the operator wants it A→Z to find things).
  const sortedFields = useMemo(
    () => FIELDS.map((f) => ({ f, txt: t(f.label) })).sort((a, b) => a.txt.localeCompare(b.txt)),
    [t],
  );
  const [conditions, setConditions] = useState<FilterCondition[]>([]);
  const [match, setMatch] = useState<'AND' | 'OR'>('AND');
  const [presets, setPresets] = useState<Record<string, QueryFilter>>({});
  const [presetName, setPresetName] = useState('');

  // Seed from the active filter each time the dialog opens.
  useEffect(() => {
    if (!open) return;
    setConditions(initial.conditions?.length ? initial.conditions.map((c) => ({ ...c })) : []);
    setMatch(initial.match === 'OR' ? 'OR' : 'AND');
    setPresets(loadPresets());
  }, [open]); // eslint-disable-line react-hooks/exhaustive-deps

  const setCond = (i: number, patch: Partial<FilterCondition>) =>
    setConditions((cs) => cs.map((c, j) => (j === i ? { ...c, ...patch } : c)));
  const addCond = () => setConditions((cs) => [...cs, { field: 'callsign', op: 'contains', value: '' }]);
  const removeCond = (i: number) => setConditions((cs) => cs.filter((_, j) => j !== i));

  function buildFilter(): QueryFilter {
    const clean = conditions.filter((c) => c.field && c.op);
    return { ...initial, conditions: clean, match };
  }

  function apply() { onApply(buildFilter()); }

  // Same silence as the alert rules: the preset landed in the list, but the list
  // is not where the operator is looking when they press Save.
  const [savedMsg, setSavedMsg] = useState('');
  const savedTimer = useRef(0);
  function flashSaved(text: string) {
    setSavedMsg(text);
    window.clearTimeout(savedTimer.current);
    savedTimer.current = window.setTimeout(() => setSavedMsg(''), 3000);
  }
  useEffect(() => () => window.clearTimeout(savedTimer.current), []);

  function saveCurrentPreset() {
    const name = presetName.trim();
    if (!name) return;
    const next = { ...presets, [name]: buildFilter() };
    savePresets(next);
    setPresets(next);
    setPresetName('');
    flashSaved(t('fltb.presetSaved', { name }));
  }
  function loadPreset(name: string) {
    const f = presets[name];
    if (!f) return;
    setConditions(f.conditions?.map((c) => ({ ...c })) ?? []);
    setMatch(f.match === 'OR' ? 'OR' : 'AND');
  }
  function deletePreset(name: string) {
    const next = { ...presets };
    delete next[name];
    savePresets(next);
    setPresets(next);
  }

  const presetNames = Object.keys(presets).sort();

  return (
    <Dialog open={open} onOpenChange={(o) => { if (!o) apply(); }}>
      <DialogContent className="max-w-3xl">
        <DialogHeader className="px-6 py-4">
          <DialogTitle>{t('fltb.title')}</DialogTitle>
        </DialogHeader>

        <div className="px-6 py-4 space-y-5">
        {/* Match mode + presets */}
        <div className="flex flex-wrap items-center gap-2 text-sm">
          <span className="text-muted-foreground">{t('fltb.match')}</span>
          <div className="inline-flex rounded-md border border-border overflow-hidden">
            {(['AND', 'OR'] as const).map((m) => (
              <button key={m} type="button" onClick={() => setMatch(m)}
                className={`px-3 py-1 text-xs font-medium ${match === m ? 'bg-primary text-primary-foreground' : 'hover:bg-accent'}`}>
                {m === 'AND' ? t('fltb.all') : t('fltb.any')}
              </button>
            ))}
          </div>
          <div className="flex-1" />
          {presetNames.length > 0 && (
            <Select onValueChange={loadPreset}>
              <SelectTrigger className="h-8 w-44 text-xs"><FolderOpen className="size-3.5 mr-1" /><SelectValue placeholder={t('fltb.loadPreset')} /></SelectTrigger>
              <SelectContent>
                {presetNames.map((n) => (
                  <SelectItem key={n} value={n}>
                    <span className="inline-flex items-center gap-2">
                      {n}
                      {/* Radix SelectItem selects on pointer-up, so an onClick here
                          fires too late — the menu has already loaded the preset and
                          closed. Intercept pointer-down (preventDefault stops Radix
                          claiming it) and delete there instead. */}
                      <Trash2
                        className="size-3 text-muted-foreground hover:text-destructive"
                        onPointerDown={(e) => { e.preventDefault(); e.stopPropagation(); deletePreset(n); }}
                      />
                    </span>
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
          )}
        </div>

        {/* Conditions */}
        <div className="space-y-2 max-h-[50vh] overflow-auto p-1">
          {conditions.length === 0 && (
            <div className="text-xs text-muted-foreground py-4 text-center">{t('fltb.noConditions')}</div>
          )}
          {conditions.map((c, i) => {
            const needsValue = c.op !== 'empty' && c.op !== 'notempty';
            const isList = c.op === 'in' || c.op === 'notin';
            const fieldType = FIELDS.find((f) => f.value === c.field)?.type ?? 'text';
            return (
              <div key={i} className="flex items-center gap-2">
                <span className="text-[10px] w-8 text-right text-muted-foreground font-mono">{i === 0 ? t('fltb.where') : match}</span>
                <Select value={c.field} onValueChange={(v) => {
                  // Reset op if the new field type doesn't allow the current one.
                  const allowed = opsFor(v).map((o) => o.value);
                  setCond(i, { field: v, op: allowed.includes(c.op) ? c.op : allowed[0] });
                }}>
                  <SelectTrigger className="h-8 w-48 text-xs"><SelectValue /></SelectTrigger>
                  <SelectContent className="max-h-72">
                    {sortedFields.map(({ f, txt }) => <SelectItem key={f.value} value={f.value}>{txt}</SelectItem>)}
                  </SelectContent>
                </Select>
                <Select value={c.op} onValueChange={(v) => setCond(i, { op: v as FilterOp })}>
                  <SelectTrigger className="h-8 w-40 text-xs"><SelectValue /></SelectTrigger>
                  <SelectContent>
                    {opsFor(c.field).map((o) => <SelectItem key={o.value} value={o.value}>{t(o.label)}</SelectItem>)}
                  </SelectContent>
                </Select>
                <Input
                  // An ADIF date is picked with a calendar but STORED as the
                  // 8-digit form the column holds, so the comparison stays a
                  // plain string one on both sides.
                  // A list is typed by hand, commas and all, so it stays a text
                  // box even on a date or number field: a calendar cannot express
                  // "any one of these".
                  type={isList ? 'text'
                    : fieldType === 'date' || fieldType === 'adifdate' ? 'date'
                      : fieldType === 'number' ? 'number' : 'text'}
                  className="h-8 flex-1 text-xs"
                  disabled={!needsValue}
                  placeholder={!needsValue ? '—'
                    : isList ? t('fltb.listPh')
                      : fieldType === 'date' ? 'YYYY-MM-DD' : t('fltb.valuePh')}
                  // \d, not d: the escape was missing, so the test never matched
                  // an 8-digit ADIF date and the calendar input was handed
                  // "20260728" — which type=date rejects, showing an empty box
                  // over a value that was really there.
                  // The ADIF reshaping is skipped for a list: it would eat the
                  // commas and leave one unreadable run of digits.
                  value={!isList && fieldType === 'adifdate' && /^\d{8}$/.test(c.value)
                    ? `${c.value.slice(0, 4)}-${c.value.slice(4, 6)}-${c.value.slice(6, 8)}`
                    : c.value}
                  onChange={(e) => setCond(i, {
                    value: !isList && fieldType === 'adifdate' ? e.target.value.replace(/-/g, '') : e.target.value,
                  })}
                  onKeyDown={(e) => { if (e.key === 'Enter') apply(); }}
                />
                <button type="button" onClick={() => removeCond(i)} className="text-muted-foreground hover:text-destructive shrink-0" title={t('fltb.remove')}>
                  <X className="size-4" />
                </button>
              </div>
            );
          })}
          <Button variant="outline" size="sm" className="h-8" onClick={addCond}>
            <Plus className="size-3.5 mr-1" /> {t('fltb.addCondition')}
          </Button>
        </div>

        {/* Save preset */}
        <div className="flex items-center gap-2 border-t border-border pt-3">
          <Input className="h-8 w-56 text-xs" placeholder={t('fltb.presetNamePh')} value={presetName}
            onChange={(e) => setPresetName(e.target.value)}
            onKeyDown={(e) => { if (e.key === 'Enter') saveCurrentPreset(); }} />
          <Button variant="outline" size="sm" className="h-8" disabled={!presetName.trim()} onClick={saveCurrentPreset}>
            <Save className="size-3.5 mr-1" /> {t('fltb.savePreset')}
          </Button>
          {savedMsg && <span className="text-[11px] text-success truncate">{savedMsg}</span>}
        </div>
        </div>

        <DialogFooter className="px-6 py-4 bg-transparent border-t-0">
          <Button variant="ghost" onClick={() => { setConditions([]); }}>{t('fltb.clear')}</Button>
          <Button variant="outline" onClick={onClose}>{t('fltb.cancel')}</Button>
          <Button onClick={apply}>{t('fltb.applyClose')}</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}
