from __future__ import annotations

from collections import Counter, defaultdict
from datetime import date, datetime
import time
from typing import Any

import httpx

from src.cache import TTLCache
from src.config import (
    CACHE_MAX_ENTRIES,
    HTTP_TIMEOUT_SECONDS,
    MAX_PERIOD_DAYS,
    MIN_HISTORICAL_DATE,
    OPEN_METEO_ARCHIVE_URL,
    TIMEZONE,
    WEATHER_CACHE_TTL_SECONDS,
    max_available_date,
)
from src.tracking_service import get_current_search_id, get_tracker

WMO_DESCRIPTIONS: dict[int, tuple[str, str]] = {
    0: ("☀️", "Ciel dégagé"),
    1: ("🌤️", "Principalement dégagé"),
    2: ("⛅", "Partiellement nuageux"),
    3: ("☁️", "Couvert"),
    45: ("🌫️", "Brouillard"),
    48: ("🌫️", "Brouillard givrant"),
    51: ("🌦️", "Bruine légère"),
    53: ("🌦️", "Bruine modérée"),
    55: ("🌧️", "Bruine forte"),
    56: ("🌧️", "Bruine verglaçante légère"),
    57: ("🌧️", "Bruine verglaçante forte"),
    61: ("🌧️", "Pluie légère"),
    63: ("🌧️", "Pluie modérée"),
    65: ("🌧️", "Pluie forte"),
    66: ("🌧️", "Pluie verglaçante légère"),
    67: ("🌧️", "Pluie verglaçante forte"),
    71: ("🌨️", "Chute de neige légère"),
    73: ("🌨️", "Chute de neige modérée"),
    75: ("🌨️", "Chute de neige forte"),
    77: ("🌨️", "Grains de neige"),
    80: ("🌦️", "Averses légères"),
    81: ("🌧️", "Averses modérées"),
    82: ("🌧️", "Averses violentes"),
    85: ("🌨️", "Averses de neige légères"),
    86: ("🌨️", "Averses de neige fortes"),
    95: ("⛈️", "Orage"),
    96: ("⛈️", "Orage avec grêle légère"),
    99: ("⛈️", "Orage avec grêle forte"),
}

DEFAULT_WMO = ("❓", "Conditions non disponibles")


class WeatherValidationError(Exception):
    pass


class WeatherUpstreamError(Exception):
    pass


