import hashlib
import math
import threading
import time
from typing import Optional, List

from .rtltcpclient import RTLTCPClient

class Seed:
    """A cryptographic seed with precomputed conversions accessible as attributes."""
    
    def __init__(self, hex_value: str):
        self.__hex = hex_value
        self.__int = int(hex_value, 16)
        self.__bytes = bytes.fromhex(hex_value)
        self.__binary = bin(self.__int)[2:].zfill(256) # 256 bits for SHA-256
    
    def __str__(self) -> str:
        """Return the hex representation when converted to string."""
        return self.__hex
    
    def __repr__(self) -> str:
        return f"Seed('{self.__hex}')"
    
    @property
    def hex(self) -> str:
        """Get the seed as a hexadecimal string."""
        return self.__hex
    
    @property
    def int(self) -> int:
        """Get the seed as an integer."""
        return self.__int
    
    @property
    def bytes(self) -> bytes:
        """Get the seed as bytes."""
        return self.__bytes
    
    @property
    def binary(self) -> str:
        """Get the seed as a binary string."""
        return self.__binary
    

class Seeder:
    """
    Generate cryptographic seeds from radio frequency noise using an RTL-SDR device.
    
    The Seeder class captures IQ samples from atmospheric radio noise across a frequency
    range and converts them into cryptographic-quality random seeds using phase angle
    analysis and SHA-256 hashing. Seeds are continuously refreshed in a background thread.
    
    Attributes:
        running: Whether the seeder is currently active.
        seed: The current SHA-256 seed derived from RF samples (read-only property).
    """

