from __future__ import annotations

import hashlib
import html
import re
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any

from bs4 import BeautifulSoup

from models import InventoryItem, Identity, norm, quality_level, parse_upgrade
from rules import RuleBook

QUALITY_WORDS = [
    "Epicki", "Epicka", "Epickie",
    "Doskonały", "Doskonała", "Doskonałe",
    "Dobry", "Dobra", "Dobre",
    "Zwykły", "Zwykła", "Zwykłe",
]
LEGENDARY_WORDS = ["Legendarny", "Legendarna", "Legendarne"]

BASE_VARIANTS = {
    "polautomat s.": ["półautomat snajperski", "polautomat snajperski"],
    "karabin m.": ["karabin myśliwski", "karabin mysliwski"],
    "karabin s.": ["karabin snajperski"],
    "miotacz p.": ["miotacz płomieni", "miotacz plomieni"],
    "shurek": ["shuriken"],
}


def strip_upgrade(name: str) -> tuple[str, int]:
    match = re.search(r"\s*\(\+(\d+)\)\s*$", name)
    if not match:
        return name.strip(), 0
    return name[:match.start()].strip(), max(0, min(5, int(match.group(1))))


def _id_has_affix(value: str | None) -> bool | None:
    if value is None or str(value).strip() == "":
        return None
    return str(value).strip() != "0"


def _find_phrase_span(text: str, candidates: list[str], rulebook: RuleBook) -> tuple[int, int, str] | None:
    words = text.split()
    nwords = [norm(word) for word in words]
    best = None
    best_score = (-1, -1)
    for candidate in candidates:
        display = rulebook.display_names.get(candidate, candidate)
        variants = {candidate, display, *(BASE_VARIANTS.get(candidate, []))}
        for variant in variants:
            cwords = norm(variant).split()
            if not cwords or len(cwords) > len(words):
                continue
            for idx in range(len(words) - len(cwords) + 1):
                if nwords[idx:idx + len(cwords)] == cwords:
                    score = (len(cwords), len(norm(variant)))
                    if score > best_score:
                        best = (idx, idx + len(cwords), candidate)
                        best_score = score
    return best


def split_item_name(
    full_name: str,
    category: str,
    prefix_id: str | None,
    suffix_id: str | None,
    rulebook: RuleBook,
    forced_base: str | None = None,
) -> dict[str, Any]:
    clean, upgrade = strip_upgrade(full_name)
    working = clean.strip()
    legendary = False
    quality = "Zwykły"

    for word in LEGENDARY_WORDS:
        if norm(working) == norm(word) or norm(working).startswith(norm(word) + " "):
            legendary = True
            working = " ".join(working.split()[1:]).strip()
            break

    for word in QUALITY_WORDS:
        if norm(working) == norm(word) or norm(working).startswith(norm(word) + " "):
            quality = working.split()[0]
            working = " ".join(working.split()[1:]).strip()
            break

    expects_prefix = _id_has_affix(prefix_id)
    expects_suffix = _id_has_affix(suffix_id)
    candidates = rulebook.bases(category)
    if forced_base:
        candidates = [forced_base] + [x for x in candidates if x != forced_base]

    prefix_raw = ""
    suffix_raw = ""
    base_key = ""
    split_method = "unresolved"
    span = _find_phrase_span(working, candidates, rulebook)
    if span:
        start, end, candidate = span
        words = working.split()
        before = " ".join(words[:start]).strip()
        after = " ".join(words[end:]).strip()
        valid = True
        if expects_prefix is False and before:
            valid = False
        if expects_suffix is False and after:
            valid = False
        if expects_prefix is True and not before:
            valid = False
        if expects_suffix is True and not after:
            valid = False
        if valid:
            base_key = candidate
            prefix_raw = before if expects_prefix is not False else ""
            suffix_raw = after if expects_suffix is not False else ""
            split_method = "base-anchor"

    if not base_key:
        # Fallback: use the longest known base anywhere in the remaining name.
        span = _find_phrase_span(working, candidates, rulebook)
        if span:
            start, end, candidate = span
            words = working.split()
            base_key = candidate
            prefix_raw = " ".join(words[:start]).strip()
            suffix_raw = " ".join(words[end:]).strip()
            split_method = "fallback-base"
        else:
            base_key = rulebook.canonical_component(category, working, "base")
            split_method = "fuzzy-base" if base_key else "unresolved"

    prefix_key = rulebook.canonical_component(category, prefix_raw, "prefix") if prefix_raw else ""
    suffix_key = rulebook.canonical_component(category, suffix_raw, "suffix") if suffix_raw else ""

    return {
        "legendary": legendary,
        "quality": quality,
        "upgrade": upgrade,
        "base": base_key,
        "prefix": prefix_key,
        "suffix": suffix_key,
        "raw_prefix": prefix_raw,
        "raw_base": rulebook.display(base_key) if base_key else working,
        "raw_suffix": suffix_raw,
        "method": split_method,
    }


