"""Sentiment aggregation logic.

Aggregates per-text sentiment scores into a single company result,
with mixed sentiment detection when evidence groups disagree.

Mixed sentiment is NOT an unconstrained fourth softmax label — it is
computed from disagreement between evidence texts (some positive,
some negative with margin > threshold).
"""

from __future__ import annotations

from services.intelligence_pipeline_v3.sentiment.models import (
    CompanySentimentResult,
    TextSentiment,
)

# Disagreement threshold: both positive and negative probabilities
# must be >= this value across evidence texts to signal disagreement.
# A text with positive_prob >= threshold AND another text with
# negative_prob >= threshold indicates conflicting evidence.
MIXED_DISAGREEMENT_THRESHOLD = 0.3


def aggregate_evidence_sentiments(
    company_id: str,
    per_text_scores: list[TextSentiment],
    model_version: str,
    calibration_version: str = "uncalibrated",
) -> CompanySentimentResult:
    """Aggregate per-text sentiment scores into a company-level result.

    Computes weighted average probabilities and detects mixed sentiment
    from evidence-group disagreement. Mixed is triggered when at least
    one text has positive_prob >= threshold AND at least one other text
    has negative_prob >= threshold.

    Parameters
    ----------
    company_id
        Resolved company identifier.
    per_text_scores
        List of TextSentiment objects, one per evidence text.
    model_version
        Sentiment model version string for lineage.
    calibration_version
        Calibration artifact version.

    Returns
    -------
    CompanySentimentResult
        Aggregated result with label, probabilities, per-text scores,
        is_mixed flag, and evidence IDs.
    """
    if not per_text_scores:
        return CompanySentimentResult(
            company_id=company_id,
            label="neutral",
            positive_prob=0.0,
            negative_prob=0.0,
            neutral_prob=1.0,
            evidence_ids=[],
            is_mixed=False,
            per_text_scores=[],
            model_version=model_version,
            calibration_version=calibration_version,
        )

    # Detect mixed sentiment from disagreement
    is_mixed = _detect_mixed_from_disagreement(per_text_scores)

    # Compute average probabilities across texts
    n = len(per_text_scores)
    avg_pos = sum(s.positive_prob for s in per_text_scores) / n
    avg_neg = sum(s.negative_prob for s in per_text_scores) / n
    avg_neu = sum(s.neutral_prob for s in per_text_scores) / n

    # Normalize to ensure probabilities sum to 1.0
    total = avg_pos + avg_neg + avg_neu
    if total > 0:
        avg_pos /= total
        avg_neg /= total
        avg_neu /= total
    else:
        avg_pos = 0.0
        avg_neg = 0.0
        avg_neu = 1.0

    # Determine label
    if is_mixed:
        label = "mixed"
    else:
        label = _argmax_label(avg_pos, avg_neg, avg_neu)

    evidence_ids = [s.evidence_id for s in per_text_scores]

    return CompanySentimentResult(
        company_id=company_id,
        label=label,
        positive_prob=round(avg_pos, 6),
        negative_prob=round(avg_neg, 6),
        neutral_prob=round(avg_neu, 6),
        evidence_ids=evidence_ids,
        is_mixed=is_mixed,
        per_text_scores=per_text_scores,
        model_version=model_version,
        calibration_version=calibration_version,
    )


def _detect_mixed_from_disagreement(per_text_scores: list[TextSentiment]) -> bool:
    """Detect mixed sentiment from evidence-group disagreement.

    Returns True when at least one text has positive_prob >= threshold
    AND at least one (different) text has negative_prob >= threshold.
    This indicates conflicting evidence directions.

    A single text cannot trigger mixed on its own (we need disagreement
    between at least 2 texts).
    """
    if len(per_text_scores) < 2:
        return False

    max_pos = max(s.positive_prob for s in per_text_scores)
    max_neg = max(s.negative_prob for s in per_text_scores)

    return max_pos >= MIXED_DISAGREEMENT_THRESHOLD and max_neg >= MIXED_DISAGREEMENT_THRESHOLD


def _argmax_label(pos: float, neg: float, neu: float) -> str:
    """Return the label with the highest probability."""
    if pos >= neg and pos >= neu:
        return "positive"
    elif neg >= pos and neg >= neu:
        return "negative"
    return "neutral"
