"""DB-IP database helpers."""

from __future__ import annotations

from collections.abc import Mapping
from dataclasses import dataclass
from pathlib import Path
from typing import Any

import maxminddb
from maxminddb.reader import Metadata


class InMemoryMMDB:
    """Simple wrapper around ``maxminddb`` that keeps data in RAM."""

    def __init__(self, path: Path) -> None:
        self.path = path
        self._reader: maxminddb.Reader | None = None
        self._metadata: Metadata | None = None
        self._open()

    def _open(self) -> None:
        if not self.path.exists():
            raise FileNotFoundError(f"Database not found: {self.path}")
        reader = maxminddb.open_database(str(self.path), mode=maxminddb.MODE_MEMORY)
        self._reader = reader
        self._metadata = reader.metadata()

    @property
    def metadata(self) -> Metadata:
        if self._metadata is None:
            raise RuntimeError("Database reader not initialised")
        return self._metadata

    def get(self, ip: str) -> Mapping[str, Any] | None:
        if self._reader is None:
            raise RuntimeError("Database reader not initialised")
        return self._reader.get(ip)

    def close(self) -> None:
        if self._reader is not None:
            self._reader.close()
            self._reader = None
            self._metadata = None


@dataclass(frozen=True)
class ContinentInfo:
    """Information about a continent extracted from the city dataset."""

    code: str | None
    geoname_id: int | None
    name: str | None

    @classmethod
    def from_record(cls, record: Mapping[str, Any] | None) -> "ContinentInfo | None":
        if record is None:
            return None
        continent = record.get("continent", {}) or {}
        names = continent.get("names", {}) or {}
        return cls(
            code=continent.get("code"),
            geoname_id=continent.get("geoname_id"),
            name=names.get("en"),
        )

    def to_dict(self) -> dict[str, Any]:
        return {
            "code": self.code,
            "geoname_id": self.geoname_id,
            "name": self.name,
        }


@dataclass(frozen=True)
class CountryInfo:
    """Structured information about a country lookup result."""

    iso_code: str | None
    name: str | None
    geoname_id: int | None
    is_in_european_union: bool | None

    @classmethod
    def from_record(cls, record: Mapping[str, Any] | None) -> "CountryInfo | None":
        if record is None:
            return None
        country = record.get("country", {}) or {}
        names = country.get("names", {}) or {}
        return cls(
            iso_code=country.get("iso_code"),
            name=names.get("en"),
            geoname_id=country.get("geoname_id"),
            is_in_european_union=country.get("is_in_european_union"),
        )

    def to_dict(self) -> dict[str, Any]:
        return {
            "iso_code": self.iso_code,
            "name": self.name,
            "geoname_id": self.geoname_id,
            "is_in_european_union": self.is_in_european_union,
        }


@dataclass(frozen=True)
class CityInfo:
    """Information about a city returned by the lite dataset."""

    name: str | None

    @classmethod
    def from_record(cls, record: Mapping[str, Any] | None) -> "CityInfo | None":
        if record is None:
            return None
        city = record.get("city", {}) or {}
        names = city.get("names", {}) or {}
        return cls(name=names.get("en"))

    def to_dict(self) -> dict[str, Any]:
        return {"name": self.name}


@dataclass(frozen=True)
class LocationInfo:
    """Geographical coordinates and related metadata."""

    latitude: float | None
    longitude: float | None
    time_zone: str | None
    accuracy_radius: int | None

    @classmethod
    def from_record(cls, record: Mapping[str, Any] | None) -> "LocationInfo | None":
        if record is None:
            return None
        location = record.get("location", {}) or {}
        return cls(
            latitude=location.get("latitude"),
            longitude=location.get("longitude"),
            time_zone=location.get("time_zone"),
            accuracy_radius=location.get("accuracy_radius"),
        )

    def to_dict(self) -> dict[str, Any]:
        return {
            "latitude": self.latitude,
            "longitude": self.longitude,
            "time_zone": self.time_zone,
            "accuracy_radius": self.accuracy_radius,
        }


@dataclass(frozen=True)
class SubdivisionInfo:
    """Administrative subdivision details."""

    name: str | None

    @classmethod
    def from_mapping(cls, mapping: Mapping[str, Any]) -> "SubdivisionInfo":
        names = mapping.get("names", {}) or {}
        return cls(name=names.get("en"))

    def to_dict(self) -> dict[str, Any]:
        return {"name": self.name}


