from __future__ import annotations

import hashlib
import time
import uuid
from typing import Optional

from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession

from db.models import BackupCode, Quota, RefreshToken, User
from deps.auth import get_current_user
from deps.db import get_db
from deps.utils import get_client_ip
from services import auth as auth_service
from services.audit import write_audit_log
from api.auth.shared import (
    limiter,
    RegisterRequest,
    LoginRequest,
    PreferencesUpdate,
    _set_refresh_cookie,
    _user_dict,
)

router = APIRouter()


# ── POST /register ────────────────────────────────────────────────────────────

@router.post("/register", status_code=status.HTTP_201_CREATED)
@limiter.limit("10/minute")
async def register(
    request: Request,
    body: RegisterRequest,
    session: AsyncSession = Depends(get_db),
):
    try:
        auth_service.validate_password_strength(body.password)
    except ValueError as exc:
        raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc))

    if await auth_service.check_hibp(body.password):
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            detail="This password has appeared in a data breach. Choose a different password.",
        )

    result = await session.execute(
        select(User).where(
            (User.email == str(body.email)) | (User.handle == body.handle)
        )
    )
    if result.scalar_one_or_none() is not None:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail="Email or handle already in use",
        )

    user_id = uuid.uuid4()
    new_user = User(
        id=user_id,
        handle=body.handle,
        email=str(body.email),
        password_hash=auth_service.hash_password(body.password),
        role="user",
        is_active=True,
        password_must_change=False,
    )
    quota = Quota(
        user_id=user_id,
        limit_bytes=104857600,  # 100 MB default (STORE-01)
        used_bytes=0,
    )
    try:
        session.add(new_user)
        await session.flush()  # persist User before Quota FK
        session.add(quota)
        await session.commit()
        await session.refresh(new_user)
    except IntegrityError:
        await session.rollback()
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail="Email or handle already in use",
        )

    return {
        "id": str(new_user.id),
        "handle": new_user.handle,
        "email": new_user.email,
        "role": new_user.role,
        "totp_enabled": new_user.totp_enabled,
        "created_at": new_user.created_at.isoformat() if new_user.created_at else None,
    }


# ── POST /login ───────────────────────────────────────────────────────────────

@router.post("/login")
@limiter.limit("10/minute")
async def login(
    request: Request,
    body: LoginRequest,
    response: Response,
    session: AsyncSession = Depends(get_db),
):
    # Per-account rate limiting (SEC-02): Redis counter keyed by email, checked before any DB lookup
    redis_client = request.app.state.redis
    rate_key = f"login_attempts:{body.email}"
    count = await redis_client.incr(rate_key)
    if count == 1:
        await redis_client.expire(rate_key, 900)
    if count > 10:
        raise HTTPException(
            status_code=status.HTTP_429_TOO_MANY_REQUESTS,
            detail="Too many login attempts. Try again in 15 minutes.",
        )

    result = await session.execute(select(User).where(User.email == str(body.email)))
    user: Optional[User] = result.scalar_one_or_none()

    _ip = get_client_ip(request)

    # Verify password (anti-enumeration: same error regardless of whether user exists)
    if user is None or not auth_service.verify_password(body.password, user.password_hash):
        await write_audit_log(
            session,
            event_type="auth.login_failed",
            user_id=user.id if user else None,
            actor_id=user.id if user else None,
            resource_id=None,
            ip_address=_ip,
            metadata_={"attempted_email_hash": hashlib.sha256(str(body.email).encode()).hexdigest()[:16]},
        )
        await session.commit()
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect email or password",
        )

    if not user.is_active:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Account deactivated",
        )

    # Password must change: return challenge without issuing tokens (T-02-16)
    if user.password_must_change:
        return {"requires_password_change": True, "user_id": str(user.id)}

    if user.totp_enabled:
        if body.totp_code is None and body.backup_code is None:
            return {"requires_totp": True}

        if body.totp_code is not None:
            # TOTP path takes precedence (even if backup_code also provided)
            ok = await auth_service.verify_totp(session, user.id, body.totp_code, redis_client)
            if not ok:
                raise HTTPException(
                    status_code=status.HTTP_401_UNAUTHORIZED,
                    detail="Incorrect code",
                )
        else:
            # Backup code path (body.backup_code is not None and body.totp_code is None)
            ok = await auth_service.verify_backup_code(session, user.id, body.backup_code)
            if not ok:
                raise HTTPException(
                    status_code=status.HTTP_401_UNAUTHORIZED,
                    detail="Invalid or already used code",
                )
            # D-13: backup code used event
            await write_audit_log(
                session,
                event_type="auth.backup_code_used",
                user_id=user.id,
                actor_id=user.id,
                resource_id=None,
                ip_address=_ip,
            )

    access_token = auth_service.create_access_token(
        str(user.id),
        user.role,
        user_agent=request.headers.get("User-Agent", ""),
        accept_lang=request.headers.get("Accept-Language", ""),
    )
    raw_refresh = await auth_service.create_refresh_token(session, user.id, remember_me=body.remember_me)
    _set_refresh_cookie(response, raw_refresh, remember_me=body.remember_me)

    # D-13: login success event
    await write_audit_log(
        session,
        event_type="auth.login",
        user_id=user.id,
        actor_id=user.id,
        resource_id=None,
        ip_address=_ip,
        metadata_={"totp_used": user.totp_enabled and body.totp_code is not None},
    )
    await session.commit()

    return {
        "access_token": access_token,
        "user": {
            "id": str(user.id),
            "handle": user.handle,
            "email": user.email,
            "role": user.role,
            "totp_enabled": user.totp_enabled,
        },
    }


