"""
Classe Metrics : Collecte et calcul des métriques de performance
"""

import math
from collections import defaultdict


class Metrics:
    """
    Collecte les métriques de performance pour chaque round.
    
    Métriques suivies:
    1. Alive nodes count
    2. Packets to cluster head
    3. Packets to base station
    4. Residual energy
    5. Muted rounds (no CH elected)
    6. First muted round (FMR)
    7. First dead node (FDN)
    8. Last dead node
    9. Dynamic Load Balancing Index (DLBI)
    10. Relative Silence Period Index (RSPI)
    """
    
    def __init__(self):
        # Données par round
        self.rounds_data = []
        
        # Statistiques globales
        self.first_dead_node_round = None
        self.last_dead_node_round = None
        self.first_muted_round = None
        self.total_muted_rounds = 0
        self.muted_rounds_list = []
        
        # Pour calcul DLBI
        self.ch_loads_per_round = {}  # round -> {ch_id -> load}
        
    def record_round(self, round_num, nodes, ch_nodes, packets_to_ch, packets_to_bs, 
                     muted=False):
        """
        Enregistre les données d'une ronde.
        
        Args:
            round_num (int): Numéro du round
            nodes (list): Liste de tous les nœuds
            ch_nodes (list): Liste des cluster heads de ce round
            packets_to_ch (int): Nombre total de paquets vers CHs
            packets_to_bs (int): Nombre total de paquets vers BS
            muted (bool): Si true, aucun CH n'a été élu ce round
        """
        alive_count = sum(1 for n in nodes if n.is_alive)
        residual_energy = [n.energy for n in nodes if n.is_alive]
        avg_residual_energy = sum(residual_energy) / len(residual_energy) if residual_energy else 0
        
        round_data = {
            "round": round_num,
            "alive_nodes": alive_count,
            "packets_to_ch": packets_to_ch,
            "packets_to_bs": packets_to_bs,
            "avg_residual_energy": avg_residual_energy,
            "ch_count": len(ch_nodes),
            "muted": muted,
        }
        
        self.rounds_data.append(round_data)
        
        # Suivi des muted rounds
        if muted:
            self.muted_rounds_list.append(round_num)
            self.total_muted_rounds += 1
            if self.first_muted_round is None:
                self.first_muted_round = round_num
        
        # Enregistrer les charges des CHs pour DLBI
        if ch_nodes:
            self.ch_loads_per_round[round_num] = {}
            for ch in ch_nodes:
                # La charge = nombre de nœuds dans le cluster
                cluster_size = 1  # Le CH lui-même
                for node in nodes:
                    if node.cluster_id == ch.node_id and not node.is_cluster_head:
                        cluster_size += 1
                self.ch_loads_per_round[round_num][ch.node_id] = cluster_size
    
    def update_dead_nodes(self, nodes):
        """Met à jour les rounds de décès des nœuds."""
        current_round = len(self.rounds_data)
        
        for node in nodes:
            if not node.is_alive:
                if self.first_dead_node_round is None:
                    self.first_dead_node_round = current_round
                self.last_dead_node_round = current_round
    
    def calculate_dlbi(self):
        """
        Calcule le Dynamic Load Balancing Index.
        
        DLBI = (1/N) * Σ(DLBI_r) pour r=1 à N
        DLBI_r = 1 - [Σ(L_j,r - L̄_r)² / (m_r * L̄_r²)]
        
        Returns:
            float: DLBI (0 à 1, plus élevé = mieux)
        """
        if not self.ch_loads_per_round:
            return 0
        
        dlbi_values = []
        
        for round_num, loads in self.ch_loads_per_round.items():
            if not loads or len(loads) == 0:
                continue
            
            loads_list = list(loads.values())
            m_r = len(loads_list)  # Nombre de CHs
            L_bar_r = sum(loads_list) / m_r  # Charge moyenne
            
            if L_bar_r == 0:
                dlbi_r = 0
            else:
                variance = sum((load - L_bar_r) ** 2 for load in loads_list)
                dlbi_r = 1 - (variance / (m_r * (L_bar_r ** 2)))
            
            dlbi_values.append(dlbi_r)
        
        if dlbi_values:
            return sum(dlbi_values) / len(dlbi_values)
        return 0
    
    def calculate_rspi(self, total_rounds):
        """
        Calcule le Relative Silence Period Index.
        
        RSPI = 2 * [(1 - FR_muted/R_max) * (1 - LR_dead/R_max)] 
               / [(1 - FR_muted/R_max) + (1 - LR_dead/R_max)]
        
        Args:
            total_rounds (int): Nombre total de rounds
        
        Returns:
            float: RSPI (0 à 1, plus élevé = mieux)
        """
        if not total_rounds:
            return 0
        
        FR_muted = self.first_muted_round if self.first_muted_round else total_rounds
        LR_dead = self.last_dead_node_round if self.last_dead_node_round else total_rounds
        R_max = total_rounds
        
        term1 = 1 - (FR_muted / R_max)
        term2 = 1 - (LR_dead / R_max)
        
        numerator = 2 * term1 * term2
        denominator = term1 + term2
        
        if denominator == 0:
            return 0
        
        return numerator / denominator
    
    def get_summary(self, total_rounds):
        """
        Retourne un résumé complet des métriques.
        
        Returns:
            dict: Dictionnaire avec toutes les métriques
        """
        if not self.rounds_data:
            return {}
        
        final_round = self.rounds_data[-1]
        
        return {
            "total_rounds_completed": len(self.rounds_data),
            "final_alive_nodes": final_round["alive_nodes"],
            "first_dead_node_round": self.first_dead_node_round,
            "last_dead_node_round": self.last_dead_node_round,
            "first_muted_round": self.first_muted_round,
            "total_muted_rounds": self.total_muted_rounds,
            "dlbi": self.calculate_dlbi(),
            "rspi": self.calculate_rspi(total_rounds),
        }
    
    def get_rounds_data(self):
        """Retourne les données de tous les rounds."""
        return self.rounds_data
