"""Reference data mappings: PokeDB identifiers → seed format values."""

from __future__ import annotations

import json
import re
import sys
from pathlib import Path
from typing import Any

from .loader import PokeDBData


# ---------------------------------------------------------------------------
# Encounter method mapping
# ---------------------------------------------------------------------------

# PokeDB encounter_method_identifier → our simplified method name.
# Keys can be exact matches or prefix patterns (ending with *).
ENCOUNTER_METHOD_MAP: dict[str, str] = {
    # Walking / grass / cave
    "walking-tall-grass": "walk",
    "walking-long-grass": "walk",
    "walking-cave": "walk",
    "walking-bridge": "walk",
    "walking-building": "walk",
    "walking-sand": "walk",
    "walking-snow": "walk",
    "walking-rough-terrain": "walk",
    "walking-marsh": "walk",
    "walking-puddle": "walk",
    "walking-flower-field": "walk",
    "walking-ice": "walk",
    "walking-forest": "walk",
    "walking-snowfield": "walk",
    "dark-grass": "walk",
    "shaking-grass": "walk",
    "rustling-grass": "walk",
    "yellow-flowers": "walk",
    "red-flowers": "walk",
    "purple-flowers": "walk",
    # Surfing
    "surfing": "surf",
    "surfing-ocean": "surf",
    "surfing-puddle": "surf",
    "surfing-rapids": "surf",
    "surfing-underwater": "surf",
    "rippling-water": "surf",
    # Fishing
    "fishing-old-rod": "old-rod",
    "fishing-good-rod": "good-rod",
    "fishing-super-rod": "super-rod",
    "fishing": "fishing",
    "fishing-special": "fishing",
    # Rock smash
    "rock-smash": "rock-smash",
    # Headbutt
    "headbutt-low": "headbutt",
    "headbutt-normal": "headbutt",
    "headbutt-high": "headbutt",
    # Gift / special acquisition
    "npc-gift": "gift",
    "egg": "gift",
    "revive": "gift",
    "fossil": "gift",
    # Trade
    "npc-trade": "trade",
    # Overworld / symbol encounters (Gen 8+)
    "symbol-encounter": "walk",
    "wanderer": "walk",
    "flying": "walk",
    # Static / fixed
    "fixed-encounter": "static",
    "static-encounter": "static",
    "legendary-encounter": "static",
    "interactable": "static",
    # Special methods
    "swarm": "swarm",
    "poke-radar": "pokeradar",
    "dual-slot-mode": "dual-slot",
    "honey-tree": "honey",
    "trophy-garden": "walk",
    "great-marsh": "walk",
    "cave-spot": "walk",
    "bubble-spot": "surf",
    "sand-spot": "walk",
    "horde": "walk",
    "sos-encounter": "walk",
    "ambush": "walk",
    # Seaweed / diving
    "diving": "surf",
    "diving-seaweed": "surf",
    "seaweed": "surf",
    # Raids
    "max-raid": "raid",
    "max-raid-battle": "raid",
    "dynamax-adventure": "raid",
    "tera-raid": "raid",
    "tera-raid-battle": "raid",
    "fixed-tera-encounter": "static",
    # Misc
    "roaming": "roaming",
    "safari-zone": "walk",
    "bug-contest": "walk",
    "dust-cloud": "walk",
    "hidden-grotto": "static",
    "hidden-encounter": "walk",
    "horde-encounter": "walk",
    "shaking-trees": "walk",
    "shaking-ore-deposits": "walk",
    "island-scan": "static",
    "mass-outbreak": "swarm",
    "npc-buy": "gift",
    "special-encounter": "static",
    "sea-skim": "surf",
    "midair": "walk",
    "mr-backlot": "walk",
    "hoenn-sound": "walk",
    "sinnoh-sound": "walk",
    "curry": "gift",
    "boxes": "gift",
    "berry-tree": "walk",
    "zygarde-cube-assemble": "static",
    "contact-flock": "walk",
    "contact-space-time-distortion": "walk",
    "contact-unown-reasearch-notes": "static",
    "flying-pokemon-shadow": "walk",
}

