from __future__ import annotations

from calendar import monthrange
from contextlib import asynccontextmanager
from datetime import date
from html import escape
import logging
from pathlib import Path
import json as json_module
import re
import secrets
import time
from uuid import uuid4

from fastapi import APIRouter, Depends, FastAPI, HTTPException, Path as FastAPIPath, Query, Request
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse, Response
from fastapi.staticfiles import StaticFiles

from src.commune_service import CommuneService, CommuneUpstreamError, CommuneValidationError
from src.config import (
    ADMIN_PASSWORD,
    ADMIN_USERNAME,
    CACHE_MAX_ENTRIES,
    COMMUNE_CACHE_TTL_SECONDS,
    ENVIRONMENT,
    ISO_DATE_PATTERN,
    MIN_HISTORICAL_DATE,
    MONTH_NAMES_FR,
    NORMALS_CACHE_MAX_ENTRIES,
    NORMALS_CACHE_TTL_SECONDS,
    OG_IMAGE_CACHE_MAX_ENTRIES,
    OG_IMAGE_CACHE_TTL_SECONDS,
    SLUG_WITH_DEPT_PATTERN,
    TRACKING_DB_PATH,
    WEATHER_CACHE_TTL_SECONDS,
    max_available_date,
)
from src.normals_service import NormalsService, NormalsUpstreamError, NormalsValidationError
from src.og_service import OGImageService, aggregate_for_og, format_og_date_range
from src.prefetch_service import prefetch_month, prefetch_period, prefetch_town
from src.tracking_service import (
    TrackingService,
    get_tracker,
    set_current_search_id,
    set_tracker,
)
from src.weather_service import WeatherService, WeatherUpstreamError, WeatherValidationError

logger = logging.getLogger(__name__)

commune_service = CommuneService()
weather_service = WeatherService()
normals_service = NormalsService()
og_service = OGImageService()
security = HTTPBasic(auto_error=False)


@asynccontextmanager
async def lifespan(_app: FastAPI):
    global _html_template
    _html_template = index_file.read_text(encoding="utf-8")
    tracker = TrackingService(TRACKING_DB_PATH)
    set_tracker(tracker)
    yield
    tracker.close()
    set_tracker(None)
    await commune_service.client.aclose()
    await weather_service.client.aclose()
    await normals_service.client.aclose()


app = FastAPI(title="HistoMétéo", lifespan=lifespan)
public_dir = Path(__file__).resolve().parent.parent / "public"
index_file = public_dir / "index.html"
admin_dir = Path(__file__).resolve().parent.parent / "admin"
_html_template = ""
_UUID_V4_PATTERN = re.compile(
    r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
    re.IGNORECASE,
)


def verify_admin(
    credentials: HTTPBasicCredentials | None = Depends(security),
) -> bool:
    if not ADMIN_PASSWORD:
        raise HTTPException(status_code=503, detail="Admin non configuré")

    if credentials is None:
        raise HTTPException(
            status_code=401,
            detail="Authentification requise",
            headers={"WWW-Authenticate": 'Basic realm="HistoMeteo Admin"'},
        )

    password_ok = secrets.compare_digest(credentials.password, ADMIN_PASSWORD)
    username_ok = secrets.compare_digest(credentials.username, ADMIN_USERNAME)
    if not (password_ok and username_ok):
        raise HTTPException(
            status_code=401,
            detail="Identifiants invalides",
            headers={"WWW-Authenticate": 'Basic realm="HistoMeteo Admin"'},
        )

    return True


@app.middleware("http")
async def admin_noindex_middleware(request: Request, call_next):
    response = await call_next(request)
    if request.url.path == "/admin" or request.url.path.startswith("/admin/"):
        response.headers["X-Robots-Tag"] = "noindex"
    return response


admin_router = APIRouter(prefix="/admin", dependencies=[Depends(verify_admin)])


def _get_html_template() -> str:
    global _html_template
    if not _html_template:
        _html_template = index_file.read_text(encoding="utf-8")
    return _html_template


def _inject_og_tags(template: str, og_values: dict[str, str]) -> str:
    html = template
    for key, value in og_values.items():
        escaped_value = escape(value, quote=True)
        if key.startswith("twitter:"):
            html = re.sub(
                rf'<meta name="{re.escape(key)}" content="[^"]*"',
                f'<meta name="{key}" content="{escaped_value}"',
                html,
                count=1,
            )
        elif key == "canonical":
            html = re.sub(
                r'<link rel="canonical" href="[^"]*"',
                f'<link rel="canonical" href="{escaped_value}"',
                html,
                count=1,
            )
        else:
            html = re.sub(
                rf'<meta property="{re.escape(key)}" content="[^"]*"',
                f'<meta property="{key}" content="{escaped_value}"',
                html,
                count=1,
            )
    return html


