"""
truerandom.py

A random-module-like interface backed by the ANU Quantum Random Number
Generator (QRNG).

The ANU service supplies quantum random uint8/uint16 values over HTTPS.
This module buffers those values locally and builds higher-level operations
from them.

Requires:
    Python 3.9+
    requests

Install:
    pip install requests

Example:
    import truerandom as random

    print(random.random())
    print(random.randint(1, 100))
    print(random.choice(["red", "green", "blue"]))

    items = list(range(10))
    random.shuffle(items)
    print(items)
"""

from __future__ import annotations

import math
import threading
from collections import deque
from collections.abc import MutableSequence, Sequence
from typing import TypeVar

import requests

T = TypeVar("T")


class ANUQRNG:
    """ANU QRNG-backed replacement for much of the stdlib random API."""

    API_URL = "https://qrng.anu.edu.au/API/jsonI.php"
    MAX_REQUEST = 1024

    def __init__(
        self,
        *,
        api_url: str | None = None,
        timeout: float = 10.0,
        buffer_size: int = 1024,
    ):
        self.api_url = api_url or self.API_URL
        self.timeout = timeout
        self.buffer_size = max(1, buffer_size)

        self._bytes = deque()
        self._lock = threading.Lock()

        self._session = requests.Session()

    # ------------------------------------------------------------------
    # Low-level QRNG access
    # ------------------------------------------------------------------

    def _request(self, data_type: str, length: int) -> list[int]:
        if not 1 <= length <= self.MAX_REQUEST:
            raise ValueError(
                f"length must be between 1 and {self.MAX_REQUEST}"
            )

        try:
            response = self._session.get(
                self.api_url,
                params={
                    "length": length,
                "type": data_type,
                },
            timeout=self.timeout,
            )


            response.raise_for_status()
        except requests.RequestException as exc:
            raise RuntimeError(
                f"ANU QRNG request failed: {exc}"
            ) from exc

        try:
            payload = response.json()
        except ValueError as exc:
            raise RuntimeError(
                f"ANU QRNG returned invalid JSON: {response.text[:500]!r}"
            ) from exc

        if not payload.get("success", False):
            raise RuntimeError(
                f"ANU QRNG request was unsuccessful: {payload!r}"
            )

        data = payload.get("data")

        if not isinstance(data, list):
            raise TypeError(
                f"Invalid ANU QRNG response: {payload!r}"
            )

        return data


    def _get_uint8(self, n: int = 1) -> list[int]:
        """Get n quantum-random bytes from ANU."""
        result = []

        while n:
            batch = min(n, self.MAX_REQUEST)
            result.extend(self._request("uint8", batch))
            n -= batch

        return result


    def _get_byte(self) -> int:
        with self._lock:
            if not self._bytes:
                self._bytes.extend(
                    self._get_uint8(self.buffer_size)
                )

            return self._bytes.popleft()


    def _get_bits(self, k: int) -> int:
        """Return k quantum-random bits as an integer."""
        if k < 0:
            raise ValueError("number of bits must be non-negative")

        if k == 0:
            return 0

        value = 0
        remaining = k

        while remaining:
            byte = self._get_byte()

            take = min(remaining, 8)

            # Take the required number of bits from this byte.
            value = (value << take) | (
                byte & ((1 << take) - 1)
            )

            remaining -= take

        return value

    # ------------------------------------------------------------------
    # Basic random functions
    # ------------------------------------------------------------------

    def getrandbits(self, k: int) -> int:
        """Return a non-negative integer with k random bits."""
        return self._get_bits(k)

    def random(self) -> float:
        """Return a random float in [0.0, 1.0)."""
        # 53 bits gives the precision used by Python's random().
        return self.getrandbits(53) / (1 << 53)

    def randbytes(self, n: int) -> bytes:
        """Return n random bytes."""
        if n < 0:
            raise ValueError("number of bytes must be non-negative")

        result = bytearray()

        while n:
            batch = min(n, self.MAX_REQUEST)
            result.extend(self._get_uint8(batch))
            n -= batch

        return bytes(result)

    # ------------------------------------------------------------------
    # Integer generation
    # ------------------------------------------------------------------

    def _randbelow(self, n: int) -> int:
        if n <= 0:
            raise ValueError("n must be greater than zero")

        k = n.bit_length()

        while True:
            r = self.getrandbits(k)
            if r < n:
                return r

    def randrange(
        self,
        start: int,
        stop: int | None = None,
        step: int = 1,
    ) -> int:
        """Return a randomly selected element from range(start, stop, step)."""

        if stop is None:
            stop = start
            start = 0

        if step == 0:
            raise ValueError("step argument must not be zero")

        width = stop - start

        if step > 0:
            n = (width + step - 1) // step
        else:
            n = (width + step + 1) // step

        if n <= 0:
            raise ValueError("empty range for randrange()")

        return start + step * self._randbelow(n)

    def randint(self, a: int, b: int) -> int:
        """Return a random integer N such that a <= N <= b."""
        return self.randrange(a, b + 1)

    # ------------------------------------------------------------------
    # Sequence functions
    # ------------------------------------------------------------------

    def choice(self, seq: Sequence[T]) -> T:
        """Return a random element from a non-empty sequence."""
        if not seq:
            raise IndexError("Cannot choose from an empty sequence")

        return seq[self._randbelow(len(seq))]

    def choices(
        self,
        population: Sequence[T],
        weights: Sequence[float] | None = None,
        *,
        cum_weights: Sequence[float] | None = None,
        k: int = 1,
    ) -> list[T]:
        """Return k elements chosen with replacement."""

        if k < 0:
            raise ValueError("k must be non-negative")

        if not population:
            raise IndexError("Cannot choose from an empty population")

        if weights is not None and cum_weights is not None:
            raise TypeError("Cannot specify both weights and cum_weights")

        if weights is not None:
            if len(weights) != len(population):
                raise ValueError("weights and population differ in length")

            cumulative = []
            total = 0.0

            for weight in weights:
                if weight < 0:
                    raise ValueError("weights must be non-negative")
                total += weight
                cumulative.append(total)

            cum_weights = cumulative

        if cum_weights is None:
            return [self.choice(population) for _ in range(k)]

        if len(cum_weights) != len(population):
            raise ValueError("cum_weights and population differ in length")

        total = float(cum_weights[-1])

        if not math.isfinite(total) or total <= 0:
            raise ValueError("total of weights must be finite and greater than zero")

        result = []

        for _ in range(k):
            r = self.random() * total

            lo = 0
            hi = len(cum_weights)

            while lo < hi:
                mid = (lo + hi) // 2

                if r < cum_weights[mid]:
                    hi = mid
                else:
                    lo = mid + 1

            result.append(population[lo])

        return result

    def sample(self, population: Sequence[T], k: int) -> list[T]:
        """Return k unique elements sampled without replacement."""

        n = len(population)

        if k < 0 or k > n:
            raise ValueError("Sample larger than population")

        # Partial Fisher-Yates shuffle.
        pool = list(population)

        for i in range(k):
            j = i + self._randbelow(n - i)
            pool[i], pool[j] = pool[j], pool[i]

        return pool[:k]

    def shuffle(self, x: MutableSequence[T]) -> None:
        """Shuffle a mutable sequence in place."""
        for i in range(len(x) - 1, 0, -1):
            j = self._randbelow(i + 1)
            x[i], x[j] = x[j], x[i]

    # ------------------------------------------------------------------
    # Distributions
    # ------------------------------------------------------------------

    def uniform(self, a: float, b: float) -> float:
        """Return a random floating-point number between a and b."""
        return a + (b - a) * self.random()

    def triangular(
        self,
        low: float = 0.0,
        high: float = 1.0,
        mode: float | None = None,
    ) -> float:
        """Triangular distribution."""
        if high < low:
            raise ValueError("high must be >= low")

        if mode is None:
            mode = (low + high) / 2

        if not low <= mode <= high:
            raise ValueError("mode must be between low and high")

        if high == low:
            return low

        u = self.random()
        c = (mode - low) / (high - low)

        if u <= c:
            return low + math.sqrt(u * (high - low) * (mode - low))

        return high - math.sqrt(
            (1 - u) * (high - low) * (high - mode)
        )

    def expovariate(self, lambd: float) -> float:
        """Exponential distribution."""
        if lambd == 0:
            raise ValueError("lambd must not be zero")

        # Avoid log(0).
        u = self.random()
        while u == 0:
            u = self.random()

        return -math.log(u) / lambd

    def betavariate(self, alpha: float, beta: float) -> float:
        """Beta distribution using gamma variates."""
        if alpha <= 0 or beta <= 0:
            raise ValueError("alpha and beta must be greater than zero")

        x = self.gammavariate(alpha, 1.0)
        y = self.gammavariate(beta, 1.0)

        return x / (x + y)

    def gammavariate(self, alpha: float, beta: float) -> float:
        """Gamma distribution."""

        if alpha <= 0 or beta <= 0:
            raise ValueError("alpha and beta must be greater than zero")

        # Marsaglia and Tsang for alpha >= 1.
        if alpha >= 1:
            d = alpha - 1 / 3
            c = 1 / math.sqrt(9 * d)

            while True:
                x = self.gauss(0.0, 1.0)
                v = 1 + c * x

                if v <= 0:
                    continue

                v = v ** 3
                u = self.random()

                if u < 1 - 0.0331 * (x ** 4):
                    return beta * d * v

                if math.log(u) < 0.5 * x * x + d * (1 - v + math.log(v)):
                    return beta * d * v

        # Johnk-style transformation for alpha < 1.
        # Gamma(alpha) = Gamma(alpha + 1) * U^(1/alpha)
        return self.gammavariate(alpha + 1, beta) * (
            self.random() ** (1 / alpha)
        )

    def normalvariate(
        self,
        mu: float = 0.0,
        sigma: float = 1.0,
    ) -> float:
        """Normal (Gaussian) distribution."""
        if sigma < 0:
            raise ValueError("sigma must be non-negative")

        while True:
            u1 = self.random()
            u2 = self.random()

            if u1 <= 0:
                continue

            z = math.sqrt(-2.0 * math.log(u1)) * math.cos(
                2.0 * math.pi * u2
            )

            return mu + sigma * z

    def gauss(
        self,
        mu: float = 0.0,
        sigma: float = 1.0,
    ) -> float:
        """Alias for normalvariate()."""
        return self.normalvariate(mu, sigma)

    def lognormvariate(
        self,
        mu: float,
        sigma: float,
    ) -> float:
        """Log-normal distribution."""
        return math.exp(self.normalvariate(mu, sigma))

    def vonmisesvariate(self, mu: float, kappa: float) -> float:
        """Von Mises circular distribution."""
        if kappa <= 1e-8:
            return 2.0 * math.pi * self.random()

        s = 0.5 / kappa
        r = s + math.sqrt(1.0 + s * s)

        while True:
            u1 = self.random()
            z = math.cos(math.pi * u1)
            d = z / (r + z)
            u2 = self.random()

            if u2 < 1.0 - d * d:
                break

            if u2 <= (1.0 - d) * (1.0 + d):
                break

        u3 = self.random()

        if u3 > 0.5:
            theta = (mu + math.acos(d)) % (2.0 * math.pi)
        else:
            theta = (mu - math.acos(d)) % (2.0 * math.pi)

        return theta

    # ------------------------------------------------------------------
    # Compatibility helpers
    # ------------------------------------------------------------------

    def seed(self, *args, **kwargs) -> None:
        """
        Compatibility stub.

        ANU QRNG is not seedable: randomness comes from the remote quantum
        random-number service. Calling seed() therefore clears the local
        buffer but does not make the sequence reproducible.
        """
        with self._lock:
            self._bytes.clear()

    def getstate(self):
        raise NotImplementedError(
            "ANU QRNG is not a deterministic PRNG; state cannot be captured."
        )

    def setstate(self, state):
        raise NotImplementedError(
            "ANU QRNG is not a deterministic PRNG; state cannot be restored."
        )


# A module-level instance, analogous to Python's random module.
_default = ANUQRNG()

random = _default.random
getrandbits = _default.getrandbits
randbytes = _default.randbytes
randrange = _default.randrange
randint = _default.randint

choice = _default.choice
choices = _default.choices
sample = _default.sample
shuffle = _default.shuffle

uniform = _default.uniform
triangular = _default.triangular
expovariate = _default.expovariate
betavariate = _default.betavariate
gammavariate = _default.gammavariate
normalvariate = _default.normalvariate
gauss = _default.gauss
lognormvariate = _default.lognormvariate
vonmisesvariate = _default.vonmisesvariate

seed = _default.seed
getstate = _default.getstate
setstate = _default.setstate


__all__ = [
    "ANUQRNG",
    "betavariate",
    "choice",
    "choices",
    "expovariate",
    "gammavariate",
    "gauss",
    "getrandbits",
    "getstate",
    "lognormvariate",
    "normalvariate",
    "randbytes",
    "randint",
    "random",
    "randrange",
    "sample",
    "seed",
    "setstate",
    "shuffle",
    "triangular",
    "uniform",
    "vonmisesvariate",
]
