"""Security middleware: API key auth, rate limiting, and request logging."""

from __future__ import annotations

import json
import logging
import time
from collections import deque
from dataclasses import dataclass
from datetime import datetime, timezone
from threading import Lock
from typing import Final

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

LOGGER = logging.getLogger("sentineledge.security")

API_KEY_HEADER: Final[str] = "X-API-Key"
ASSISTANT_QUERY_PATH: Final[str] = "/assistant/query"


@dataclass(frozen=True)
class SecurityConfig:
    """Runtime security configuration."""

    mode: str
    api_key: str | None
    auth_enabled: bool
    assistant_rate_limit_per_minute: int = 20


class _RateLimiter:
    """Simple in-memory sliding window limiter by client key."""

    def __init__(self, limit: int, window_seconds: int = 60):
        self._limit = max(limit, 1)
        self._window_seconds = max(window_seconds, 1)
        self._events: dict[str, deque[float]] = {}
        self._lock = Lock()

    def allow(self, key: str, now: float | None = None) -> tuple[bool, int]:
        """Return (allowed, retry_after_seconds)."""
        ts = now if now is not None else time.monotonic()
        threshold = ts - self._window_seconds

        with self._lock:
            bucket = self._events.setdefault(key, deque())
            while bucket and bucket[0] <= threshold:
                bucket.popleft()

            if len(bucket) >= self._limit:
                retry_after = int(max(self._window_seconds - (ts - bucket[0]), 1))
                return False, retry_after

            bucket.append(ts)
            return True, 0


class SecurityMiddleware(BaseHTTPMiddleware):
    """Apply API key auth, selective rate limiting, and structured request logs."""

    def __init__(self, app, config: SecurityConfig):
        super().__init__(app)
        self._config = config
        self._assistant_limiter = _RateLimiter(limit=config.assistant_rate_limit_per_minute)

    @staticmethod
    def _client_ip(request: Request) -> str:
        """Extract client IP with optional proxy header support."""
        forwarded_for = request.headers.get("X-Forwarded-For", "")
        if forwarded_for:
            first = forwarded_for.split(",", maxsplit=1)[0].strip()
            if first:
                return first

        client = request.client
        return client.host if client and client.host else "unknown"

    @staticmethod
    def _is_sensitive_path(path: str) -> bool:
        """Return True when path requires API key auth in production mode."""
        if path == ASSISTANT_QUERY_PATH:
            return True
        if path == "/coverage" or path.startswith("/coverage/"):
            return True
        if path.startswith("/tasks"):
            return True
        if path.startswith("/vault"):
            return True
        if path.startswith("/export/"):
            return True
        if path == "/report/export":
            return True
        if path == "/notes/generate":
            return True
        return False

    def _unauthorized_response(self, detail: str) -> JSONResponse:
        """Build a consistent unauthorized response."""
        return JSONResponse(status_code=401, content={"detail": detail})

    def _rate_limited_response(self, retry_after: int) -> JSONResponse:
        """Build rate-limit response with retry hint."""
        return JSONResponse(
            status_code=429,
            content={"detail": "Rate limit exceeded: max 20 requests per minute"},
            headers={"Retry-After": str(retry_after)},
        )

    def _log_request(self, request: Request, status_code: int, ip: str) -> None:
        """Emit one structured log line per request."""
        payload = {
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "endpoint": request.url.path,
            "method": request.method,
            "status": status_code,
            "ip": ip,
        }
        LOGGER.info(json.dumps(payload, ensure_ascii=True))

    async def dispatch(self, request: Request, call_next) -> Response:
        ip = self._client_ip(request)
        path = request.url.path

        if request.method == "OPTIONS":
            try:
                response = await call_next(request)
            except Exception:
                self._log_request(request=request, status_code=500, ip=ip)
                raise
            self._log_request(request=request, status_code=response.status_code, ip=ip)
            return response

        if self._config.auth_enabled and self._is_sensitive_path(path):
            incoming_key = request.headers.get(API_KEY_HEADER)
            expected_key = self._config.api_key
            if not expected_key:
                response = self._unauthorized_response("API key is required in production mode")
                self._log_request(request=request, status_code=response.status_code, ip=ip)
                return response

            if incoming_key != expected_key:
                response = self._unauthorized_response("Invalid API key")
                self._log_request(request=request, status_code=response.status_code, ip=ip)
                return response

        if path == ASSISTANT_QUERY_PATH:
            allowed, retry_after = self._assistant_limiter.allow(ip)
            if not allowed:
                response = self._rate_limited_response(retry_after=retry_after)
                self._log_request(request=request, status_code=response.status_code, ip=ip)
                return response

        try:
            response = await call_next(request)
        except Exception:
            self._log_request(request=request, status_code=500, ip=ip)
            raise
        self._log_request(request=request, status_code=response.status_code, ip=ip)
        return response
