import requests
import os
import math
import itertools
import bisect

class QueyRandom:
    """
    Core generator pulling true quantum entropy from a local Quey Node.
    Includes advanced statistical distributions for scientific simulations.
    """
    def __init__(self, host='localhost', port=5000, key_file='quey_api.key'):
        self.base_url = f"http://{host}:{port}/key"
        self.api_key = self._load_key(key_file)
        self._has_spare_gauss = False
        self._spare_gauss = None

    def _load_key(self, key_file):
        try:
            if os.path.exists(key_file):
                with open(key_file, 'r') as f:
                    return f.read().strip()
            fallback = os.path.expanduser(f"~/Quey/{key_file}")
            if os.path.exists(fallback):
                 with open(fallback, 'r') as f:
                    return f.read().strip()
            return None
        except Exception:
            return None

    def _get_raw_hex(self, size_bytes=32):
        headers = {'X-API-Key': self.api_key} if self.api_key else {}
        try:
            response = requests.get(self.base_url, headers=headers, params={'size': size_bytes}, timeout=5)
            if response.status_code == 200:
                return response.json().get('key')
        except requests.exceptions.RequestException:
            pass
        raise ConnectionError("Quey Node unavailable or unauthorized. Check API key.")

    # --- CORE UNIFORM DISTRIBUTIONS ---

    def random(self):
        """Returns a true random float in the range [0.0, 1.0)."""
        hex_val = self._get_raw_hex(8)
        int_val = int(hex_val, 16)
        return int_val / (2**64)

    def randint(self, a, b):
        """Returns a true random integer in range [a, b]."""
        if a > b:
            raise ValueError("Empty range for randint")
        range_size = b - a + 1
        bytes_needed = math.ceil(range_size.bit_length() / 8)
        if bytes_needed == 0: bytes_needed = 1
        hex_val = self._get_raw_hex(bytes_needed)
        int_val = int(hex_val, 16)
        return a + (int_val % range_size)

    def choice(self, seq):
        """Chooses a true random element from a non-empty sequence."""
        if not seq: raise IndexError("Cannot choose from an empty sequence")
        return seq[self.randint(0, len(seq) - 1)]

    def choices(self, population, weights=None, k=1):
        """Return a k sized list of elements chosen from the population with optional weights."""
        if weights is None:
            return [self.choice(population) for _ in range(k)]
        
        if len(population) != len(weights):
            raise ValueError("The number of weights does not match the population")

        cum_weights = list(itertools.accumulate(weights))
        total = cum_weights[-1]
        
        return [population[bisect.bisect(cum_weights, self.random() * total)] for _ in range(k)]

    # --- SCIENTIFIC DISTRIBUTIONS ---

    def gauss(self, mu=0.0, sigma=1.0):
        """Gaussian distribution using the Box-Muller transform."""
        if self._has_spare_gauss:
            self._has_spare_gauss = False
            return mu + sigma * self._spare_gauss

        u1 = self.random()
        while u1 == 0.0: u1 = self.random()
        u2 = self.random()

        r = math.sqrt(-2.0 * math.log(u1))
        theta = 2.0 * math.pi * u2

        self._spare_gauss = r * math.sin(theta)
        self._has_spare_gauss = True

        return mu + sigma * (r * math.cos(theta))

    def expovariate(self, lambd):
        """Exponential distribution. lambd is 1.0 divided by the desired mean."""
        u = self.random()
        while u == 0.0: u = self.random()
        return -math.log(u) / lambd

    def poisson(self, lambd):
        """Poisson distribution (Knuth's algorithm). Good for photon counting."""
        L = math.exp(-lambd)
        k = 0
        p = 1.0
        while p > L:
            k += 1
            p *= self.random()
        return k - 1

    def complex_phase(self):
        """Returns a true random complex number on the unit circle (e^(i*theta))."""
        theta = 2.0 * math.pi * self.random()
        return complex(math.cos(theta), math.sin(theta))

# Global instance
_inst = QueyRandom()

random = _inst.random
randint = _inst.randint
choice = _inst.choice
choices = _inst.choices
gauss = _inst.gauss
expovariate = _inst.expovariate
poisson = _inst.poisson
complex_phase = _inst.complex_phase
