"""Pythonic read access to the SRD.

    from dnd5e_db.srd import SRD, SRD2024

    srd = SRD.open()                    # 2014/en; builds the db if missing
    aboleth = srd.monsters["aboleth"]   # a validated Monster instance
    srd.spells.named("Fireball")        # lookup by display name
    [m.name for m in srd.monsters if m.challenge_rating >= 20]

    SRD2024.open()                      # same interface over the 2024 preview

Every record comes back as its strict pydantic model — plain attributes,
autocomplete, no query language. Both editions read the same way; they differ
only in which collections exist (2024: species/subspecies instead of
races/subraces, plus poisons and weapon mastery; no spells or rules yet) and
which model package types them.
"""

from __future__ import annotations

import json
import sqlite3
from collections.abc import Iterator
from pathlib import Path
from typing import Self

from dnd5e_db.build import DIST_DIR, build, db_filename, table_name
from dnd5e_db.fetch import DATA_DIR, UPSTREAM_TAG
from dnd5e_db.models import srd2014, srd2024
from dnd5e_db.models.base import SRDModel

_BUNDLED_DIR = Path(__file__).resolve().parent / "_data"


def _bundled_db_path(edition: str, language: str) -> Path:
    """Db snapshot bundled inside the installed package, for use when there's
    no vendored data/ next to dnd5e_db/ to build from (a pip install, as
    opposed to a repo checkout). Matched by glob, not an exact tag - an
    installed package has no pyproject.toml to know the current pin against
    (UPSTREAM_TAG is None there), so "does the bundle match the pin" can only
    be a build-time check (the maintainer's job before packaging), never a
    consumer-runtime one."""
    matches = sorted(_BUNDLED_DIR.glob(f"dnd5e-{edition}-{language}-*.sqlite3"))
    if not matches:
        raise FileNotFoundError(
            f"no bundled database for {edition}/{language} at {_BUNDLED_DIR}, "
            "and no vendored data/ to build one from. Dev checkout fix: "
            "rm -f dnd5e_db/_data/*.sqlite3 && "
            "poetry run python -m dnd5e_db.build --out dnd5e_db/_data"
        )
    return matches[-1]


class Collection[M: SRDModel]:
    """Read-only, dict-like view of one table, yielding validated models."""

    def __init__(self, conn: sqlite3.Connection, table: str, model: type[M]) -> None:
        self._conn = conn
        self._table = table
        self._model = model

    def _to_model(self, data: str) -> M:
        return self._model.model_validate(json.loads(data))

    def __getitem__(self, index: str) -> M:
        row = self._conn.execute(
            f'SELECT data FROM "{self._table}" WHERE "index" = ?', (index,)
        ).fetchone()
        if row is None:
            raise KeyError(index)
        return self._to_model(row[0])

    def get(self, index: str, default: M | None = None) -> M | None:
        try:
            return self[index]
        except KeyError:
            return default

    def __contains__(self, index: str) -> bool:
        return self.get(index) is not None

    def __iter__(self) -> Iterator[M]:
        for (data,) in self._conn.execute(f'SELECT data FROM "{self._table}"'):
            yield self._to_model(data)

    def __len__(self) -> int:
        (count,) = self._conn.execute(f'SELECT count(*) FROM "{self._table}"').fetchone()
        return count

    def named(self, name: str) -> list[M]:
        """All records with this display name (names repeat, e.g. features)."""
        rows = self._conn.execute(
            f'SELECT data FROM "{self._table}" WHERE name = ?', (name,)
        )
        return [self._to_model(data) for (data,) in rows]


class _Reader:
    """Edition-independent machinery; subclasses pin an edition and its models."""

    _edition: str
    _language = "en"

    def __init__(self, db_path: Path | str) -> None:
        self._conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)

    @classmethod
    def open(cls, path: Path | str | None = None) -> Self:
        """Open this edition's default database: build it from vendored data/
        when this is a repo checkout, or fall back to the db bundled inside
        the installed package when it isn't (e.g. a pip install)."""
        if path is None:
            path = DIST_DIR / db_filename(cls._edition, cls._language, UPSTREAM_TAG)
            if not Path(path).exists():
                path = (
                    build(edition=cls._edition, language=cls._language)
                    if DATA_DIR.is_dir()
                    else _bundled_db_path(cls._edition, cls._language)
                )
        return cls(path)

    def close(self) -> None:
        self._conn.close()

    def __enter__(self) -> Self:
        return self

    def __exit__(self, *exc) -> None:
        self.close()

    @property
    def metadata(self) -> dict[str, str]:
        return dict(self._conn.execute("SELECT key, value FROM metadata"))

    def _collection[M: SRDModel](self, filename: str, model: type[M]) -> Collection[M]:
        return Collection(self._conn, table_name(filename), model)


