/**
 * Prereq DSL parser + evaluator + formatter (D-01..D-04).
 *
 * Three-layer module:
 *   1. parse() — turns a prereq string into an AST of Atoms combined with AND/OR.
 *   2. evaluate() — walks the AST against a CharacterContext.
 *   3. formatReason() — German user-facing reason for failures.
 *
 * UNKNOWN-aggressive: when ANY atom is non-classifiable (deity, spellcasting-tradition,
 * age, ethnicity, vision/sense, free-text), the whole prereq returns { unknown: true, raw }.
 * Per RESEARCH.md line 550 and D-03 — when in doubt, ask the user (no hard-block).
 *
 * No NestJS, no Prisma, no I/O — pure function module.
 */
import type { CharacterContext, EvalResult, Proficiency } from './types';

const PROFICIENCY_RANK_ORDER: readonly Proficiency[] = [
  'UNTRAINED',
  'TRAINED',
  'EXPERT',
  'MASTER',
  'LEGENDARY',
];

const KNOWN_CLASS_NAMES: ReadonlySet<string> = new Set([
  'Alchemist',
  'Barbarian',
  'Bard',
  'Champion',
  'Cleric',
  'Druid',
  'Fighter',
  'Investigator',
  'Monk',
  'Oracle',
  'Ranger',
  'Rogue',
  'Sorcerer',
  'Swashbuckler',
  'Witch',
  'Wizard',
]);

const KNOWN_ANCESTRY_NAMES: ReadonlySet<string> = new Set([
  'Human',
  'Elf',
  'Dwarf',
  'Halfling',
  'Gnome',
  'Goblin',
  'Hobgoblin',
  'Leshy',
  'Lizardfolk',
  'Catfolk',
  'Kobold',
  'Orc',
  'Ratfolk',
  'Tengu',
  'Tiefling',
  'Aasimar',
]);

/** Patterns that mark the entire prereq as non-evaluable (D-02). */
const NON_EVALUABLE_PATTERNS: readonly RegExp[] = [
  /spellcasting\s+class\s+feature/i,
  /\bspellcaster\b/i,
  /(divine|arcane|primal|occult)\s+spells?/i,
  /\bcantrip\b/i,
  /worship(?:per|s|ing)?\s+of/i,
  /follower\s+of/i,
  /\bdeity\b/i,
  /at\s+least\s+\d+\s+years?\s+old/i,
  /\bethnicity\b/i,
  /low-light\s+vision/i,
  /\bdarkvision\b/i,
  /\bscent\b/i,
];

// ---------------------------------------------------------------------------
// AST types
// ---------------------------------------------------------------------------

type Atom =
  | { kind: 'skill'; skill: string; required: Proficiency }
  | { kind: 'heritage'; name: string }
  | { kind: 'class'; name: string }
  | { kind: 'ancestry'; name: string }
  | { kind: 'level'; min: number }
  | { kind: 'feat'; name: string }
  | { kind: 'unknown'; raw: string };

type Node =
  | { kind: 'atom'; atom: Atom }
  | { kind: 'and'; children: Node[] }
  | { kind: 'or'; children: Node[] };

// ---------------------------------------------------------------------------
// Parser
// ---------------------------------------------------------------------------

const SKILL_RANK_RE = /^(trained|expert|master|legendary)\s+in\s+(.+)$/i;
const HERITAGE_RE = /^(.+?)\s+heritage$/i;
const LEVEL_RE = /^level\s+(\d+)$/i;

