"""
Entropy-aware identifier generation for idio.
"""

from __future__ import annotations

import math
import secrets
from dataclasses import dataclass
from enum import Enum
from typing import Iterable, List, Sequence

from .data import DEFAULT_BANK, IdioWordBank


class Pattern(str, Enum):
    """Supported grammar patterns."""

    M1 = "lat_adj_lat_noun_gr_noun"  # <latin_adjective>_<latin_noun>_<greek_noun>
    M2 = "gr_mod_gr_noun_lat_noun"  # <greek_modifier>_<greek_noun>_<latin_noun>
    M3 = "lat_noun_lat_adj_gr_noun"  # <latin_noun>_<latin_adjective>_<greek_noun>


@dataclass
class IdioGenerator:
    """
    Generator configured with a word bank and PRNG.

    The PRNG must be cryptographically secure; by default we rely on
    ``secrets.SystemRandom`` to satisfy that guarantee. Separators default to
    DNS-safe choices: ``-`` between words, ``.`` between segments.
    """

    bank: IdioWordBank = DEFAULT_BANK
    patterns: Sequence[Pattern] = (Pattern.M1, Pattern.M2, Pattern.M3)
    word_separator: str = "-"
    segment_separator: str = "."
    _rand = secrets.SystemRandom()

    def __post_init__(self) -> None:
        self._validate_separator(self.word_separator, allow_dot=False, name="word_separator")
        self._validate_separator(self.segment_separator, allow_dot=True, name="segment_separator")

    @staticmethod
    def _validate_separator(separator: str, *, allow_dot: bool, name: str) -> None:
        allowed = set("-_")
        if allow_dot:
            allowed.add(".")

        allowed_desc = ", ".join(sorted(allowed))
        if (
            not separator
            or any(ch not in allowed for ch in separator)
            or separator[0] not in allowed
            or separator[-1] not in allowed
        ):
            raise ValueError(f"{name} must be a non-empty string made only of {allowed_desc} characters.")

    def segment(self, *, pattern: Pattern | None = None) -> str:
        """Generate a single segment using one of the patterns."""

        chosen = pattern or self._rand.choice(self.patterns)
        if chosen == Pattern.M1:
            parts = (
                self._rand.choice(self.bank.latin_adjectives),
                self._rand.choice(self.bank.latin_nouns),
                self._rand.choice(self.bank.greek_nouns),
            )
        elif chosen == Pattern.M2:
            parts = (
                self._rand.choice(self.bank.greek_modifiers),
                self._rand.choice(self.bank.greek_nouns),
                self._rand.choice(self.bank.latin_nouns),
            )
        else:
            parts = (
                self._rand.choice(self.bank.latin_nouns),
                self._rand.choice(self.bank.latin_adjectives),
                self._rand.choice(self.bank.greek_nouns),
            )

        return self.word_separator.join(parts)

    def identifier(self, *, segments: int = 2, patterns: Sequence[Pattern] | None = None) -> str:
        """
        Generate a full identifier with the given number of segments.

        The default of ``segments=2`` meets the spec's >32-bit entropy
        requirement when using the bundled wordlists.
        """

        if segments < 1:
            raise ValueError("segments must be >= 1")

        available_patterns: Sequence[Pattern] = patterns if patterns else self.patterns
        segs: List[str] = []
        for _ in range(segments):
            segs.append(self.segment(pattern=self._rand.choice(available_patterns)))
        return self.segment_separator.join(segs)


def generate_segment(
    *,
    pattern: Pattern | None = None,
    bank: IdioWordBank = DEFAULT_BANK,
    word_separator: str = "-",
) -> str:
    """Convenience wrapper that uses the default generator to emit one segment."""

    return IdioGenerator(bank=bank, word_separator=word_separator).segment(pattern=pattern)


def generate_identifier(
    *,
    segments: int = 2,
    patterns: Sequence[Pattern] | None = None,
    bank: IdioWordBank = DEFAULT_BANK,
    word_separator: str = "-",
    segment_separator: str = ".",
) -> str:
    """
    Generate a full identifier using the default generator.

    Parameters mirror :meth:`IdioGenerator.identifier` and add explicit
    control over separators for DNS/subdomain compatibility.
    """

    return IdioGenerator(
        bank=bank,
        patterns=patterns or (Pattern.M1, Pattern.M2, Pattern.M3),
        word_separator=word_separator,
        segment_separator=segment_separator,
    ).identifier(
        segments=segments,
        patterns=patterns,
    )


def entropy_bits(
    *,
    segments: int = 2,
    patterns: Iterable[Pattern] = (Pattern.M1, Pattern.M2, Pattern.M3),
    bank: IdioWordBank = DEFAULT_BANK,
) -> float:
    """
    Compute the log2 of the identifier space for the given configuration.

    The calculation assumes independent segments and counts the total
    combination space across the provided patterns.
    """

    pattern_list = tuple(patterns)
    if len(pattern_list) == 0:
        raise ValueError("At least one pattern is required to calculate entropy.")

    def combinations_for_pattern(pattern: Pattern) -> int:
        if pattern == Pattern.M2:
            return bank.size_for_greek_modifier_pattern()
        return bank.size_per_pattern()

    combinations_per_segment = sum(combinations_for_pattern(p) for p in pattern_list)
    if combinations_per_segment <= 0:
        raise ValueError("Word banks must contain entries for all slots.")

    return segments * math.log2(combinations_per_segment)