# Prefix-based fallbacks for methods not explicitly listed above.
_METHOD_PREFIX_MAP: list[tuple[str, str]] = [
    ("walking-", "walk"),
    ("surfing-", "surf"),
    ("fishing-", "fishing"),
    ("headbutt-", "headbutt"),
    ("flying-", "walk"),
    ("ambush-", "walk"),
    ("contact-", "walk"),
]


def map_encounter_method(method_identifier: str) -> str | None:
    """Map a PokeDB encounter method to our simplified method name.

    Returns None if the method is unrecognized.
    """
    if method_identifier in ENCOUNTER_METHOD_MAP:
        return ENCOUNTER_METHOD_MAP[method_identifier]

    for prefix, mapped in _METHOD_PREFIX_MAP:
        if method_identifier.startswith(prefix):
            return mapped

    return None


# ---------------------------------------------------------------------------
# Version mapping
# ---------------------------------------------------------------------------

# PokeDB version identifiers that differ from our game slugs.
# Most are 1:1, these handle exceptions.
_VERSION_OVERRIDES: dict[str, str] = {
    "lets-go-pikachu": "lets-go-pikachu",
    "lets-go-eevee": "lets-go-eevee",
}


def build_version_map(
    pokedb: PokeDBData,
    version_groups: dict[str, Any],
) -> dict[str, str]:
    """Build a mapping from PokeDB version_identifier → our game slug.

    Returns the mapping dict. Logs warnings for unmapped versions.
    """
    # Collect all our known game slugs
    our_slugs: set[str] = set()
    for vg in version_groups.values():
        for slug in vg.get("versions", []):
            our_slugs.add(slug)

    # Collect all PokeDB version identifiers
    pokedb_versions: set[str] = set()
    for v in pokedb.versions:
        identifier = v.get("identifier", "")
        if identifier:
            pokedb_versions.add(identifier)

    mapping: dict[str, str] = {}

    for pdb_ver in sorted(pokedb_versions):
        if pdb_ver in _VERSION_OVERRIDES:
            mapping[pdb_ver] = _VERSION_OVERRIDES[pdb_ver]
        elif pdb_ver in our_slugs:
            mapping[pdb_ver] = pdb_ver
        # else: PokeDB version not in our version_groups (expected for some)

    # Report our games that have no PokeDB mapping
    mapped_slugs = set(mapping.values())
    unmapped_ours = our_slugs - mapped_slugs
    if unmapped_ours:
        print(f"  Versions in our config with no PokeDB match: {sorted(unmapped_ours)}")

    return mapping


# ---------------------------------------------------------------------------
# Pokemon form mapping
# ---------------------------------------------------------------------------


