"""Auth endpoints (spec §51). Argon2 password hashing, JWT access+refresh.

AUDIT FIX (StockLab overhaul, security audit): `limiter` (slowapi) was already constructed in
deps.py and wired into main.py's exception handler / app.state, but no route anywhere actually
carried an `@limiter.limit(...)` decorator -- slowapi only enforces a limit on routes explicitly
decorated with one, so despite RATE_LIMIT_PER_MINUTE existing in config, nothing was rate-limited
and /login had no brute-force throttling at all. Fixed by decorating the three auth routes below.
/login gets a tighter limit than the general API default (settings.RATE_LIMIT_PER_MINUTE, still
used as the default for non-auth routes) since it's the credential-guessing target; /register and
/refresh get the general default. This is IP-keyed (get_remote_address in deps.py) -- it slows a
single-source brute force but does not stop a distributed one; a per-account lockout/backoff would
need persistent state this build doesn't have and is documented as a further gap, not built here.

AUDIT FIX (StockLab overhaul, final engineering pass, Part A4, docs/AUDIT_AUTH_A4.md): before this
pass, a refresh token was valid for its full 14-day lifetime no matter what -- there was no way to
log out server-side (a client discarding the token was the only "logout"), and a leaked refresh
token worked until it naturally expired. /refresh now ROTATES: the presented refresh token is
revoked the moment a new pair is issued, so it cannot be replayed. A new /logout route revokes a
refresh token on demand. Access tokens are unaffected (still stateless, still validated with zero
DB cost per request, app/api/v1/deps.py::get_current_user) -- only the refresh flow gained a DB
dependency, by design (see app/core/token_revocation.py's docstring for why).
"""

from datetime import datetime, timezone

from fastapi import APIRouter, Depends, HTTPException, Request, status
from sqlalchemy.orm import Session

from app.api.v1.deps import limiter
from app.core.config import get_settings
from app.core.db import get_db
from app.core.security import create_access_token, create_refresh_token, decode_token, hash_password, verify_password
from app.core.token_revocation import is_token_revoked, revoke_refresh_token
from app.models import AuditLogEntry, User
from app.schemas.auth import LoginRequest, RefreshRequest, RegisterRequest, TokenResponse

router = APIRouter(prefix="/v1/auth", tags=["auth"])

# Computed once at import time from settings.RATE_LIMIT_PER_MINUTE rather than re-read per
# request -- get_settings() is itself lru_cache'd (one Settings instance per process), so this is
# equivalent to reading it live and avoids slowapi's dynamic-limit (callable) form for a value
# that never changes after process start anyway.
_GENERAL_RATE_LIMIT = f"{get_settings().RATE_LIMIT_PER_MINUTE}/minute"
_LOGIN_RATE_LIMIT = "10/minute"  # deliberately tighter than the general API default -- see module docstring


@router.post("/register", response_model=TokenResponse, status_code=status.HTTP_201_CREATED)
@limiter.limit(_GENERAL_RATE_LIMIT)
def register(request: Request, payload: RegisterRequest, db: Session = Depends(get_db)):
    if db.query(User).filter_by(email=payload.email).first():
        raise HTTPException(status.HTTP_409_CONFLICT, "Email already registered")
    user = User(email=payload.email, hashed_password=hash_password(payload.password), display_name=payload.display_name)
    db.add(user)
    db.commit()
    db.refresh(user)
    db.add(AuditLogEntry(actor_user_id=user.id, event_type="REGISTER"))
    db.commit()
    return TokenResponse(access_token=create_access_token(user.id), refresh_token=create_refresh_token(user.id))


