"""Build link index JSON for WikiServ.

Usage:
  python index_links.py --root "A:\\WorkProj\\OverPunk\\WikiServ\\Картотека" --out "A:\\WorkProj\\OverPunk\\WikiServ\\index.json"
"""

from __future__ import annotations

import argparse
import json
import re
from pathlib import Path
from urllib.parse import urlencode, quote

# Optional morphological support (install pymorphy2 to enable)
try:
    import pymorphy2

    _MORPH = pymorphy2.MorphAnalyzer()
except Exception:
    _MORPH = None


def _collapse_spaces(value: str) -> str:
    return re.sub(r"\s+", " ", value.strip())


def _strip_brackets(value: str) -> str:
    return _collapse_spaces(re.sub(r"\s*\([^)]*\)", "", value))


def _variants(value: str) -> list[str]:
    base = _collapse_spaces(value)
    variants = {base, base.lower()}
    if "ё" in base or "Ё" in base:
        variants.add(base.replace("ё", "е").replace("Ё", "Е"))
    if "е" in base or "Е" in base:
        variants.add(base.replace("е", "ё").replace("Е", "Ё"))
    stripped = _strip_brackets(base)
    if stripped and stripped != base:
        variants.add(stripped)
        variants.add(stripped.lower())
    # Add morphological lemmas when pymorphy2 is available
    if _MORPH is not None:
        # extract word tokens and build lemma forms
        tokens = re.findall(r"[\wА-Яа-яЁё]+", base)
        if tokens:
            try:
                lemmas = [str(_MORPH.parse(t)[0].normal_form) for t in tokens]
                joined = _collapse_spaces(" ".join(lemmas))
                variants.add(joined)
                variants.add(joined.lower())
                for l in lemmas:
                    variants.add(l)
                    variants.add(l.lower())
            except Exception:
                # fail silently and continue with basic variants
                pass
    return [v for v in variants if v]


def _is_valid_key(key: str) -> bool:
    if len(key) < 3:
        return False
    if not any(ch.isalnum() for ch in key):
        return False
    return True


def _better_choice(current: tuple[int, str] | None, candidate: tuple[int, str]) -> bool:
    if current is None:
        return True
    cur_prio, cur_path = current
    cand_prio, cand_path = candidate
    if cand_prio != cur_prio:
        return cand_prio > cur_prio
    if len(cand_path) != len(cur_path):
        return len(cand_path) < len(cur_path)
    return cand_path < cur_path


def _add_entry(
    index_best: dict[str, tuple[int, str]],
    entries: list[dict[str, object]],
    key: str,
    target: str,
    kind: str,
    priority: int,
) -> None:
    for variant in _variants(key):
        if not _is_valid_key(variant):
            continue
        entries.append({"key": variant, "target": target, "kind": kind, "priority": priority})
        candidate = (priority, target)
        current = index_best.get(variant)
        if _better_choice(current, candidate):
            index_best[variant] = candidate


def _target_for_file(root: Path, file_path: Path) -> str:
    rel_dir = file_path.parent.relative_to(root).as_posix()
    if rel_dir == ".":
        rel_dir = ""
    query = urlencode({"a": file_path.name}, quote_via=quote)
    if rel_dir:
        return f"{rel_dir}/?{query}"
    return f"?{query}"


def _normalize_manual_target(root: Path, target: str) -> str:
    target = target.strip()
    if not target:
        return ""
    path_part = target.split("?", 1)[0].rstrip("/")
    if path_part.lower().endswith(".txt"):
        file_path = (root / Path(path_part)).resolve()
        if not str(Path(path_part)).startswith(str(root)):
            return target
        try:
            file_path.relative_to(root.resolve())
        except Exception:
            return target
        return _target_for_file(root, file_path)
    return target


def _load_manual_index(manual_file: Path, root: Path) -> dict[str, str]:
    if not manual_file.exists():
        return {}
    try:
        data = json.loads(manual_file.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError):
        return {}
    if not isinstance(data, dict):
        return {}
    normalized: dict[str, str] = {}
    for key, value in data.items():
        key_str = str(key).strip()
        value_str = str(value).strip()
        if not key_str or not value_str:
            continue
        normalized_target = _normalize_manual_target(root, value_str)
        if normalized_target:
            normalized[key_str] = normalized_target
    return normalized


def build_link_index(root: Path, manual_index: dict[str, str] | None = None) -> tuple[dict[str, str], list[dict[str, object]]]:
    special_files = [
        "Описание.txt",
        "Способности.txt",
        "Способность.txt",
        "способность.txt",
        "Внешний вид.txt",
        "описание.txt",
        "сбособности.txt",
        "способности.txt",
        "слабости.txt",
        "история.txt",
    ]

    index_best: dict[str, tuple[int, str]] = {}
    entries: list[dict[str, object]] = []

    if manual_index:
        for key, target in manual_index.items():
            _add_entry(index_best, entries, key, target, "manual", 1000)

    for path in root.rglob("*"):
        if path.is_dir():
            rel_dir = path.relative_to(root).as_posix()
            if rel_dir != ".":
                _add_entry(index_best, entries, path.name, rel_dir + "/", "dir", 10)

            found_special = None
            for name in special_files:
                candidate = path / name
                if candidate.is_file():
                    found_special = candidate
                    break
            if found_special is not None:
                target = _target_for_file(root, found_special)
                _add_entry(index_best, entries, found_special.name, target, "special", 100)
                _add_entry(index_best, entries, found_special.stem, target, "special", 100)
            continue

        if path.is_file():
            rel = path.relative_to(root).as_posix()
            if path.suffix.lower() == ".txt":
                target = _target_for_file(root, path)
            else:
                target = rel
            _add_entry(index_best, entries, path.name, target, "file", 50)
            _add_entry(index_best, entries, path.stem, target, "file", 50)

    index = {key: value for key, (_, value) in index_best.items()}
    return index, entries


def main() -> int:
    parser = argparse.ArgumentParser(description="Build link index JSON for WikiServ")
    parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parent / "Картотека")
    parser.add_argument("--out", type=Path, default=Path(__file__).resolve().parent / "index.json")
    parser.add_argument("--full-out", type=Path, default=Path(__file__).resolve().parent / "index_full.json")
    parser.add_argument("--manual", type=Path, default=Path(__file__).resolve().parent / "index_manual.json")
    args = parser.parse_args()

    root = args.root
    if not root.exists():
        raise SystemExit(f"Root path '{root}' not found. Ensure it exists and is accessible.")

    manual_index = _load_manual_index(args.manual, root)
    data, entries = build_link_index(root, manual_index=manual_index)
    args.out.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
    args.full_out.write_text(json.dumps({"entries": entries}, ensure_ascii=False, indent=2), encoding="utf-8")
    if manual_index:
        args.manual.write_text(json.dumps(manual_index, ensure_ascii=False, indent=2), encoding="utf-8")
    print(f"Wrote {len(data)} entries to {args.out}")
    print(f"Wrote {len(entries)} detailed entries to {args.full_out}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