class Seeder:
    def __init__(self, rtl_tcp_client: RTLTCPClient, gain: float = 49.6, freq_range: List[int] = [110, 120], samples_count: int = 1024, refresh_rate: int = 5000):
        """
        Initialize the Seeder with an RTL-TCP client connection.

        Sets up the seeder to generate cryptographic seeds from radio frequency samples.
        Configures the RTL-SDR device, retrieves initial samples, and starts a background
        thread for continuous seed refreshing.

        Args:
            rtl_tcp_client: An RTLTCPClient instance for communicating with the RTL-TCP server.
            gain: RTL-SDR gain value (default 49.6).
            freq_range: Frequency range to sample [min_freq, max_freq] (default [110, 120]).
            samples_count: Number of samples to read per fetch (default 1024).
            refresh_rate: Seed refresh rate in milliseconds (default 5000).

        Raises:
            ValueError: If connection to the RTL-TCP server fails or initial samples cannot be retrieved.
        """
        
        self.running: bool = False
        self.__seed: Optional[Seed] = None

        # Configurable parameters
        self.__client: RTLTCPClient = rtl_tcp_client
        self.__gain: float = gain
        self.__freq_range: List[int] = freq_range
        self.__current_freq: int = freq_range[0]
        self.__samples_count: int = samples_count
        self.__refresh_rate: int = refresh_rate

        self.__thread = threading.Thread(target=self.__runner, daemon=True)
        
        if not self.__client.connect():
            raise ValueError("Could not connect to the RTL-TCP server.")
        
        self.running = True
        
        self.__client.configure(freq_mhz=self.__current_freq, gain_db=self.__gain)
        
        self.__seed = self.__samples_to_seed(self.__client.read_samples(self.__samples_count))

        if self.__seed is None:
            raise ValueError("Could not retrieve samples from the RTL-TCP server.")
        
        self.__thread.start()


    @property
    def seed(self) -> Optional[Seed]:
        """
        Get the current cryptographic seed.
        
        Returns:
            Seed: A Seed object with conversion methods, or None if not available.
        """
            
        return self.__seed
    

    def stop(self) -> None:
        """
        Stop the seeder and disconnect from the RTL-TCP server.
        
        Halts the background thread and closes the connection to the RTL-TCP server.
        """

        self.running = False
        self.__client.disconnect()


    def __get_next_freq(self) -> int:
        """
        Calculate the next frequency in the scanning range.
        
        Increments the current frequency by 1 MHz and wraps back to the minimum
        frequency when the maximum is exceeded.
        
        Returns:
            int: The next frequency in MHz to scan.
        """

        self.__current_freq += 1
        
        # If it exceeds max, wrap back to min
        if self.__current_freq > self.__freq_range[1]:
            self.__current_freq = self.__freq_range[0]
        return self.__current_freq


    def __samples_to_seed(self, samples: Optional[List[complex]]) -> Optional[Seed]:
        """
        Convert IQ samples to a cryptographic seed using phase angle analysis.
        
        Processes complex IQ samples by:
        1. Computing phase angle deltas between consecutive samples
        2. Converting deltas to bits (1 if positive, 0 if negative)
        3. Applying XOR whitening to balance bit distribution
        4. Packing bits into bytes and hashing with SHA-256
        
        Args:
            samples: A list of complex IQ samples from the RTL-SDR, or None.
        
        Returns:
            Seed or None: A Seed object if successful, the previous seed if samples
                        are None, or None if the seeder is not running.
        """

        if not self.running: 
            return None
        
        if samples is None:
            return self.__seed
        
        try:
            samples_count: int = self.__samples_count

            bits: List[int] = []
            x = samples # just for better maths readablity, same for n

            for n in range(samples_count):
                current: complex = x[n]
                previous: complex = x[samples_count - 1] if n == 0 else x[n-1]
                
                current_angle: float = math.atan2(current.imag, current.real)
                previous_angle: float = math.atan2(previous.imag, previous.real)

                delta: float = (current_angle - previous_angle + math.pi) % (2 * math.pi) - math.pi

                bits.append(1 if delta > 0 else 0)

            # end of the first step. However, during tests, ive witnessed more 1s than 0s (about 200 more). 
            # So we'll then uniformize all this

            # we're going to use XOR:
            # 0 0 -> 0
            # 0 1 -> 1
            # 1 0 -> 1
            # 1 1 -> 0

            for n in range(samples_count):
                current: int = bits[n]
                previous: int = bits[samples_count - 1] if n == 0 else bits[n-1]

                bits[n] = current ^ previous

            # OK, now we're in a more even count of 1 and 0s

            # let's do an integer seed, to then do whatever we want with it.
            # We could just do bits[n] + bits[n+1] + bits[n+...] but we're going
            # to hash it, for our seed to always be the same size
            
            byte_array = []
            for n in range(0, len(bits), 8):
                byte = (
                    bits[n] << 7 |   # << shifts the bit left by 7 positions
                    bits[n+1] << 6 | # << shifts the bit left by 6 positions
                    bits[n+2] << 5 | # << shifts the bit left by 5 positions
                    bits[n+3] << 4 | # << shifts the bit left by 4 positions
                    bits[n+4] << 3 | # << shifts the bit left by 3 positions
                    bits[n+5] << 2 | # << shifts the bit left by 2 positions
                    bits[n+6] << 1 | # << shifts the bit left by 1 position
                    bits[n+7] << 0 | # << shifts the bit left by 0 positions (keeps it)
                    0                # | combines all bits (bitwise OR)
                )

                byte_array.append(byte)

            byte_array = bytes(byte_array)

            return Seed(hashlib.sha256(byte_array).hexdigest())
        
        except:
            return self.__seed


    def __runner(self) -> None:
        """
        Background thread worker for continuous seed generation.
        
        Continuously cycles through the frequency range, collecting samples and
        updating the seed at the configured refresh rate. Runs until stopped.
        """

        while self.running:
            start_time: float = time.time()  # mark the start of the iteration

            self.__client.configure(freq_mhz=self.__get_next_freq(), gain_db=self.__gain)  # configure with the shift

            samples: Optional[List[int]] = self.__client.read_samples(self.__samples_count)  # read samples

            if samples is not None:
                self.__seed = self.__samples_to_seed(samples)

            end_time: float = time.time()
            elapsed_ms: float = (end_time - start_time) * 1000 # to ms

            sleep_time_ms: float = max(0, self.__refresh_rate - elapsed_ms)
            time.sleep(sleep_time_ms / 1000)