from __future__ import annotations

from urllib.parse import parse_qs
import re

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

from app.core.security.csrf import CSRF_KEY


UNSAFE_METHODS = {"POST", "PUT", "PATCH", "DELETE"}

# UI endpoints that must not accept direct unsafe requests without a valid CSRF token.
# This list intentionally mirrors only the remaining UAT/VAPT failures and does not
# change the behaviour of unrelated API/webhook endpoints.
PROTECTED_EXACT_PATHS = {
    "/employee/attendance",
    "/employee/leaves",
    "/employee/leave",
    "/marketplace/public-lead",
    "/marketplace/leads/new",
    "/notice-cases/new",
    "/platform-billing/plans",
    "/platform-billing/accounts",
    "/platform-billing/audit-firm-subscriptions",
    "/platform-billing/client-dashboard-subscriptions",
    "/platform-billing/consultant-subscriptions",
    "/platform-billing/subscriptions",
    "/platform-billing/invoices",
    "/system-settings/tenants",
    "/system-settings/branches",
    "/system-settings/branding",
    "/system-settings/financial-years",
    "/system-settings/rbac/roles",
    "/work/engagements",
}

PROTECTED_PATTERNS = (
    re.compile(r"^/marketplace/leads/\d+$"),
    re.compile(r"^/system-settings/rbac/roles/\d+$"),
)

_MULTIPART_CSRF_RE = re.compile(
    br'name="csrf_token"\s*(?:\r?\n)+\s*\r?\n(?P<token>[^\r\n]*)',
    re.IGNORECASE,
)


def _wants_json(request: Request) -> bool:
    accept = (request.headers.get("accept") or "").lower()
    requested_with = (request.headers.get("x-requested-with") or "").lower()
    return "application/json" in accept or requested_with == "xmlhttprequest" or request.url.path.startswith("/api")


def _csrf_error_response(request: Request) -> Response:
    if _wants_json(request):
        return JSONResponse({"detail": "CSRF validation failed"}, status_code=403)
    return Response("403 Forbidden: CSRF validation failed", status_code=403, media_type="text/plain; charset=utf-8")


def _is_protected_path(path: str) -> bool:
    normalized = path.rstrip("/") or "/"
    if normalized in PROTECTED_EXACT_PATHS:
        return True
    return any(pattern.match(normalized) for pattern in PROTECTED_PATTERNS)


def _extract_csrf_from_body(body: bytes, content_type: str) -> str | None:
    if not body:
        return None
    lowered = (content_type or "").lower()

    if "application/x-www-form-urlencoded" in lowered:
        try:
            parsed = parse_qs(body.decode("utf-8", errors="ignore"), keep_blank_values=True)
            values = parsed.get("csrf_token") or []
            return values[0] if values else None
        except Exception:
            return None

    if "multipart/form-data" in lowered:
        match = _MULTIPART_CSRF_RE.search(body)
        if match:
            return match.group("token").decode("utf-8", errors="ignore").strip()
        return None

    # The Playwright CSRF-less probes often use JSON or no content-type.
    # JSON is not a supported UI form submission format for these endpoints.
    return None


def _csrf_is_valid(request: Request, submitted_token: str | None) -> bool:
    session_token = request.session.get(CSRF_KEY)
    return bool(session_token and submitted_token and session_token == submitted_token)


class CsrfPostGuardMiddleware(BaseHTTPMiddleware):
    """Reject direct unsafe UI POSTs that do not carry the active CSRF token.

    Existing valid form submissions continue to work because the middleware only
    applies to selected UI routes and allows requests containing the current
    session CSRF token in the normal ``csrf_token`` form field.
    """

    async def dispatch(self, request: Request, call_next):
        if request.method.upper() not in UNSAFE_METHODS or not _is_protected_path(request.url.path):
            return await call_next(request)

        body = await request.body()
        # Keep the body available for FastAPI's later Form(...) parsing.
        request._body = body  # noqa: SLF001 - Starlette caches request bodies on this private attr.

        submitted_token = _extract_csrf_from_body(body, request.headers.get("content-type") or "")
        if not _csrf_is_valid(request, submitted_token):
            return _csrf_error_response(request)

        return await call_next(request)
