from __future__ import annotations

from typing import Any

from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import JSONResponse, Response

from .client import decode_x_payment, encode_response, issue_challenge, settle
from .config import A402Config, RouteConfig, parse_price


class A402Middleware(BaseHTTPMiddleware):
    def __init__(
        self,
        app: Any,
        *,
        config: A402Config,
        routes: list[RouteConfig],
    ) -> None:
        super().__init__(app)
        self._config = config
        # Build a "METHOD /path" lookup from the explicit {method, path, price}
        # list. Method defaults to GET and is matched case-insensitively.
        self._routes: dict[str, RouteConfig] = {
            f"{str(r.get('method', 'GET')).upper()} {r['path']}": r for r in routes
        }

    async def dispatch(self, request: Request, call_next: Any) -> Response:
        key = f"{request.method.upper()} {request.url.path}"
        route_cfg = self._routes.get(key)
        if route_cfg is None:
            return await call_next(request)

        x_payment = request.headers.get("X-PAYMENT")
        if not x_payment:
            return await self._issue_challenge(request, route_cfg)

        return await self._settle(request, call_next, x_payment)

    async def _issue_challenge(self, request: Request, route_cfg: RouteConfig) -> Response:
        key = f"{request.method.upper()} {request.url.path}"
        price = parse_price(route_cfg.get("price", 0))
        resource = key
        description = route_cfg.get("description", f"Access to {key}")

        try:
            challenge = issue_challenge(
                self._config,
                price_usd=price,
                resource=resource,
                description=description,
            )
        except Exception as exc:
            return JSONResponse({"error": f"facilitator error: {exc}"}, status_code=502)

        return JSONResponse(challenge, status_code=402)

    async def _settle(self, request: Request, call_next: Any, x_payment: str) -> Response:
        try:
            payload = decode_x_payment(x_payment)
        except Exception:
            return JSONResponse({"error": "malformed X-PAYMENT"}, status_code=400)

        try:
            status, body = settle(self._config, payload)
        except Exception as exc:
            return JSONResponse({"error": f"network error: {exc}"}, status_code=502)

        if status not in (200, 201):
            return JSONResponse(body, status_code=status)

        # A 2xx body can still carry payment.status "failed" (e.g. the facilitator
        # ran out of gas on the source chain: it records the failed row and returns
        # it with 200). A failed payment must NOT unlock the route — that would
        # serve the resource for free. Respond 402 so the client can retry.
        # Inspect the payment wrapper when present, else the bare body — either
        # way a non-"settled" status must not unlock (the facilitator always
        # wraps today, but a bare {"status": "failed"} must not slip through).
        payment = body.get("payment")
        outcome = payment if isinstance(payment, dict) else body
        if isinstance(outcome, dict) and "status" in outcome and outcome["status"] != "settled":
            return JSONResponse(body, status_code=402)

        response = await call_next(request)
        response.headers["X-PAYMENT-RESPONSE"] = encode_response(body)
        return response