# PokeDB uses adjectival region forms ("alolan") while PokeAPI/our data uses
# region names ("alola"). This maps PokeDB suffixes → our suffixes.
_FORM_SUFFIX_MAP: dict[str, str] = {
    "alolan": "alola",
    "galarian": "galar",
    "hisuian": "hisui",
    "paldean": "paldea",
    # Totem forms
    "alolan-totem": "totem-alola",
    # Basculin stripes
    "blue-stripe": "blue-striped",
    "red-stripe": "red-striped",
    "white-stripe": "white-striped",
    # Sea forms
    "west-sea": "west",
    "east-sea": "east",
    # Cloak forms
    "plant-cloak": "plant",
    "sandy-cloak": "sandy",
    "trash-cloak": "trash",
    # Eiscue
    "ice-face": "ice",
    # Misc forms
    "pompom": "pom-pom",
    "10p": "10",
    "50p": "50",
    "owntempo": "own-tempo",
    "two": "two-segment",
    "chest": "chest-form",
    "ice-rider": "ice",
    "shadow-rider": "shadow",
    "apex": "apex-build",
    "ultimate": "ultimate-mode",
    "black-activated": "black",
    "white-activated": "white",
    "hero": "hero",
    "sword": "crowned",
    "shield": "crowned",
    # Gigantamax
    "gigantamax": "gmax",
    # Partner forms
    "partner": "partner-cap",
    # Flabébé / Floette / Florges color forms — these don't have form suffixes in our data
    # since each color is just the base form. Map to base.
    "blue": "blue",
    "orange": "orange",
    "red": "red",
    "white": "white",
    "yellow": "yellow",
    # Gender forms
    "female": "female",
    "male": "male",
    # Furfrou
    "natural": "natural",
    # Cherrim
    "overcast": "overcast",
    # Sinistea / Polteageist
    "antique": "antique",
    "phony": "phony",
    # Poltchageist / Sinistcha
    "artisan": "artisan",
    "counterfeit": "counterfeit",
    "masterpiece": "masterpiece",
    "unremarkable": "unremarkable",
    # Minior cores
    "blue-core": "blue",
    "green-core": "green",
    "indigo-core": "indigo",
    "orange-core": "orange",
    "red-core": "red",
    "violet-core": "violet",
    "yellow-core": "yellow",
    # Vivillon
    "fancy": "fancy",
    # Squawkabilly
    # these use same name
    # Xerneas
    "neutral": "neutral",
    # Deerling / Sawsbuck
    "spring": "spring",
    "summer": "summer",
    "autumn": "autumn",
    "winter": "winter",
    # Spiky-ears Pichu
    "spiky-ears": "spiky-eared",
    # Paldean breeds
    "paldean-combat-breed": "paldea-combat-breed",
    "paldean-blaze-breed": "paldea-blaze-breed",
    "paldean-aqua-breed": "paldea-aqua-breed",
}


# PokeDB type IDs → type names (from PokeDB's type system)
TYPE_ID_MAP: dict[int, str] = {
    1: "normal",
    2: "fighting",
    3: "flying",
    4: "poison",
    5: "ground",
    6: "rock",
    7: "bug",
    8: "ghost",
    9: "steel",
    10: "fire",
    11: "water",
    12: "grass",
    13: "electric",
    14: "psychic",
    15: "ice",
    16: "dragon",
    17: "dark",
    18: "fairy",
}


def _normalize_slug(identifier: str) -> str:
    """Normalize a PokeDB pokemon_form_identifier to a PokeAPI-style slug.

    PokeDB uses "pidgey-default" for base forms — strip the "-default" suffix.
    For alternate forms, translate PokeDB naming conventions to ours.
    """
    if identifier.endswith("-default"):
        return identifier[: -len("-default")]

    # Try suffix-based mapping: split into species + form suffix
    # e.g. "rattata-alolan" → species="rattata", suffix="alolan"
    # e.g. "mr-mime-galarian" → need to find the right split point
    # Strategy: try longest suffix first
    for pokedb_suffix, our_suffix in sorted(
        _FORM_SUFFIX_MAP.items(), key=lambda x: -len(x[0])
    ):
        if identifier.endswith("-" + pokedb_suffix):
            species = identifier[: -(len(pokedb_suffix) + 1)]
            return f"{species}-{our_suffix}"

    return identifier


def _name_to_slug(name: str) -> str:
    """Convert a display name to a PokeAPI-style slug.

    "Bulbasaur" → "bulbasaur"
    "Mr. Mime" → "mr-mime"
    "Farfetch'd" → "farfetchd"
    "Nidoran♀" → "nidoran-f"
    "Nidoran♂" → "nidoran-m"
    "Flabébé" → "flabebe"
    "Type: Null" → "type-null"
    """
    s = name.lower()
    s = s.replace("♀", "-f").replace("♂", "-m")
    s = s.replace("'", "").replace("'", "").replace(".", "").replace(":", "")
    s = s.replace("é", "e").replace("É", "e")
    s = s.replace(" ", "-")
    # Collapse multiple hyphens
    s = re.sub(r"-+", "-", s)
    return s.strip("-")


