from __future__ import annotations

import json
from dataclasses import dataclass
from difflib import SequenceMatcher
from pathlib import Path
from typing import Iterable

from models import Identity, norm
from grammar import (
    BASE_DISPLAY_OVERRIDES, PREFIX_LEMMA_DISPLAY, base_grammar,
    inflect_adjective, legendary_word, semantic_prefix_key,
)

CATEGORY_TO_CATALOG = {
    "head": "glowa",
    "torso": "zbroja",
    "pants": "gacie",
    "rings": "pierscienie",
    "amulets": "szyja",
    "melee1h": "biala1h",
    "melee2h": "biala2h",
    "ranged": "dystans",
    "firearm1h": "palna1h",
    "firearm2h": "palna2h",
}

CATEGORY_LABELS = {
    "head": "Hełm",
    "torso": "Zbroja",
    "pants": "Spodnie",
    "rings": "Pierścień",
    "amulets": "Amulet",
    "melee1h": "Broń biała 1H",
    "melee2h": "Broń biała 2H",
    "ranged": "Broń dystansowa",
    "firearm1h": "Broń palna 1H",
    "firearm2h": "Broń palna 2H",
}

# User-verified in-game results for asymmetric cells in the source matrix.
# Keys are unordered: ingredient order never matters.
KNOWN_OVERRIDES = {
    ("pants", "base", tuple(sorted(("spodnie", "szorty")))): "spodnica",
    ("head", "base", tuple(sorted(("kapelusz", "bandana")))): "bandana",
    ("pants", "base", tuple(sorted(("szorty", "kilt")))): "kilt",
    ("melee1h", "base", tuple(sorted(("topor", "piesc niebios")))): "wakizashi",
    ("melee1h", "prefix", tuple(sorted(("opiekunczy", "demoniczny")))): "przyjacielski",
}


@dataclass(frozen=True)
class ComponentResult:
    value: str
    conflict: bool = False
    candidates: tuple[str, ...] = ()