def _inject_prefetched_data(html: str, data: dict | None) -> str:
    if data is None:
        return html

    json_str = json_module.dumps(data, ensure_ascii=False)
    json_str = json_str.replace("</", "<\\/")
    script_tag = (
        '<script id="prefetched-data" type="application/json">'
        f"{json_str}"
        "</script>"
    )
    return html.replace("</head>", f"{script_tag}\n</head>", 1)


def _commune_name_from_slug(slug_dept: str) -> tuple[str, str]:
    match = re.match(SLUG_WITH_DEPT_PATTERN, slug_dept)
    if not match:
        return slug_dept, ""

    name_part, dept = match.groups()
    commune_name = " ".join(word.capitalize() for word in name_part.split("-"))
    return commune_name, dept


def _validate_search_id(value: str | None) -> str | None:
    if value and _UUID_V4_PATTERN.match(value):
        return value
    return None


async def _resolve_commune_name(slug_dept: str) -> str:
    fallback_name, _ = _commune_name_from_slug(slug_dept)
    try:
        commune = await commune_service.resolve_slug(slug_dept)
        if commune and commune.get("nom"):
            return str(commune["nom"])
    except Exception:
        return fallback_name
    return fallback_name


@app.get("/api/communes")
async def get_communes(q: str | None = None) -> JSONResponse:
    if q is None:
        return JSONResponse(
            status_code=400,
            content={
                "error": "Le paramètre de recherche doit contenir au moins 2 caractères."
            },
        )

    try:
        communes = await commune_service.search_communes(q)
        return JSONResponse(status_code=200, content=communes)
    except CommuneValidationError as exc:
        return JSONResponse(status_code=400, content={"error": str(exc)})
    except CommuneUpstreamError:
        return JSONResponse(
            status_code=502,
            content={
                "error": "Le service de recherche de communes est temporairement indisponible. Réessayez dans quelques instants."
            },
        )


@app.get("/api/weather")
async def get_weather(
    lat: float | None = None,
    lon: float | None = None,
    start: str | None = None,
    end: str | None = None,
    commune: str | None = None,
    slug: str | None = None,
    search_id: str | None = Query(default=None, alias="search_id"),
) -> JSONResponse:
    if lat is None or lon is None or start is None or end is None:
        return JSONResponse(
            status_code=400,
            content={"error": "Tous les paramètres lat, lon, start et end sont requis."},
        )

    validated_id = _validate_search_id(search_id)
    sid = validated_id or str(uuid4())
    set_current_search_id(sid)
    start_monotonic = time.monotonic()
    tracker = get_tracker()
    if tracker:
        tracker.create_search(
            search_id=sid,
            environment=ENVIRONMENT,
            source="api",
            search_type="period",
            commune_1=commune,
            commune_1_slug=slug,
            latitude=lat,
            longitude=lon,
            start_date=start,
            end_date=end,
        )

    try:
        data = await weather_service.get_weather(lat, lon, start, end)
        if tracker:
            duration_ms = int((time.monotonic() - start_monotonic) * 1000)
            tracker.complete_search(
                search_id=sid,
                status="success",
                duration_ms=duration_ms,
            )
        return JSONResponse(status_code=200, content=data)
    except WeatherValidationError as exc:
        if tracker:
            duration_ms = int((time.monotonic() - start_monotonic) * 1000)
            tracker.complete_search(
                search_id=sid,
                status="error",
                duration_ms=duration_ms,
                error_message=str(exc),
            )
        return JSONResponse(status_code=400, content={"error": str(exc)})
    except WeatherUpstreamError:
        if tracker:
            duration_ms = int((time.monotonic() - start_monotonic) * 1000)
            tracker.complete_search(
                search_id=sid,
                status="error",
                duration_ms=duration_ms,
                error_message="weather upstream error",
            )
        return JSONResponse(
            status_code=502,
            content={
                "error": "Le service de données météo est temporairement indisponible. Réessayez dans quelques instants."
            },
        )
    finally:
        set_current_search_id(None)