function classifyAtom(raw: string): Atom {
  const trimmed = raw.trim();
  if (trimmed === '') {
    return { kind: 'unknown', raw: trimmed };
  }

  // Skill rank: "Trained in Athletics"
  const skillMatch = SKILL_RANK_RE.exec(trimmed);
  if (skillMatch) {
    const required = skillMatch[1].toUpperCase() as Proficiency;
    const skillName = skillMatch[2].trim();
    return { kind: 'skill', skill: skillName, required };
  }

  // Heritage: "Unbreakable Goblin heritage"
  const heritageMatch = HERITAGE_RE.exec(trimmed);
  if (heritageMatch) {
    return { kind: 'heritage', name: heritageMatch[1].trim() };
  }

  // Level: "level 5"
  const levelMatch = LEVEL_RE.exec(trimmed);
  if (levelMatch) {
    return { kind: 'level', min: Number.parseInt(levelMatch[1], 10) };
  }

  // Class ref (exact match against known class names)
  if (KNOWN_CLASS_NAMES.has(trimmed)) {
    return { kind: 'class', name: trimmed };
  }

  // Ancestry ref (exact match against known ancestry names)
  if (KNOWN_ANCESTRY_NAMES.has(trimmed)) {
    return { kind: 'ancestry', name: trimmed };
  }

  // Bare capitalized phrase → feat lookup last-resort.
  // Real PF2e feat names are short (≤4 words), Title Case, and never contain
  // lowercase function-words like "you", "a", "the", "of", "and", "to" mid-sentence.
  // This guards against classifying free-text sentences as feat lookups.
  if (/^[A-Z][A-Za-z' \-]*$/.test(trimmed)) {
    const words = trimmed.split(/\s+/);
    const hasFreeTextMarker = /(?:^| )(you|a|the|of|and|to|with|from|by)(?: |$)/i.test(
      trimmed,
    );
    if (words.length <= 4 && !hasFreeTextMarker) {
      return { kind: 'feat', name: trimmed };
    }
  }

  return { kind: 'unknown', raw: trimmed };
}

/**
 * Splits a clause on " or " and "," into OR-disjuncts when the clause looks like
 * a comma-separated list of skill rank atoms (the common PF2e shape).
 * Otherwise treats commas as soft AND (rare in real corpus).
 */
function parseClause(clause: string): Node {
  const trimmed = clause.trim();
  if (trimmed === '') {
    return { kind: 'atom', atom: { kind: 'unknown', raw: trimmed } };
  }

  // Detect OR-list: presence of ' or ' (case-insensitive, with surrounding spaces).
  // Real PF2e corpus uses "X, Y, or Z" — split on commas AND on ' or '.
  const hasOrConnector = / or /i.test(trimmed);

  if (hasOrConnector) {
    // Normalize the Oxford-comma form "X, Y, or Z" by collapsing ", or " → " or "
    // before splitting. Without this, splitting on /,\s*/ first would leave
    // a stray leading "or " on the last segment.
    const normalized = trimmed.replace(/,\s*or\s+/gi, ' or ');
    const parts = normalized
      .split(/,\s*|\s+or\s+/i)
      .map((p) => p.trim())
      .filter((p) => p.length > 0);
    if (parts.length === 1) {
      return { kind: 'atom', atom: classifyAtom(parts[0]) };
    }
    const children = parts.map<Node>((p) => ({ kind: 'atom', atom: classifyAtom(p) }));
    return { kind: 'or', children };
  }

  // Single atom (no or-connector, no comma — or comma but treat single)
  if (trimmed.includes(',')) {
    // Comma without "or" — treat as AND of atoms (soft conjunction).
    const parts = trimmed
      .split(/,\s*/)
      .map((p) => p.trim())
      .filter((p) => p.length > 0);
    const children = parts.map<Node>((p) => ({ kind: 'atom', atom: classifyAtom(p) }));
    return { kind: 'and', children };
  }

  return { kind: 'atom', atom: classifyAtom(trimmed) };
}

function parsePrereq(input: string): Node {
  // Top-level split on ';' for AND clauses
  const clauses = input
    .split(';')
    .map((c) => c.trim())
    .filter((c) => c.length > 0);

  if (clauses.length === 0) {
    return { kind: 'atom', atom: { kind: 'unknown', raw: input } };
  }
  if (clauses.length === 1) {
    return parseClause(clauses[0]);
  }
  return { kind: 'and', children: clauses.map((c) => parseClause(c)) };
}

// ---------------------------------------------------------------------------
// Evaluator
// ---------------------------------------------------------------------------

function rankAtLeast(have: Proficiency, need: Proficiency): boolean {
  return PROFICIENCY_RANK_ORDER.indexOf(have) >= PROFICIENCY_RANK_ORDER.indexOf(need);
}

function rankToGerman(rank: Proficiency): string {
  // German proficiency labels — keep canonical English ranks but quote them as identifiers.
  const label: Record<Proficiency, string> = {
    UNTRAINED: 'Untrained',
    TRAINED: 'Trained',
    EXPERT: 'Expert',
    MASTER: 'Master',
    LEGENDARY: 'Legendary',
  };
  return label[rank];
}

type AtomResult =
  | { ok: true }
  | { ok: false; reason: string }
  | { unknown: true; raw: string };

function evaluateAtom(atom: Atom, ctx: CharacterContext): AtomResult {
  switch (atom.kind) {
    case 'unknown':
      return { unknown: true, raw: atom.raw };
    case 'skill': {
      const have = ctx.skills[atom.skill] ?? 'UNTRAINED';
      if (rankAtLeast(have, atom.required)) return { ok: true };
      return {
        ok: false,
        reason: `Du benötigst mindestens '${rankToGerman(atom.required)}' in ${atom.skill}`,
      };
    }
    case 'heritage':
      if (ctx.heritageName === atom.name) return { ok: true };
      return {
        ok: false,
        reason: `Voraussetzung nicht erfüllt: Abstammungsmerkmal '${atom.name}'`,
      };
    case 'class':
      if (ctx.className === atom.name) return { ok: true };
      return {
        ok: false,
        reason: `Voraussetzung nicht erfüllt: Klasse '${atom.name}'`,
      };
    case 'ancestry':
      if (ctx.ancestryName === atom.name) return { ok: true };
      return {
        ok: false,
        reason: `Voraussetzung nicht erfüllt: Volk '${atom.name}'`,
      };
    case 'level':
      if (ctx.level >= atom.min) return { ok: true };
      return {
        ok: false,
        reason: `Du benötigst mindestens Stufe ${atom.min}`,
      };
    case 'feat':
      if (ctx.feats.has(atom.name)) return { ok: true };
      return {
        ok: false,
        reason: `Dir fehlt das Talent: ${atom.name}`,
      };
  }
}

type NodeResult =
  | { ok: true }
  | { ok: false; reason: string }
  | { unknown: true; raw: string };

function isUnknown(r: NodeResult): r is { unknown: true; raw: string } {
  return 'unknown' in r;
}

function isOk(r: NodeResult): r is { ok: true } {
  return 'ok' in r && r.ok === true;
}

function isFail(r: NodeResult): r is { ok: false; reason: string } {
  return 'ok' in r && r.ok === false;
}

function evaluateNode(node: Node, ctx: CharacterContext, raw: string): NodeResult {
  if (node.kind === 'atom') {
    return evaluateAtom(node.atom, ctx);
  }

  if (node.kind === 'or') {
    let firstFailReason: string | null = null;
    for (const child of node.children) {
      const r = evaluateNode(child, ctx, raw);
      if (isUnknown(r)) {
        // UNKNOWN-aggressive: any unknown atom poisons the whole prereq.
        return { unknown: true, raw };
      }
      if (isOk(r)) return { ok: true };
      if (firstFailReason === null && isFail(r)) {
        firstFailReason = r.reason;
      }
    }
    return {
      ok: false,
      reason: firstFailReason ?? `Voraussetzung nicht erfüllt: ${raw}`,
    };
  }

  // AND
  for (const child of node.children) {
    const r = evaluateNode(child, ctx, raw);
    if (isUnknown(r)) {
      return { unknown: true, raw };
    }
    if (isFail(r)) return r;
  }
  return { ok: true };
}

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

/**
 * Evaluate a PF2e prerequisite string against a character context.
 * Returns one of:
 *   - { ok: true }                       — prereq is met (or empty/null)
 *   - { ok: false, reason }              — evaluable AND failed (German reason)
 *   - { unknown: true, raw }             — non-evaluable (deity, spellcasting, etc.)
 */
export function evaluatePrereq(
  prereqString: string | null,
  ctx: CharacterContext,
): EvalResult {
  if (prereqString === null) return { ok: true };
  const trimmed = prereqString.trim();
  if (trimmed === '') return { ok: true };

  // Quick reject — if any non-evaluable pattern matches anywhere, return unknown.
  if (NON_EVALUABLE_PATTERNS.some((rx) => rx.test(trimmed))) {
    return { unknown: true, raw: trimmed };
  }

  const tree = parsePrereq(trimmed);
  return evaluateNode(tree, ctx, trimmed);
}

/** Internal parser exposed for testing/debugging. */
export { parsePrereq };
