"""
Security utilities: password hashing and JWT token management.

Design decisions:
- direct bcrypt — industry standard for password hashing with automatic salting.
- PyJWT for JWT — signed with HS256, configurable expiry from settings.
- Access tokens are short-lived (default 30 min), refresh tokens long-lived (7 days).
- Refresh tokens are stored in PostgreSQL on issue and deleted on logout (Commit #18).
  This makes logout truly effective — stateless JWTs alone cannot be invalidated.

Token payload claims:
  sub   → user UUID (string)
  type  → "access" | "refresh"
  iat   → issued-at (auto-added by PyJWT)
  exp   → expiry (auto-added by PyJWT)
"""

import datetime
from uuid import uuid4

import bcrypt
import jwt
from jwt.exceptions import PyJWTError

from app.config import settings
from app.core.exceptions import UnauthorizedException

# ── Password Hashing ──────────────────────────────────────────────────────────


def hash_password(plain: str) -> str:
    """
    Hash a plain-text password using bcrypt.

    Uses automatic salt generation with default rounds (12).
    """
    pwd_bytes = plain.encode("utf-8")
    salt = bcrypt.gensalt()
    return bcrypt.hashpw(pwd_bytes, salt).decode("utf-8")


def verify_password(plain: str, hashed: str) -> bool:
    """
    Verify a plain-text password against a stored bcrypt hash.

    Returns False (does not raise) on mismatch or invalid hash format.
    """
    try:
        return bcrypt.checkpw(plain.encode("utf-8"), hashed.encode("utf-8"))
    except Exception:
        return False


# ── JWT Utilities ─────────────────────────────────────────────────────────────

_ACCESS_TOKEN_TYPE = "access"
_REFRESH_TOKEN_TYPE = "refresh"


def create_access_token(
    subject: str,
    extra_claims: dict[str, object] | None = None,
) -> str:
    """
    Create a short-lived JWT access token.

    Args:
        subject:      The user UUID as a string — stored as the `sub` claim.
        extra_claims: Optional additional claims to embed (e.g. lab_id, role).

    Returns:
        Signed JWT string.
    """
    expire = datetime.datetime.now(datetime.UTC) + datetime.timedelta(
        minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES
    )
    payload: dict[str, object] = {
        "sub": subject,
        "type": _ACCESS_TOKEN_TYPE,
        "jti": uuid4().hex,
        "exp": expire,
        **(extra_claims or {}),
    }
    return jwt.encode(payload, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM)


def create_refresh_token(subject: str) -> str:
    """
    Create a long-lived JWT refresh token.

    Refresh tokens contain only `sub` and `type` — no extra claims.
    The access token is the authority for permissions; refresh tokens
    are only valid for issuing new access tokens.

    Args:
        subject: The user UUID as a string.

    Returns:
        Signed JWT string.
    """
    expire = datetime.datetime.now(datetime.UTC) + datetime.timedelta(
        days=settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS
    )
    payload: dict[str, object] = {
        "sub": subject,
        "type": _REFRESH_TOKEN_TYPE,
        "jti": uuid4().hex,
        "exp": expire,
    }
    return jwt.encode(payload, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM)


def decode_token(token: str) -> dict[str, object]:
    """
    Decode and validate a JWT token.

    Verifies:
      - Signature (using JWT_SECRET_KEY)
      - Expiry (`exp` claim)
      - Algorithm matches JWT_ALGORITHM

    Raises:
        UnauthorizedException: Token is invalid, expired, or malformed.

    Returns:
        Decoded payload dict.
    """
    try:
        payload: dict[str, object] = jwt.decode(
            token,
            settings.JWT_SECRET_KEY,
            algorithms=[settings.JWT_ALGORITHM],
            options={"require": ["exp", "sub", "type"]},
        )
        return payload
    except PyJWTError as exc:
        raise UnauthorizedException(
            message="Token is invalid or has expired. Please log in again.",
            detail=str(exc),
        ) from exc


def decode_access_token(token: str) -> dict[str, object]:
    """
    Decode a token and assert it is an access token.

    Raises:
        UnauthorizedException: Token is a refresh token or invalid.
    """
    payload = decode_token(token)
    if payload.get("type") != _ACCESS_TOKEN_TYPE:
        raise UnauthorizedException(message="An access token is required for this endpoint.")
    return payload


def decode_refresh_token(token: str) -> dict[str, object]:
    """
    Decode a token and assert it is a refresh token.

    Raises:
        UnauthorizedException: Token is an access token or invalid.
    """
    payload = decode_token(token)
    if payload.get("type") != _REFRESH_TOKEN_TYPE:
        raise UnauthorizedException(message="A refresh token is required to obtain a new access token.")
    return payload
