"""Event slug derivation (design D12, spec event-management).

A slug is the public address of an event — `/e/{slug}` goes on printed QR cards — so it is
allocated once at creation and never recomputed. Renaming an event deliberately leaves the
slug alone: a card already in a guest's hand cannot be reprinted.
"""

import re
import secrets
import unicodedata

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

from app.models import Event

MAX_LENGTH = 60
_NON_SLUG = re.compile(r"[^a-z0-9]+")
_TRIM = re.compile(r"^-+|-+$")

#: Same unambiguous alphabet as short codes: a slug suffix sometimes gets read aloud or typed.
_SUFFIX_ALPHABET = "23456789abcdefghjkmnpqrstuvwxyz"
_MAX_ATTEMPTS = 20


def slugify(name: str) -> str:
    """Reduce a display name to a URL-safe stem.

    Accented Latin is folded to ASCII, so "Réception" becomes "reception". Bangla has no
    ASCII equivalent and folds away to nothing — callers must handle an empty result rather
    than assume one, which is why `allocate` supplies a fallback stem.
    """
    folded = unicodedata.normalize("NFKD", name)
    ascii_only = folded.encode("ascii", "ignore").decode("ascii")
    slug = _NON_SLUG.sub("-", ascii_only.lower())
    return _TRIM.sub("", slug)[:MAX_LENGTH]


async def allocate(session: AsyncSession, name: str, *, fallback: str) -> str:
    """Return a slug for `name` that no event is using.

    `fallback` is the stem used when the name yields nothing usable — the event type, in
    practice, so a Bangla-only name still produces `walima-k4m2` rather than a bare code.
    """
    stem = slugify(name) or slugify(fallback) or "event"

    if await _is_free(session, stem):
        return stem

    # Suffix rather than a counter: `walima-2` invites the reading "the second Walima", which
    # is a claim about the event rather than about its address.
    for _ in range(_MAX_ATTEMPTS):
        suffix = "".join(secrets.choice(_SUFFIX_ALPHABET) for _ in range(4))
        candidate = f"{stem[: MAX_LENGTH - 5]}-{suffix}"
        if await _is_free(session, candidate):
            return candidate

    raise RuntimeError(f"Could not allocate a unique slug for {name!r}")


async def _is_free(session: AsyncSession, candidate: str) -> bool:
    existing = await session.scalar(select(Event.id).where(Event.slug == candidate).limit(1))
    return existing is None
