"""Detect and resolve divergent reviewer recommendations (PRD §5.1 FRFA-AM027–028)."""

from __future__ import annotations

from collections import Counter
from dataclasses import dataclass
from decimal import Decimal

from apps.rims.grants.models import Application, GrantCall, Review


def _normalize_rec(text: str) -> str:
    return " ".join(text.strip().lower().split())


def recommendation_divergence(application: Application) -> tuple[bool, list[Review]]:
    """
    True when at least two submitted reviews have non-empty recommendations
    that normalize to different strings.
    """
    submitted = list(application.reviews.filter(submitted_at__isnull=False).order_by("pk"))
    with_text: list[tuple[Review, str]] = []
    for r in submitted:
        raw = (r.recommendation or "").strip()
        if raw:
            with_text.append((r, raw))
    if len(with_text) < 2:
        return False, submitted
    norms = {_normalize_rec(t) for _, t in with_text}
    return len(norms) > 1, submitted


# PRD §5.1 FRFA-AM027–028 — automated resolution of conflicting reviewer
# recommendations. Each policy returns a :class:`ConflictResolutionOutcome`.

RESOLVED_CODE_APPROVE = Review.RecommendationCode.APPROVE
RESOLVED_CODE_REJECT = Review.RecommendationCode.REJECT
RESOLVED_CODE_REVISE = Review.RecommendationCode.REVISE
RESOLVED_CODE_HOLD = Review.RecommendationCode.HOLD


@dataclass(frozen=True)
class ConflictResolutionOutcome:
    """Structured result of applying a call's conflict_resolution policy.

    - `policy`: the policy that was applied (matches GrantCall.ConflictResolution).
    - `has_conflict`: True when the applicable policy detected divergence.
    - `resolved_code`: a Review.RecommendationCode value when policy resolves
      unambiguously; None when the outcome requires manual adjudication
      (ESCALATE, unresolvable majority, or insufficient data).
    - `rationale`: short human-readable explanation for audit logs / manager UI.
    - `reviews`: the submitted Review rows considered.
    """

    policy: str
    has_conflict: bool
    resolved_code: str | None
    rationale: str
    reviews: list[Review]


def _recommendation_codes(reviews: list[Review]) -> list[str]:
    return [r.recommendation_code for r in reviews if r.recommendation_code]


def _majority(codes: list[str]) -> tuple[str | None, str]:
    """Return (winning_code, rationale) for simple-majority voting.

    Returns (None, reason) when there is no strict majority.
    """
    if not codes:
        return None, "No reviewer recommendation codes available."
    counter = Counter(codes)
    top_code, top_count = counter.most_common(1)[0]
    # Strict majority: > half the total votes.
    total = sum(counter.values())
    if top_count * 2 > total:
        return top_code, f"Majority {top_count}/{total} voted {top_code}."
    # Plurality tie — escalate.
    return None, f"No strict majority among {dict(counter)}."


def _average_threshold_resolution(
    reviews: list[Review], threshold: Decimal | None
) -> tuple[str | None, str]:
    if not reviews:
        return None, "No submitted reviews."
    if threshold is None:
        return None, "Call does not define a review_score_threshold."
    totals = [r.total_score for r in reviews if r.total_score is not None]
    if not totals:
        return None, "No reviewer total scores recorded."
    avg = sum(totals, Decimal("0")) / Decimal(len(totals))
    if avg >= threshold:
        return (
            RESOLVED_CODE_APPROVE,
            f"Average score {avg:.2f} ≥ threshold {threshold}.",
        )
    return (
        RESOLVED_CODE_REJECT,
        f"Average score {avg:.2f} < threshold {threshold}.",
    )


def resolve_conflict(application: Application) -> ConflictResolutionOutcome:
    """PRD §5.1 FRFA-AM028 — apply the call's configured conflict resolution
    protocol to an application with divergent reviewer recommendations.

    Safe to call regardless of whether divergence is actually present: the
    returned outcome carries `has_conflict` and an informative `rationale`.
    """
    has_conflict, reviews = recommendation_divergence(application)
    policy = application.call.conflict_resolution

    if not has_conflict:
        return ConflictResolutionOutcome(
            policy=policy,
            has_conflict=False,
            resolved_code=None,
            rationale="No divergent recommendations detected.",
            reviews=reviews,
        )

    submitted = [r for r in reviews if r.submitted_at is not None]

    if policy == GrantCall.ConflictResolution.MAJORITY:
        code, rationale = _majority(_recommendation_codes(submitted))
        return ConflictResolutionOutcome(
            policy=policy,
            has_conflict=True,
            resolved_code=code,
            rationale=rationale,
            reviews=reviews,
        )

    if policy == GrantCall.ConflictResolution.AVERAGE_THRESHOLD:
        code, rationale = _average_threshold_resolution(
            submitted, application.call.review_score_threshold
        )
        return ConflictResolutionOutcome(
            policy=policy,
            has_conflict=True,
            resolved_code=code,
            rationale=rationale,
            reviews=reviews,
        )

    # ESCALATE (default) — the call manager decides.
    return ConflictResolutionOutcome(
        policy=policy,
        has_conflict=True,
        resolved_code=None,
        rationale="Escalated to call manager.",
        reviews=reviews,
    )
