from __future__ import annotations

import re
import time
import unicodedata
from typing import Any

import httpx

from src.cache import TTLCache
from src.config import (
    COMMUNES_FIELDS,
    COMMUNES_LIMIT,
    COMMUNE_CACHE_TTL_SECONDS,
    GEO_API_URL,
    HTTP_TIMEOUT_SECONDS,
    MIN_SEARCH_LENGTH,
    CACHE_MAX_ENTRIES,
    GEO_API_PROVIDER,
    SLUG_WITH_DEPT_PATTERN,
)
from src.tracking_service import get_current_search_id, get_tracker


class CommuneValidationError(Exception):
    pass


class CommuneUpstreamError(Exception):
    pass


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

    async def search_communes(self, query: str) -> list[dict[str, Any]]:
        q = (query or "").strip()
        if len(q) < MIN_SEARCH_LENGTH:
            raise CommuneValidationError(
                "Le paramètre de recherche doit contenir au moins 2 caractères."
            )

        cache_key = f"communes:{q.lower()}"
        cached = self.cache.get(cache_key)
        if cached is not None:
            return cached

        params = {
            "nom": q,
            "fields": COMMUNES_FIELDS,
            "boost": "population",
            "limit": COMMUNES_LIMIT,
        }

        tracker = get_tracker()
        search_id = get_current_search_id()
        api_start = time.monotonic()
        try:
            response = await self.client.get(GEO_API_URL, params=params)
            response.raise_for_status()
            payload = response.json()
            api_duration = int((time.monotonic() - api_start) * 1000)
            if tracker:
                tracker.log_api_call(
                    search_id=search_id,
                    service="communes",
                    provider=GEO_API_PROVIDER,
                    endpoint=GEO_API_URL,
                    params_summary=f"nom={q},limit={COMMUNES_LIMIT}",
                    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)
            if tracker:
                status_code = getattr(getattr(exc, "response", None), "status_code", None)
                tracker.log_api_call(
                    search_id=search_id,
                    service="communes",
                    provider=GEO_API_PROVIDER,
                    endpoint=GEO_API_URL,
                    params_summary=f"nom={q},limit={COMMUNES_LIMIT}",
                    cache_key=cache_key,
                    cache_status="miss",
                    status_code=status_code,
                    duration_ms=api_duration,
                    success=False,
                    error_message=str(exc),
                )
            raise CommuneUpstreamError from exc

        communes = [self._normalize_commune(item) for item in payload]
        self.cache.set(cache_key, communes)
        return communes

    async def resolve_slug(self, slug_with_dept: str) -> dict[str, Any] | None:
        slug = (slug_with_dept or "").strip().lower()
        match = re.match(SLUG_WITH_DEPT_PATTERN, slug)
        if not match:
            return None

        name_slug, dept_code = match.groups()
        cache_key = f"slug:{name_slug}-{dept_code}"
        cached = self.slug_cache.get(cache_key)
        if cached is not None:
            return cached

        query = name_slug.replace("-", " ")
        try:
            candidates = await self.search_communes(query)
        except CommuneValidationError:
            return None

        for commune in candidates:
            if commune.get("departement") != dept_code:
                continue
            if self.generate_slug(str(commune.get("nom", ""))) != name_slug:
                continue

            resolved = {
                "nom": commune.get("nom", ""),
                "departement": commune.get("departement", ""),
                "latitude": commune.get("latitude"),
                "longitude": commune.get("longitude"),
                "slug": f"{name_slug}-{dept_code}",
                "population": commune.get("population"),
                "surface_km2": commune.get("surface_km2"),
                "departement_nom": commune.get("departement_nom"),
                "region_nom": commune.get("region_nom"),
            }
            self.slug_cache.set(cache_key, resolved)
            return resolved

        return None

    @staticmethod
    def generate_slug(name: str) -> str:
        value = (name or "").lower()
        value = unicodedata.normalize("NFD", value)
        value = re.sub(r"[\u0300-\u036f]", "", value)
        value = re.sub(r"[''ʼ]+", "", value)
        value = re.sub(r"\s+", "-", value)
        value = re.sub(r"[^a-z0-9-]", "", value)
        value = re.sub(r"-{2,}", "-", value)
        return value.strip("-")

    @staticmethod
    def _normalize_commune(item: dict[str, Any]) -> dict[str, Any]:
        centre = item.get("centre") or {}
        coordinates = centre.get("coordinates") or [None, None]
        lon = coordinates[0] if len(coordinates) > 0 else None
        lat = coordinates[1] if len(coordinates) > 1 else None
        surface_ha = item.get("surface")
        surface_km2 = round(surface_ha / 100, 1) if surface_ha else None
        dept_obj = item.get("departement") or {}
        region_obj = item.get("region") or {}

        return {
            "nom": item.get("nom", ""),
            "departement": item.get("codeDepartement", ""),
            "latitude": lat,
            "longitude": lon,
            "population": item.get("population"),
            "surface_km2": surface_km2,
            "departement_nom": dept_obj.get("nom"),
            "region_nom": region_obj.get("nom"),
        }