def _name_to_form_slug(name: str) -> str | None:
    """Convert a display name with form suffix to a PokeAPI-style slug.

    "Rattata (Alola)" → "rattata-alola"
    "Basculin (Blue Striped)" → "basculin-blue-striped"
    "Deoxys Normal" → "deoxys-normal"  (space-separated variant)
    """
    # Try parenthesized form: "Base (Suffix)"
    m = re.match(r"^(.+?)\s*\((.+)\)$", name)
    if m:
        base = _name_to_slug(m.group(1))
        suffix = _name_to_slug(m.group(2))
        return f"{base}-{suffix}"

    # Try space-separated form: "Deoxys Normal"
    parts = name.split()
    if len(parts) >= 2:
        return _name_to_slug(name)

    return None


# Manual overrides for PokeDB identifiers that can't be resolved generically.
# These are cases where our pokemon.json uses non-standard base form names
# (e.g. "Deoxys Normal" instead of "Deoxys").
_FORM_OVERRIDES: dict[str, tuple[int, str]] = {
    "deoxys-default": (386, "Deoxys Normal"),
    "darmanitan-galarian": (10177, "Darmanitan (Galar Standard)"),
    "mimikyu-totem": (10144, "Mimikyu (Totem Disguised)"),
    "squawkabilly-green": (931, "Squawkabilly Green Plumage"),
    "squawkabilly-blue": (10260, "Squawkabilly (Blue Plumage)"),
    "squawkabilly-white": (10262, "Squawkabilly (White Plumage)"),
    "squawkabilly-yellow": (10261, "Squawkabilly (Yellow Plumage)"),
    "toxtricity-gigantamax": (849, "Toxtricity Amped"),
}