@app.get("/api/normals")
async def get_normals(
    lat: float | None = None,
    lon: float | None = None,
    start: str | None = None,
    end: str | None = None,
    search_id: str | None = Query(default=None, alias="search_id"),
) -> JSONResponse:
    if lat is None or lon is None or start is None or end is None:
        return JSONResponse(
            status_code=400,
            content={"error": "Tous les paramètres lat, lon, start et end sont requis."},
        )

    validated_id = _validate_search_id(search_id)
    if validated_id:
        set_current_search_id(validated_id)
    try:
        data = await normals_service.get_normals(lat, lon, start, end)
        return JSONResponse(status_code=200, content=data)
    except NormalsValidationError as exc:
        return JSONResponse(status_code=400, content={"error": str(exc)})
    except NormalsUpstreamError:
        return JSONResponse(
            status_code=502,
            content={
                "error": "Le service de normales climatiques est temporairement indisponible."
            },
        )
    finally:
        set_current_search_id(None)


@app.get("/api/normals/annual")
async def get_annual_normals(
    lat: float | None = None,
    lon: float | None = None,
    search_id: str | None = Query(default=None, alias="search_id"),
) -> JSONResponse:
    if lat is None or lon is None:
        return JSONResponse(
            status_code=400,
            content={"error": "Tous les paramètres lat et lon sont requis."},
        )

    validated_id = _validate_search_id(search_id)
    if validated_id:
        set_current_search_id(validated_id)
    try:
        data = await normals_service.get_annual_normals(lat, lon)
        return JSONResponse(status_code=200, content=data)
    except NormalsValidationError as exc:
        return JSONResponse(status_code=400, content={"error": str(exc)})
    except NormalsUpstreamError:
        return JSONResponse(
            status_code=502,
            content={
                "error": "Le service de normales climatiques est temporairement indisponible."
            },
        )
    finally:
        set_current_search_id(None)


@app.get("/api/resolve/{slug}")
async def resolve_commune_slug(slug: str) -> JSONResponse:
    try:
        commune = await commune_service.resolve_slug(slug)
    except CommuneUpstreamError:
        return JSONResponse(
            status_code=502,
            content={
                "error": "Le service de recherche de communes est temporairement indisponible. Réessayez dans quelques instants."
            },
        )

    if commune is None:
        return JSONResponse(status_code=404, content={"error": "Commune introuvable."})

    return JSONResponse(status_code=200, content=commune)


@app.get("/ville/{slug_dept}")
async def seo_ville_page(
    request: Request,
    slug_dept: str,
) -> Response:
    search_id = str(uuid4())
    set_current_search_id(search_id)
    start_monotonic = time.monotonic()
    tracker = get_tracker()
    try:
        commune_name = await _resolve_commune_name(slug_dept)
        if tracker:
            tracker.create_search(
                search_id=search_id,
                environment=ENVIRONMENT,
                source="seo",
                search_type="town",
                commune_1=commune_name,
                commune_1_slug=slug_dept,
            )

        canonical_path = f"/ville/{slug_dept}"
        base_url = str(request.base_url).rstrip("/")
        og_image = f"{base_url}/assets/logo-histometeo.png"
        og_title = f"Météo et climat à {commune_name}"
        og_description = (
            f"Consultez l'historique météo et le climat à {commune_name}. "
            "Retrouvez les températures, précipitations et conditions météo passées "
            "pour chaque période de l'année."
        )

        html = _inject_og_tags(
            _get_html_template(),
            {
                "og:title": og_title,
                "og:description": og_description,
                "og:image": og_image,
                "og:url": f"{base_url}{canonical_path}",
                "og:type": "website",
                "twitter:card": "summary_large_image",
                "twitter:title": og_title,
                "twitter:description": og_description,
                "twitter:image": og_image,
                "canonical": canonical_path,
            },
        )
        prefetched = await prefetch_town(commune_service, normals_service, slug_dept)
        html = _inject_prefetched_data(html, prefetched)
        if tracker:
            duration_ms = int((time.monotonic() - start_monotonic) * 1000)
            tracker.complete_search(
                search_id=search_id,
                status="success",
                duration_ms=duration_ms,
            )
        return Response(content=html, media_type="text/html")
    except Exception as exc:
        if tracker:
            duration_ms = int((time.monotonic() - start_monotonic) * 1000)
            tracker.complete_search(
                search_id=search_id,
                status="error",
                duration_ms=duration_ms,
                error_message=str(exc),
            )
        raise
    finally:
        set_current_search_id(None)


