"""Decision fusion implementation for multi-agent consensus.

This module implements algorithms to combine opinions from multiple
agents (MarketAnalyst, SentimentAnalyst, FundamentalAnalyst, BullResearcher,
BearResearcher, RiskManager) into a unified trading decision.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any, Dict, List, Optional

from loguru import logger


class SignalType(Enum):
    """Types of trading signals."""

    STRONG_BUY = 2
    BUY = 1
    HOLD = 0
    SELL = -1
    STRONG_SELL = -2


class AgentRole(Enum):
    """Agent roles for weighting purposes."""

    MARKET_ANALYST = "market_analyst"
    SENTIMENT_ANALYST = "sentiment_analyst"
    FUNDAMENTAL_ANALYST = "fundamental_analyst"
    BULL_RESEARCHER = "bull_researcher"
    BEAR_RESEARCHER = "bear_researcher"
    RISK_MANAGER = "risk_manager"


@dataclass
class AgentOpinion:
    """Opinion from a single agent.

    Attributes:
        agent_id: Unique agent identifier
        role: Agent's role
        signal: Trading signal (buy/sell/hold)
        confidence: Confidence level (0.0 to 1.0)
        reasoning: Explanation for the opinion
        factors: Key factors influencing the opinion
        metadata: Additional agent-specific data
    """

    agent_id: str
    role: AgentRole
    signal: SignalType
    confidence: float
    reasoning: str
    factors: List[str] = field(default_factory=list)
    metadata: Dict[str, Any] = field(default_factory=dict)
    timestamp: datetime = field(default_factory=datetime.now)

    def __post_init__(self):
        """Validate confidence is in valid range."""
        self.confidence = max(0.0, min(1.0, self.confidence))

    def to_dict(self) -> Dict[str, Any]:
        """Convert opinion to dictionary."""
        return {
            "agent_id": self.agent_id,
            "role": self.role.value,
            "signal": self.signal.name,
            "signal_value": self.signal.value,
            "confidence": round(self.confidence, 4),
            "reasoning": self.reasoning,
            "factors": self.factors,
            "metadata": self.metadata,
            "timestamp": self.timestamp.isoformat(),
        }


@dataclass
class WeightedVote:
    """A weighted vote from an agent.

    Attributes:
        opinion: The original opinion
        weight: Calculated weight for this opinion
        weighted_score: Signal value multiplied by weight and confidence
    """

    opinion: AgentOpinion
    weight: float
    weighted_score: float

    def to_dict(self) -> Dict[str, Any]:
        """Convert vote to dictionary."""
        return {
            "opinion": self.opinion.to_dict(),
            "weight": round(self.weight, 4),
            "weighted_score": round(self.weighted_score, 4),
        }


@dataclass
class FusionResult:
    """Result of decision fusion.

    Attributes:
        symbol: Trading symbol
        final_signal: Aggregated trading signal
        final_confidence: Confidence in the final decision
        consensus_level: Level of agreement among agents
        supporting_opinions: Opinions supporting the final decision
        opposing_opinions: Opinions opposing the final decision
        risk_assessment: Risk considerations
        execution_plan: Suggested execution strategy
    """

    symbol: str
    final_signal: SignalType
    final_confidence: float
    consensus_level: float
    supporting_opinions: List[AgentOpinion] = field(default_factory=list)
    opposing_opinions: List[AgentOpinion] = field(default_factory=list)
    risk_assessment: str = ""
    execution_plan: Dict[str, Any] = field(default_factory=dict)
    weighted_votes: List[WeightedVote] = field(default_factory=list)
    timestamp: datetime = field(default_factory=datetime.now)

    def to_dict(self) -> Dict[str, Any]:
        """Convert result to dictionary."""
        return {
            "symbol": self.symbol,
            "final_signal": self.final_signal.name,
            "final_signal_value": self.final_signal.value,
            "final_confidence": round(self.final_confidence, 4),
            "consensus_level": round(self.consensus_level, 4),
            "supporting_opinions": [o.to_dict() for o in self.supporting_opinions],
            "opposing_opinions": [o.to_dict() for o in self.opposing_opinions],
            "risk_assessment": self.risk_assessment,
            "execution_plan": self.execution_plan,
            "weighted_votes": [v.to_dict() for v in self.weighted_votes],
            "timestamp": self.timestamp.isoformat(),
        }

    def get_recommendation_text(self) -> str:
        """Get human-readable recommendation."""
        signal_map = {
            SignalType.STRONG_BUY: "强烈买入 (Strong Buy)",
            SignalType.BUY: "买入 (Buy)",
            SignalType.HOLD: "持有 (Hold)",
            SignalType.SELL: "卖出 (Sell)",
            SignalType.STRONG_SELL: "强烈卖出 (Strong Sell)",
        }
        return signal_map.get(self.final_signal, "未知 (Unknown)")


@dataclass
class FusionConfig:
    """Configuration for decision fusion.

    Attributes:
        role_weights: Base weights for each agent role
        confidence_threshold: Minimum confidence to consider an opinion
        consensus_threshold: Threshold for high consensus
        enable_risk_override: Whether risk manager can override decisions
        min_agreement_ratio: Minimum ratio of agreeing agents
    """

    role_weights: Dict[AgentRole, float] = field(default_factory=lambda: {
        AgentRole.MARKET_ANALYST: 1.0,
        AgentRole.SENTIMENT_ANALYST: 0.9,
        AgentRole.FUNDAMENTAL_ANALYST: 1.2,
        AgentRole.BULL_RESEARCHER: 0.8,
        AgentRole.BEAR_RESEARCHER: 0.8,
        AgentRole.RISK_MANAGER: 1.5,  # Risk manager has highest weight
    })
    confidence_threshold: float = 0.3
    consensus_threshold: float = 0.7
    enable_risk_override: bool = True
    min_agreement_ratio: float = 0.5

    def __post_init__(self):
        """Validate configuration."""
        if self.confidence_threshold < 0 or self.confidence_threshold > 1:
            raise ValueError("confidence_threshold must be between 0 and 1")
        if self.consensus_threshold < 0 or self.consensus_threshold > 1:
            raise ValueError("consensus_threshold must be between 0 and 1")


class DecisionFusion:
    """Decision fusion engine for combining agent opinions.

    Combines opinions from multiple agents using weighted voting,
    confidence calibration, and risk-aware decision making.
    """

    def __init__(self, config: Optional[FusionConfig] = None):
        """Initialize decision fusion.

        Args:
            config: Fusion configuration
        """
        self.config = config or FusionConfig()
        self._fusion_history: List[FusionResult] = []

    def add_opinion(self, opinion: AgentOpinion) -> None:
        """Add an agent opinion to be considered.

        Args:
            opinion: The agent's opinion
        """
        if not hasattr(self, '_current_opinions'):
            self._current_opinions: List[AgentOpinion] = []
        self._current_opinions.append(opinion)
        logger.debug(f"Added opinion from {opinion.agent_id} ({opinion.role.value})")

    def start_fusion(self, symbol: str) -> None:
        """Start a new fusion process.

        Args:
            symbol: The trading symbol
        """
        self.symbol = symbol
        self._current_opinions = []
        self.start_time = datetime.now()
        logger.info(f"Starting decision fusion for {symbol}")

    def _calculate_weight(self, opinion: AgentOpinion) -> float:
        """Calculate weight for an opinion.

        Args:
            opinion: The agent's opinion

        Returns:
            Calculated weight
        """
        # Base weight from role
        base_weight = self.config.role_weights.get(opinion.role, 1.0)

        # Adjust by confidence
        confidence_factor = opinion.confidence

        # Role-specific adjustments
        if opinion.role == AgentRole.RISK_MANAGER:
            # Risk manager gets extra weight when signaling danger
            if opinion.signal in [SignalType.SELL, SignalType.STRONG_SELL]:
                base_weight *= 1.5

        return base_weight * confidence_factor

    def _perform_weighted_voting(self) -> List[WeightedVote]:
        """Perform weighted voting on all opinions.

        Returns:
            List of weighted votes
        """
        votes = []

        for opinion in self._current_opinions:
            # Skip low-confidence opinions
            if opinion.confidence < self.config.confidence_threshold:
                continue

            weight = self._calculate_weight(opinion)
            weighted_score = opinion.signal.value * weight * opinion.confidence

            vote = WeightedVote(
                opinion=opinion,
                weight=weight,
                weighted_score=weighted_score,
            )
            votes.append(vote)

        return votes

    def _aggregate_scores(self, votes: List[WeightedVote]) -> float:
        """Aggregate weighted scores into final score.

        Args:
            votes: List of weighted votes

        Returns:
            Aggregated score
        """
        if not votes:
            return 0.0

        total_score = sum(v.weighted_score for v in votes)
        total_weight = sum(v.weight for v in votes)

        if total_weight == 0:
            return 0.0

        return total_score / total_weight

    def _score_to_signal(self, score: float) -> SignalType:
        """Convert aggregated score to signal.

        Args:
            score: Aggregated score

        Returns:
            Trading signal
        """
        if score >= 1.5:
            return SignalType.STRONG_BUY
        elif score >= 0.5:
            return SignalType.BUY
        elif score <= -1.5:
            return SignalType.STRONG_SELL
        elif score <= -0.5:
            return SignalType.SELL
        else:
            return SignalType.HOLD

    def _calculate_consensus(self, votes: List[WeightedVote]) -> float:
        """Calculate consensus level among agents.

        Args:
            votes: List of weighted votes

        Returns:
            Consensus level (0.0 to 1.0)
        """
        if not votes:
            return 0.0

        # Group by signal direction
        buy_votes = [v for v in votes if v.opinion.signal.value > 0]
        sell_votes = [v for v in votes if v.opinion.signal.value < 0]
        hold_votes = [v for v in votes if v.opinion.signal.value == 0]

        # Calculate total weights
        buy_weight = sum(v.weight for v in buy_votes)
        sell_weight = sum(v.weight for v in sell_votes)
        hold_weight = sum(v.weight for v in hold_votes)

        total_weight = buy_weight + sell_weight + hold_weight
        if total_weight == 0:
            return 0.0

        # Find dominant direction
        weights = [buy_weight, sell_weight, hold_weight]
        dominant_weight = max(weights)

        # Consensus is the ratio of dominant weight to total
        return dominant_weight / total_weight

    def _categorize_opinions(
        self,
        votes: List[WeightedVote],
        final_signal: SignalType,
    ) -> tuple[List[AgentOpinion], List[AgentOpinion]]:
        """Categorize opinions as supporting or opposing.

        Args:
            votes: List of weighted votes
            final_signal: Final decision signal

        Returns:
            Tuple of (supporting, opposing) opinions
        """
        supporting = []
        opposing = []

        for vote in votes:
            opinion = vote.opinion
            opinion_direction = opinion.signal.value
            final_direction = final_signal.value

            # Check if opinion aligns with final decision
            if (opinion_direction > 0 and final_direction > 0) or \
               (opinion_direction < 0 and final_direction < 0) or \
               (opinion_direction == 0 and final_direction == 0):
                supporting.append(opinion)
            else:
                opposing.append(opinion)

        return supporting, opposing

    def _check_risk_override(self, votes: List[WeightedVote]) -> Optional[SignalType]:
        """Check if risk manager should override decision.

        Args:
            votes: List of weighted votes

        Returns:
            Override signal if risk manager triggers, None otherwise
        """
        if not self.config.enable_risk_override:
            return None

        # Find risk manager vote
        risk_votes = [v for v in votes if v.opinion.role == AgentRole.RISK_MANAGER]

        for vote in risk_votes:
            # Risk manager strongly warns against trade
            if vote.opinion.signal in [SignalType.SELL, SignalType.STRONG_SELL]:
                if vote.opinion.confidence > 0.8:
                    logger.warning(
                        f"Risk manager override triggered: {vote.opinion.reasoning}"
                    )
                    return SignalType.SELL

        return None

    def _generate_execution_plan(
        self,
        final_signal: SignalType,
        confidence: float,
        consensus: float,
    ) -> Dict[str, Any]:
        """Generate execution plan based on decision.

        Args:
            final_signal: Final trading signal
            confidence: Confidence level
            consensus: Consensus level

        Returns:
            Execution plan dictionary
        """
        plan = {
            "action": final_signal.name,
            "urgency": "normal",
            "position_size": "standard",
            "timing": "market_hours",
            "notes": [],
        }

        # Adjust based on confidence
        if confidence > 0.8 and consensus > 0.7:
            plan["urgency"] = "high"
            plan["position_size"] = "full"
        elif confidence > 0.6:
            plan["position_size"] = "standard"
        else:
            plan["position_size"] = "reduced"
            plan["notes"].append("Low confidence - reduce position size")

        # Add signal-specific notes
        if final_signal == SignalType.STRONG_BUY:
            plan["notes"].append("Strong bullish consensus - consider aggressive entry")
        elif final_signal == SignalType.STRONG_SELL:
            plan["notes"].append("Strong bearish consensus - consider immediate exit")
        elif final_signal == SignalType.HOLD:
            plan["notes"].append("No clear direction - wait for better setup")

        return plan

    def fuse(self) -> FusionResult:
        """Execute decision fusion on collected opinions.

        Returns:
            FusionResult with final decision
        """
        if not self._current_opinions:
            raise ValueError("No opinions to fuse")

        # Perform weighted voting
        votes = self._perform_weighted_voting()

        if not votes:
            logger.warning("No votes passed confidence threshold")
            return FusionResult(
                symbol=self.symbol,
                final_signal=SignalType.HOLD,
                final_confidence=0.0,
                consensus_level=0.0,
            )

        # Calculate aggregated score
        aggregated_score = self._aggregate_scores(votes)

        # Check for risk override
        override = self._check_risk_override(votes)

        # Determine final signal
        if override:
            final_signal = override
            logger.warning(f"Risk override applied: {override.name}")
        else:
            final_signal = self._score_to_signal(aggregated_score)

        # Calculate consensus
        consensus = self._calculate_consensus(votes)

        # Calculate final confidence
        final_confidence = min(
            consensus * (1 + abs(aggregated_score) / 2),
            0.95
        )

        # Categorize opinions
        supporting, opposing = self._categorize_opinions(votes, final_signal)

        # Get risk assessment
        risk_votes = [v for v in votes if v.opinion.role == AgentRole.RISK_MANAGER]
        risk_assessment = ""
        if risk_votes:
            risk_assessment = risk_votes[0].opinion.reasoning

        # Generate execution plan
        execution_plan = self._generate_execution_plan(
            final_signal, final_confidence, consensus
        )

        result = FusionResult(
            symbol=self.symbol,
            final_signal=final_signal,
            final_confidence=final_confidence,
            consensus_level=consensus,
            supporting_opinions=supporting,
            opposing_opinions=opposing,
            risk_assessment=risk_assessment,
            execution_plan=execution_plan,
            weighted_votes=votes,
        )

        self._fusion_history.append(result)
        logger.info(
            f"Fusion complete for {self.symbol}: {final_signal.name} "
            f"(confidence={final_confidence:.2f}, consensus={consensus:.2f})"
        )

        return result

    def get_fusion_history(self) -> List[FusionResult]:
        """Get history of all fusion results."""
        return self._fusion_history.copy()

    def get_latest_fusion(self, symbol: str) -> Optional[FusionResult]:
        """Get most recent fusion result for a symbol.

        Args:
            symbol: Trading symbol

        Returns:
            Most recent FusionResult or None
        """
        for result in reversed(self._fusion_history):
            if result.symbol == symbol:
                return result
        return None
