from __future__ import annotations

import json
import os
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass
from typing import Any, Dict, Tuple

from src.errors import UserError

REVERSE_GEOCODE_BASE_URL_ENV_VAR = "REVERSE_GEOCODE_BASE_URL"
REVERSE_GEOCODE_API_KEY_ENV_VAR = "REVERSE_GEOCODE_API_KEY"
REVERSE_GEOCODE_ALLOW_INSECURE_HTTP_ENV_VAR = "REVERSE_GEOCODE_ALLOW_INSECURE_HTTP"

REVERSE_GEOCODE_LAT_PARAM_ENV_VAR = "REVERSE_GEOCODE_LAT_PARAM"
REVERSE_GEOCODE_LON_PARAM_ENV_VAR = "REVERSE_GEOCODE_LON_PARAM"
REVERSE_GEOCODE_KEY_PARAM_ENV_VAR = "REVERSE_GEOCODE_KEY_PARAM"
REVERSE_GEOCODE_PLACE_KEY_ENV_VAR = "REVERSE_GEOCODE_PLACE_KEY"
REVERSE_GEOCODE_REGION_KEY_ENV_VAR = "REVERSE_GEOCODE_REGION_KEY"


@dataclass(frozen=True)
class ReverseGeocodeClient:
    base_url: str
    api_key: str
    lat_param: str
    lon_param: str
    key_param: str
    place_key: str
    region_key: str

    def reverse_geocode(self, lat: float, lon: float) -> Tuple[str, str]:
        url = _build_url(
            self.base_url,
            params={
                self.lat_param: str(lat),
                self.lon_param: str(lon),
                self.key_param: self.api_key,
            },
        )
        payload = _get_json(url)
        place = payload.get(self.place_key)
        region = payload.get(self.region_key)
        if not place or not region:
            raise UserError("Reverse geocode response missing place/region.")
        return (str(place), str(region))


def load_reverse_geocode_client() -> ReverseGeocodeClient:
    base_url = os.environ.get(REVERSE_GEOCODE_BASE_URL_ENV_VAR, "").strip()
    if not base_url:
        raise UserError(
            f"Missing reverse geocode base URL. Set {REVERSE_GEOCODE_BASE_URL_ENV_VAR}."
        )

    parsed = urllib.parse.urlparse(base_url)
    if parsed.scheme not in {"https", "http"} or not parsed.netloc:
        raise UserError(f"Invalid {REVERSE_GEOCODE_BASE_URL_ENV_VAR}: {base_url!r}")
    if (
        parsed.scheme == "http"
        and os.environ.get(REVERSE_GEOCODE_ALLOW_INSECURE_HTTP_ENV_VAR) != "1"
    ):
        raise UserError(
            "Refusing insecure http reverse geocode URL. "
            f"Set {REVERSE_GEOCODE_ALLOW_INSECURE_HTTP_ENV_VAR}=1 to override."
        )

    api_key = os.environ.get(REVERSE_GEOCODE_API_KEY_ENV_VAR, "").strip()
    if not api_key:
        raise UserError(f"Missing reverse geocode API key. Set {REVERSE_GEOCODE_API_KEY_ENV_VAR}.")

    return ReverseGeocodeClient(
        base_url=base_url,
        api_key=api_key,
        lat_param=os.environ.get(REVERSE_GEOCODE_LAT_PARAM_ENV_VAR, "lat").strip() or "lat",
        lon_param=os.environ.get(REVERSE_GEOCODE_LON_PARAM_ENV_VAR, "lon").strip() or "lon",
        key_param=os.environ.get(REVERSE_GEOCODE_KEY_PARAM_ENV_VAR, "api_key").strip() or "api_key",
        place_key=os.environ.get(REVERSE_GEOCODE_PLACE_KEY_ENV_VAR, "place").strip() or "place",
        region_key=os.environ.get(REVERSE_GEOCODE_REGION_KEY_ENV_VAR, "region").strip() or "region",
    )


def _build_url(base_url: str, *, params: Dict[str, str]) -> str:
    parsed = urllib.parse.urlparse(base_url)
    existing = urllib.parse.parse_qsl(parsed.query, keep_blank_values=True)
    query = urllib.parse.urlencode(existing + list(params.items()))
    return urllib.parse.urlunparse(parsed._replace(query=query))


def _get_json(url: str) -> Dict[str, Any]:
    try:
        with urllib.request.urlopen(url, timeout=20) as resp:
            body = resp.read()
            payload = json.loads(body.decode("utf-8"))
    except urllib.error.HTTPError as exc:
        raise UserError(f"Reverse geocode HTTP error: {exc.code} {exc.reason}") from exc
    except urllib.error.URLError as exc:
        raise UserError(f"Reverse geocode network error: {exc.reason}") from exc
    except json.JSONDecodeError as exc:
        raise UserError(f"Reverse geocode invalid JSON: {exc.msg}") from exc

    if not isinstance(payload, dict):
        raise UserError("Reverse geocode response must be a JSON object.")
    return payload
