"""Random number generation for the Mantimon TCG game engine.

This module provides a RandomProvider protocol with two implementations:
- SeededRandom: Deterministic RNG for testing and replays
- SecureRandom: Cryptographically secure RNG for production PvP

The RandomProvider abstraction allows the game engine to be tested with
predictable random outcomes while using secure randomness in production.

Usage:
    # In tests - predictable outcomes
    rng = SeededRandom(seed=42)
    result = rng.coin_flip()  # Always the same with same seed

    # In production - secure randomness
    rng = SecureRandom()
    result = rng.coin_flip()  # Unpredictable

    # In game engine
    class GameEngine:
        def __init__(self, rng: RandomProvider | None = None):
            self.rng = rng or SecureRandom()
"""

import random
import secrets
from collections.abc import MutableSequence, Sequence
from typing import Protocol, TypeVar

T = TypeVar("T")


class RandomProvider(Protocol):
    """Protocol for random number generation in the game engine.

    This protocol defines the interface for all random operations needed
    by the game engine. Implementations can be deterministic (for testing)
    or cryptographically secure (for production).

    All methods should be treated as if they have side effects (advancing
    the RNG state), even if the underlying implementation is deterministic.
    """

    def random(self) -> float:
        """Return a random float in the range [0.0, 1.0).

        Returns:
            A random float between 0.0 (inclusive) and 1.0 (exclusive).
        """
        ...

    def randint(self, a: int, b: int) -> int:
        """Return a random integer N such that a <= N <= b.

        Args:
            a: Lower bound (inclusive).
            b: Upper bound (inclusive).

        Returns:
            A random integer between a and b, inclusive.
        """
        ...

    def choice(self, seq: Sequence[T]) -> T:
        """Return a random element from a non-empty sequence.

        Args:
            seq: A non-empty sequence to choose from.

        Returns:
            A randomly selected element from the sequence.

        Raises:
            IndexError: If the sequence is empty.
        """
        ...

    def shuffle(self, seq: MutableSequence[T]) -> None:
        """Shuffle a mutable sequence in place.

        Args:
            seq: A mutable sequence to shuffle. Modified in place.
        """
        ...

    def coin_flip(self) -> bool:
        """Simulate a fair coin flip.

        Returns:
            True for heads, False for tails.
        """
        ...

    def sample(self, population: Sequence[T], k: int) -> list[T]:
        """Return k unique random elements from population.

        Args:
            population: Sequence to sample from.
            k: Number of elements to select.

        Returns:
            A list of k unique elements from population.

        Raises:
            ValueError: If k is larger than the population.
        """
        ...


class SeededRandom:
    """Deterministic random number generator for testing and replays.

    Uses Python's random.Random with a seed for reproducible sequences.
    Given the same seed, the sequence of random values will always be identical.

    Example:
        rng1 = SeededRandom(seed=12345)
        rng2 = SeededRandom(seed=12345)

        # These will always be equal
        assert rng1.randint(1, 100) == rng2.randint(1, 100)
        assert rng1.coin_flip() == rng2.coin_flip()
    """

    def __init__(self, seed: int | None = None) -> None:
        """Initialize with an optional seed.

        Args:
            seed: Integer seed for the RNG. If None, uses a random seed
                  (making this instance non-deterministic).
        """
        self._rng = random.Random(seed)
        self._seed = seed

    @property
    def seed(self) -> int | None:
        """The seed used to initialize this RNG, if any."""
        return self._seed

    def random(self) -> float:
        """Return a random float in the range [0.0, 1.0)."""
        return self._rng.random()

    def randint(self, a: int, b: int) -> int:
        """Return a random integer N such that a <= N <= b."""
        return self._rng.randint(a, b)

    def choice(self, seq: Sequence[T]) -> T:
        """Return a random element from a non-empty sequence."""
        return self._rng.choice(seq)

    def shuffle(self, seq: MutableSequence[T]) -> None:
        """Shuffle a mutable sequence in place."""
        self._rng.shuffle(seq)

    def coin_flip(self) -> bool:
        """Simulate a fair coin flip. True = heads, False = tails."""
        return self._rng.random() < 0.5

    def sample(self, population: Sequence[T], k: int) -> list[T]:
        """Return k unique random elements from population."""
        return self._rng.sample(list(population), k)


class SecureRandom:
    """Cryptographically secure random number generator for production.

    Uses Python's secrets module for unpredictable randomness. This should
    be used for all PvP games where predictability would be a security issue.

    Note: This implementation cannot be seeded and is not reproducible,
    which is the intended behavior for production use.
    """

    def random(self) -> float:
        """Return a random float in the range [0.0, 1.0)."""
        # secrets.randbelow returns [0, n), so we divide to get [0.0, 1.0)
        return secrets.randbelow(2**53) / (2**53)

    def randint(self, a: int, b: int) -> int:
        """Return a random integer N such that a <= N <= b."""
        if a > b:
            raise ValueError(f"a ({a}) must be <= b ({b})")
        return a + secrets.randbelow(b - a + 1)

    def choice(self, seq: Sequence[T]) -> T:
        """Return a random element from a non-empty sequence."""
        if not seq:
            raise IndexError("Cannot choose from empty sequence")
        return seq[secrets.randbelow(len(seq))]

    def shuffle(self, seq: MutableSequence[T]) -> None:
        """Shuffle a mutable sequence in place using Fisher-Yates algorithm."""
        n = len(seq)
        for i in range(n - 1, 0, -1):
            j = secrets.randbelow(i + 1)
            seq[i], seq[j] = seq[j], seq[i]

    def coin_flip(self) -> bool:
        """Simulate a fair coin flip. True = heads, False = tails."""
        return secrets.randbelow(2) == 0

    def sample(self, population: Sequence[T], k: int) -> list[T]:
        """Return k unique random elements from population."""
        if k > len(population):
            raise ValueError(f"Sample size {k} is larger than population size {len(population)}")
        if k < 0:
            raise ValueError(f"Sample size {k} must be non-negative")

        # Use a set to track selected indices
        pool = list(population)
        result: list[T] = []
        for _ in range(k):
            idx = secrets.randbelow(len(pool))
            result.append(pool.pop(idx))
        return result


def create_rng(seed: int | None = None, secure: bool = False) -> RandomProvider:
    """Factory function to create an appropriate RandomProvider.

    Args:
        seed: If provided, creates a SeededRandom with this seed.
        secure: If True and no seed provided, creates SecureRandom.
                If False and no seed provided, creates unseeded SeededRandom.

    Returns:
        A RandomProvider instance.

    Example:
        # For testing with reproducible results
        rng = create_rng(seed=42)

        # For production PvP
        rng = create_rng(secure=True)
    """
    if seed is not None:
        return SeededRandom(seed=seed)
    if secure:
        return SecureRandom()
    return SeededRandom()
