"""Security middleware: headers, rate limiting, and an API auth gate.

Design principles (per docs/MASTER_AUDIT_PROMPT.md):
- Fail closed: in production mode the app REFUSES to start with default secrets.
- Non-bypassable: the auth gate is middleware — no route can opt out except
  the auth endpoints themselves and health checks.
- Defense in depth: headers + rate limits apply even in development.
"""
import os
import time
from collections import defaultdict, deque

import jwt
from fastapi import Request
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware

from .config import settings

_INSECURE_SECRETS = {"dev-secret-change-me", "change-me-to-a-long-random-string", "", "secret"}

# Paths that never require auth
_PUBLIC_PREFIXES = ("/api/auth", "/health", "/docs", "/redoc", "/openapi.json")


def enforce_production_safety() -> None:
    """Fail closed: refuse to boot a production deployment with unsafe config."""
    if os.getenv("ENV", "development").lower() != "production":
        return
    if settings.SECRET_KEY in _INSECURE_SECRETS or len(settings.SECRET_KEY) < 32:
        raise RuntimeError(
            "REFUSING TO START: ENV=production requires a strong SECRET_KEY "
            "(32+ random characters) in the environment."
        )
    if os.getenv("REQUIRE_AUTH", "").lower() != "true":
        raise RuntimeError(
            "REFUSING TO START: ENV=production requires REQUIRE_AUTH=true "
            "so the API is not publicly writable."
        )


class SecurityHeadersMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        response = await call_next(request)
        response.headers["X-Content-Type-Options"] = "nosniff"
        response.headers["X-Frame-Options"] = "DENY"
        response.headers["Referrer-Policy"] = "no-referrer"
        response.headers["Cache-Control"] = "no-store"
        response.headers["Permissions-Policy"] = "geolocation=(), microphone=(), camera=()"
        if os.getenv("ENV", "development").lower() == "production":
            response.headers["Strict-Transport-Security"] = "max-age=63072000; includeSubDomains"
        return response


class RateLimitMiddleware(BaseHTTPMiddleware):
    """Simple in-memory per-IP sliding-window limiter (single-process).
    For multi-worker production, add nginx `limit_req` as the outer layer
    (see deployment/nginx.conf.example) — this is defense in depth."""

    def __init__(self, app):
        super().__init__(app)
        self.hits: dict[str, deque] = defaultdict(deque)

    async def dispatch(self, request: Request, call_next):
        limit = int(os.getenv("RATE_LIMIT_PER_MINUTE", "240"))
        if limit <= 0:  # disabled (tests)
            return await call_next(request)
        ip = request.client.host if request.client else "unknown"
        now = time.monotonic()
        window = self.hits[ip]
        while window and now - window[0] > 60:
            window.popleft()
        if len(window) >= limit:
            return JSONResponse({"detail": "Rate limit exceeded"}, status_code=429)
        window.append(now)
        return await call_next(request)


class AuthGateMiddleware(BaseHTTPMiddleware):
    """When REQUIRE_AUTH=true, every /api route (except auth) needs a valid
    JWT. Middleware placement makes this non-bypassable by any router."""

    async def dispatch(self, request: Request, call_next):
        if os.getenv("REQUIRE_AUTH", "false").lower() != "true":
            return await call_next(request)
        path = request.url.path
        if not path.startswith("/api") or path.startswith(_PUBLIC_PREFIXES):
            return await call_next(request)
        auth = request.headers.get("Authorization", "")
        token = auth.removeprefix("Bearer ").strip()
        if not token:
            return JSONResponse({"detail": "Not authenticated"}, status_code=401)
        try:
            jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
        except jwt.PyJWTError:
            return JSONResponse({"detail": "Invalid or expired token"}, status_code=401)
        return await call_next(request)