# ── POST /refresh ─────────────────────────────────────────────────────────────

@router.post("/refresh")
@limiter.limit("10/minute")
async def refresh_token(
    request: Request,
    response: Response,
    session: AsyncSession = Depends(get_db),
):
    # Token reuse (revoked token presented) revokes entire family — family revocation security contract
    raw_token = request.cookies.get("refresh_token")
    if not raw_token:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="No refresh token",
        )

    try:
        new_raw, user_id_str = await auth_service.rotate_refresh_token(session, raw_token)
    except ValueError as exc:
        if "token_family_revoked" in str(exc):
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Session revoked",
            ) from exc
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid or expired refresh token",
        ) from exc

    user = await session.get(User, uuid.UUID(user_id_str))
    if user is None or not user.is_active:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="User not found or deactivated",
        )

    _set_refresh_cookie(response, new_raw)

    access_token = auth_service.create_access_token(
        user_id_str,
        user.role,
        user_agent=request.headers.get("User-Agent", ""),
        accept_lang=request.headers.get("Accept-Language", ""),
    )
    return {
        "access_token": access_token,
        "user": {
            "id": str(user.id),
            "handle": user.handle,
            "email": user.email,
            "role": user.role,
            "totp_enabled": user.totp_enabled,
        },
    }


# ── POST /logout ──────────────────────────────────────────────────────────────

@router.post("/logout")
async def logout(request: Request, response: Response, session: AsyncSession = Depends(get_db)):
    import hashlib as _hashlib

    _ip = get_client_ip(request)

    raw_token = request.cookies.get("refresh_token")
    _logout_user_id = None
    if raw_token:
        token_hash = _hashlib.sha256(raw_token.encode()).hexdigest()
        result = await session.execute(
            select(RefreshToken).where(RefreshToken.token_hash == token_hash)
        )
        row: Optional[RefreshToken] = result.scalar_one_or_none()
        if row is not None:
            _logout_user_id = row.user_id
            row.revoked = True
            # D-13: logout event (written before commit, within same transaction)
            await write_audit_log(
                session,
                event_type="auth.logout",
                user_id=_logout_user_id,
                actor_id=_logout_user_id,
                resource_id=None,
                ip_address=_ip,
            )
            await session.commit()

    response.delete_cookie("refresh_token", path="/api/auth/refresh")
    return {"message": "Logged out"}


# ── POST /logout-all ──────────────────────────────────────────────────────────

@router.post("/logout-all")
async def logout_all(
    request: Request,
    response: Response,
    session: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    _ip = get_client_ip(request)
    count = await auth_service.revoke_all_refresh_tokens(session, current_user.id)
    # D-13: sign-out-all event
    await write_audit_log(
        session,
        event_type="auth.sign_out_all",
        user_id=current_user.id,
        actor_id=current_user.id,
        resource_id=None,
        ip_address=_ip,
        metadata_={"sessions_revoked": count},
    )
    await session.commit()
    response.delete_cookie("refresh_token", path="/api/auth/refresh")
    return {"message": f"Signed out of {count} session(s)"}


# ── GET /me ───────────────────────────────────────────────────────────────────

@router.get("/me")
async def get_me(current_user: User = Depends(get_current_user)):
    return _user_dict(current_user)


# ── GET /me/quota ─────────────────────────────────────────────────────────────

@router.get("/me/quota")
async def get_my_quota(
    current_user: User = Depends(get_current_user),
    session: AsyncSession = Depends(get_db),
):
    q = await session.get(Quota, current_user.id)
    if q is None:
        raise HTTPException(status_code=404, detail="Quota not found")
    return {"used_bytes": q.used_bytes, "limit_bytes": q.limit_bytes}


# ── GET /me/preferences ───────────────────────────────────────────────────────

@router.get("/me/preferences")
async def get_my_preferences(
    current_user: User = Depends(get_current_user),
):
    # Falls back to 'in_app' if the column is absent (migration not yet run)
    try:
        pdf_open_mode = current_user.pdf_open_mode
    except AttributeError:
        pdf_open_mode = "in_app"
    return {"pdf_open_mode": pdf_open_mode}


# ── PATCH /me/preferences ─────────────────────────────────────────────────────

@router.patch("/me/preferences")
async def update_my_preferences(
    body: PreferencesUpdate,
    session: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    # Pydantic Literal["in_app", "new_tab"] enforces strict allowlist (T-04-05-05)
    user = await session.get(User, current_user.id)
    if user is None:
        raise HTTPException(status_code=404, detail="User not found")
    user.pdf_open_mode = body.pdf_open_mode
    session.add(user)
    await session.commit()
    return {"pdf_open_mode": user.pdf_open_mode}
