"""Expo Updates serving endpoints (OTA JS bundle distribution).

Mobile sprawdza `/expo-updates/manifest` przy każdym launch (lub on-foreground).
Serwer zwraca aktualny manifest dla danego `expo-runtime-version`. Mobile pobiera
launchAsset (bundle) + assets, zapisuje, restartuje aplikację z nowym bundle.

Każdy update wgrany przez `scripts/publish_update.py` ląduje w
`app/static/expo-updates/<runtime>/<update_id>/`. Plik
`app/static/expo-updates/<runtime>/current.json` wskazuje aktywny update_id.

Endpointy SĄ PUBLICZNE (no auth) — Expo Updates SDK nie wstrzykuje X-API-Key.
Bezpieczeństwo opiera się na TLS pinningu (mobile ufa tylko naszej self-signed
cert SPKI z network_security_config) — ktoś bez tego pinu nie podstawi MITM
manifestu. Jeśli kiedyś trzeba twardo: dorobić expo-updates code signing key.
"""
from __future__ import annotations

import json
import logging
from pathlib import Path

from fastapi import APIRouter, Header, HTTPException, Query
from fastapi.responses import FileResponse, JSONResponse, Response

log = logging.getLogger(__name__)

router = APIRouter(tags=["expo-updates"])

_STATIC_DIR = Path(__file__).resolve().parent.parent / "static" / "expo-updates"


@router.get("/expo-updates/manifest")
def get_manifest(
    expo_runtime_version: str | None = Header(default=None, alias="expo-runtime-version"),
    expo_platform: str | None = Header(default=None, alias="expo-platform"),
) -> Response:
    """Zwraca aktualny manifest dla podanego `expo-runtime-version` (default 1.0)
    + platform (default android — i tak tylko Android wspieramy).

    204 No Content gdy nie ma update'u dla tego runtime'u → klient nadal odpala
    embedded bundle z APK. Mobile zna `expo-protocol-version` (single-manifest
    Mode), więc nie potrzebujemy multipart.
    """
    runtime = expo_runtime_version or "1.0"
    runtime_dir = _STATIC_DIR / runtime
    current_file = runtime_dir / "current.json"
    if not current_file.exists():
        return Response(status_code=204)

    try:
        current = json.loads(current_file.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as e:
        log.warning("expo-updates: bad current.json for runtime=%s: %s", runtime, e)
        return Response(status_code=204)

    update_id = current.get("update_id")
    if not update_id:
        return Response(status_code=204)

    manifest_file = runtime_dir / update_id / "manifest.json"
    if not manifest_file.exists():
        log.warning("expo-updates: current points to missing update %s", update_id)
        return Response(status_code=204)

    try:
        manifest = json.loads(manifest_file.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as e:
        log.error("expo-updates: bad manifest.json for %s: %s", update_id, e)
        return Response(status_code=204)

    return JSONResponse(
        manifest,
        headers={
            "expo-protocol-version": "1",
            "expo-sfv-version": "0",
            "cache-control": "private, max-age=0",
            "content-type": "application/json; charset=utf-8",
        },
    )


@router.get("/expo-updates/asset")
def get_asset(
    asset: str = Query(..., description="Relative path do pliku w runtime dir"),
    runtimeVersion: str = Query("1.0"),
    platform: str = Query("android"),
) -> Response:
    """Serwuje pojedynczy asset (JS bundle, image, font) z update directory.

    `asset` to relative path względem `static/expo-updates/<runtime>/` —
    zwykle `<update_id>/_expo/static/js/android/<hash>.js` lub
    `<update_id>/assets/<hash>`. Path traversal blocked przez resolve+is_relative.
    """
    # Windows publish quirk: Expo metadata.json zapisuje assets[].path z backslashami
    # (os.sep) na Windowsie. publish_update.py kopiuje to do URL → manifest zawiera
    # `?asset=<update>/assets\<hash>`. Na Linux backslash nie jest separatorem path-a,
    # więc Path resolve nie znalazłby pliku (404 na każdy asset → mobile odrzuca cały
    # update). Normalizujemy tutaj zamiast wymagać re-publishu starych bundle'i.
    asset = asset.replace("\\", "/")
    runtime_dir = (_STATIC_DIR / runtimeVersion).resolve()
    target = (runtime_dir / asset).resolve()
    if not str(target).startswith(str(runtime_dir)):
        raise HTTPException(status_code=400, detail="invalid asset path")
    if not target.exists() or not target.is_file():
        raise HTTPException(status_code=404, detail="asset not found")
    # Content type — bundle to text/javascript, reszta autodetect przez FileResponse.
    media_type = None
    if target.suffix in (".js", ".bundle"):
        media_type = "application/javascript"
    return FileResponse(target, media_type=media_type)
