from __future__ import annotations

import logging
import secrets
import time
from collections import defaultdict

from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer

from sitedrop.server.auth import (
    create_token,
    hash_password,
    verify_password,
    verify_token,
)
from sitedrop.server.models import (
    AuthRequest,
    AuthResponse,
    HealthResponse,
    MessageResponse,
    PasswordChangeRequest,
    PasswordChangeResponse,
    SiteListResponse,
    SiteResponse,
)
from sitedrop.server.storage import InvalidNameError

logger = logging.getLogger("sitedrop")

AUTH_RATE_LIMIT = 5  # max attempts
AUTH_RATE_WINDOW = 60  # per this many seconds
_auth_attempts: dict[str, list[float]] = defaultdict(list)

MAX_UPLOAD_BYTES = 10 * 1024 * 1024  # 10 MB

router = APIRouter(prefix="/api")
bearer_scheme = HTTPBearer()


def get_config(request: Request):
    return request.app.state.config


def get_storage(request: Request):
    return request.app.state.storage


def require_auth(
    request: Request,
    credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
):
    config = get_config(request)
    payload = verify_token(credentials.credentials, config.jwt_secret)
    if payload is None:
        raise HTTPException(status_code=401, detail="Invalid or expired token")
    return payload


def _check_rate_limit(ip: str) -> None:
    now = time.time()
    attempts = _auth_attempts[ip]
    # Remove expired entries
    _auth_attempts[ip] = [t for t in attempts if now - t < AUTH_RATE_WINDOW]
    if len(_auth_attempts[ip]) >= AUTH_RATE_LIMIT:
        logger.warning("Rate limit exceeded for %s", ip)
        raise HTTPException(
            status_code=429, detail="Too many login attempts. Try again later."
        )


@router.get("/health", response_model=HealthResponse)
def health():
    return HealthResponse(status="ok")


@router.post("/auth", response_model=AuthResponse)
def authenticate(body: AuthRequest, request: Request):
    config = get_config(request)
    if not config.password_hash:
        raise HTTPException(status_code=500, detail="Server not configured")
    ip = request.client.host if request.client else "unknown"
    _check_rate_limit(ip)
    _auth_attempts[ip].append(time.time())
    if not verify_password(body.password, config.password_hash):
        logger.warning("Failed authentication attempt from %s", ip)
        raise HTTPException(status_code=401, detail="Invalid password")
    logger.info("Successful authentication from %s", ip)
    token = create_token(config.jwt_secret)
    return AuthResponse(token=token)


@router.get("/sites", response_model=SiteListResponse)
def list_sites(
    storage=Depends(get_storage),
    _auth=Depends(require_auth),
):
    sites = storage.list()
    return SiteListResponse(sites=[SiteResponse(**s.__dict__) for s in sites])


@router.put("/sites/{name}", response_model=SiteResponse)
async def put_site(
    name: str,
    request: Request,
    storage=Depends(get_storage),
    _auth=Depends(require_auth),
):
    body = await request.body()
    if len(body) > MAX_UPLOAD_BYTES:
        raise HTTPException(
            status_code=413,
            detail=f"File too large. Maximum size is {MAX_UPLOAD_BYTES // (1024 * 1024)} MB.",
        )
    try:
        content = body.decode("utf-8")
    except UnicodeDecodeError:
        raise HTTPException(status_code=400, detail="File must be valid UTF-8 text.")
    try:
        if_none_match = request.headers.get("if-none-match")
        if if_none_match == "*" and storage.exists(name):
            raise HTTPException(
                status_code=412,
                detail=f"Site '{name}' already exists. Omit If-None-Match header to overwrite.",
            )
        info = storage.put(name, content)
        logger.info("Uploaded site %r (%d bytes)", name, info.size)
        return SiteResponse(**info.__dict__)
    except InvalidNameError as e:
        raise HTTPException(status_code=400, detail=str(e))


@router.delete("/sites/{name}", response_model=MessageResponse)
def delete_site(
    name: str,
    storage=Depends(get_storage),
    _auth=Depends(require_auth),
):
    try:
        if not storage.delete(name):
            raise HTTPException(status_code=404, detail="Site not found")
        logger.info("Deleted site %r", name)
        return MessageResponse(message=f"Deleted {name}")
    except InvalidNameError as e:
        raise HTTPException(status_code=400, detail=str(e))


@router.post("/password", response_model=PasswordChangeResponse)
def change_password(
    body: PasswordChangeRequest,
    request: Request,
    _auth=Depends(require_auth),
):
    config = get_config(request)
    if not verify_password(body.current_password, config.password_hash):
        raise HTTPException(status_code=401, detail="Current password is incorrect")
    config.password_hash = hash_password(body.new_password)
    config.jwt_secret = secrets.token_hex(32)
    config.save()
    token = create_token(config.jwt_secret)
    return PasswordChangeResponse(message="Password updated", token=token)
