"""
Hybrid SimPy-Based Event-Driven Simulator for LEACH and LEACH-C Protocols.

This module implements a complete discrete event simulation using Simpy framework,
combining the best of both approaches:

FROM PAUL'S APPROACH:
- Full refactor to use SimPy properly with event-driven architecture
- Parallel node mobility processes
- Proper discrete event simulation model
- Sequential round events with all phases

FROM SORTI'S APPROACH:
- DRY (Don't Repeat Yourself) principles
- KISS (Keep It Simple) architecture
- Reusable helper methods
- Clean code structure with proper separation of concerns
- Comprehensive event logging
- Support for static/dynamic modes

Key Features:
- SimPy Environment for discrete event management
- Parallel node mobility as background processes
- Structured round phases: initialization → CH election → communication → cleanup
- Complete metrics collection
- Event logging for debugging and analysis
- Static/Dynamic network support via ENABLE_MOBILITY flag
"""

import simpy
import random
from typing import List, Dict, Optional, Tuple
from node import Node
from metrics import Metrics
from config import (
    FIELD_WIDTH, FIELD_HEIGHT, INITIAL_ENERGY, BS_POSITION,
    ENABLE_MOBILITY, DEBUG
)


class HybridSimPySimulator:
    """
    Hybrid event-driven simulator combining Paul's full refactor with Sorti's quality approach.

    Architecture:
    - Main process: Handles round execution (election, communication, metrics)
    - Background processes: Node mobility (runs in parallel)
    - Event model: Discrete events at each round boundary
    - State management: Proper environment tracking

    Args:
        protocol_name: "LEACH" or "LEACH-C"
        nodes: List of Node objects
        packet_size: Data packet size in bits
        probability_ch: Probability of becoming cluster head
        max_rounds: Maximum simulation rounds
    """

    def __init__(
        self,
        protocol_name: str,
        nodes: List[Node],
        packet_size: int,
        probability_ch: float,
        max_rounds: int
    ):
        self.env = simpy.Environment()
        self.protocol_name = protocol_name
        self.nodes = nodes
        self.packet_size = packet_size
        self.probability_ch = probability_ch
        self.max_rounds = max_rounds

        # State management
        self.round_num = 0
        self.cluster_heads = []
        self.clusters: Dict[int, List[int]] = {}
        self.events_log = []

        # Statistics
        self.total_packets_to_ch = 0
        self.total_packets_to_bs = 0
        self.muted_rounds = []

        # Metrics collector
        self.metrics = Metrics()

    # ========== DRY: Reusable Helper Methods ==========

    def _log_event(self, event_type: str, round_num: int = 0, **details) -> None:
        """
        DRY: Single method for all event logging.

        Args:
            event_type: Type of event (e.g., 'CH_ELECTED', 'COMMUNICATION', 'MOBILITY')
            round_num: Current round number
            details: Additional event details
        """
        self.events_log.append({
            'time': self.env.now,
            'event': event_type,
            'round': round_num,
            **details
        })

    def _get_alive_nodes(self) -> List[Node]:
        """DRY: Get list of currently alive nodes."""
        return [n for n in self.nodes if n.is_alive]

    def _find_closest_cluster_head(self, node: Node) -> Optional[int]:
        """
        DRY: Find the closest cluster head for a node.

        Args:
            node: Node looking for closest CH

        Returns:
            Cluster head node ID or None if no CHs exist
        """
        if not self.cluster_heads:
            return None

        closest_ch = None
        min_distance = float('inf')

        for ch_id in self.cluster_heads:
            ch_node = self.nodes[ch_id]
            distance = node.distance_to(ch_node.x, ch_node.y)
            if distance < min_distance:
                min_distance = distance
                closest_ch = ch_id

        return closest_ch

    # ========== CH Election: Protocol-Specific ==========

    def _elect_cluster_heads_leach(self) -> None:
        """
        LEACH: Distributed cluster head election.
        Each alive node has probability p of becoming a CH.
        """
        self.cluster_heads = []
        self.clusters = {}

        # Phase 1: Nodes decide if they become CH
        for node in self._get_alive_nodes():
            if random.random() < self.probability_ch:
                node.is_cluster_head = True
                self.cluster_heads.append(node.node_id)
                self.clusters[node.node_id] = [node.node_id]
                node.cluster_id = node.node_id

        # Phase 2: Non-CH nodes join closest CH
        for node in self._get_alive_nodes():
            if not node.is_cluster_head:
                closest_ch = self._find_closest_cluster_head(node)
                if closest_ch is not None:
                    node.cluster_id = closest_ch
                    if closest_ch not in self.clusters:
                        self.clusters[closest_ch] = []
                    self.clusters[closest_ch].append(node.node_id)

        self._log_event(
            'CH_ELECTED_LEACH',
            self.round_num,
            num_ch=len(self.cluster_heads),
            alive_nodes=len(self._get_alive_nodes())
        )

    def _elect_cluster_heads_leachc(self) -> None:
        """
        LEACH-C: Centralized cluster head election by base station.
        BS selects top 10% nodes by energy as CHs.
        """
        self.cluster_heads = []
        self.clusters = {}

        alive_nodes = self._get_alive_nodes()
        if not alive_nodes:
            return

        # Phase 1: BS collects node information
        # Each node sends ~32 bits (position + energy) to BS
        for node in alive_nodes:
            distance_to_bs = node.distance_to(*BS_POSITION)
            node.transmit(32, distance_to_bs)

        # Phase 2: BS selects CHs (top 10% by energy)
        num_expected_ch = max(1, int(len(alive_nodes) * 0.1))
        sorted_nodes = sorted(alive_nodes, key=lambda n: n.energy, reverse=True)
        selected_ch = sorted_nodes[:num_expected_ch]

        for node in selected_ch:
            node.is_cluster_head = True
            self.cluster_heads.append(node.node_id)
            self.clusters[node.node_id] = [node.node_id]
            node.cluster_id = node.node_id

        # Phase 3: BS broadcasts CH list to all nodes
        for node in alive_nodes:
            if not node.is_cluster_head:
                distance_to_bs = node.distance_to(*BS_POSITION)
                node.receive(len(self.cluster_heads) * 8)

        # Phase 4: Non-CH nodes join closest CH
        for node in alive_nodes:
            if not node.is_cluster_head:
                closest_ch = self._find_closest_cluster_head(node)
                if closest_ch is not None:
                    node.cluster_id = closest_ch
                    if closest_ch not in self.clusters:
                        self.clusters[closest_ch] = []
                    self.clusters[closest_ch].append(node.node_id)

        self._log_event(
            'CH_ELECTED_LEACHC',
            self.round_num,
            num_ch=len(self.cluster_heads),
            alive_nodes=len(alive_nodes)
        )

    # ========== Communication Phase ==========

    def _communication_phase(self) -> None:
        """
        Execute communication phase: transmission from nodes to CH to BS.
        """
        if not self.cluster_heads:
            # Muted round: no CHs available
            self.muted_rounds.append(self.round_num)
            self._log_event('MUTED_ROUND', self.round_num)
            return

        packets_this_round = {'to_ch': 0, 'to_bs': 0}

        # Phase 1: Non-CH nodes send to their CH
        for node in self._get_alive_nodes():
            if node.is_alive and not node.is_cluster_head:
                # Data transmission probability
                if random.random() < self.probability_ch:
                    ch_node = self.nodes[node.cluster_id] if node.cluster_id else None
                    if ch_node and ch_node.is_alive:
                        distance = node.distance_to(ch_node.x, ch_node.y)
                        node.transmit(self.packet_size, distance)
                        ch_node.receive(self.packet_size)
                        packets_this_round['to_ch'] += 1
                        self.total_packets_to_ch += 1

        # Phase 2: CHs aggregate and send to BS
        for ch_id in self.cluster_heads:
            ch_node = self.nodes[ch_id]
            if ch_node.is_alive:
                # Aggregation: nodes in cluster - 1 (excluding CH itself)
                num_packets = len(self.clusters.get(ch_id, [1])) - 1

                if num_packets > 0:
                    aggregated_data = self.packet_size
                    ch_node.aggregate(aggregated_data)

                    distance_to_bs = ch_node.distance_to(*BS_POSITION)
                    ch_node.transmit(aggregated_data, distance_to_bs)
                    packets_this_round['to_bs'] += 1
                    self.total_packets_to_bs += 1

        self._log_event(
            'COMMUNICATION',
            self.round_num,
            packets_to_ch=packets_this_round['to_ch'],
            packets_to_bs=packets_this_round['to_bs']
        )

    # ========== Mobility Phase ==========

    def _mobility_phase(self) -> None:
        """Execute mobility phase: update node positions (if enabled)."""
        if not ENABLE_MOBILITY:
            return

        moved_count = 0
        for node in self._get_alive_nodes():
            node.move()
            moved_count += 1

        if moved_count > 0:
            self._log_event('MOBILITY', self.round_num, nodes_moved=moved_count)

    # ========== Parallel Node Mobility Process ==========

    def _node_mobility_background_process(self, node: Node) -> None:
        """
        Background SimPy process for continuous node mobility.
        Runs in parallel with main simulation, independent of rounds.

        This is optional - for more realistic continuous movement.
        Currently, we use discrete mobility in each round.

        Args:
            node: Node to move
        """
        while node.is_alive and self.round_num < self.max_rounds:
            yield self.env.timeout(1.0)  # Wait for 1 time unit
            if ENABLE_MOBILITY and node.is_alive:
                node.move()

    # ========== Main Round Process ==========

    def _round_process(self) -> None:
        """
        Main SimPy process: Execute protocol rounds as discrete events.

        Each iteration:
        1. Advance time
        2. Reset node states
        3. Elect cluster heads
        4. Execute communication
        5. Update mobility
        6. Record metrics
        7. Check termination condition
        """
        while self.round_num < self.max_rounds:
            # Advance time by 1 round unit
            yield self.env.timeout(1.0)

            # Reset node states for this round
            for node in self.nodes:
                node.reset_for_round()

            # CH Election
            if self.protocol_name == "LEACH":
                self._elect_cluster_heads_leach()
            elif self.protocol_name == "LEACH-C":
                self._elect_cluster_heads_leachc()

            # Communication Phase
            self._communication_phase()

            # Mobility Phase
            self._mobility_phase()

            # Record metrics for this round
            alive_nodes = self._get_alive_nodes()
            self.metrics.record_round(
                round_num=self.round_num,
                nodes=self.nodes,
                ch_nodes=[self.nodes[ch_id] for ch_id in self.cluster_heads],
                packets_to_ch=self.total_packets_to_ch,
                packets_to_bs=self.total_packets_to_bs,
                muted=(len(self.cluster_heads) == 0)
            )

            # Update dead node tracking
            self.metrics.update_dead_nodes(self.nodes)

            # Debug output
            if DEBUG and self.round_num % 100 == 0:
                alive_count = len(alive_nodes)
                avg_energy = sum(n.energy for n in self.nodes) / len(self.nodes)
                print(
                    f"  Round {self.round_num}: {alive_count} alive, "
                    f"{len(self.cluster_heads)} CHs, avg_energy={avg_energy:.2e}"
                )

            self.round_num += 1

            # Termination: All nodes dead
            if not alive_nodes:
                if DEBUG:
                    print(f"  All nodes dead at round {self.round_num}")
                break

    # ========== Simulation Execution ==========

    def run(self) -> Dict:
        """
        Execute the complete simulation.

        Starts main round process and optional background mobility processes.
        Returns complete metrics after simulation ends.

        Returns:
            Dict with all metrics (FDN, FMR, DLBI, RSPI, etc.)
        """
        if DEBUG:
            print(f"\n{'='*60}")
            print(f"Hybrid SimPy Simulation")
            print(f"Protocol: {self.protocol_name}")
            print(f"Nodes: {len(self.nodes)}, Packet size: {self.packet_size}")
            print(f"Probability CH: {self.probability_ch}, Max rounds: {self.max_rounds}")
            print(f"{'='*60}")

        # Start background mobility processes (optional)
        if ENABLE_MOBILITY:
            for node in self.nodes:
                self.env.process(self._node_mobility_background_process(node))

        # Start main round process
        self.env.process(self._round_process())

        # Execute simulation
        self.env.run()

        # Calculate final metrics
        # FDN and FMR are tracked during execution
        fdn = self.metrics.first_dead_node_round
        fmr = self.metrics.first_muted_round
        dlbi = self.metrics.calculate_dlbi()
        rspi = self.metrics.calculate_rspi(self.max_rounds)

        if DEBUG:
            print(f"\n{self.protocol_name} Results:")
            print(f"  FDN: {fdn}, FMR: {fmr}")
            print(f"  DLBI: {dlbi:.4f}, RSPI: {rspi:.4f}")

        return {
            "fdn": fdn,
            "fmr": fmr,
            "dlbi": dlbi,
            "rspi": rspi,
            "metrics": self.metrics,
            "rounds_data": self.metrics.rounds_data,
            "events_log": self.events_log,
            "num_nodes": len(self.nodes),
            "num_rounds": self.round_num,
            "total_packets_to_ch": self.total_packets_to_ch,
            "total_packets_to_bs": self.total_packets_to_bs
        }


