"""Module 06 — Solution autonome : Validation avancée + Rate limiting.

Lancer :
  python solution.py        → démo validators
  pytest solution.py -v     → tests
  uvicorn solution:app --reload → API
"""
from __future__ import annotations

import logging
import math
import time
from collections import defaultdict
from datetime import datetime
from enum import Enum
from typing import Generic, TypeVar

import pytest
from fastapi import Depends, FastAPI, HTTPException, Query, Request, Response, status
from fastapi.middleware.base import BaseHTTPMiddleware
from fastapi.middleware.cors import CORSMiddleware
from fastapi.testclient import TestClient
from pydantic import BaseModel, EmailStr, Field, field_validator, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("taskapi.mod06")
T = TypeVar("T")


# ── Configuration ─────────────────────────────────────────────

class AppSettings(BaseSettings):
    app_name: str = "TaskAPI"
    debug: bool = False
    database_url: str = "sqlite:///./taskapi.db"
    secret_key: str = "dev-secret-key-change-in-production"
    max_page_size: int = Field(100, ge=1, le=500)
    rate_limit_per_minute: int = 60

    model_config = SettingsConfigDict(
        env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="ignore"
    )

settings = AppSettings()


# ── Schemas Pydantic avancés ──────────────────────────────────

class TaskStatus(str, Enum):
    TODO = "todo"
    IN_PROGRESS = "in_progress"
    DONE = "done"
    CANCELLED = "cancelled"


class TaskCreateSchema(BaseModel):
    title: str = Field(..., min_length=3, max_length=200)
    description: str | None = Field(None, max_length=2000)
    status: TaskStatus = TaskStatus.TODO
    due_date: datetime | None = None
    tags: list[str] = Field(default_factory=list)
    priority: int = Field(0, ge=0, le=3)

    @field_validator("title", mode="before")
    @classmethod
    def normalize_title(cls, v: object) -> str:
        return str(v).strip().title()

    @field_validator("tags", each_item=True)
    @classmethod
    def normalize_tag(cls, v: str) -> str:
        return v.lower().strip()

    @field_validator("due_date")
    @classmethod
    def due_date_future(cls, v: datetime | None) -> datetime | None:
        if v is not None and v <= datetime.now():
            raise ValueError("La date d'échéance doit être dans le futur")
        return v

    @model_validator(mode="after")
    def auto_complete_done(self) -> TaskCreateSchema:
        if self.status == TaskStatus.DONE and self.due_date is None:
            self.due_date = datetime.now()
        return self


class UserRegister(BaseModel):
    email: EmailStr
    username: str = Field(..., min_length=3, max_length=50)
    password: str = Field(..., min_length=8)


class PaginatedResponse(BaseModel, Generic[T]):
    items: list[T]
    total: int
    page: int
    limit: int
    pages: int
    has_next: bool
    has_prev: bool

    @classmethod
    def create(cls, items: list[T], total: int, page: int, limit: int) -> "PaginatedResponse[T]":
        pages = math.ceil(total / limit) if total > 0 else 1
        return cls(
            items=items, total=total, page=page, limit=limit,
            pages=pages, has_next=page < pages, has_prev=page > 1,
        )


# ── Middlewares ───────────────────────────────────────────────

class ProcessTimeMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next) -> Response:  # type: ignore[override]
        t0 = time.perf_counter()
        response = await call_next(request)
        ms = (time.perf_counter() - t0) * 1000
        response.headers["X-Process-Time"] = f"{ms:.2f}ms"
        if ms > 500:
            logger.warning("Requête lente : %s %s %.0fms", request.method, request.url.path, ms)
        return response


class SecurityHeadersMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next) -> Response:  # type: ignore[override]
        response = await call_next(request)
        response.headers["X-Frame-Options"] = "DENY"
        response.headers["X-Content-Type-Options"] = "nosniff"
        response.headers["X-XSS-Protection"] = "1; mode=block"
        response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
        response.headers["Server"] = "TaskAPI"
        return response


# ── Rate limiter ──────────────────────────────────────────────

_rate_limits: dict[str, list[float]] = defaultdict(list)

