"""Signal aggregation module for combining multiple strategy signals.

This module provides methods for aggregating trading signals from multiple
strategies, including voting mechanisms, weighted signals, and confidence thresholds.
"""

from dataclasses import dataclass, field
from enum import Enum, auto
from typing import Dict, List, Optional, Protocol, Tuple

import numpy as np


class AggregationMethod(str, Enum):
    """Supported signal aggregation methods."""

    VOTING = "voting"
    WEIGHTED = "weighted"
    CONFIDENCE_THRESHOLD = "confidence_threshold"
    MAJORITY_VOTE = "majority_vote"
    UNANIMOUS = "unanimous"


@dataclass
class StrategySignal:
    """Signal from a single strategy.

    Attributes:
        strategy_id: Identifier of the strategy
        signal: Trading signal ("buy", "sell", "hold", or numeric)
        confidence: Confidence level (0.0 to 1.0)
        metadata: Additional signal metadata
    """

    strategy_id: str
    signal: str
    confidence: float = 0.5
    metadata: Dict[str, float] = field(default_factory=dict)

    def __post_init__(self) -> None:
        """Validate signal parameters."""
        self.confidence = max(0.0, min(1.0, self.confidence))


@dataclass
class AggregatedSignal:
    """Aggregated signal from multiple strategies.

    Attributes:
        aggregated_signal: Final aggregated signal
        confidence: Aggregated confidence level
        contribution_breakdown: Contribution from each strategy
        method: Aggregation method used
        raw_signals: Original signals before aggregation
    """

    aggregated_signal: str
    confidence: float
    contribution_breakdown: Dict[str, float] = field(default_factory=dict)
    method: AggregationMethod = AggregationMethod.VOTING
    raw_signals: List[StrategySignal] = field(default_factory=list)

    @property
    def is_bullish(self) -> bool:
        """Check if signal is bullish (buy)."""
        return self.aggregated_signal.lower() in ("buy", "long", "bullish")

    @property
    def is_bearish(self) -> bool:
        """Check if signal is bearish (sell)."""
        return self.aggregated_signal.lower() in ("sell", "short", "bearish")

    @property
    def is_neutral(self) -> bool:
        """Check if signal is neutral (hold)."""
        return self.aggregated_signal.lower() in ("hold", "neutral", "flat")


