"""Refresh-token revocation/rotation store (StockLab overhaul, final engineering pass, Part A4).

Kept separate from app/core/security.py (which stays DB-free/pure -- JWT encode/decode only) since
this needs a live Session. Only refresh tokens are tracked here, deliberately: see
app/models/governance.py::RevokedToken's docstring for why access tokens are out of scope.

Design, in one sentence: on `/v1/auth/refresh`, the presented refresh token's `jti` is checked
against this table (reject if present -- already used or explicitly logged out), a new
access+refresh pair is issued (rotation), and the OLD refresh token's `jti` is recorded here as
revoked so it cannot be replayed. `/v1/auth/logout` does the same revocation step directly, with no
new token issued.
"""
from __future__ import annotations

from datetime import datetime, timezone
from typing import Optional

from sqlalchemy import delete, select
from sqlalchemy.orm import Session

from app.models import RevokedToken


def is_token_revoked(db: Session, jti: str) -> bool:
    """True if this jti has already been revoked (via logout or a prior rotation) -- callers
    should treat this as "reject the request" (401), not as an error condition itself."""
    return db.execute(select(RevokedToken.id).where(RevokedToken.jti == jti)).first() is not None


def revoke_refresh_token(db: Session, jti: str, user_id: Optional[str], expires_at: datetime, reason: str) -> None:
    """Record a refresh token's jti as revoked. Idempotent by design: if the jti is already
    revoked (e.g. a double-submitted logout, or a race between two rotations of the same token),
    this is a silent no-op rather than a unique-constraint error surfacing as a 500 -- revoking an
    already-revoked token should never be a caller-visible failure."""
    if is_token_revoked(db, jti):
        return
    db.add(RevokedToken(jti=jti, user_id=user_id, token_type="refresh", expires_at=expires_at, reason=reason))
    db.commit()


def prune_expired_revoked_tokens(db: Session) -> int:
    """Delete revoked-token rows past their own original expiry. Once a token's `exp` claim has
    passed, app/core/security.py::decode_token() rejects it on that basis alone (jose checks `exp`
    before this table would ever be consulted) -- so a revoked-row past its expiry carries zero
    remaining security value, only storage growth. Intended to run periodically (a Celery beat
    task, see app/workers/celery_app.py's beat_schedule and app/workers/token_cleanup.py) rather
    than on every request. Returns the number of rows deleted.
    """
    now = datetime.now(timezone.utc)
    result = db.execute(delete(RevokedToken).where(RevokedToken.expires_at < now))
    db.commit()
    return int(result.rowcount or 0)