def rate_limit(max_requests: int = 10, window: int = 60):
    async def _check(request: Request) -> None:
        ip = request.client.host if request.client else "test"
        key = f"{ip}:{request.url.path}"
        now = time.time()
        _rate_limits[key] = [t for t in _rate_limits[key] if now - t < window]
        if len(_rate_limits[key]) >= max_requests:
            raise HTTPException(
                status_code=429,
                detail=f"Trop de requêtes. Max {max_requests}/{window}s",
                headers={"Retry-After": str(window)},
            )
        _rate_limits[key].append(now)
    return _check


# ── Application ───────────────────────────────────────────────

app = FastAPI(title=settings.app_name, debug=settings.debug)
app.add_middleware(ProcessTimeMiddleware)
app.add_middleware(SecurityHeadersMiddleware)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

_users: list[dict] = []
_tasks: list[dict] = []


@app.post("/auth/register", status_code=201)
async def register(data: UserRegister, _=Depends(rate_limit(5, 60))) -> dict:
    if any(u["email"] == data.email for u in _users):
        raise HTTPException(409, "Email déjà utilisé")
    user = {"id": len(_users) + 1, "email": data.email, "username": data.username}
    _users.append(user)
    return user


@app.post("/tasks", status_code=201)
async def create_task(data: TaskCreateSchema) -> dict:
    task = {"id": len(_tasks) + 1, **data.model_dump(mode="json")}
    _tasks.append(task)
    return task


@app.get("/tasks")
async def list_tasks(
    page: int = Query(1, ge=1),
    limit: int = Query(10, ge=1, le=settings.max_page_size),
) -> dict:
    total = len(_tasks)
    offset = (page - 1) * limit
    items = _tasks[offset : offset + limit]
    return PaginatedResponse[dict].create(items=items, total=total, page=page, limit=limit).model_dump()


# ── Tests ─────────────────────────────────────────────────────

@pytest.fixture
def client():
    _users.clear()
    _tasks.clear()
    _rate_limits.clear()
    return TestClient(app)


def test_password_too_short(client: TestClient) -> None:
    resp = client.post("/auth/register", json={"email": "a@b.com", "username": "abc", "password": "123"})
    assert resp.status_code == 422


def test_email_duplicate(client: TestClient) -> None:
    data = {"email": "dup@test.com", "username": "dup1", "password": "DupPass123"}
    client.post("/auth/register", json=data)
    resp = client.post("/auth/register", json={**data, "username": "dup2"})
    assert resp.status_code == 409


def test_pagination_defaults(client: TestClient) -> None:
    resp = client.get("/tasks")
    assert resp.status_code == 200
    body = resp.json()
    assert body["page"] == 1
    assert body["limit"] == 10


def test_process_time_header(client: TestClient) -> None:
    resp = client.get("/tasks")
    assert "X-Process-Time" in resp.headers
    assert resp.headers["X-Process-Time"].endswith("ms")


def test_security_headers(client: TestClient) -> None:
    resp = client.get("/tasks")
    assert resp.headers["X-Frame-Options"] == "DENY"
    assert resp.headers["X-Content-Type-Options"] == "nosniff"


def test_due_date_past(client: TestClient) -> None:
    resp = client.post("/tasks", json={
        "title": "Tâche Passée",
        "due_date": "2020-01-01T00:00:00",
    })
    assert resp.status_code == 422


# ── Démo validators ───────────────────────────────────────────

if __name__ == "__main__":
    print("\n=== Démo Validators Pydantic v2 ===\n")

    # Test 1 : normalisation title + tags
    task = TaskCreateSchema(title="  apprendre python  ", tags=["PYTHON", "  fastapi  "])
    print(f"title: {task.title!r}")         # "Apprendre Python"
    print(f"tags: {task.tags}")             # ["python", "fastapi"]

    # Test 2 : task done → due_date auto
    task2 = TaskCreateSchema(title="Tâche terminée", status="done")
    print(f"due_date auto: {task2.due_date is not None}")   # True

    # Test 3 : BaseSettings
    print(f"\nSettings app_name: {settings.app_name}")
    print(f"Settings max_page_size: {settings.max_page_size}")

    # Test 4 : PaginatedResponse
    items = [{"id": i} for i in range(1, 11)]
    page = PaginatedResponse[dict].create(items[:3], total=10, page=1, limit=3)
    print(f"\nPagination: pages={page.pages} has_next={page.has_next}")  # pages=4 has_next=True

    print("\n✅ Tous les validators fonctionnent !")
