import { MAX_GROUP_DEPTH, type FieldType, type Group, type Operator, type Rule } from './types';
import { parseRelative, RELATIVE_SYNTHETIC } from './dateMacro';

// Split a group body into top-level parts separated by a single connective, respecting quotes and
// parens to any depth. Returns the parts and the connective, or null if the split is malformed or the
// connectives are mixed (mixed AND/OR at one level is out of subset).
function splitTopLevel(input: string): { parts: string[]; match: 'all' | 'any' } | null {
  const parts: string[] = [];
  const ops: string[] = [];
  let depth = 0;
  let quoted = false;
  let token = '';

  for (let i = 0; i < input.length; i++) {
    const ch = input[i];
    if (ch === '\\') { token += ch + (input[i + 1] ?? ''); i++; continue; } // escape pair, anywhere
    if (ch === '"') { quoted = !quoted; token += ch; continue; }
    if (quoted) { token += ch; continue; }
    if (ch === '(') { depth++; token += ch; continue; }
    if (ch === ')') { depth--; token += ch; continue; }
    if (depth === 0 && input.startsWith(' AND ', i)) { parts.push(token); ops.push('AND'); token = ''; i += 4; continue; }
    if (depth === 0 && input.startsWith(' OR ', i)) { parts.push(token); ops.push('OR'); token = ''; i += 3; continue; }
    token += ch;
  }
  if (depth !== 0 || quoted) return null;
  parts.push(token);

  if (ops.length === 0) return { parts, match: 'all' };
  const allAnd = ops.every((o) => o === 'AND');
  const allOr = ops.every((o) => o === 'OR');
  if (!allAnd && !allOr) return null; // mixed connectives
  return { parts, match: allAnd ? 'all' : 'any' };
}

// Reverse of compile's date normalization: the index format yyyyMMdd back to the input's yyyy-MM-dd.
function fromIndexDate(value: string): string {
  return /^\d{8}$/.test(value) ? `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}` : value;
}

function unquote(value: string): string {
  return value.replace(/\\(["\\])/g, '$1');
}

// A trailing '*' is an operator wildcard iff it is not escaped — i.e. preceded by an even run of backslashes.
function endsWithOperatorStar(s: string): boolean {
  if (!s.endsWith('*')) return false;
  let backslashes = 0;
  let i = s.length - 2;
  while (i >= 0 && s[i] === '\\') { backslashes++; i--; }
  return backslashes % 2 === 0;
}

function parseAtom(atom: string, fieldTypes: Record<string, FieldType>): Rule | null {
  const trimmed = atom.trim();

  // #435: relative-date synthetic fields are not in the catalog; match them first.
  const relColon = trimmed.indexOf(':');
  if (relColon > 0 && RELATIVE_SYNTHETIC.test(trimmed.slice(0, relColon))) {
    const rawRel = trimmed.slice(relColon + 1);
    if (rawRel.startsWith('"') && rawRel.endsWith('"') && rawRel.length >= 2) {
      return parseRelative(trimmed.slice(0, relColon), rawRel.slice(1, -1));
    }
    return null;
  }

  const negate = trimmed.startsWith('NOT ');
  const body = negate ? trimmed.slice(4).trim() : trimmed;

  const colon = body.indexOf(':');
  if (colon < 0) return null;
  const field = body.slice(0, colon);
  const raw = body.slice(colon + 1);
  const type = fieldTypes[field];
  if (!type) return null;

  // Ranges: {a TO b} (exclusive) and [a TO b] (inclusive between)
  const range = raw.match(/^([[{])(\S+) TO (\S+)([\]}])$/);
  if (range) {
    const [, , lo, hi, close] = range;
    if (type !== 'number' && type !== 'date') return null;
    const fmt = (x: string) => (type === 'date' ? fromIndexDate(x) : x);
    if (close === ']') return mk(field, 'between', fmt(lo), fmt(hi));
    if (lo === '*') return mk(field, type === 'date' ? 'before' : 'lt', fmt(hi));
    if (hi === '*') return mk(field, type === 'date' ? 'after' : 'gt', fmt(lo));
    return null;
  }

  // Quoted phrase → is/isNot (text, enum) or matches/notMatches (fulltext) by field type
  if (raw.startsWith('"') && raw.endsWith('"') && raw.length >= 2) {
    const v = unquote(raw.slice(1, -1));
    if (type === 'fulltext') return mk(field, negate ? 'notMatches' : 'matches', v);
    if (type === 'text' || type === 'enum') return mk(field, negate ? 'isNot' : 'is', v);
    return null;
  }

  if (negate) return null; // NOT only valid with quoted forms above

  // Wildcards (text only): *v* → contains, v* → startsWith. The operator stars are the
  // unescaped boundary stars; any literal * in the value was escaped to \* by the compiler.
  if (type === 'text' && endsWithOperatorStar(raw)) {
    if (raw.startsWith('*') && raw.length >= 2) {
      return mk(field, 'contains', unescapeWild(raw.slice(1, -1)));
    }
    if (raw[0] !== '*') {
      return mk(field, 'startsWith', unescapeWild(raw.slice(0, -1)));
    }
    return null;
  }

  // Bare token → numeric eq only (reject fuzzy ~, boosts ^, etc.)
  if (type === 'number' && /^[0-9.]+$/.test(raw)) return mk(field, 'eq', raw);

  return null;
}

function unescapeWild(value: string): string {
  return value.replace(/\\([\s\S])/g, '$1');
}

function mk(field: string, operator: Operator, value: string, value2?: string): Rule {
  return value2 === undefined ? { field, operator, value } : { field, operator, value, value2 };
}

// True iff the leading '(' is closed by the trailing ')' — i.e. the whole part is one parenthesized
// sub-group, not two adjacent ones (`(a)x(b)`). Quote- and escape-aware, mirroring splitTopLevel.
function isParenWrapped(s: string): boolean {
  if (!s.startsWith('(') || !s.endsWith(')') || s.length < 2) return false;
  let depth = 0;
  let quoted = false;
  for (let i = 0; i < s.length; i++) {
    const ch = s[i];
    if (ch === '\\') { i++; continue; }
    if (ch === '"') { quoted = !quoted; continue; }
    if (quoted) continue;
    if (ch === '(') depth++;
    else if (ch === ')') {
      depth--;
      if (depth === 0) return i === s.length - 1; // the opener closed here
      if (depth < 0) return false;
    }
  }
  return false;
}

// `depth` is the nesting depth of the group being parsed (root = 0). A sub-group is in subset only
// while its own depth stays within MAX_GROUP_DEPTH — the same cap the builder UI authors within.
function parseGroup(input: string, fieldTypes: Record<string, FieldType>, depth: number): Group | null {
  const split = splitTopLevel(input.trim());
  if (!split) return null;

  const children: Array<Rule | Group> = [];
  for (const part of split.parts) {
    const p = part.trim();
    if (isParenWrapped(p)) {
      if (depth + 1 > MAX_GROUP_DEPTH) return null; // deeper than the authoring cap
      const inner = parseGroup(p.slice(1, -1), fieldTypes, depth + 1);
      if (!inner) return null;
      children.push(inner);
    } else {
      const rule = parseAtom(p, fieldTypes);
      if (!rule) return null;
      children.push(rule);
    }
  }
  if (children.length === 0) return null;
  return { match: split.match, children };
}

export function parse(input: string, fieldTypes: Record<string, FieldType>): Group | null {
  if (!input.trim()) return null;
  return parseGroup(input, fieldTypes, 0);
}
