"""Security helpers and middleware for the public event site."""
from __future__ import annotations

import json
import re
import time
from collections import defaultdict, deque
from dataclasses import asdict, dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Callable, Awaitable

from fastapi import HTTPException, Request
from itsdangerous import BadSignature, SignatureExpired, URLSafeTimedSerializer
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response

from .config import settings

serializer = URLSafeTimedSerializer(settings.secret_key, salt="bcs-ict-fest-csrf")

SAFE_TEXT_RE = re.compile(r"^[\w\s@.,:;()\-+/&'\"]{1,300}$", re.UNICODE)
EMAIL_RE = re.compile(r"^[^@\s]{1,100}@[^@\s]{1,120}\.[^@\s]{2,20}$")


class SecurityHeadersMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
        response = await call_next(request)
        response.headers.setdefault("X-Content-Type-Options", "nosniff")
        response.headers.setdefault("X-Frame-Options", "DENY")
        response.headers.setdefault("Referrer-Policy", "strict-origin-when-cross-origin")
        response.headers.setdefault(
            "Permissions-Policy",
            "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()",
        )
        response.headers.setdefault("Cross-Origin-Opener-Policy", "same-origin")
        response.headers.setdefault("Cross-Origin-Resource-Policy", "same-origin")
        response.headers.setdefault(
            "Content-Security-Policy",
            "default-src 'self'; "
            "script-src 'self'; "
            "style-src 'self' 'unsafe-inline'; "
            "img-src 'self' data:; "
            "font-src 'self'; "
            "connect-src 'self'; "
            "frame-ancestors 'none'; "
            "base-uri 'self'; "
            "form-action 'self'; "
            "upgrade-insecure-requests",
        )
        if settings.enforce_https:
            response.headers.setdefault("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload")
        return response


class SimpleRateLimitMiddleware(BaseHTTPMiddleware):
    """Small in-memory rate limiter for public pages and form abuse protection.

    In multi-worker production deployments, use a shared backend such as Redis.
    """

    def __init__(self, app, limit_per_minute: int = 120) -> None:
        super().__init__(app)
        self.limit = max(10, limit_per_minute)
        self.window = 60.0
        self.requests: dict[str, deque[float]] = defaultdict(deque)

    async def dispatch(self, request: Request, call_next):
        client_host = request.client.host if request.client else "unknown"
        now = time.monotonic()
        bucket = self.requests[client_host]
        while bucket and now - bucket[0] > self.window:
            bucket.popleft()
        if len(bucket) >= self.limit:
            return Response("Too many requests. Please try again later.", status_code=429)
        bucket.append(now)
        return await call_next(request)


def create_csrf_token(action: str) -> str:
    return serializer.dumps({"action": action, "ts": int(time.time())})


def validate_csrf_token(token: str, action: str) -> None:
    try:
        data = serializer.loads(token, max_age=settings.csrf_max_age_seconds)
    except SignatureExpired as exc:
        raise HTTPException(status_code=400, detail="Form token expired. Please reload the page.") from exc
    except BadSignature as exc:
        raise HTTPException(status_code=400, detail="Invalid form token.") from exc
    if data.get("action") != action:
        raise HTTPException(status_code=400, detail="Invalid form action.")


def clean_text(value: str, *, max_len: int = 300, required: bool = True) -> str:
    value = (value or "").strip()
    value = re.sub(r"[\x00-\x1f\x7f]", "", value)
    if required and not value:
        raise HTTPException(status_code=400, detail="Required field is missing.")
    if len(value) > max_len:
        raise HTTPException(status_code=400, detail="Input is too long.")
    return value


def clean_email(value: str) -> str:
    value = clean_text(value, max_len=150)
    if not EMAIL_RE.fullmatch(value):
        raise HTTPException(status_code=400, detail="Invalid email address.")
    return value.lower()


@dataclass(frozen=True)
class RegistrationInterest:
    name: str
    email: str
    organization: str
    category: str
    message: str
    created_at: str


def save_registration_interest(item: RegistrationInterest, path: Path | None = None) -> None:
    target = path or settings.registration_file
    target.parent.mkdir(parents=True, exist_ok=True)
    with target.open("a", encoding="utf-8") as handle:
        handle.write(json.dumps(asdict(item), ensure_ascii=False) + "\n")


def build_registration_interest(form: dict[str, str]) -> RegistrationInterest:
    # Honeypot field: bots often fill this hidden field.
    if form.get("website"):
        raise HTTPException(status_code=400, detail="Invalid form submission.")
    return RegistrationInterest(
        name=clean_text(form.get("name", ""), max_len=120),
        email=clean_email(form.get("email", "")),
        organization=clean_text(form.get("organization", ""), max_len=160, required=False),
        category=clean_text(form.get("category", ""), max_len=80),
        message=clean_text(form.get("message", ""), max_len=500, required=False),
        created_at=datetime.now(timezone.utc).isoformat(),
    )