class RuleBook:
    def __init__(self, data_dir: Path):
        self.data_dir = Path(data_dir)
        self.rules_raw = json.loads((self.data_dir / "moria_rules.json").read_text(encoding="utf-8"))
        self.catalog = json.loads((self.data_dir / "moria_catalog.json").read_text(encoding="utf-8"))
        self.display_names = json.loads((self.data_dir / "display_names.json").read_text(encoding="utf-8"))
        self.rules: dict[str, dict[str, dict[tuple[str, str], dict]]] = {}
        self.labels: dict[str, dict[str, list[str]]] = {}
        self._build_rules()
        self.reverse_display = {norm(v): k for k, v in self.display_names.items()}
        for key in self.display_names:
            self.reverse_display.setdefault(norm(key), key)

    def _build_rules(self) -> None:
        for category, sections in self.rules_raw["categories"].items():
            self.rules[category] = {}
            self.labels[category] = {}
            for kind, section in sections.items():
                if not section:
                    self.rules[category][kind] = {}
                    self.labels[category][kind] = []
                    continue
                self.labels[category][kind] = list(section.get("labels") or [])
                mapping: dict[tuple[str, str], dict] = {}
                for row in section.get("rules") or []:
                    key = tuple(sorted((row["left"], row["right"])))
                    mapping[key] = row
                self.rules[category][kind] = mapping
        # User-verified correction.
        for (category, kind, key), result in KNOWN_OVERRIDES.items():
            self.rules.setdefault(category, {}).setdefault(kind, {})[key] = {
                "left": key[0], "right": key[1], "result": result,
                "conflict": False, "candidates": [result], "override": True,
            }

    def categories(self) -> list[str]:
        return [c for c in CATEGORY_LABELS if c in self.rules]

    def category_label(self, category: str) -> str:
        return CATEGORY_LABELS.get(category, category)

    def catalog_section(self, category: str) -> dict:
        return self.catalog[CATEGORY_TO_CATALOG[category]]

    def bases(self, category: str) -> list[str]:
        return list(self.catalog_section(category).get("subtypes") or [])

    def prefixes(self, category: str) -> list[str]:
        return list(self.catalog_section(category).get("prefixes") or [])

    def suffixes(self, category: str) -> list[str]:
        return list(self.catalog_section(category).get("suffixes") or [])

    @staticmethod
    def _upper_first(value: str) -> str:
        return value[:1].upper() + value[1:] if value else value

    def display(self, value: str) -> str:
        if not value:
            return "Brak"
        if value in BASE_DISPLAY_OVERRIDES:
            return BASE_DISPLAY_OVERRIDES[value]
        return self._upper_first(self.display_names.get(value, value))

    def prefix_semantic_key(self, value: str | None) -> str:
        return semantic_prefix_key(value)

    def prefix_filter_label(self, value: str | None) -> str:
        if not value:
            return "Brak"
        lemma = self.prefix_semantic_key(value)
        text = PREFIX_LEMMA_DISPLAY.get(lemma, self.display_names.get(lemma, lemma))
        return self._upper_first(text)

    def prefix_display(self, value: str | None, base: str | None) -> str:
        if not value:
            return "Brak"
        lemma = self.prefix_semantic_key(value)
        masculine = PREFIX_LEMMA_DISPLAY.get(lemma, self.display_names.get(lemma, lemma))
        return self._upper_first(inflect_adjective(masculine, base_grammar(base)))

    def prefix_key_from_filter_label(self, category: str, label: str) -> str:
        if not label or label == "Brak":
            return ""
        candidates = self.prefixes(category)
        for candidate in candidates:
            if self.prefix_filter_label(candidate) == label:
                return candidate
        return ""

    def identity_display(self, identity: Identity) -> str:
        parts = []
        if identity.legendary:
            parts.append(legendary_word(identity.base))
        if identity.prefix:
            parts.append(self.prefix_display(identity.prefix, identity.base))
        parts.append(self.display(identity.base))
        if identity.suffix:
            parts.append(self.display(identity.suffix))
        return " ".join(parts)

    def _best_label(self, value: str | None, labels: Iterable[str], kind: str) -> str:
        value_n = norm(value)
        if not value_n:
            return ""
        labels = list(labels)
        if value_n in labels:
            return value_n
        mapped = self.reverse_display.get(value_n)
        if mapped in labels:
            return mapped
        best = ""
        best_score = 0.0
        second = 0.0
        for label in labels:
            score = SequenceMatcher(None, value_n, norm(self.display_names.get(label, label))).ratio()
            score = max(score, SequenceMatcher(None, value_n, label).ratio())
            if score > best_score:
                second = best_score
                best_score = score
                best = label
            elif score > second:
                second = score
        threshold = 0.70 if kind == "prefix" else 0.82
        if best_score >= threshold and (best_score - second >= 0.025 or best_score >= 0.92):
            return best
        return ""

    def canonical_component(self, category: str, value: str | None, kind: str) -> str:
        labels = self.labels.get(category, {}).get(kind) or []
        if not labels:
            if kind == "base":
                labels = self.bases(category)
            elif kind == "prefix":
                labels = self.prefixes(category)
            else:
                labels = self.suffixes(category)
        return self._best_label(value, labels, kind)

    def component_result(self, category: str, kind: str, left: str, right: str) -> ComponentResult | None:
        # Confirmed game rule: two identical components preserve that component.
        # Some source matrices omit their diagonal cells, so this must be handled
        # explicitly rather than relying on the imported table.
        if left == right:
            return ComponentResult(left)
        # Confirmed blank-affix rule: any missing affix makes the output blank.
        if kind in {"prefix", "suffix"} and (not left or not right):
            return ComponentResult("")
        if kind in {"prefix", "suffix"} and not self.rules.get(category, {}).get(kind):
            return ComponentResult("")
        key = tuple(sorted((left, right)))
        row = self.rules.get(category, {}).get(kind, {}).get(key)
        if not row:
            return None
        if row.get("conflict"):
            return ComponentResult("", True, tuple(row.get("candidates") or ()))
        return ComponentResult(row.get("result") or "")

    def combine_identity(self, left: Identity, right: Identity) -> tuple[Identity | None, list[str]]:
        notes: list[str] = []
        if left.category != right.category or left.legendary != right.legendary:
            return None, notes
        category = left.category
        base = self.component_result(category, "base", left.base, right.base)
        prefix = self.component_result(category, "prefix", left.prefix, right.prefix)
        suffix = self.component_result(category, "suffix", left.suffix, right.suffix)
        if base is None or prefix is None or suffix is None:
            return None, notes
        for kind, component in (("typ", base), ("prefix", prefix), ("suffix", suffix)):
            if component.conflict:
                notes.append(f"Konflikt tabeli {kind}: {' / '.join(component.candidates)}")
        if notes:
            return None, notes
        return Identity(category, base.value, prefix.value, suffix.value, left.legendary), notes

    def reverse_component_pairs(self, category: str, kind: str, target: str) -> list[tuple[str, str]]:
        # Every component can always be produced from two identical copies of
        # itself. The official/source matrices do not consistently include
        # these diagonal entries, so seed them here for reverse planning.
        out: list[tuple[str, str]] = [(target, target)]
        if kind in {"prefix", "suffix"} and target == "":
            labels = self.labels.get(category, {}).get(kind) or []
            # Blank result is possible whenever at least one ingredient is blank.
            out.extend(("", label) for label in labels)
            out.extend((label, "") for label in labels)
        else:
            for key, row in self.rules.get(category, {}).get(kind, {}).items():
                if not row.get("conflict") and (row.get("result") or "") == target:
                    out.append(key)
        # Preserve deterministic order while removing duplicates.
        seen: set[tuple[str, str]] = set()
        unique: list[tuple[str, str]] = []
        for left, right in out:
            key = tuple(sorted((left, right)))
            if key not in seen:
                seen.add(key)
                unique.append(key)
        return unique

    def reverse_recipes(self, target: Identity, reachable: set[str] | None = None, limit: int = 500) -> list[tuple[Identity, Identity]]:
        base_pairs = self.reverse_component_pairs(target.category, "base", target.base)
        prefix_pairs = self.reverse_component_pairs(target.category, "prefix", target.prefix)
        suffix_pairs = self.reverse_component_pairs(target.category, "suffix", target.suffix)
        recipes: list[tuple[Identity, Identity]] = []
        seen: set[str] = set()
        for b1, b2 in base_pairs:
            for p1, p2 in prefix_pairs:
                for s1, s2 in suffix_pairs:
                    left = Identity(target.category, b1, p1, s1, target.legendary)
                    right = Identity(target.category, b2, p2, s2, target.legendary)
                    if reachable is not None and left.key() not in reachable and right.key() not in reachable:
                        continue
                    a, b = sorted((left.key(), right.key()))
                    key = a + "#" + b
                    if key in seen:
                        continue
                    seen.add(key)
                    recipes.append((left, right))
                    if len(recipes) >= limit:
                        return recipes
        return recipes