class SignalAggregator:
    """Aggregator for combining multiple strategy signals.

    This class provides various methods to aggregate trading signals from
    multiple strategies, including voting, weighted aggregation, and
    confidence-based filtering.

    Args:
        method: Default aggregation method
        confidence_threshold: Minimum confidence for signal acceptance
        weights: Optional strategy weights for weighted aggregation
    """

    # Signal to numeric mapping for calculations
    SIGNAL_VALUES = {
        "buy": 1.0,
        "long": 1.0,
        "bullish": 1.0,
        "sell": -1.0,
        "short": -1.0,
        "bearish": -1.0,
        "hold": 0.0,
        "neutral": 0.0,
        "flat": 0.0,
    }

    def __init__(
        self,
        method: AggregationMethod = AggregationMethod.WEIGHTED,
        confidence_threshold: float = 0.5,
        weights: Optional[Dict[str, float]] = None,
    ):
        self.method = method
        self.confidence_threshold = confidence_threshold
        self.weights = weights or {}
        self._aggregation_methods = {
            AggregationMethod.VOTING: self._aggregate_voting,
            AggregationMethod.WEIGHTED: self._aggregate_weighted,
            AggregationMethod.CONFIDENCE_THRESHOLD: self._aggregate_confidence_threshold,
            AggregationMethod.MAJORITY_VOTE: self._aggregate_majority_vote,
            AggregationMethod.UNANIMOUS: self._aggregate_unanimous,
        }

    def aggregate(
        self,
        signals: List[StrategySignal],
        method: Optional[AggregationMethod] = None,
        weights: Optional[Dict[str, float]] = None,
    ) -> AggregatedSignal:
        """Aggregate multiple strategy signals.

        Args:
            signals: List of strategy signals to aggregate
            method: Override default aggregation method
            weights: Override default weights

        Returns:
            Aggregated signal result
        """
        if not signals:
            return AggregatedSignal(
                aggregated_signal="hold",
                confidence=0.0,
                method=method or self.method,
                raw_signals=[],
            )

        use_method = method or self.method
        use_weights = weights or self.weights

        aggregator = self._aggregation_methods.get(
            use_method, self._aggregate_weighted
        )

        return aggregator(signals, use_weights)

    def _signal_to_numeric(self, signal: str) -> float:
        """Convert signal string to numeric value."""
        return self.SIGNAL_VALUES.get(signal.lower(), 0.0)

    def _numeric_to_signal(self, value: float, threshold: float = 0.1) -> str:
        """Convert numeric value back to signal string."""
        if value > threshold:
            return "buy"
        elif value < -threshold:
            return "sell"
        else:
            return "hold"

    def _aggregate_voting(
        self,
        signals: List[StrategySignal],
        weights: Dict[str, float],
    ) -> AggregatedSignal:
        """Simple voting aggregation - each strategy gets one vote."""
        votes: Dict[str, int] = {"buy": 0, "sell": 0, "hold": 0}

        for sig in signals:
            signal_lower = sig.signal.lower()
            if signal_lower in ("buy", "long", "bullish"):
                votes["buy"] += 1
            elif signal_lower in ("sell", "short", "bearish"):
                votes["sell"] += 1
            else:
                votes["hold"] += 1

        # Determine winner
        max_votes = max(votes.values())
        winners = [k for k, v in votes.items() if v == max_votes]

        # Break ties with hold
        aggregated = winners[0] if len(winners) == 1 else "hold"

        # Calculate confidence based on vote proportion
        total_votes = sum(votes.values())
        confidence = max_votes / total_votes if total_votes > 0 else 0.0

        # Calculate contribution breakdown
        contributions = {}
        for sig in signals:
            if sig.signal.lower() == aggregated or (
                aggregated == "hold" and sig.signal.lower() not in ("buy", "sell")
            ):
                contributions[sig.strategy_id] = 1.0 / max_votes if max_votes > 0 else 0.0
            else:
                contributions[sig.strategy_id] = 0.0

        return AggregatedSignal(
            aggregated_signal=aggregated,
            confidence=confidence,
            contribution_breakdown=contributions,
            method=AggregationMethod.VOTING,
            raw_signals=signals,
        )

    def _aggregate_weighted(
        self,
        signals: List[StrategySignal],
        weights: Dict[str, float],
    ) -> AggregatedSignal:
        """Weighted aggregation using strategy weights."""
        total_weight = 0.0
        weighted_sum = 0.0
        contributions: Dict[str, float] = {}

        for sig in signals:
            weight = weights.get(sig.strategy_id, 1.0 / len(signals))
            numeric_signal = self._signal_to_numeric(sig.signal)

            # Apply confidence adjustment
            effective_weight = weight * sig.confidence

            weighted_sum += numeric_signal * effective_weight
            total_weight += effective_weight
            contributions[sig.strategy_id] = effective_weight

        if total_weight == 0:
            return AggregatedSignal(
                aggregated_signal="hold",
                confidence=0.0,
                contribution_breakdown=contributions,
                method=AggregationMethod.WEIGHTED,
                raw_signals=signals,
            )

        # Normalize weighted sum
        normalized_value = weighted_sum / total_weight
        aggregated = self._numeric_to_signal(normalized_value)

        # Calculate confidence as absolute normalized value
        confidence = abs(normalized_value)

        # Normalize contributions
        contributions = {
            k: v / total_weight for k, v in contributions.items()
        }

        return AggregatedSignal(
            aggregated_signal=aggregated,
            confidence=confidence,
            contribution_breakdown=contributions,
            method=AggregationMethod.WEIGHTED,
            raw_signals=signals,
        )

    def _aggregate_confidence_threshold(
        self,
        signals: List[StrategySignal],
        weights: Dict[str, float],
    ) -> AggregatedSignal:
        """Aggregate with confidence threshold filtering."""
        # Filter signals by confidence threshold
        qualified_signals = [
            sig for sig in signals
            if sig.confidence >= self.confidence_threshold
        ]

        if not qualified_signals:
            # No signals meet threshold
            return AggregatedSignal(
                aggregated_signal="hold",
                confidence=0.0,
                contribution_breakdown={},
                method=AggregationMethod.CONFIDENCE_THRESHOLD,
                raw_signals=signals,
            )

        # Use weighted aggregation on qualified signals
        result = self._aggregate_weighted(qualified_signals, weights)
        result.method = AggregationMethod.CONFIDENCE_THRESHOLD
        result.raw_signals = signals  # Include all raw signals

        return result

    def _aggregate_majority_vote(
        self,
        signals: List[StrategySignal],
        weights: Dict[str, float],
    ) -> AggregatedSignal:
        """Majority vote requiring >50% agreement."""
        if not signals:
            return AggregatedSignal(
                aggregated_signal="hold",
                confidence=0.0,
                method=AggregationMethod.MAJORITY_VOTE,
                raw_signals=signals,
            )

        # Count weighted votes
        votes: Dict[str, float] = {"buy": 0.0, "sell": 0.0, "hold": 0.0}

        for sig in signals:
            weight = weights.get(sig.strategy_id, 1.0 / len(signals))
            signal_lower = sig.signal.lower()

            if signal_lower in ("buy", "long", "bullish"):
                votes["buy"] += weight
            elif signal_lower in ("sell", "short", "bearish"):
                votes["sell"] += weight
            else:
                votes["hold"] += weight

        total_votes = sum(votes.values())
        if total_votes == 0:
            return AggregatedSignal(
                aggregated_signal="hold",
                confidence=0.0,
                method=AggregationMethod.MAJORITY_VOTE,
                raw_signals=signals,
            )

        # Find if any signal has majority (>50%)
        majority_threshold = total_votes * 0.5

        for signal_type, vote_count in votes.items():
            if vote_count > majority_threshold:
                confidence = vote_count / total_votes
                contributions = {
                    sig.strategy_id: weights.get(sig.strategy_id, 1.0 / len(signals))
                    for sig in signals
                    if sig.signal.lower() == signal_type or
                    (signal_type == "hold" and sig.signal.lower() not in ("buy", "sell"))
                }

                return AggregatedSignal(
                    aggregated_signal=signal_type,
                    confidence=confidence,
                    contribution_breakdown=contributions,
                    method=AggregationMethod.MAJORITY_VOTE,
                    raw_signals=signals,
                )

        # No majority - return hold
        return AggregatedSignal(
            aggregated_signal="hold",
            confidence=votes["hold"] / total_votes,
            contribution_breakdown={
                sig.strategy_id: weights.get(sig.strategy_id, 1.0 / len(signals))
                for sig in signals
            },
            method=AggregationMethod.MAJORITY_VOTE,
            raw_signals=signals,
        )

    def _aggregate_unanimous(
        self,
        signals: List[StrategySignal],
        weights: Dict[str, float],
    ) -> AggregatedSignal:
        """Require unanimous agreement for signal."""
        if not signals:
            return AggregatedSignal(
                aggregated_signal="hold",
                confidence=0.0,
                method=AggregationMethod.UNANIMOUS,
                raw_signals=signals,
            )

        # Get all unique signals
        unique_signals = set(sig.signal.lower() for sig in signals)

        # Check for unanimous agreement
        if len(unique_signals) == 1:
            signal_type = signals[0].signal.lower()
            avg_confidence = float(np.mean([sig.confidence for sig in signals]))

            contributions = {
                sig.strategy_id: weights.get(sig.strategy_id, 1.0 / len(signals))
                for sig in signals
            }

            return AggregatedSignal(
                aggregated_signal=signal_type,
                confidence=avg_confidence,
                contribution_breakdown=contributions,
                method=AggregationMethod.UNANIMOUS,
                raw_signals=signals,
            )

        # No unanimous agreement - hold
        return AggregatedSignal(
            aggregated_signal="hold",
            confidence=0.0,
            contribution_breakdown={},
            method=AggregationMethod.UNANIMOUS,
            raw_signals=signals,
        )

    def update_weights(self, weights: Dict[str, float]) -> None:
        """Update default strategy weights.

        Args:
            weights: New strategy weights dictionary
        """
        self.weights = weights.copy()

    def set_confidence_threshold(self, threshold: float) -> None:
        """Set the confidence threshold.

        Args:
            threshold: New confidence threshold (0.0 to 1.0)
        """
        self.confidence_threshold = max(0.0, min(1.0, threshold))