@app.get("/meteo/{slug_dept}/{year:int}/{month:int}")
async def seo_month_page(
    request: Request,
    slug_dept: str,
    year: int,
    month: int,
) -> Response:
    today_limit = max_available_date()
    if (
        month < 1
        or month > 12
        or year < MIN_HISTORICAL_DATE.year
        or year > today_limit.year
    ):
        return Response(content=_get_html_template(), media_type="text/html")

    first_day = date(year, month, 1)
    if first_day > today_limit:
        return Response(content=_get_html_template(), media_type="text/html")

    _, days_in_month = monthrange(year, month)
    last_day = date(year, month, days_in_month)
    if last_day > today_limit:
        last_day = today_limit

    if last_day < first_day:
        return Response(content=_get_html_template(), media_type="text/html")

    search_id = str(uuid4())
    set_current_search_id(search_id)
    start_monotonic = time.monotonic()
    tracker = get_tracker()

    commune_name = await _resolve_commune_name(slug_dept)
    try:
        if tracker:
            tracker.create_search(
                search_id=search_id,
                environment=ENVIRONMENT,
                source="seo",
                search_type="month",
                commune_1=commune_name,
                commune_1_slug=slug_dept,
                start_date=first_day.isoformat(),
                end_date=last_day.isoformat(),
            )

        month_name = MONTH_NAMES_FR[month]
        canonical_path = f"/meteo/{slug_dept}/{year}/{month:02d}"
        base_url = str(request.base_url).rstrip("/")
        og_image = (
            f"{base_url}/api/og-image/{slug_dept}/"
            f"{first_day.isoformat()}/{last_day.isoformat()}"
        )
        og_title = f"Météo à {commune_name} en {month_name} {year}"
        og_description = (
            f"Historique météo à {commune_name} pour le mois de {month_name} {year}\u00a0: "
            "températures, précipitations et conditions météo jour par jour."
        )

        html = _inject_og_tags(
            _get_html_template(),
            {
                "og:title": og_title,
                "og:description": og_description,
                "og:image": og_image,
                "og:url": f"{base_url}{canonical_path}",
                "og:type": "website",
                "twitter:card": "summary_large_image",
                "twitter:title": og_title,
                "twitter:description": og_description,
                "twitter:image": og_image,
                "canonical": canonical_path,
            },
        )
        prefetched = await prefetch_month(
            commune_service,
            weather_service,
            normals_service,
            slug_dept,
            first_day.isoformat(),
            last_day.isoformat(),
            year,
            month,
        )
        html = _inject_prefetched_data(html, prefetched)
        if tracker:
            duration_ms = int((time.monotonic() - start_monotonic) * 1000)
            tracker.complete_search(
                search_id=search_id,
                status="success",
                duration_ms=duration_ms,
            )
        return Response(content=html, media_type="text/html")
    except Exception as exc:
        if tracker:
            duration_ms = int((time.monotonic() - start_monotonic) * 1000)
            tracker.complete_search(
                search_id=search_id,
                status="error",
                duration_ms=duration_ms,
                error_message=str(exc),
            )
        raise
    finally:
        set_current_search_id(None)


@app.get("/meteo/{slug_dept}/{start}/{end}")
async def seo_meteo_page(
    request: Request,
    slug_dept: str,
    start: str = FastAPIPath(pattern=ISO_DATE_PATTERN),
    end: str = FastAPIPath(pattern=ISO_DATE_PATTERN),
) -> Response:
    search_id = str(uuid4())
    set_current_search_id(search_id)
    start_monotonic = time.monotonic()
    tracker = get_tracker()

    try:
        commune_name, _ = _commune_name_from_slug(slug_dept)
        if tracker:
            tracker.create_search(
                search_id=search_id,
                environment=ENVIRONMENT,
                source="seo",
                search_type="period",
                commune_1=commune_name,
                commune_1_slug=slug_dept,
                start_date=start,
                end_date=end,
            )

        canonical_path = f"/meteo/{slug_dept}/{start}/{end}"
        base_url = str(request.base_url).rstrip("/")
        og_image = f"{base_url}/api/og-image/{slug_dept}/{start}/{end}"

        date_range = format_og_date_range(start, end)
        og_title = f"Météo à {commune_name} {date_range}"
        og_description = (
            f"Consultez la météo passée à {commune_name}\u00a0: "
            "températures, pluie, vent et conditions heure par heure."
        )

        html = _inject_og_tags(
            _get_html_template(),
            {
                "og:title": og_title,
                "og:description": og_description,
                "og:image": og_image,
                "og:url": f"{base_url}{canonical_path}",
                "og:type": "website",
                "twitter:card": "summary_large_image",
                "twitter:title": og_title,
                "twitter:description": og_description,
                "twitter:image": og_image,
                "canonical": canonical_path,
            },
        )
        prefetched = await prefetch_period(
            commune_service,
            weather_service,
            normals_service,
            slug_dept,
            start,
            end,
        )
        html = _inject_prefetched_data(html, prefetched)
        if tracker:
            duration_ms = int((time.monotonic() - start_monotonic) * 1000)
            tracker.complete_search(
                search_id=search_id,
                status="success",
                duration_ms=duration_ms,
            )
        return Response(content=html, media_type="text/html")
    except Exception as exc:
        if tracker:
            duration_ms = int((time.monotonic() - start_monotonic) * 1000)
            tracker.complete_search(
                search_id=search_id,
                status="error",
                duration_ms=duration_ms,
                error_message=str(exc),
            )
        raise
    finally:
        set_current_search_id(None)


