"""Heuristic Pipeline (Pipeline A) — Deterministic scoring and verdict.

Computes ``S_total = S_company + S_macro + S_competitive`` from confluence-
filtered signals and produces a confidence-gated BUY / WATCH / SKIP verdict.

The pipeline reuses the existing ``compute_signal_weight`` infrastructure
from ``services.aggregation.scoring`` for signal weighting and follows the
three-layer signal aggregation model (company, macro, competitive).

Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7
"""

from __future__ import annotations

import logging

from services.signal_engine.config import HeuristicConfig
from services.signal_engine.models import (
    ConfluenceSignal,
    HeuristicResult,
    NormalizedInput,
    SignalDirection,
    Verdict,
)

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Signal classification — which confluence signals belong to which layer
# ---------------------------------------------------------------------------

# Company-level technical signals (Layer 1)
COMPANY_SIGNAL_TYPES: frozenset[str] = frozenset({
    "fibonacci",
    "ma_stack",
    "rsi",
    "cup_handle",
    "elliott_wave",
})

# Competitive signals (Layer 3) — future expansion
COMPETITIVE_SIGNAL_TYPES: frozenset[str] = frozenset()

# Macro weight applied to macro_bias to produce S_macro
_MACRO_WEIGHT: float = 0.5


# ---------------------------------------------------------------------------
# Score computation helpers
# ---------------------------------------------------------------------------


def _compute_s_company(confluence_signals: list[ConfluenceSignal]) -> tuple[float, list[dict]]:
    """Sum confluence scores for company-level signals.

    Returns the total S_company score and a list of per-signal weight
    breakdowns for audit.
    """
    s_company = 0.0
    weights: list[dict] = []

    for sig in confluence_signals:
        if sig.signal_type in COMPANY_SIGNAL_TYPES:
            # Direction-aware: bullish contributes positively, bearish negatively
            direction_sign = _direction_sign(sig.direction)
            contribution = sig.confluence_score * direction_sign
            s_company += contribution
            weights.append({
                "signal_type": sig.signal_type,
                "layer": "company",
                "confluence_score": sig.confluence_score,
                "direction": sig.direction.value,
                "contribution": contribution,
                "active_timeframes": sig.active_timeframes,
            })

    return s_company, weights


def _compute_s_macro(normalized: NormalizedInput) -> float:
    """Compute macro score from macro_bias.

    S_macro = macro_bias * weight, where macro_bias is in [-1.0, 1.0].
    A positive macro_bias contributes positively; negative contributes
    negatively.
    """
    return normalized.macro_bias * _MACRO_WEIGHT


def _compute_s_competitive(confluence_signals: list[ConfluenceSignal]) -> float:
    """Sum confluence scores for competitive-layer signals.

    Currently returns 0.0 as no competitive signal types are defined in
    the signal library.  This is a placeholder for future expansion.
    """
    s_competitive = 0.0
    for sig in confluence_signals:
        if sig.signal_type in COMPETITIVE_SIGNAL_TYPES:
            direction_sign = _direction_sign(sig.direction)
            s_competitive += sig.confluence_score * direction_sign
    return s_competitive


def _direction_sign(direction: SignalDirection) -> float:
    """Map signal direction to a numeric sign."""
    if direction == SignalDirection.BULLISH:
        return 1.0
    if direction == SignalDirection.BEARISH:
        return -1.0
    return 0.0


# ---------------------------------------------------------------------------
# Confidence computation
# ---------------------------------------------------------------------------


def _compute_confidence(
    confluence_signals: list[ConfluenceSignal],
) -> float:
    """Compute pipeline confidence from confluence signals.

    Confidence is derived from:
    1. **Base confidence** — average signal strength across all confluence
       signals (mean of confluence_score values).
    2. **Source count boost** — more active signals increase confidence
       (diminishing returns, capped contribution).
    3. **Signal agreement boost** — if all signals point in the same
       direction, confidence is boosted.
    4. **Contradiction penalty** — if signals disagree on direction,
       confidence is penalised.

    Returns a value clamped to [0.0, 1.0].
    """
    if not confluence_signals:
        return 0.0

    # 1. Base confidence: average confluence score (already weighted by
    #    timeframe importance)
    total_score = sum(s.confluence_score for s in confluence_signals)
    base_confidence = total_score / len(confluence_signals)

    # 2. Source count factor: more signals → higher confidence, with
    #    diminishing returns.  1 signal → 0.6, 2 → 0.75, 3 → 0.85,
    #    4 → 0.90, 5+ → 0.95 (asymptotic).
    n = len(confluence_signals)
    source_factor = 1.0 - (0.4 / n)  # approaches 1.0 as n grows

    # 3. Signal agreement / contradiction
    directions = [s.direction for s in confluence_signals]
    bullish_count = sum(1 for d in directions if d == SignalDirection.BULLISH)
    bearish_count = sum(1 for d in directions if d == SignalDirection.BEARISH)

    if n == 1:
        agreement_factor = 1.0
    elif bullish_count == n or bearish_count == n:
        # Perfect agreement — boost
        agreement_factor = 1.15
    elif bullish_count > 0 and bearish_count > 0:
        # Contradiction — penalty proportional to minority fraction
        minority = min(bullish_count, bearish_count)
        contradiction_ratio = minority / n
        agreement_factor = 1.0 - (0.3 * contradiction_ratio)
    else:
        # Mix of directional and neutral — mild boost
        agreement_factor = 1.05

    confidence = base_confidence * source_factor * agreement_factor
    return max(0.0, min(confidence, 1.0))


