import numpy as np
import pandas as pd
from typing import List, Tuple, Dict, Any

class ElectreSolver:
    """
    Implementation of ELECTRE I method for Multi-Criteria Decision Making
    """
    
    def __init__(self, alternatives: List[str], criteria: List[str], 
                 criteria_types: List[str], weights: List[float]):
        """
        Initialize ELECTRE solver
        
        Parameters:
        alternatives: List of alternative names
        criteria: List of criterion names
        criteria_types: List of 'max' or 'min' for each criterion
        weights: List of weights for each criterion (should sum to 1)
        """
        self.alternatives = alternatives
        self.criteria = criteria
        self.criteria_types = criteria_types
        self.weights = weights
        self.decision_matrix = None
        self.normalized_matrix = None
        self.weighted_matrix = None
        
        self._validate_inputs()
    
    def _validate_inputs(self):
        """Validate input parameters"""
        if len(self.criteria) != len(self.weights):
            raise ValueError("Number of criteria must match number of weights")
        if len(self.criteria) != len(self.criteria_types):
            raise ValueError("Number of criteria must match number of criterion types")
        if not all(ct in ['max', 'min'] for ct in self.criteria_types):
            raise ValueError("Criterion types must be 'max' or 'min'")
        if abs(sum(self.weights) - 1.0) > 1e-10:
            raise ValueError("Weights must sum to 1")
    
    def set_decision_matrix(self, matrix: np.ndarray):
        """
        Set decision matrix
        
        Parameters:
        matrix: 2D numpy array with shape (n_alternatives, n_criteria)
        """
        if matrix.shape != (len(self.alternatives), len(self.criteria)):
            raise ValueError("Matrix shape must match (n_alternatives, n_criteria)")
        self.decision_matrix = matrix.astype(float)
    
    def normalize_matrix(self):
        """Normalize the decision matrix"""
        if self.decision_matrix is None:
            raise ValueError("Decision matrix not set")
        
        self.normalized_matrix = np.zeros_like(self.decision_matrix)
        
        for j in range(len(self.criteria)):
            column = self.decision_matrix[:, j]
            
            if self.criteria_types[j] == 'max':
                # For maximization criteria
                denominator = np.sqrt(np.sum(column ** 2))
                if denominator > 0:
                    self.normalized_matrix[:, j] = column / denominator
            else:
                # For minimization criteria - invert the values
                inverted_column = 1 / (column + 1e-10)  # Add small value to avoid division by zero
                denominator = np.sqrt(np.sum(inverted_column ** 2))
                if denominator > 0:
                    self.normalized_matrix[:, j] = inverted_column / denominator
    
    def apply_weights(self):
        """Apply weights to normalized matrix"""
        if self.normalized_matrix is None:
            raise ValueError("Matrix not normalized")
        
        self.weighted_matrix = self.normalized_matrix * self.weights
    
    def calculate_concordance_discordance(self) -> Tuple[np.ndarray, np.ndarray]:
        """
        Calculate concordance and discordance matrices
        
        Returns:
        concordance_matrix: Concordance values between alternatives
        discordance_matrix: Discordance values between alternatives
        """
        if self.weighted_matrix is None:
            raise ValueError("Weighted matrix not calculated")
        
        n_alternatives = len(self.alternatives)
        concordance_matrix = np.zeros((n_alternatives, n_alternatives))
        discordance_matrix = np.zeros((n_alternatives, n_alternatives))
        
        for i in range(n_alternatives):
            for k in range(n_alternatives):
                if i == k:
                    concordance_matrix[i, k] = 1.0
                    discordance_matrix[i, k] = 0.0
                    continue
                
                # Calculate concordance
                concordance_set = []
                for j in range(len(self.criteria)):
                    if self.weighted_matrix[i, j] >= self.weighted_matrix[k, j]:
                        concordance_set.append(j)
                
                concordance_matrix[i, k] = sum(self.weights[j] for j in concordance_set)
                
                # Calculate discordance
                discordance_values = []
                max_difference = 0
                
                for j in range(len(self.criteria)):
                    if j not in concordance_set:
                        difference = abs(self.weighted_matrix[k, j] - self.weighted_matrix[i, j])
                        discordance_values.append(difference)
                    difference_all = abs(self.weighted_matrix[i, j] - self.weighted_matrix[k, j])
                    max_difference = max(max_difference, difference_all)
                
                if max_difference > 0:
                    discordance_matrix[i, k] = max(discordance_values) / max_difference if discordance_values else 0
                else:
                    discordance_matrix[i, k] = 0
        
        return concordance_matrix, discordance_matrix
    
    def solve(self, c_threshold: float = 0.7, d_threshold: float = 0.3) -> Dict[str, Any]:
        """
        Solve the ELECTRE problem
        
        Parameters:
        c_threshold: Concordance threshold (0-1)
        d_threshold: Discordance threshold (0-1)
        
        Returns:
        Dictionary containing results
        """
        # Perform calculations
        self.normalize_matrix()
        self.apply_weights()
        concordance, discordance = self.calculate_concordance_discordance()
        
        # Determine dominance
        n_alternatives = len(self.alternatives)
        dominance_matrix = np.zeros((n_alternatives, n_alternatives), dtype=bool)
        
        for i in range(n_alternatives):
            for k in range(n_alternatives):
                if i != k:
                    dominance_matrix[i, k] = (concordance[i, k] >= c_threshold and 
                                            discordance[i, k] <= d_threshold)
        
        # Find kernel (non-dominated alternatives)
        kernel_indices = []
        for i in range(n_alternatives):
            dominated = False
            for k in range(n_alternatives):
                if i != k and dominance_matrix[k, i]:
                    dominated = True
                    break
            if not dominated:
                kernel_indices.append(i)
        
        kernel_alternatives = [self.alternatives[i] for i in kernel_indices]
        
        return {
            'kernel': kernel_alternatives,
            'kernel_indices': kernel_indices,
            'concordance_matrix': concordance,
            'discordance_matrix': discordance,
            'dominance_matrix': dominance_matrix,
            'normalized_matrix': self.normalized_matrix,
            'weighted_matrix': self.weighted_matrix
        }
    
    def get_results_dataframe(self, results: Dict[str, Any]) -> pd.DataFrame:
        """Convert results to pandas DataFrame for better visualization"""
        data = {
            'Alternative': self.alternatives,
            'In_Kernel': [i in results['kernel_indices'] for i in range(len(self.alternatives))]
        }
        
        return pd.DataFrame(data)