"""
src/models/environment.py

Implements Module 1: Environment & Channel Simulator
"""

import numpy as np
from typing import NamedTuple, Tuple


class EnvironmentConfig(NamedTuple):
    """Configuration for the channel simulator."""

    num_users: int = 10
    num_channels: int = 10
    bandwidth: float = 1e6  # Hz, 1 MHz per channel
    radius: float = 0.5  # km, cell radius
    shadow_fading_std: float = 6.0  # dB
    noise_psd_dbm: float = -174.0  # dBm/Hz


class ChannelSimulator:
    """
    Simulates the wireless channel environment.

    Paper Reference:
    - Pathloss: 128.1 + 37.6 lg[d(km)] dB
    - Shadow fading: 6 dB
    """

    def __init__(self, config: EnvironmentConfig):
        self.config = config

    def _calculate_pathloss(self, distances: np.ndarray) -> np.ndarray:
        """Calculate pathloss for given distances in km."""
        return 128.1 + 37.6 * np.log10(distances)

    def generate_channels(
        self, transmit_power_dbm: float
    ) -> Tuple[np.ndarray, np.ndarray]:
        """
        Generate channel conditions (SNR) for all users and channels.

        Args:
            transmit_power_dbm: Transmit power in dBm

        Returns:
            Tuple of (snr_db, snr_linear) with shape (num_users, num_channels)
        """
        N, M = self.config.num_users, self.config.num_channels

        # 1. Distances (randomly distributed between 0.05km and cell radius)
        min_dist = 0.05
        distances = np.random.uniform(min_dist, self.config.radius, size=N)

        # 2. Pathloss
        path_loss_db = self._calculate_pathloss(distances)

        # 3. Shadow fading
        shadowing_db = np.random.normal(0, self.config.shadow_fading_std, size=N)

        # 4. Total large scale fading (dB)
        large_scale_db = path_loss_db + shadowing_db

        # 5. Rayleigh fading (small scale)
        # Power of Rayleigh follows exponential distribution (mean=1)
        small_scale_power = np.random.exponential(1.0, size=(N, M))
        small_scale_db = 10 * np.log10(small_scale_power)

        # 6. Noise power
        # Noise = PSD (dBm/Hz) + 10*log10(BW)
        noise_power_dbm = self.config.noise_psd_dbm + 10 * np.log10(
            self.config.bandwidth
        )

        # 7. Calculate SNR
        # SNR(dB) = Pt(dBm) - LargeScale(dB) + SmallScale(dB) - Noise(dBm)
        snr_db = np.zeros((N, M))
        for n in range(N):
            snr_db[n, :] = (
                transmit_power_dbm
                - large_scale_db[n]
                + small_scale_db[n, :]
                - noise_power_dbm
            )

        snr_linear = 10 ** (snr_db / 10)

        return snr_db, snr_linear
