from __future__ import annotations

import asyncio
import hmac
import logging
import uuid
from typing import Any

from aiohttp import web

from .config import Settings
from .db import CloudSqlClaimReader

logger = logging.getLogger("teleo.observatory_read_adapter")

SETTINGS_KEY = web.AppKey("observatory_settings", Settings)
READER_KEY = web.AppKey("observatory_reader", object)
PUBLIC_PATHS = frozenset({"/healthz", "/readyz"})


def _private_json(payload: dict[str, Any], *, status: int = 200) -> web.Response:
    response = web.json_response(payload, status=status)
    response.headers["Cache-Control"] = "private, no-store, max-age=0"
    response.headers["Pragma"] = "no-cache"
    response.headers["Vary"] = "X-Api-Key"
    response.headers["X-Content-Type-Options"] = "nosniff"
    return response


@web.middleware
async def api_key_auth(request: web.Request, handler: Any) -> web.StreamResponse:
    if request.path in PUBLIC_PATHS:
        return await handler(request)
    settings = request.app[SETTINGS_KEY]
    provided = request.headers.get("X-Api-Key", "")
    try:
        authenticated = hmac.compare_digest(
            provided.encode("ascii"),
            settings.api_key.encode("ascii"),
        )
    except UnicodeEncodeError:
        authenticated = False
    if not authenticated:
        return _private_json({"error": "unauthorized"}, status=401)
    return await handler(request)


async def healthz(_request: web.Request) -> web.Response:
    return web.json_response({"status": "ok"})


async def readyz(request: web.Request) -> web.Response:
    reader = request.app[READER_KEY]
    try:
        await asyncio.wait_for(asyncio.to_thread(reader.check_ready), timeout=12.0)
    except Exception as exc:
        logger.warning("readiness check failed (%s)", type(exc).__name__)
        return web.json_response({"status": "unavailable"}, status=503)
    return web.json_response({"status": "ready"})


async def get_sample_claim(request: web.Request) -> web.Response:
    return await _claim_response(request, None)


async def get_claim(request: web.Request) -> web.Response:
    raw_claim_id = request.match_info["claim_id"]
    try:
        claim_id = str(uuid.UUID(raw_claim_id))
    except ValueError:
        return _private_json({"error": "invalid_claim_id"}, status=400)
    return await _claim_response(request, claim_id)


async def _claim_response(request: web.Request, claim_id: str | None) -> web.Response:
    reader = request.app[READER_KEY]
    try:
        payload = await asyncio.wait_for(
            asyncio.to_thread(reader.read_claim, claim_id),
            timeout=12.0,
        )
    except Exception as exc:
        logger.error("canonical claim read failed (%s)", type(exc).__name__)
        return _private_json({"error": "canonical_claim_unavailable"}, status=503)
    if payload is None:
        return _private_json({"error": "claim_not_found"}, status=404)
    return _private_json(payload)


async def _cleanup(app: web.Application) -> None:
    reader = app[READER_KEY]
    await asyncio.to_thread(reader.close)


def create_app(
    settings: Settings | None = None,
    *,
    reader: Any | None = None,
) -> web.Application:
    settings = settings or Settings.from_env()
    settings.validate()
    reader = reader or CloudSqlClaimReader(settings)
    app = web.Application(middlewares=[api_key_auth], client_max_size=16 * 1024)
    app[SETTINGS_KEY] = settings
    app[READER_KEY] = reader
    app.router.add_get("/healthz", healthz)
    app.router.add_get("/readyz", readyz)
    app.router.add_get("/v1/claims/sample", get_sample_claim)
    app.router.add_get("/v1/claims/{claim_id}", get_claim)
    app.on_cleanup.append(_cleanup)
    return app