class WeatherService:
    def __init__(self, client: httpx.AsyncClient | None = None) -> None:
        self.client = client or httpx.AsyncClient(timeout=HTTP_TIMEOUT_SECONDS)
        self.cache: TTLCache[dict[str, list[dict[str, Any]]]] = TTLCache(
            ttl_seconds=WEATHER_CACHE_TTL_SECONDS,
            max_entries=CACHE_MAX_ENTRIES,
        )

    async def get_weather(
        self,
        latitude: float,
        longitude: float,
        start: str,
        end: str,
    ) -> dict[str, list[dict[str, Any]]]:
        self._validate_coordinates(latitude, longitude)
        start_date, end_date = self._validate_dates(start, end)

        cache_key = f"weather:{latitude:.2f}:{longitude:.2f}:{start}:{end}"
        cached = self.cache.get(cache_key)
        if cached is not None:
            return cached

        params = {
            "latitude": latitude,
            "longitude": longitude,
            "start_date": start_date.isoformat(),
            "end_date": end_date.isoformat(),
            "hourly": "temperature_2m,precipitation,relative_humidity_2m,wind_speed_10m,weather_code",
            "timezone": TIMEZONE,
        }

        api_start = time.monotonic()
        try:
            response = await self.client.get(OPEN_METEO_ARCHIVE_URL, params=params)
            response.raise_for_status()
            payload = response.json()

            api_duration = int((time.monotonic() - api_start) * 1000)
            tracker = get_tracker()
            search_id = get_current_search_id()
            if tracker:
                tracker.log_api_call(
                    search_id=search_id,
                    service="weather",
                    provider="open-meteo",
                    endpoint=OPEN_METEO_ARCHIVE_URL,
                    params_summary=f"lat={latitude:.2f},lon={longitude:.2f},{start}/{end}",
                    cache_key=cache_key,
                    cache_status="miss",
                    status_code=response.status_code,
                    duration_ms=api_duration,
                    success=True,
                    error_message=None,
                )
        except (httpx.TimeoutException, httpx.HTTPError, ValueError) as exc:
            api_duration = int((time.monotonic() - api_start) * 1000)
            tracker = get_tracker()
            search_id = get_current_search_id()
            if tracker:
                status_code = getattr(getattr(exc, "response", None), "status_code", None)
                tracker.log_api_call(
                    search_id=search_id,
                    service="weather",
                    provider="open-meteo",
                    endpoint=OPEN_METEO_ARCHIVE_URL,
                    params_summary=f"lat={latitude:.2f},lon={longitude:.2f},{start}/{end}",
                    cache_key=cache_key,
                    cache_status="miss",
                    status_code=status_code,
                    duration_ms=api_duration,
                    success=False,
                    error_message=str(exc),
                )
            raise WeatherUpstreamError from exc

        hourly_data = self._normalize_payload(payload)
        daily_summary = self._compute_daily_summary(hourly_data)
        response_data = [self._public_row(row) for row in hourly_data]
        result = {"data": response_data, "daily_summary": daily_summary}
        self.cache.set(cache_key, result)
        return result

    async def get_weather_for_prefetch(
        self,
        latitude: float,
        longitude: float,
        start: str,
        end: str,
    ) -> dict[str, list[dict[str, Any]]]:
        return await self.get_weather(latitude, longitude, start, end)

    @staticmethod
    def _validate_coordinates(latitude: float, longitude: float) -> None:
        if latitude < -90 or latitude > 90:
            raise WeatherValidationError("La latitude doit être comprise entre -90 et 90.")
        if longitude < -180 or longitude > 180:
            raise WeatherValidationError("La longitude doit être comprise entre -180 et 180.")

    @staticmethod
    def _parse_date(date_value: str) -> date:
        try:
            return datetime.strptime(date_value, "%Y-%m-%d").date()
        except ValueError as exc:
            raise WeatherValidationError("Le format de date doit être YYYY-MM-DD.") from exc

    def _validate_dates(self, start: str, end: str) -> tuple[date, date]:
        start_date = self._parse_date(start)
        end_date = self._parse_date(end)

        if start_date < MIN_HISTORICAL_DATE:
            raise WeatherValidationError(
                "La date de début ne peut pas être antérieure au 1er janvier 1940."
            )

        if end_date < start_date:
            raise WeatherValidationError(
                "La date de fin doit être postérieure ou égale à la date de début."
            )

        if (end_date - start_date).days > MAX_PERIOD_DAYS:
            raise WeatherValidationError("La période est limitée à 31 jours maximum.")

        latest = max_available_date()
        if start_date > latest or end_date > latest:
            raise WeatherValidationError("Seules les dates passées sont disponibles.")

        return start_date, end_date

    @staticmethod
    def _to_float_or_none(value: Any, precision: int) -> float | None:
        if value is None:
            return None
        return round(float(value), precision)

    @staticmethod
    def _to_int_or_none(value: Any) -> int | None:
        if value is None:
            return None
        return int(value)

    @staticmethod
    def _public_row(row: dict[str, Any]) -> dict[str, Any]:
        return {
            "time": row["time"],
            "temperature": row["temperature"],
            "precipitation": row["precipitation"],
            "humidity": row["humidity"],
            "wind_speed": row["wind_speed"],
            "icon": row["icon"],
            "description": row["description"],
        }

    def _normalize_payload(self, payload: dict[str, Any]) -> list[dict[str, Any]]:
        hourly = payload.get("hourly") or {}
        times = hourly.get("time") or []
        temperatures = hourly.get("temperature_2m") or []
        precipitations = hourly.get("precipitation") or []
        humidities = hourly.get("relative_humidity_2m") or []
        wind_speeds = hourly.get("wind_speed_10m") or []
        weather_codes = hourly.get("weather_code") or []

        rows = min(
            len(times),
            len(temperatures),
            len(precipitations),
            len(humidities),
            len(wind_speeds),
            len(weather_codes),
        )

        result: list[dict[str, Any]] = []
        for idx in range(rows):
            code = weather_codes[idx]
            icon, description = WMO_DESCRIPTIONS.get(code, DEFAULT_WMO)

            temperature = self._to_float_or_none(temperatures[idx], 1)
            precipitation = self._to_float_or_none(precipitations[idx], 1)
            humidity = self._to_int_or_none(humidities[idx])
            wind_speed = self._to_float_or_none(wind_speeds[idx], 1)

            result.append(
                {
                    "time": times[idx],
                    "temperature": temperature,
                    "precipitation": precipitation,
                    "humidity": humidity,
                    "wind_speed": wind_speed,
                    "weather_code": code,
                    "icon": icon,
                    "description": description,
                }
            )

        return result

    def _compute_daily_summary(
        self, hourly_data: list[dict[str, Any]]
    ) -> list[dict[str, Any]]:
        grouped_by_day: dict[str, list[dict[str, Any]]] = defaultdict(list)
        for row in hourly_data:
            date_value = row["time"].split("T", 1)[0]
            grouped_by_day[date_value].append(row)

        summaries: list[dict[str, Any]] = []
        for date_value in sorted(grouped_by_day.keys()):
            day_rows = grouped_by_day[date_value]

            temperatures = [row["temperature"] for row in day_rows if row["temperature"] is not None]
            precipitations = [row["precipitation"] for row in day_rows if row["precipitation"] is not None]
            humidities = [row["humidity"] for row in day_rows if row["humidity"] is not None]
            wind_speeds = [row["wind_speed"] for row in day_rows if row["wind_speed"] is not None]

            wmo_counts: Counter[int] = Counter(
                row["weather_code"] for row in day_rows if row["weather_code"] is not None
            )
            if wmo_counts:
                dominant_code = max(wmo_counts.items(), key=lambda item: (item[1], item[0]))[0]
                icon, description = WMO_DESCRIPTIONS.get(dominant_code, DEFAULT_WMO)
            else:
                icon, description = DEFAULT_WMO

            humidity_avg = (
                round(sum(humidities) / len(humidities)) if humidities else None
            )
            wind_speed_avg = (
                round(sum(wind_speeds) / len(wind_speeds), 1) if wind_speeds else None
            )

            summaries.append(
                {
                    "date": date_value,
                    "temp_min": min(temperatures) if temperatures else None,
                    "temp_max": max(temperatures) if temperatures else None,
                    "precipitation_sum": round(sum(precipitations), 1)
                    if precipitations
                    else None,
                    "humidity_avg": humidity_avg,
                    "wind_speed_avg": wind_speed_avg,
                    "icon": icon,
                    "description": description,
                }
            )

        return summaries