@app.get("/comparaison/{slug1_dept}/vs/{slug2_dept}/{start}/{end}")
async def seo_comparison_page(
    slug1_dept: str,
    slug2_dept: str,
    start: str = FastAPIPath(pattern=ISO_DATE_PATTERN),
    end: str = FastAPIPath(pattern=ISO_DATE_PATTERN),
) -> Response:
    search_id = str(uuid4())
    set_current_search_id(search_id)
    start_monotonic = time.monotonic()
    tracker = get_tracker()
    commune_1, _ = _commune_name_from_slug(slug1_dept)
    commune_2, _ = _commune_name_from_slug(slug2_dept)

    try:
        if tracker:
            tracker.create_search(
                search_id=search_id,
                environment=ENVIRONMENT,
                source="seo",
                search_type="comparison",
                commune_1=commune_1,
                commune_1_slug=slug1_dept,
                commune_2=commune_2,
                commune_2_slug=slug2_dept,
                start_date=start,
                end_date=end,
            )
            duration_ms = int((time.monotonic() - start_monotonic) * 1000)
            tracker.complete_search(
                search_id=search_id,
                status="success",
                duration_ms=duration_ms,
            )
        return Response(content=_get_html_template(), media_type="text/html")
    except Exception as exc:
        if tracker:
            duration_ms = int((time.monotonic() - start_monotonic) * 1000)
            tracker.complete_search(
                search_id=search_id,
                status="error",
                duration_ms=duration_ms,
                error_message=str(exc),
            )
        raise
    finally:
        set_current_search_id(None)


@app.get("/api/og-image/{slug_dept}/{start}/{end}")
async def get_og_image(
    slug_dept: str,
    start: str = FastAPIPath(pattern=ISO_DATE_PATTERN),
    end: str = FastAPIPath(pattern=ISO_DATE_PATTERN),
) -> Response:
    commune_name, _ = _commune_name_from_slug(slug_dept)
    temp_min = None
    temp_max = None
    dominant_desc = None
    dominant_icon = None

    try:
        commune = await commune_service.resolve_slug(slug_dept)
        if commune:
            latitude = commune.get("latitude")
            longitude = commune.get("longitude")
            if latitude is not None and longitude is not None:
                weather = await weather_service.get_weather(latitude, longitude, start, end)
                og_agg = aggregate_for_og(weather.get("daily_summary", []))
                temp_min = og_agg["temp_min"]
                temp_max = og_agg["temp_max"]
                dominant_desc = og_agg["dominant_desc"]
                dominant_icon = og_agg["dominant_icon"]
            commune_name = commune.get("nom", commune_name)
    except Exception:
        pass

    image_bytes = og_service.generate(
        commune_name=commune_name,
        start=start,
        end=end,
        temp_min=temp_min,
        temp_max=temp_max,
        dominant_desc=dominant_desc,
        dominant_icon=dominant_icon,
    )
    return Response(
        content=image_bytes,
        media_type="image/png",
        headers={"Cache-Control": "public, max-age=86400"},
    )


@admin_router.get("/")
async def admin_index() -> Response:
    index_path = admin_dir / "index.html"
    if not index_path.exists():
        raise HTTPException(status_code=404, detail="Interface admin introuvable")
    return Response(content=index_path.read_text(encoding="utf-8"), media_type="text/html")


@admin_router.get("/admin.js")
async def admin_script() -> Response:
    script_path = admin_dir / "admin.js"
    if not script_path.exists():
        raise HTTPException(status_code=404, detail="Script admin introuvable")
    return Response(
        content=script_path.read_text(encoding="utf-8"),
        media_type="application/javascript",
    )


