"""
Password hashing and JWT issuance/verification for the self-contained auth
path (see app/core/security.py::get_auth_context, which consumes
decode_access_token). Uses the `bcrypt` package directly rather than
passlib — passlib 1.7.4 (its last release) probes bcrypt's internals in a
way that's broken against bcrypt>=4.1, so it's not a safe pairing here.
"""

import uuid
from datetime import datetime, timedelta, timezone

from jose import JWTError, jwt

from app.core.config import settings

import bcrypt

_BCRYPT_MAX_BYTES = 72  # bcrypt silently ignores/rejects input past this


def hash_password(password: str) -> str:
    truncated = password.encode("utf-8")[:_BCRYPT_MAX_BYTES]
    return bcrypt.hashpw(truncated, bcrypt.gensalt()).decode("utf-8")


def verify_password(password: str, password_hash: str) -> bool:
    truncated = password.encode("utf-8")[:_BCRYPT_MAX_BYTES]
    try:
        return bcrypt.checkpw(truncated, password_hash.encode("utf-8"))
    except ValueError:
        return False


def session_lifetime(lifetime: timedelta | None = None) -> timedelta:
    return lifetime or timedelta(minutes=settings.jwt_expiry_minutes)


def create_access_token(
    *,
    tenant_id: uuid.UUID,
    user_id: uuid.UUID,
    vendor_id: uuid.UUID | None,
    amr: list[str] | None = None,
    lifetime: timedelta | None = None,
    session_id: uuid.UUID | None = None,
) -> str:
    """A session token. `amr` says how the person signed in ("pwd", "otp",
    "sso" — the OpenID Connect claim of that name); `lifetime` is the
    tenant's session length, when it sets one; `session_id` the sign-in it
    belongs to (app/crud/sessions.py), so it can be signed out."""
    now = datetime.now(timezone.utc)
    claims = {
        "tenant_id": str(tenant_id),
        "user_id": str(user_id),
        "vendor_id": str(vendor_id) if vendor_id else None,
        "amr": amr or ["pwd"],
        "iat": int(now.timestamp()),
        "exp": now + session_lifetime(lifetime),
    }
    if session_id is not None:
        claims["sid"] = str(session_id)
    return jwt.encode(claims, settings.jwt_secret, algorithm=settings.jwt_algorithm)


def create_platform_admin_token(*, admin_id: uuid.UUID) -> str:
    """Deliberately a different claim shape than create_access_token above —
    no "tenant_id" key at all (not even null) — so a regular session token
    and a Super Admin token are structurally distinguishable, not just by a
    flag that could be misread. See app/core/security.py::
    get_platform_admin_context, which rejects any token carrying tenant_id."""
    expires_at = datetime.now(timezone.utc) + timedelta(minutes=settings.jwt_expiry_minutes)
    claims = {"platform_admin_id": str(admin_id), "exp": expires_at}
    return jwt.encode(claims, settings.jwt_secret, algorithm=settings.jwt_algorithm)


class TokenError(Exception):
    pass


def decode_access_token(token: str) -> dict:
    try:
        return jwt.decode(token, settings.jwt_secret, algorithms=[settings.jwt_algorithm])
    except JWTError as exc:
        raise TokenError(str(exc)) from exc