class SRD(_Reader):
    """The 2014 SRD, read from a built database file."""

    _edition = "2014"

    @property
    def ability_scores(self) -> Collection[srd2014.AbilityScore]:
        return self._collection("5e-SRD-Ability-Scores.json", srd2014.AbilityScore)

    @property
    def alignments(self) -> Collection[srd2014.Alignment]:
        return self._collection("5e-SRD-Alignments.json", srd2014.Alignment)

    @property
    def backgrounds(self) -> Collection[srd2014.Background]:
        return self._collection("5e-SRD-Backgrounds.json", srd2014.Background)

    @property
    def classes(self) -> Collection[srd2014.Class]:
        return self._collection("5e-SRD-Classes.json", srd2014.Class)

    @property
    def conditions(self) -> Collection[srd2014.Condition]:
        return self._collection("5e-SRD-Conditions.json", srd2014.Condition)

    @property
    def damage_types(self) -> Collection[srd2014.DamageType]:
        return self._collection("5e-SRD-Damage-Types.json", srd2014.DamageType)

    @property
    def equipment(self) -> Collection[srd2014.Equipment]:
        return self._collection("5e-SRD-Equipment.json", srd2014.Equipment)

    @property
    def equipment_categories(self) -> Collection[srd2014.EquipmentCategory]:
        return self._collection("5e-SRD-Equipment-Categories.json", srd2014.EquipmentCategory)

    @property
    def feats(self) -> Collection[srd2014.Feat]:
        return self._collection("5e-SRD-Feats.json", srd2014.Feat)

    @property
    def features(self) -> Collection[srd2014.Feature]:
        return self._collection("5e-SRD-Features.json", srd2014.Feature)

    @property
    def languages(self) -> Collection[srd2014.Language]:
        return self._collection("5e-SRD-Languages.json", srd2014.Language)

    @property
    def levels(self) -> Collection[srd2014.Level]:
        return self._collection("5e-SRD-Levels.json", srd2014.Level)

    @property
    def magic_items(self) -> Collection[srd2014.MagicItem]:
        return self._collection("5e-SRD-Magic-Items.json", srd2014.MagicItem)

    @property
    def magic_schools(self) -> Collection[srd2014.MagicSchool]:
        return self._collection("5e-SRD-Magic-Schools.json", srd2014.MagicSchool)

    @property
    def monsters(self) -> Collection[srd2014.Monster]:
        return self._collection("5e-SRD-Monsters.json", srd2014.Monster)

    @property
    def proficiencies(self) -> Collection[srd2014.Proficiency]:
        return self._collection("5e-SRD-Proficiencies.json", srd2014.Proficiency)

    @property
    def races(self) -> Collection[srd2014.Race]:
        return self._collection("5e-SRD-Races.json", srd2014.Race)

    @property
    def rule_sections(self) -> Collection[srd2014.RuleSection]:
        return self._collection("5e-SRD-Rule-Sections.json", srd2014.RuleSection)

    @property
    def rules(self) -> Collection[srd2014.Rule]:
        return self._collection("5e-SRD-Rules.json", srd2014.Rule)

    @property
    def skills(self) -> Collection[srd2014.Skill]:
        return self._collection("5e-SRD-Skills.json", srd2014.Skill)

    @property
    def spells(self) -> Collection[srd2014.Spell]:
        return self._collection("5e-SRD-Spells.json", srd2014.Spell)

    @property
    def subclasses(self) -> Collection[srd2014.Subclass]:
        return self._collection("5e-SRD-Subclasses.json", srd2014.Subclass)

    @property
    def subraces(self) -> Collection[srd2014.Subrace]:
        return self._collection("5e-SRD-Subraces.json", srd2014.Subrace)

    @property
    def traits(self) -> Collection[srd2014.Trait]:
        return self._collection("5e-SRD-Traits.json", srd2014.Trait)

    @property
    def weapon_properties(self) -> Collection[srd2014.WeaponProperty]:
        return self._collection("5e-SRD-Weapon-Properties.json", srd2014.WeaponProperty)