@admin_router.get("/api/searches")
async def admin_searches(
    limit: int = Query(default=50, ge=1, le=200),
    offset: int = Query(default=0, ge=0),
    status: str | None = Query(default=None),
    environment: str | None = Query(default=None),
    commune: str | None = Query(default=None),
    sort: str = Query(default="date"),
    order: str = Query(default="desc"),
) -> JSONResponse:
    tracker = get_tracker()
    if tracker is None:
        raise HTTPException(status_code=503, detail="Tracking indisponible")
    data = tracker.list_searches(
        limit=limit,
        offset=offset,
        status=status,
        environment=environment,
        commune=commune,
        sort=sort,
        order=order,
    )
    return JSONResponse(status_code=200, content=data)


@admin_router.get("/api/searches/{search_id}")
async def admin_search_detail(search_id: str) -> JSONResponse:
    tracker = get_tracker()
    if tracker is None:
        raise HTTPException(status_code=503, detail="Tracking indisponible")
    data = tracker.get_search_detail(search_id)
    if data is None:
        raise HTTPException(status_code=404, detail="Recherche introuvable")
    return JSONResponse(status_code=200, content=data)


@admin_router.get("/api/dashboard")
async def admin_dashboard(
    date_value: str | None = Query(default=None, alias="date", pattern=ISO_DATE_PATTERN),
) -> JSONResponse:
    tracker = get_tracker()
    if tracker is None:
        raise HTTPException(status_code=503, detail="Tracking indisponible")
    data = tracker.get_dashboard(date_value=date_value)
    return JSONResponse(status_code=200, content=data)


@admin_router.get("/api/cache")
async def admin_cache() -> JSONResponse:
    def _section(cache, max_entries: int, ttl_seconds: int) -> dict:
        entries = cache.snapshot()
        return {
            "entries": entries,
            "total": len(entries),
            "max_entries": max_entries,
            "ttl_seconds": ttl_seconds,
        }

    data = {
        "weather": _section(
            weather_service.cache,
            CACHE_MAX_ENTRIES,
            WEATHER_CACHE_TTL_SECONDS,
        ),
        "normals": _section(
            normals_service.cache,
            NORMALS_CACHE_MAX_ENTRIES,
            NORMALS_CACHE_TTL_SECONDS,
        ),
        "communes_search": _section(
            commune_service.cache,
            CACHE_MAX_ENTRIES,
            COMMUNE_CACHE_TTL_SECONDS,
        ),
        "communes_slug": _section(
            commune_service.slug_cache,
            CACHE_MAX_ENTRIES,
            COMMUNE_CACHE_TTL_SECONDS,
        ),
        "og_image": _section(
            og_service._cache,
            OG_IMAGE_CACHE_MAX_ENTRIES,
            OG_IMAGE_CACHE_TTL_SECONDS,
        ),
    }
    return JSONResponse(status_code=200, content=data)


@app.get("/")
async def homepage(
    request: Request,
    commune: str | None = Query(default=None),
    dept: str | None = Query(default=None),
    start: str | None = Query(default=None),
    end: str | None = Query(default=None),
    commune2: str | None = Query(default=None),
    dept2: str | None = Query(default=None),
) -> Response:
    has_dates = bool(start and end)
    dates_are_iso = bool(
        has_dates
        and re.match(ISO_DATE_PATTERN, start or "")
        and re.match(ISO_DATE_PATTERN, end or "")
    )

    if commune and dept and dates_are_iso:
        dept_clean = dept.strip()
        if not re.match(r"^\d{2,3}$", dept_clean):
            return FileResponse(index_file)

        slug1 = f"{commune_service.generate_slug(commune)}-{dept_clean}"

        if commune2 and dept2:
            dept2_clean = dept2.strip()
            if not re.match(r"^\d{2,3}$", dept2_clean):
                return FileResponse(index_file)
            slug2 = f"{commune_service.generate_slug(commune2)}-{dept2_clean}"
            path = f"/comparaison/{slug1}/vs/{slug2}/{start}/{end}"
        else:
            path = f"/meteo/{slug1}/{start}/{end}"

        if request.url.path == "/":
            return RedirectResponse(url=path, status_code=301)

    return FileResponse(index_file)


app.include_router(admin_router)
app.mount("/", StaticFiles(directory=str(public_dir), html=True), name="public")