class PokemonMapper:
    """Maps PokeDB pokemon_form_identifier → (pokeapi_id, display_name)."""

    def __init__(self, pokemon_json_path: Path, pokedb: PokeDBData) -> None:
        # Build slug → (pokeapi_id, name) from existing pokemon.json
        self._slug_to_info: dict[str, tuple[int, str]] = {}
        self._id_to_info: dict[int, tuple[int, str]] = {}  # pokeapi_id → (national_dex, name)
        self._existing_types: dict[int, list[str]] = {}  # pokeapi_id → types (fallback)
        self._unmapped: set[str] = set()

        if pokemon_json_path.exists():
            with open(pokemon_json_path) as f:
                pokemon_list = json.load(f)

            for p in pokemon_list:
                pid = p["pokeapi_id"]
                name = p["name"]
                ndex = p["national_dex"]
                self._id_to_info[pid] = (ndex, name)
                if p.get("types"):
                    self._existing_types[pid] = p["types"]

                # Index by base slug (from pokeapi_id for base forms)
                slug = _name_to_slug(name)
                self._slug_to_info[slug] = (pid, name)

                # Also index by form slug if it has a form suffix
                form_slug = _name_to_form_slug(name)
                if form_slug and form_slug != slug:
                    self._slug_to_info[form_slug] = (pid, name)

        # Build index from PokeDB pokemon_forms.json if it has useful fields
        self._pokedb_form_index: dict[str, dict] = {}
        # Reverse index: pokeapi_id → PokeDB form record (for non-encountered lookups)
        self._id_to_pokedb_form: dict[int, dict] = {}
        for form in pokedb.pokemon_forms:
            identifier = form.get("identifier", "")
            if identifier:
                self._pokedb_form_index[identifier] = form

        # Build reverse index from pokeapi_id → PokeDB form
        # First, for all encountered lookups that succeed, we cache the mapping.
        # Here we pre-build for default forms using ndex_id.
        for form in pokedb.pokemon_forms:
            ndex = form.get("ndex_id")
            if ndex and form.get("is_default_form"):
                # Default form matches the base species (ndex == pokeapi_id for base forms)
                if ndex in self._id_to_info:
                    self._id_to_pokedb_form[ndex] = form
                # Also look for alternate-form pokeapi_ids that share the same ndex
                for pid, (p_ndex, _) in self._id_to_info.items():
                    if p_ndex == ndex and pid not in self._id_to_pokedb_form:
                        self._id_to_pokedb_form[pid] = form

        # Map non-default forms to their specific pokeapi_ids where possible
        for form in pokedb.pokemon_forms:
            identifier = form.get("identifier", "")
            if not identifier or form.get("is_default_form"):
                continue
            slug = _normalize_slug(identifier)
            if slug in self._slug_to_info:
                pid, _ = self._slug_to_info[slug]
                self._id_to_pokedb_form[pid] = form

    def lookup(self, pokemon_form_identifier: str | None) -> tuple[int, str] | None:
        """Look up a PokeDB pokemon_form_identifier.

        Returns (pokeapi_id, display_name) or None if unmapped.
        """
        if not pokemon_form_identifier:
            return None

        # Check manual overrides first
        if pokemon_form_identifier in _FORM_OVERRIDES:
            return _FORM_OVERRIDES[pokemon_form_identifier]

        slug = _normalize_slug(pokemon_form_identifier)

        # Direct slug match
        if slug in self._slug_to_info:
            return self._slug_to_info[slug]

        # Try the PokeDB form record for a pokemon_id field
        form_record = self._pokedb_form_index.get(pokemon_form_identifier, {})
        pokemon_id = form_record.get("pokemon_id")
        if pokemon_id and pokemon_id in self._id_to_info:
            ndex, name = self._id_to_info[pokemon_id]
            # Cache for future lookups
            self._slug_to_info[slug] = (pokemon_id, name)
            return (pokemon_id, name)

        # Fallback: strip form suffix to find base species.
        # Many cosmetic forms (colors, genders, seasons) don't have separate
        # entries in our pokemon.json — they use the base species entry.
        # Try progressively shorter slugs: "flabebe-blue" → "flabebe"
        parts = slug.split("-")
        for i in range(len(parts) - 1, 0, -1):
            base = "-".join(parts[:i])
            if base in self._slug_to_info:
                result = self._slug_to_info[base]
                # Cache for future lookups
                self._slug_to_info[slug] = result
                return result

        # Track unmapped
        if pokemon_form_identifier not in self._unmapped:
            self._unmapped.add(pokemon_form_identifier)

        return None

    def lookup_by_id(self, pokeapi_id: int) -> tuple[int, str] | None:
        """Look up a pokemon by its pokeapi_id.

        Returns (pokeapi_id, display_name) or None if not found.
        """
        if pokeapi_id in self._id_to_info:
            _, name = self._id_to_info[pokeapi_id]
            return (pokeapi_id, name)
        return None

    def get_sprite_url(self, pokemon_form_identifier: str | None) -> str | None:
        """Get the PokeDB CDN sprite URL (100x100 medium) for a form identifier."""
        if not pokemon_form_identifier:
            return None
        form_record = self._pokedb_form_index.get(pokemon_form_identifier, {})
        return form_record.get("main_image_normal_path_medium")

    def get_form_data(self, pokemon_form_identifier: str | None) -> dict | None:
        """Get the full PokeDB form record for a form identifier."""
        if not pokemon_form_identifier:
            return None
        return self._pokedb_form_index.get(pokemon_form_identifier)

    def all_pokemon(self) -> list[tuple[int, tuple[int, str]]]:
        """Return all known pokemon as [(pokeapi_id, (national_dex, name)), ...].

        Sourced from the existing pokemon.json.
        """
        return sorted(self._id_to_info.items())

    def get_types_for_id(self, pokeapi_id: int) -> list[str]:
        """Get types for a pokemon by pokeapi_id, looking up via PokeDB form data.

        Falls back to existing types from pokemon.json if no PokeDB form found.
        """
        form = self._find_form_for_id(pokeapi_id)
        if form:
            types = []
            t1 = form.get("type_1_id")
            t2 = form.get("type_2_id")
            if t1 and t1 in TYPE_ID_MAP:
                types.append(TYPE_ID_MAP[t1])
            if t2 and t2 in TYPE_ID_MAP:
                types.append(TYPE_ID_MAP[t2])
            if types:
                return types
        # Fallback to existing types from pokemon.json
        return self._existing_types.get(pokeapi_id, [])

    def _find_form_for_id(self, pokeapi_id: int) -> dict | None:
        """Find the PokeDB form record for a pokeapi_id."""
        # Check pre-built reverse index first
        if pokeapi_id in self._id_to_pokedb_form:
            return self._id_to_pokedb_form[pokeapi_id]

        if pokeapi_id not in self._id_to_info:
            return None
        _, name = self._id_to_info[pokeapi_id]
        slug = _name_to_slug(name)
        for suffix in ["-default", ""]:
            candidate = slug + suffix
            if candidate in self._pokedb_form_index:
                return self._pokedb_form_index[candidate]
        form_slug = _name_to_form_slug(name)
        if form_slug:
            for suffix in ["-default", ""]:
                candidate = form_slug + suffix
                if candidate in self._pokedb_form_index:
                    return self._pokedb_form_index[candidate]
        return None

    def has_sprite_for_id(self, pokeapi_id: int) -> bool:
        """Check if a sprite exists for a pokemon by pokeapi_id."""
        form = self._find_form_for_id(pokeapi_id)
        return bool(form and form.get("main_image_normal_path_medium"))

    def get_sprite_url_for_id(self, pokeapi_id: int) -> str | None:
        """Get the PokeDB CDN sprite URL for a pokemon by pokeapi_id."""
        form = self._find_form_for_id(pokeapi_id)
        return form.get("main_image_normal_path_medium") if form else None

    def report_unmapped(self) -> None:
        """Print warnings for any unmapped identifiers."""
        if self._unmapped:
            print(
                f"\nWarning: {len(self._unmapped)} unmapped pokemon form identifiers:",
                file=sys.stderr,
            )
            for ident in sorted(self._unmapped):
                print(f"  - {ident}", file=sys.stderr)