@dataclass(frozen=True)
class ASNInfo:
    """Information about an autonomous system."""

    number: int | None
    organization: str | None

    @classmethod
    def from_record(cls, record: Mapping[str, Any] | None) -> "ASNInfo | None":
        if record is None:
            return None
        return cls(
            number=record.get("autonomous_system_number"),
            organization=record.get("autonomous_system_organization"),
        )

    def to_dict(self) -> dict[str, Any]:
        return {"number": self.number, "organization": self.organization}


@dataclass(frozen=True)
class CityDatasetRecord:
    """Structured view of a DB-IP city record."""

    city: CityInfo | None
    continent: ContinentInfo | None
    country: CountryInfo | None
    location: LocationInfo | None
    subdivisions: tuple[SubdivisionInfo, ...]

    @classmethod
    def from_record(cls, record: Mapping[str, Any] | None) -> "CityDatasetRecord | None":
        if record is None:
            return None
        subdivisions_field = record.get("subdivisions") or []
        subdivisions: list[SubdivisionInfo] = []
        for entry in subdivisions_field:
            subdivisions.append(SubdivisionInfo.from_mapping(entry))
        return cls(
            city=CityInfo.from_record(record),
            continent=ContinentInfo.from_record(record),
            country=CountryInfo.from_record(record),
            location=LocationInfo.from_record(record),
            subdivisions=tuple(subdivisions),
        )

    def to_dict(self) -> dict[str, Any]:
        return {
            "city": self.city.to_dict() if self.city else None,
            "continent": self.continent.to_dict() if self.continent else None,
            "country": self.country.to_dict() if self.country else None,
            "location": self.location.to_dict() if self.location else None,
            "subdivisions": [subdivision.to_dict() for subdivision in self.subdivisions],
        }


@dataclass(frozen=True)
class DBIPLookupResult:
    """Combined lookup result from the DB-IP datasets."""

    ip: str
    city: CityInfo | None
    continent: ContinentInfo | None
    country: CountryInfo | None
    location: LocationInfo | None
    subdivisions: tuple[SubdivisionInfo, ...]
    asn: ASNInfo | None

    def to_dict(self) -> dict[str, Any]:
        return {
            "ip": self.ip,
            "city": self.city.to_dict() if self.city else None,
            "continent": self.continent.to_dict() if self.continent else None,
            "country": self.country.to_dict() if self.country else None,
            "location": self.location.to_dict() if self.location else None,
            "subdivisions": [subdivision.to_dict() for subdivision in self.subdivisions],
            "asn": self.asn.to_dict() if self.asn else None,
        }


class DBIPCityDatabase(InMemoryMMDB):
    """Wrapper for the DB-IP city dataset exposing rich information."""

    def lookup(self, ip: str) -> CityDatasetRecord | None:
        record = self.get(ip)
        return CityDatasetRecord.from_record(record)


class DBIPASNDatabase(InMemoryMMDB):
    """Wrapper for the DB-IP ASN dataset."""

    def lookup(self, ip: str) -> ASNInfo | None:
        record = self.get(ip)
        return ASNInfo.from_record(record)


class DBIPResolver:
    """High level helper combining the DB-IP ASN and city datasets."""

    def __init__(self, database_dir: Path | None = None) -> None:
        if database_dir is None:
            database_dir = Path(__file__).resolve().parent
        self.database_dir = database_dir
        self.city_db = DBIPCityDatabase(database_dir / "dbip-city.mmdb")
        self.asn_db = DBIPASNDatabase(database_dir / "dbip-asn.mmdb")

    def lookup(self, ip: str) -> DBIPLookupResult:
        city_record = self.city_db.lookup(ip)
        asn = self.asn_db.lookup(ip)
        if city_record is None:
            city = None
            continent = None
            country = None
            location = None
            subdivisions: tuple[SubdivisionInfo, ...] = tuple()
        else:
            city = city_record.city
            continent = city_record.continent
            country = city_record.country
            location = city_record.location
            subdivisions = city_record.subdivisions
        return DBIPLookupResult(
            ip=ip,
            city=city,
            continent=continent,
            country=country,
            location=location,
            subdivisions=subdivisions,
            asn=asn,
        )

    def close(self) -> None:
        self.city_db.close()
        self.asn_db.close()
