from __future__ import annotations

import json
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from src.errors import UserError


@dataclass(frozen=True)
class ExifResult:
    path: Path
    datetime_utc: datetime
    datetime_source: str
    gps_lat: float | None
    gps_lon: float | None
    gps_source: str

    def to_dict(self) -> dict[str, object]:
        return {
            "path": str(self.path),
            "datetime_utc": self.datetime_utc.isoformat(),
            "datetime_source": self.datetime_source,
            "gps_lat": self.gps_lat,
            "gps_lon": self.gps_lon,
            "gps_source": self.gps_source,
        }

    def to_json(self) -> str:
        return json.dumps(self.to_dict(), indent=2, sort_keys=True)


def read_exif(path: Path) -> ExifResult:
    _validate_path(path)
    exif = _try_load_exif(path)
    dt, dt_source = _extract_datetime_utc(exif, path)
    lat, lon = _extract_gps_decimal(exif)
    gps_source = "exif" if (lat is not None and lon is not None) else "missing"
    return ExifResult(
        path=path,
        datetime_utc=dt,
        datetime_source=dt_source,
        gps_lat=lat,
        gps_lon=lon,
        gps_source=gps_source,
    )


def _validate_path(path: Path) -> None:
    if not path.exists():
        raise UserError(f"Path does not exist: {path}")
    if not path.is_file():
        raise UserError(f"Path is not a file: {path}")


def _try_load_exif(path: Path) -> dict[str, Any] | None:
    try:
        import piexif  # type: ignore[import-untyped]
    except ModuleNotFoundError as exc:
        raise UserError("Missing dependency: piexif (install via pip)") from exc

    try:
        return piexif.load(str(path))
    except piexif.InvalidImageDataError:
        return None
    except Exception as exc:
        raise UserError(f"Failed to read EXIF: {path} ({exc})") from exc


def _extract_datetime_utc(exif: dict[str, Any] | None, path: Path) -> tuple[datetime, str]:
    raw = None if exif is None else exif.get("Exif", {}).get(_exif_datetime_tag())
    if raw:
        try:
            dt = datetime.strptime(_decode_exif_ascii(raw), "%Y:%m:%d %H:%M:%S")
        except ValueError as exc:
            raise UserError(f"Invalid EXIF DateTimeOriginal: {raw!r} ({exc})") from exc
        return (dt.replace(tzinfo=timezone.utc), "exif")

    try:
        stat = path.stat()
    except OSError as exc:
        raise UserError(f"Failed to stat file: {path} ({exc})") from exc
    return (datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc), "mtime")


def _extract_gps_decimal(exif: dict[str, Any] | None) -> tuple[float | None, float | None]:
    if exif is None:
        return (None, None)

    gps = exif.get("GPS", {})
    lat_ref = gps.get(_gps_lat_ref_tag())
    lon_ref = gps.get(_gps_lon_ref_tag())
    lat = gps.get(_gps_lat_tag())
    lon = gps.get(_gps_lon_tag())
    if not (lat_ref and lon_ref and lat and lon):
        return (None, None)

    lat_deg = _gps_dms_to_decimal(lat)
    lon_deg = _gps_dms_to_decimal(lon)
    lat_sign = -1.0 if _decode_exif_ascii(lat_ref).upper() == "S" else 1.0
    lon_sign = -1.0 if _decode_exif_ascii(lon_ref).upper() == "W" else 1.0
    return (lat_deg * lat_sign, lon_deg * lon_sign)


def _gps_dms_to_decimal(dms: Any) -> float:
    if not isinstance(dms, tuple) or len(dms) != 3:
        raise UserError(f"Invalid GPS DMS tuple: {dms!r}")
    deg = _rat_to_float(dms[0])
    minutes = _rat_to_float(dms[1])
    seconds = _rat_to_float(dms[2])
    return deg + minutes / 60.0 + seconds / 3600.0


def _rat_to_float(rat: Any) -> float:
    if (
        not isinstance(rat, tuple)
        or len(rat) != 2
        or not isinstance(rat[0], int)
        or not isinstance(rat[1], int)
        or rat[1] == 0
    ):
        raise UserError(f"Invalid rational value: {rat!r}")
    return rat[0] / rat[1]


def _decode_exif_ascii(value: Any) -> str:
    if isinstance(value, bytes):
        return value.decode("ascii", errors="strict").strip("\x00").strip()
    if isinstance(value, str):
        return value.strip()
    raise UserError(f"Invalid EXIF value type: {type(value).__name__}")


def _exif_datetime_tag() -> int:
    import piexif

    return piexif.ExifIFD.DateTimeOriginal


def _gps_lat_ref_tag() -> int:
    import piexif

    return piexif.GPSIFD.GPSLatitudeRef


def _gps_lon_ref_tag() -> int:
    import piexif

    return piexif.GPSIFD.GPSLongitudeRef


def _gps_lat_tag() -> int:
    import piexif

    return piexif.GPSIFD.GPSLatitude


def _gps_lon_tag() -> int:
    import piexif

    return piexif.GPSIFD.GPSLongitude