# ---------------------------------------------------------------------------
# Location area mapping
# ---------------------------------------------------------------------------

# Region prefixes to strip from location identifiers (matching Go tool behavior).
_REGION_PREFIXES = [
    "kanto-", "johto-", "hoenn-", "sinnoh-",
    "unova-", "kalos-", "alola-", "galar-",
    "hisui-", "paldea-",
]


def _identifier_to_name(identifier: str) -> str:
    """Convert a hyphenated identifier to a Title Case display name.

    "route-01-kanto" → "Route 01 Kanto"  (region stripping done separately)
    "viridian-forest" → "Viridian Forest"
    """
    return identifier.replace("-", " ").title()


class LocationMapper:
    """Maps PokeDB location_area_identifier → (location_name, area_suffix)."""

    def __init__(self, pokedb: PokeDBData) -> None:
        # Build location_area_identifier → location_identifier lookup
        self._area_to_location: dict[str, str] = {}
        # location_identifier → location display name
        self._location_names: dict[str, str] = {}
        # location_area_identifier → area display name
        self._area_names: dict[str, str] = {}

        # Index locations
        for loc in pokedb.locations:
            identifier = loc.get("identifier", "")
            name = loc.get("name", "")
            if identifier:
                self._location_names[identifier] = name if name else self._clean_location_name(identifier)

        # Index location areas
        for area in pokedb.location_areas:
            area_id = area.get("identifier", "")
            loc_id = area.get("location_identifier", "")
            area_name = area.get("name", "")
            if area_id:
                self._area_to_location[area_id] = loc_id
                self._area_names[area_id] = area_name if area_name else ""

    @staticmethod
    def _clean_location_name(identifier: str) -> str:
        """Clean a location identifier into a display name."""
        name = identifier
        for prefix in _REGION_PREFIXES:
            if name.startswith(prefix):
                name = name[len(prefix):]
                break
        return _identifier_to_name(name)

    def get_location_name(self, location_area_identifier: str) -> str:
        """Get the display name for a location area's parent location."""
        loc_id = self._area_to_location.get(location_area_identifier, "")
        if loc_id and loc_id in self._location_names:
            return self._location_names[loc_id]
        # Fallback: derive from the area identifier itself
        return self._clean_location_name(location_area_identifier)

    def get_area_name(self, location_area_identifier: str) -> str:
        """Get the display name for a specific location area."""
        return self._area_names.get(location_area_identifier, "")

    def get_location_identifier(self, location_area_identifier: str) -> str:
        """Get the parent location identifier for a location area."""
        return self._area_to_location.get(location_area_identifier, "")