@router.post("/login", response_model=TokenResponse)
@limiter.limit(_LOGIN_RATE_LIMIT)
def login(request: Request, payload: LoginRequest, db: Session = Depends(get_db)):
    user = db.query(User).filter_by(email=payload.email).first()
    if not user or not verify_password(payload.password, user.hashed_password):
        db.add(AuditLogEntry(event_type="LOGIN_FAILED", detail={"email": payload.email}))
        db.commit()
        raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Incorrect email or password")
    db.add(AuditLogEntry(actor_user_id=user.id, event_type="LOGIN"))
    db.commit()
    return TokenResponse(access_token=create_access_token(user.id), refresh_token=create_refresh_token(user.id))


@router.post("/refresh", response_model=TokenResponse)
@limiter.limit(_GENERAL_RATE_LIMIT)
def refresh(request: Request, payload: RefreshRequest, db: Session = Depends(get_db)):
    data = decode_token(payload.refresh_token)
    if data is None or data.get("type") != "refresh" or "jti" not in data:
        raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid refresh token")
    # AUDIT FIX (StockLab overhaul, Part A4): reject a refresh token that was already used-and-
    # rotated, or explicitly logged out, even though its signature and `exp` are still valid --
    # this is exactly the check a stateless-only JWT scheme cannot make, and the reason this
    # endpoint needs a DB round-trip at all.
    if is_token_revoked(db, data["jti"]):
        raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Refresh token has been revoked")
    user = db.get(User, data["sub"])
    if user is None or not user.is_active:
        raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid refresh token")

    new_access_token = create_access_token(user.id)
    new_refresh_token = create_refresh_token(user.id)
    # AUDIT FIX (StockLab overhaul, Part A4): ROTATION -- the just-presented refresh token is
    # revoked the instant its replacement is issued, so it cannot be replayed (by an attacker who
    # captured it, or by a client bug that resubmits it). `data["exp"]` is the token's own Unix
    # timestamp claim (python-jose converts the datetime given to create_refresh_token() at encode
    # time) -- reused here rather than recomputed, so the stored expiry always matches what the
    # token itself claimed, never drifts from it.
    revoke_refresh_token(
        db, jti=data["jti"], user_id=user.id,
        expires_at=datetime.fromtimestamp(data["exp"], tz=timezone.utc), reason="rotated",
    )
    db.add(AuditLogEntry(actor_user_id=user.id, event_type="TOKEN_REFRESH"))
    db.commit()
    return TokenResponse(access_token=new_access_token, refresh_token=new_refresh_token)


@router.post("/logout", status_code=status.HTTP_204_NO_CONTENT)
@limiter.limit(_GENERAL_RATE_LIMIT)
def logout(request: Request, payload: RefreshRequest, db: Session = Depends(get_db)):
    """AUDIT FIX (StockLab overhaul, final engineering pass, Part A4): revokes a refresh token on
    demand -- the server-side "logout" that didn't exist before this pass (a client discarding its
    tokens was the only prior mechanism, which does nothing if the token was already copied
    elsewhere). Deliberately takes only the refresh token itself, not an access-token-authenticated
    `Depends(get_current_user)` -- consistent with /refresh's own model (possession of a valid
    refresh token is what it means to hold that session), and with RFC 7009-style token revocation
    endpoints. An already-invalid token (expired, malformed, or already revoked) is treated as
    "already logged out" -- 204 either way -- rather than surfaced as an error a client would need
    to handle specially; logout is idempotent by design (see revoke_refresh_token()'s docstring).
    Access tokens already issued from this refresh token remain valid until their own short expiry
    -- see this module's docstring for why that's an accepted scope boundary, not an oversight.
    """
    data = decode_token(payload.refresh_token)
    if data is None or data.get("type") != "refresh" or "jti" not in data:
        return  # nothing to revoke -- 204, not an error (see docstring)
    revoke_refresh_token(
        db, jti=data["jti"], user_id=data.get("sub"),
        expires_at=datetime.fromtimestamp(data["exp"], tz=timezone.utc), reason="logout",
    )
    db.add(AuditLogEntry(actor_user_id=data.get("sub"), event_type="LOGOUT"))
    db.commit()
