"""Invitation token and short-code generation (design D11, spec guest-management).

A token is a bearer secret: whoever holds it can see and change that guest's RSVP. It must
therefore be unguessable and must never be logged in full or leaked through a Referer.
"""

import secrets

from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.models import Invitation

# 16 random bytes -> 22 URL-safe characters, 128 bits of entropy.
TOKEN_BYTES = 16
TOKEN_LENGTH = 22

# Short codes get read off a printed card and typed by hand, so the alphabet excludes
# characters people confuse: 0/O, 1/I/L. 31^6 is about 887 million combinations, ample for
# an event with at most a few thousand invitations.
SHORT_CODE_ALPHABET = "23456789ABCDEFGHJKMNPQRSTUVWXYZ"
SHORT_CODE_LENGTH = 6

_MAX_ATTEMPTS = 10


def generate_token() -> str:
    """Return a 22-character URL-safe token with 128 bits of entropy."""
    return secrets.token_urlsafe(TOKEN_BYTES)


def generate_short_code() -> str:
    """Return a 6-character human-typeable code from an unambiguous alphabet."""
    return "".join(secrets.choice(SHORT_CODE_ALPHABET) for _ in range(SHORT_CODE_LENGTH))


def mask_token(token: str) -> str:
    """Render a token safe for logs. Never log the raw value."""
    if len(token) <= 6:
        return "***"
    return f"{token[:3]}...{token[-2:]}"


def normalize_short_code(raw: str) -> str:
    """Accept what a guest actually types: lowercase, spaces, confusable characters."""
    cleaned = raw.strip().upper().replace(" ", "").replace("-", "")
    # The two-argument form avoids the invariant dict[str, str] that mypy rejects,
    # and reads as what it is: a character-for-character substitution.
    return cleaned.translate(str.maketrans("OIL", "011"))


async def allocate_token(session: AsyncSession) -> str:
    """Generate a token that is not already in use.

    A collision at 128 bits is not a realistic event; the retry exists so that if the
    impossible happens we fail by trying again rather than by raising an integrity error
    in the middle of a bulk import.
    """
    for _ in range(_MAX_ATTEMPTS):
        candidate = generate_token()
        exists = await session.scalar(
            select(Invitation.id).where(Invitation.token == candidate).limit(1)
        )
        if exists is None:
            return candidate
    raise RuntimeError("Could not allocate a unique invitation token")


async def allocate_short_code(session: AsyncSession) -> str:
    """Generate a short code that is not already in use.

    Collisions here are plausible rather than theoretical, so the retry does real work.
    """
    for _ in range(_MAX_ATTEMPTS):
        candidate = generate_short_code()
        exists = await session.scalar(
            select(Invitation.id).where(Invitation.short_code == candidate).limit(1)
        )
        if exists is None:
            return candidate
    raise RuntimeError("Could not allocate a unique invitation short code")