# ---------------------------------------------------------------------------
# Verdict logic
# ---------------------------------------------------------------------------


def _determine_verdict(
    confidence: float,
    s_total: float,
    normalized: NormalizedInput,
    config: HeuristicConfig,
) -> tuple[Verdict, list[str]]:
    """Apply threshold logic to determine BUY / WATCH / SKIP verdict.

    Returns the verdict and a list of reasoning strings explaining the
    decision.
    """
    reasoning: list[str] = []

    valuation_score = normalized.valuation_score if normalized.valuation_score is not None else 0.0
    earnings_days = normalized.earnings_proximity_days if normalized.earnings_proximity_days is not None else 0

    # --- Check BUY conditions ---
    buy_conditions = {
        "confidence": confidence >= config.buy_confidence,
        "s_total": s_total >= config.buy_s_total,
        "valuation": valuation_score >= config.buy_valuation_min,
        "macro_bias": normalized.macro_bias > config.macro_bias_threshold,
        "earnings_proximity": earnings_days > config.earnings_days_threshold,
    }

    all_buy_met = all(buy_conditions.values())

    if all_buy_met:
        reasoning.append(
            f"BUY: all conditions met — confidence={confidence:.3f} "
            f"(>= {config.buy_confidence}), S_total={s_total:.3f} "
            f"(>= {config.buy_s_total}), valuation={valuation_score:.2f} "
            f"(>= {config.buy_valuation_min}), macro_bias={normalized.macro_bias:.2f} "
            f"(> {config.macro_bias_threshold}), earnings_days={earnings_days} "
            f"(> {config.earnings_days_threshold})"
        )
        return Verdict.BUY, reasoning

    # --- Check WATCH conditions ---
    if confidence >= config.watch_confidence:
        # WATCH: confidence is sufficient but not all BUY conditions met
        failed_conditions = [k for k, v in buy_conditions.items() if not v]
        reasoning.append(
            f"WATCH: confidence={confidence:.3f} (>= {config.watch_confidence}) "
            f"but BUY conditions not fully met — failed: {', '.join(failed_conditions)}"
        )
        for cond_name, met in buy_conditions.items():
            if not met:
                reasoning.append(f"  - {cond_name} not met")
        return Verdict.WATCH, reasoning

    # --- SKIP ---
    reasoning.append(
        f"SKIP: confidence={confidence:.3f} < {config.watch_confidence} "
        f"(watch threshold)"
    )
    return Verdict.SKIP, reasoning


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


def run_heuristic_pipeline(
    normalized: NormalizedInput,
    confluence_signals: list[ConfluenceSignal],
    config: HeuristicConfig,
) -> HeuristicResult:
    """Run the deterministic heuristic pipeline.

    Computes ``S_total = S_company + S_macro + S_competitive`` using the
    existing three-layer signal aggregation model and produces a
    confidence-gated BUY / WATCH / SKIP verdict.

    Args:
        normalized: The unified input structure for this evaluation tick.
        confluence_signals: Signals that passed multi-timeframe confluence
            filtering.
        config: Heuristic pipeline thresholds.

    Returns:
        A :class:`HeuristicResult` with verdict, scores, weights, and
        reasoning.

    Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7
    """
    # 1. Compute three-layer scores
    s_company, signal_weights = _compute_s_company(confluence_signals)
    s_macro = _compute_s_macro(normalized)
    s_competitive = _compute_s_competitive(confluence_signals)
    s_total = s_company + s_macro + s_competitive

    # 2. Compute confidence
    confidence = _compute_confidence(confluence_signals)

    # 3. Determine verdict
    verdict, reasoning = _determine_verdict(confidence, s_total, normalized, config)

    logger.info(
        "Heuristic pipeline [%s]: verdict=%s confidence=%.3f "
        "S_total=%.3f (company=%.3f macro=%.3f competitive=%.3f) "
        "signals=%d",
        normalized.ticker,
        verdict.value,
        confidence,
        s_total,
        s_company,
        s_macro,
        s_competitive,
        len(confluence_signals),
    )

    return HeuristicResult(
        verdict=verdict,
        confidence=confidence,
        s_total=s_total,
        s_company=s_company,
        s_macro=s_macro,
        s_competitive=s_competitive,
        signal_weights=signal_weights,
        reasoning=reasoning,
    )
