import type { Column } from './types.js';

type FilterMode = 'table' | 'data';

export type RowPredicate<T> = (row: T) => boolean;

interface Term {
  field?: string; // if undefined, match across all fields
  value?: string; // lowercased string
  negate?: boolean;
  range?: { lower: string; upper: string; inclusive: boolean };
}

interface Clause {
  terms: Term[]; // AND across terms
}

interface LuceneQuery {
  clauses: Clause[]; // OR across clauses
}

function stripQuotes(s: string): string {
  if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
    return s.slice(1, -1);
  }
  return s;
}

function splitByOr(input: string): string[] {
  return input.split(/\s+OR\s+/i).map((s) => s.trim()).filter(Boolean);
}

function splitByAnd(input: string): string[] {
  return input.split(/\s+AND\s+/i).map((s) => s.trim()).filter(Boolean);
}

function parseTerm(raw: string): Term | null {
  if (!raw) return null;
  let negate = false;
  // handle NOT prefix or leading '-'
  const notMatch = raw.match(/^\s*(NOT\s+|-)\s*(.*)$/i);
  if (notMatch) {
    negate = true;
    raw = notMatch[2];
  }
  // range: field:[lower TO upper]
  const rangeMatch = raw.match(/^([^:\s]+)\s*:\s*\[(.*?)\s+TO\s+(.*?)\]$/i);
  if (rangeMatch) {
    return {
      field: rangeMatch[1],
      negate,
      range: { lower: stripQuotes(rangeMatch[2]).toLowerCase(), upper: stripQuotes(rangeMatch[3]).toLowerCase(), inclusive: true },
    };
  }
  // field:value (value may be quoted)
  const m = raw.match(/^([^:\s]+)\s*:\s*("[^"]*"|'[^']*'|[^"'\s]+)$/);
  if (m) {
    return { field: m[1], value: stripQuotes(m[2]).toLowerCase(), negate };
  }
  // plain term
  if (raw.length > 0) {
    return { value: stripQuotes(raw).toLowerCase(), negate };
  }
  return null;
}

function parseLucene(input: string): LuceneQuery | null {
  if (!input) return null;
  const clauses = splitByOr(input).map((clauseStr) => {
    const terms = splitByAnd(clauseStr)
      .map(parseTerm)
      .filter((t): t is Term => !!t && !!t.value);
    return { terms } as Clause;
  }).filter((c) => c.terms.length > 0);
  if (clauses.length === 0) return null;
  return { clauses };
}

export function compileLucenePredicate<T>(
  input: string,
  mode: FilterMode,
  columns: Column<T>[]
): RowPredicate<T> | null {
  const ast = parseLucene(input);
  if (!ast) return null;
  const colMap = new Map<string, Column<T>>(
    columns.map((c) => [String(c.key), c])
  );
  const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0);
  const coerce = (s: any) => {
    const str = String(s ?? '').toLowerCase();
    const num = Number(str);
    const date = Date.parse(str);
    if (!Number.isNaN(num) && str.trim() !== '') return { t: 'n' as const, v: num };
    if (!Number.isNaN(date)) return { t: 'd' as const, v: date };
    return { t: 's' as const, v: str };
  };
  const inRange = (val: any, lower: string, upper: string) => {
    const a = coerce(val);
    const lo = coerce(lower);
    const up = coerce(upper);
    // All strings: lexical compare
    if (a.t === 's' && lo.t === 's' && up.t === 's') {
      const av = a.v as string;
      return cmp(av, lo.v as string) >= 0 && cmp(av, up.v as string) <= 0;
    }
    // All numbers
    if (a.t === 'n' && lo.t === 'n' && up.t === 'n') {
      const av = a.v as number;
      return av >= (lo.v as number) && av <= (up.v as number);
    }
    // All dates (as numbers)
    if (a.t === 'd' && lo.t === 'd' && up.t === 'd') {
      const av = a.v as number;
      return av >= (lo.v as number) && av <= (up.v as number);
    }
    // Fallback: compare string forms
    const as = String(val ?? '').toLowerCase();
    return cmp(as, lower) >= 0 && cmp(as, upper) <= 0;
  };
  return (row: T) => {
    for (const clause of ast.clauses) {
      let clauseOk = true;
      for (const term of clause.terms) {
        let ok = false;
        if (term.range && term.field) {
          // range compare on field
          if (mode === 'data') {
            ok = inRange((row as any)[term.field], term.range.lower, term.range.upper);
          } else {
            const col = colMap.get(term.field);
            if (!col || col.hidden) { ok = false; } else {
              const val = col.value ? col.value(row) : (row as any)[col.key as any];
              ok = inRange(val, term.range.lower, term.range.upper);
            }
          }
        } else if (term.field && term.value != null) {
          if (mode === 'data') {
            const s = String((row as any)[term.field] ?? '').toLowerCase();
            ok = s.includes(term.value);
          } else {
            const col = colMap.get(term.field);
            if (!col || col.hidden === true) { ok = false; }
            else {
              const val = col.value ? col.value(row) : (row as any)[col.key as any];
              const s = String(val ?? '').toLowerCase();
              ok = s.includes(term.value);
            }
          }
        } else if (term.value != null) {
          // search across all visible/raw fields
          if (mode === 'data') {
            ok = Object.values(row as any).some((v) => String(v ?? '').toLowerCase().includes(term.value!));
          } else {
            ok = columns.some((col) => {
              if (col.hidden) return false;
              const val = col.value ? col.value(row) : (row as any)[col.key as any];
              const s = String(val ?? '').toLowerCase();
              return s.includes(term.value!);
            });
          }
        }
        if (term.negate) ok = !ok;
        if (!ok) { clauseOk = false; break; }
      }
      if (clauseOk) return true;
    }
    return false;
  };
}