def extract_param(decoded_hover: str, name: str) -> str | None:
    match = re.search(rf"[?&]{re.escape(name)}=([^&'\"\\]+)", decoded_hover)
    return match.group(1) if match else None


def get_item_type_from_hover(decoded_hover: str) -> str:
    match = re.search(r"<b>\s*<i>\s*(.*?)\s*</i>\s*</b>", decoded_hover, flags=re.I | re.S)
    if not match:
        return ""
    raw = BeautifulSoup(match.group(1), "html.parser").get_text(" ", strip=True)
    return re.sub(r"\s+", " ", raw.replace("\xa0", " ")).strip()


def category_from_html_type(value: str) -> str:
    text = norm(value)
    if text.startswith("helm"):
        return "head"
    if text.startswith("zbroja"):
        return "torso"
    if text.startswith("spodnie"):
        return "pants"
    if text.startswith("pierscien"):
        return "rings"
    if text.startswith("amulet"):
        return "amulets"
    if "bron biala jednoreczna" in text:
        return "melee1h"
    if "bron biala dwureczna" in text:
        return "melee2h"
    if "bron dystansowa" in text:
        return "ranged"
    if "bron palna jednoreczna" in text:
        return "firearm1h"
    if "bron palna dwureczna" in text:
        return "firearm2h"
    return ""


def _page_declared_count(soup: BeautifulSoup) -> int | None:
    match = re.search(r"PRZEDMIOT[ÓO]W W ZBROJOWNI:\s*(\d+)", soup.get_text(" ", strip=True), flags=re.I)
    return int(match.group(1)) if match else None


def _profile_metadata(soup: BeautifulSoup, source_path: Path) -> dict[str, Any]:
    character = ""
    for selector in ("a.me", "a.nickNameStats", ".stats-player a", ".character a"):
        node = soup.select_one(selector)
        if node and node.get_text(" ", strip=True):
            character = node.get_text(" ", strip=True)
            break
    text = soup.get_text(" ", strip=True)
    realm = "Moria" if re.search(r"\bMoria\b", text, flags=re.I) else ("Necropolia" if re.search(r"\bNecropolia\b", text, flags=re.I) else "")
    server = ""
    match = re.search(r"var\s+serverTitle\s*=\s*['\"]([^'\"]+)", str(soup))
    if match:
        server = match.group(1)
    if not server:
        title = soup.title.get_text(" ", strip=True) if soup.title else source_path.stem
        server_match = re.search(r"(R\d+PL)", title, flags=re.I)
        server = server_match.group(1).upper() if server_match else ""
    return {"character": character or "Nieznana postać", "realm": realm, "server": server}


