from __future__ import annotations

import json
import math
from dataclasses import asdict, dataclass
from typing import Iterable, List

from src.config.matching_config import MatchingConfig
from src.exif.exif import ExifResult
from src.pipedrive.mapping import NormalizedDeal


@dataclass(frozen=True)
class MatchResult:
    photo_path: str
    deal_id: int | None
    distance_m: float | None
    date_delta_days: int | None
    confidence: float
    match_reason: str
    tie_breaker_reason: str | None

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


def match_photo(
    photo: ExifResult, deals: List[NormalizedDeal], config: MatchingConfig
) -> MatchResult:
    if photo.gps_lat is None or photo.gps_lon is None:
        return MatchResult(
            photo_path=str(photo.path),
            deal_id=None,
            distance_m=None,
            date_delta_days=None,
            confidence=0.0,
            match_reason="missing_gps",
            tie_breaker_reason=None,
        )

    candidates = _candidate_matches(photo, deals, config)
    if not candidates:
        return MatchResult(
            photo_path=str(photo.path),
            deal_id=None,
            distance_m=None,
            date_delta_days=None,
            confidence=0.0,
            match_reason="no_candidates",
            tie_breaker_reason=None,
        )

    candidates.sort(key=lambda c: (c.distance_m, c.date_delta_days, c.deal.deal_id))
    best = candidates[0]
    tie_reason = _tie_breaker_reason(candidates)
    confidence = _confidence(best.distance_m, best.date_delta_days, config)
    return MatchResult(
        photo_path=str(photo.path),
        deal_id=best.deal.deal_id,
        distance_m=round(best.distance_m, 3),
        date_delta_days=best.date_delta_days,
        confidence=confidence,
        match_reason="matched",
        tie_breaker_reason=tie_reason,
    )


def match_photos(
    photos: Iterable[ExifResult], deals: List[NormalizedDeal], config: MatchingConfig
) -> List[MatchResult]:
    return [match_photo(p, deals, config) for p in photos]


@dataclass(frozen=True)
class _Candidate:
    deal: NormalizedDeal
    distance_m: float
    date_delta_days: int


def _candidate_matches(
    photo: ExifResult, deals: List[NormalizedDeal], config: MatchingConfig
) -> List[_Candidate]:
    photo_date = photo.datetime_utc.date()
    out: List[_Candidate] = []
    for deal in deals:
        delta_days = abs((photo_date - deal.event_date).days)
        if delta_days > config.date_window_days:
            continue
        dist = haversine_m(photo.gps_lat or 0.0, photo.gps_lon or 0.0, deal.lat, deal.lon)
        if dist > config.max_distance_m:
            continue
        out.append(_Candidate(deal=deal, distance_m=dist, date_delta_days=delta_days))
    return out


def _tie_breaker_reason(candidates: List[_Candidate]) -> str:
    if len(candidates) == 1:
        return "single_candidate"
    best, second = candidates[0], candidates[1]
    if best.distance_m != second.distance_m:
        return "distance"
    if best.date_delta_days != second.date_delta_days:
        return "date_delta_days"
    return "deal_id"


def _confidence(distance_m: float, date_delta_days: int, config: MatchingConfig) -> float:
    date_score = 1.0 - (date_delta_days / max(1, config.date_window_days))
    dist_score = 1.0 - (distance_m / max(1, config.max_distance_m))
    score = max(0.0, min(1.0, 0.5 * date_score + 0.5 * dist_score))
    return round(score, 6)


def haversine_m(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
    r = 6_371_000.0
    phi1 = math.radians(lat1)
    phi2 = math.radians(lat2)
    dphi = math.radians(lat2 - lat1)
    dlambda = math.radians(lon2 - lon1)

    a = math.sin(dphi / 2.0) ** 2 + math.cos(phi1) * math.cos(phi2) * math.sin(dlambda / 2.0) ** 2
    c = 2.0 * math.atan2(math.sqrt(a), math.sqrt(1.0 - a))
    return r * c
