from __future__ import annotations

import json
from dataclasses import asdict, dataclass
from typing import Callable, Dict, Iterable, Tuple

from src.errors import UserError
from src.pipedrive.mapping import NormalizedDeal

ReverseGeocodeFn = Callable[[float, float], Tuple[str, str]]


@dataclass(frozen=True)
class EnrichedDeal:
    deal_id: int
    event_date: str
    place: str
    region: str
    source: str

    def to_json(self) -> str:
        return json.dumps(asdict(self), ensure_ascii=True, sort_keys=True)


def needs_reverse_geocode(deals: Iterable[NormalizedDeal]) -> bool:
    for d in deals:
        if _normalize_location_text(d.place) is None or _normalize_location_text(d.region) is None:
            return True
    return False


def enrich_deals(
    deals: Iterable[NormalizedDeal], *, reverse_geocode: ReverseGeocodeFn | None
) -> list[EnrichedDeal]:
    cache: Dict[Tuple[float, float], Tuple[str, str]] = {}
    out: list[EnrichedDeal] = []

    for deal in deals:
        place = _normalize_location_text(deal.place)
        region = _normalize_location_text(deal.region)
        if place is not None and region is not None:
            out.append(_finalize(deal, place, region, "pipedrive"))
            continue

        place, region, source = _enrich_via_reverse_geocode(
            deal, place, region, reverse_geocode, cache
        )
        out.append(_finalize(deal, place, region, source))

    return out


def _enrich_via_reverse_geocode(
    deal: NormalizedDeal,
    place: str | None,
    region: str | None,
    reverse_geocode: ReverseGeocodeFn | None,
    cache: Dict[Tuple[float, float], Tuple[str, str]],
) -> tuple[str, str, str]:
    if reverse_geocode is None:
        raise UserError(
            "Reverse geocode required but not configured. "
            "Set REVERSE_GEOCODE_BASE_URL and REVERSE_GEOCODE_API_KEY."
        )

    key = (_round_coord(deal.lat), _round_coord(deal.lon))
    if key not in cache:
        cache[key] = reverse_geocode(deal.lat, deal.lon)
    g_place, g_region = cache[key]

    g_place_n = _normalize_location_text(g_place)
    g_region_n = _normalize_location_text(g_region)
    place_final = place or g_place_n
    region_final = region or g_region_n
    if place_final is None or region_final is None:
        raise UserError(f"Reverse geocode returned missing place/region for deal {deal.deal_id}.")

    source = "reverse_geocode" if place is None and region is None else "mixed"
    return (place_final, region_final, source)


def _finalize(deal: NormalizedDeal, place: str, region: str, source: str) -> EnrichedDeal:
    return EnrichedDeal(
        deal_id=deal.deal_id,
        event_date=deal.event_date.isoformat(),
        place=place,
        region=region,
        source=source,
    )


def _normalize_location_text(value: object | None) -> str | None:
    if value is None:
        return None
    text = str(value).strip().strip(",").strip()
    if not text:
        return None
    return " ".join(text.split())


def _round_coord(v: float) -> float:
    return round(float(v), 6)