if __name__ == "__main__":
    """
    Demo: Hybrid SimPy Simulator combining best of both approaches.
    Shows full discrete event simulation with parallel processes.
    """
    from config import get_num_rounds_for_scenario

    print("=" * 70)
    print("HYBRID SIMPY SIMULATOR DEMONSTRATION")
    print("Combines Paul's full refactor with Sorti's quality approach")
    print("=" * 70)

    # Create test scenario
    random.seed(42)
    num_nodes = 50
    packet_size = 2000
    probability_ch = 0.05
    max_rounds = get_num_rounds_for_scenario(num_nodes)

    # Create nodes
    test_nodes = []
    for i in range(num_nodes):
        x = random.uniform(0, FIELD_WIDTH)
        y = random.uniform(0, FIELD_HEIGHT)
        test_nodes.append(Node(i, x, y, INITIAL_ENERGY))

    # Run LEACH simulation
    print(f"\nRunning LEACH with {num_nodes} nodes, {max_rounds} rounds...")
    sim_leach = HybridSimPySimulator(
        protocol_name="LEACH",
        nodes=test_nodes,
        packet_size=packet_size,
        probability_ch=probability_ch,
        max_rounds=max_rounds
    )

    leach_results = sim_leach.run()

    print(f"\n✓ LEACH Simulation Complete")
    print(f"  Events logged: {len(leach_results['events_log'])}")
    print(f"  Rounds executed: {leach_results['num_rounds']}")
    print(f"  Final metrics:")
    print(f"    FDN: {leach_results['fdn']}")
    print(f"    FMR: {leach_results['fmr']}")
    print(f"    DLBI: {leach_results['dlbi']:.4f}")
    print(f"    RSPI: {leach_results['rspi']:.4f}")
