import logging
import numpy as np
from typing import List, Callable, Optional, Any, Union, Tuple

from ..core.interfaces import (
    IEvaluator,
    IIndividual,
)

class Evaluator(IEvaluator):
    """
    Standard implementation of an evolutionary evaluator.
    
    Transforms individual or lists of functions into a high-performance,
    concatenated evaluation engine.
    """

    def __init__(self, 
                 features_funcs: Optional[Union[Callable, List[Callable]]] = None, 
                 objectives_funcs: Optional[Union[Callable, List[Callable]]] = None,
                 logger: Optional[logging.Logger] = None,
                 debug: bool = False):
        """
        Initializes the Evaluator with feature and objective callables.

        Args:
            features_funcs: Single callable or list of callables for feature extraction.
            objectives_funcs: Single callable or list of callables for objective evaluation.
            logger: Optional logger instance for status reporting.
            debug: If True, enables verbose logging/checks.
        """
        self.logger = logger or logging.getLogger(__name__)
        self.debug = debug

        # --- NORMALIZATION ---
        # Transform inputs into fixed-signature callables: (List[IIndividual]) -> np.ndarray (N, K)
        # We KEEP the names expected by factory.py and other modules for compatibility
        self.features_funcs = self._build_combined_callable(features_funcs, default_val=1.0)
        self.objectives_funcs = self._build_combined_callable(objectives_funcs, default_val=0.0)

    def _build_combined_callable(self, input_funcs: Any, default_val: float = 0.0) -> Callable[[List[IIndividual]], np.ndarray]:
        """
        Internal factory that wraps multiple functions into a single concatenated result.
        
        Args:
            input_funcs: The user-provided callables.
            default_val: Value used if input_funcs is None.
            
        Returns:
            A single callable that returns a 2D NumPy array of shape (N, TotalDimensions).
        """
        if input_funcs is None:
            return lambda x: np.full((len(x), 1), default_val, dtype=np.float64)

        # Ensure we work with a list of functions
        funcs = input_funcs if isinstance(input_funcs, list) else [input_funcs]

        def combined_wrapper(individuals: List[IIndividual]) -> np.ndarray:
            if not individuals:
                return np.empty((0, len(funcs)))
                
            results: List[np.ndarray] = []
            for func in funcs:
                res = np.atleast_2d(func(individuals))
                # If result is (N,) convert it to (N, 1)
                # If result is (1, N) (transposed), fix it
                if res.shape[0] != len(individuals) and res.shape[1] == len(individuals):
                    res = res.T
                results.append(res)
            
            return np.concatenate(results, axis=1)

        return combined_wrapper

    def evaluate(self, individuals: List[IIndividual], ctx: Any) -> List[IIndividual]:
        """
        Full evaluation pass: updates features and objectives for the given individuals.
        
        Args:
            individuals: The list of structure containers to evaluate.
            ctx: Execution context (unused in this implementation but required by protocol).
            
        Returns:
            The list of evaluated individuals (mutates in place).
        """
        # Typically the actual storage in individual.E or similar happens here or in get methods.
        # This is a protocol requirement.
        _ = self.get_features(individuals)
        _ = self.get_objectives(individuals)
        return individuals

    def get_features(self, dataset: List[IIndividual]) -> np.ndarray:
        """Computes concatenated feature matrix."""
        return self.features_funcs(dataset)

    def get_objectives(self, dataset: List[IIndividual]) -> np.ndarray:
        """Computes concatenated objective matrix."""
        return self.objectives_funcs(dataset)

    def evaluate_features_objectives(self, individuals: List[IIndividual]) -> Tuple[np.ndarray, np.ndarray]:
        """Deprecated: use get_features and get_objectives instead."""
        return self.get_features(individuals), self.get_objectives(individuals)