def parse_armory_html(path: Path, rulebook: RuleBook) -> tuple[list[InventoryItem], dict[str, Any]]:
    path = Path(path)
    raw_bytes = path.read_bytes()
    soup = BeautifulSoup(raw_bytes.decode("utf-8", errors="ignore"), "html.parser")
    raw_items: list[dict[str, Any]] = []

    for tab in soup.select('div[id^="armoryTabContainer_"]'):
        tab_id_match = re.search(r"armoryTabContainer_(\d+)", tab.get("id", ""))
        tab_id = tab_id_match.group(1) if tab_id_match else ""
        tab_name = tab.select_one(f"#itemTabName_{tab_id}") or tab.select_one(".itemTabName")
        shelf = re.sub(r"\s+", " ", tab_name.get_text(" ", strip=True)).strip() if tab_name else f"Półka {tab_id}"
        list_el = tab.select_one(f"#armoryTabItemList_{tab_id}") or tab

        for node in list_el.select('div[id^="masterNode_armory_"]'):
            name_el = node.select_one(".item-link")
            checkbox = node.select_one("input[data-itemid], input[data-itemname]")
            full_name = ""
            if checkbox and checkbox.get("data-itemname"):
                full_name = checkbox.get("data-itemname") or ""
            elif name_el:
                full_name = name_el.get_text(" ", strip=True)
            full_name = re.sub(r"\s+", " ", full_name).strip()
            if not full_name:
                continue
            item_id = checkbox.get("data-itemid", "") if checkbox else ""
            if not item_id:
                id_match = re.search(r"masterNode_armory_(\d+)", node.get("id", ""))
                item_id = id_match.group(1) if id_match else ""
            hover_raw = ""
            if name_el:
                hover_raw = name_el.get("onclick", "") or name_el.get("data-tippy-content", "") or ""
            hover = html.unescape(hover_raw).replace("\\'", "'").replace('\\"', '"')
            html_type = get_item_type_from_hover(hover)
            category = category_from_html_type(html_type)
            if not category:
                continue
            raw_items.append({
                "item_id": item_id,
                "full_name": full_name,
                "shelf": shelf,
                "html_type": html_type,
                "category": category,
                "base_type": extract_param(hover, "baseType") or extract_param(hover, "itemType") or "",
                "prefix_id": extract_param(hover, "prefix"),
                "suffix_id": extract_param(hover, "sufix") or extract_param(hover, "suffix"),
                "legendary_param": extract_param(hover, "legendary") or extract_param(hover, "legend"),
            })

    first_parts: list[dict[str, Any]] = []
    learned: dict[tuple[str, str], Counter] = defaultdict(Counter)
    for raw in raw_items:
        parts = split_item_name(raw["full_name"], raw["category"], raw["prefix_id"], raw["suffix_id"], rulebook)
        first_parts.append(parts)
        if raw["base_type"] and parts["base"] and parts["method"] == "base-anchor":
            learned[(raw["category"], str(raw["base_type"]))][parts["base"]] += 1
    learned_base = {key: counts.most_common(1)[0][0] for key, counts in learned.items() if counts}

    items: list[InventoryItem] = []
    warnings: list[str] = []
    for raw, parts in zip(raw_items, first_parts):
        pref_expected = _id_has_affix(raw["prefix_id"]) is True
        suf_expected = _id_has_affix(raw["suffix_id"]) is True
        unresolved = (pref_expected and not parts["prefix"]) or (suf_expected and not parts["suffix"]) or not parts["base"]
        forced = learned_base.get((raw["category"], str(raw["base_type"])))
        if unresolved and forced:
            retried = split_item_name(raw["full_name"], raw["category"], raw["prefix_id"], raw["suffix_id"], rulebook, forced)
            if retried["base"]:
                parts = retried
        if raw["legendary_param"] == "1":
            parts["legendary"] = True

        notes = []
        if pref_expected and not parts["prefix"]:
            notes.append("Nie rozdzielono prefixu")
        if suf_expected and not parts["suffix"]:
            notes.append("Nie rozdzielono suffixu")
        if not parts["base"]:
            notes.append("Nie rozpoznano bazy")
        if notes:
            warnings.append(f"{raw['full_name']}: {'; '.join(notes)}")
        identity = Identity(raw["category"], parts["base"], parts["prefix"], parts["suffix"], parts["legendary"])
        level = quality_level(parts["quality"], parts["upgrade"])
        items.append(InventoryItem(
            db_id=None,
            profile_id=None,
            import_id=None,
            item_id=raw["item_id"],
            full_name=raw["full_name"],
            identity=identity,
            quality=parts["quality"],
            upgrade=parse_upgrade(parts["upgrade"]),
            level=level,
            shelf=raw["shelf"],
            html_type=raw["html_type"],
            parser_notes="; ".join(notes),
            raw_prefix=parts["raw_prefix"],
            raw_base=parts["raw_base"],
            raw_suffix=parts["raw_suffix"],
        ))

    metadata = _profile_metadata(soup, path)
    metadata.update({
        "source_file": str(path),
        "source_hash": hashlib.sha256(raw_bytes).hexdigest(),
        "declared_count": _page_declared_count(soup),
        "parsed_count": len(items),
        "warnings": warnings,
    })
    return items, metadata