class SRD2024(_Reader):
    """The 2024 SRD preview tier, read from a built database file.

    Same interface as SRD; collections follow the 2024 corpus (species and
    subspecies replace races and subraces; poisons and weapon mastery are new;
    spells, rules, and rule sections have not been ported upstream yet).
    """

    _edition = "2024"

    @property
    def ability_scores(self) -> Collection[srd2024.AbilityScore]:
        return self._collection("5e-SRD-Ability-Scores.json", srd2024.AbilityScore)

    @property
    def alignments(self) -> Collection[srd2024.Alignment]:
        return self._collection("5e-SRD-Alignments.json", srd2024.Alignment)

    @property
    def backgrounds(self) -> Collection[srd2024.Background]:
        return self._collection("5e-SRD-Backgrounds.json", srd2024.Background)

    @property
    def classes(self) -> Collection[srd2024.Class]:
        return self._collection("5e-SRD-Classes.json", srd2024.Class)

    @property
    def conditions(self) -> Collection[srd2024.Condition]:
        return self._collection("5e-SRD-Conditions.json", srd2024.Condition)

    @property
    def damage_types(self) -> Collection[srd2024.DamageType]:
        return self._collection("5e-SRD-Damage-Types.json", srd2024.DamageType)

    @property
    def equipment(self) -> Collection[srd2024.Equipment]:
        return self._collection("5e-SRD-Equipment.json", srd2024.Equipment)

    @property
    def equipment_categories(self) -> Collection[srd2024.EquipmentCategory]:
        return self._collection("5e-SRD-Equipment-Categories.json", srd2024.EquipmentCategory)

    @property
    def feats(self) -> Collection[srd2024.Feat]:
        return self._collection("5e-SRD-Feats.json", srd2024.Feat)

    @property
    def features(self) -> Collection[srd2024.Feature]:
        return self._collection("5e-SRD-Features.json", srd2024.Feature)

    @property
    def languages(self) -> Collection[srd2024.Language]:
        return self._collection("5e-SRD-Languages.json", srd2024.Language)

    @property
    def levels(self) -> Collection[srd2024.Level]:
        return self._collection("5e-SRD-Levels.json", srd2024.Level)

    @property
    def magic_items(self) -> Collection[srd2024.MagicItem]:
        return self._collection("5e-SRD-Magic-Items.json", srd2024.MagicItem)

    @property
    def magic_schools(self) -> Collection[srd2024.MagicSchool]:
        return self._collection("5e-SRD-Magic-Schools.json", srd2024.MagicSchool)

    @property
    def monsters(self) -> Collection[srd2024.Monster]:
        return self._collection("5e-SRD-Monsters.json", srd2024.Monster)

    @property
    def poisons(self) -> Collection[srd2024.Poison]:
        return self._collection("5e-SRD-Poisons.json", srd2024.Poison)

    @property
    def proficiencies(self) -> Collection[srd2024.Proficiency]:
        return self._collection("5e-SRD-Proficiencies.json", srd2024.Proficiency)

    @property
    def skills(self) -> Collection[srd2024.Skill]:
        return self._collection("5e-SRD-Skills.json", srd2024.Skill)

    @property
    def species(self) -> Collection[srd2024.Species]:
        return self._collection("5e-SRD-Species.json", srd2024.Species)

    @property
    def subclasses(self) -> Collection[srd2024.Subclass]:
        return self._collection("5e-SRD-Subclasses.json", srd2024.Subclass)

    @property
    def subspecies(self) -> Collection[srd2024.Subspecies]:
        return self._collection("5e-SRD-Subspecies.json", srd2024.Subspecies)

    @property
    def traits(self) -> Collection[srd2024.Trait]:
        return self._collection("5e-SRD-Traits.json", srd2024.Trait)

    @property
    def weapon_mastery_properties(self) -> Collection[srd2024.WeaponMasteryProperty]:
        return self._collection(
            "5e-SRD-Weapon-Mastery-Properties.json", srd2024.WeaponMasteryProperty
        )

    @property
    def weapon_properties(self) -> Collection[srd2024.WeaponProperty]:
        return self._collection("5e-SRD-Weapon-Properties.json", srd2024.WeaponProperty)
