"""
APEX V16 — Price Action engine.

Extracts and scores 10 candlestick patterns from a `tech` dict produced
by the indicator pipeline. Reused by brain_tf and brain_mr (the patterns
are the same; only the *reaction rules* differ between the two Brains).

Design:
  - extract_pa_signals(tech)  -> PASignals (typed bool flags)
  - score_pa(signals, dir)    -> PAScore (bullish/bearish/dominant/strength)

Pattern weights are calibrated on V14/V15 historical performance
(stronger reversal patterns get higher weight). The dominant-vs-direction
relation is computed in score_pa() so callers don't have to repeat the
"is this pattern adverse for my direction?" mapping.

Brain code does NOT consume the raw tech keys for patterns anymore;
it always goes through PASignals (single typed source of truth, fixes
V15-pattern style "tech.get('hammer', False)" sprinkled across files).
"""

from __future__ import annotations

from dataclasses import dataclass, asdict
from typing import Final


# ============================================================
# WEIGHTS — calibrated from V14/V15 historical pattern performance.
# Higher weight = stronger reversal signal. Engulfing > Star > Hammer.
# ============================================================

PATTERN_WEIGHTS: Final[dict] = {
    # Bullish reversal patterns
    "hammer":         1.0,
    "bull_engulfing": 1.5,
    "morning_star":   1.6,
    "piercing":       1.1,
    "doji_bull":      0.6,   # doji at support
    # Bearish reversal patterns
    "shooting_star":  1.0,
    "bear_engulfing": 1.5,
    "evening_star":   1.6,
    "dark_cloud":     1.1,
    "doji_bear":      0.6,   # doji at resistance
}

BULLISH_KEYS: Final[tuple] = (
    "hammer", "bull_engulfing", "morning_star", "piercing", "doji_bull",
)
BEARISH_KEYS: Final[tuple] = (
    "shooting_star", "bear_engulfing", "evening_star", "dark_cloud", "doji_bear",
)


# ============================================================
# DATACLASSES
# ============================================================

@dataclass(frozen=True)
class PASignals:
    """Typed boolean flags for the 10 PA patterns we recognize."""
    # Bullish
    hammer:         bool = False
    bull_engulfing: bool = False
    morning_star:   bool = False
    piercing:       bool = False
    doji_bull:      bool = False
    # Bearish
    shooting_star:  bool = False
    bear_engulfing: bool = False
    evening_star:   bool = False
    dark_cloud:     bool = False
    doji_bear:      bool = False
    # Companion signals (kept here so Brain doesn't re-derive them)
    volume_weak:    bool = False
    candle_strength: float = 1.0

    def bullish_names(self) -> list[str]:
        """Pretty names of bullish patterns currently active."""
        return [k for k in BULLISH_KEYS if getattr(self, k)]

    def bearish_names(self) -> list[str]:
        return [k for k in BEARISH_KEYS if getattr(self, k)]

    def to_dict(self) -> dict:
        return asdict(self)

    def render_explicit(self) -> str:
        """
        Multi-line dump of all 10 patterns with V15 weights.
        Active patterns marked '⭐ (weight)'; inactive shown for completeness
        so the AI sees explicitly what is ABSENT (confluence reasoning).
        """
        def _fmt(name: str) -> str:
            active = bool(getattr(self, name))
            w = PATTERN_WEIGHTS.get(name, 0.0)
            return f"{name}={'⭐' if active else '·'}({w:.1f})"
        bull = ", ".join(_fmt(k) for k in BULLISH_KEYS)
        bear = ", ".join(_fmt(k) for k in BEARISH_KEYS)
        return f"Bullish: {bull}\nBearish: {bear}"


@dataclass(frozen=True)
class PAScore:
    """
    Result of scoring PASignals against a trade direction.

    bullish_score / bearish_score: weighted sums of active patterns.
    dominant: "BULLISH" | "BEARISH" | "NEUTRAL".
    strength: difference (dominant_score - opposing_score), >= 0.
    favor_direction: patterns FAVORING the trade direction (str list).
    adverse_direction: patterns ADVERSE for the trade direction (str list).
    """
    bullish_score: float
    bearish_score: float
    dominant: str
    strength: float
    favor_direction: list[str]
    adverse_direction: list[str]


# ============================================================
# EXTRACTION
# ============================================================

def _g(src, key, default):
    """Attribute-or-key getter for TechSnapshot (attrs) or dict (test mocks)."""
    if hasattr(src, key):
        val = getattr(src, key)
        return val if val is not None else default
    if isinstance(src, dict):
        return src.get(key, default)
    return default


def extract_pa_signals(source) -> PASignals:
    """
    Read pattern flags from a TechSnapshot (preferred) or a dict (legacy
    mock for tests).

    Doji is split into doji_bull / doji_bear via `doji_type`:
      "dragonfly" -> doji_bull
      "gravestone" -> doji_bear
      "standard" or anything else -> neither

    Fields consumed: hammer, shooting_star, bull_engulfing, bear_engulfing,
    morning_star, evening_star, piercing, dark_cloud, doji, doji_type,
    volume_weak, candle_strength.
    """
    doji = bool(_g(source, "doji", False))
    doji_type = str(_g(source, "doji_type", "") or "").lower() if doji else ""
    is_doji_bull = doji and doji_type in ("bull", "dragonfly")
    is_doji_bear = doji and doji_type in ("bear", "gravestone")

    return PASignals(
        hammer=bool(_g(source, "hammer", False)),
        bull_engulfing=bool(_g(source, "bull_engulfing", False)),
        morning_star=bool(_g(source, "morning_star", False)),
        piercing=bool(_g(source, "piercing", False)),
        doji_bull=is_doji_bull,
        shooting_star=bool(_g(source, "shooting_star", False)),
        bear_engulfing=bool(_g(source, "bear_engulfing", False)),
        evening_star=bool(_g(source, "evening_star", False)),
        dark_cloud=bool(_g(source, "dark_cloud", False)),
        doji_bear=is_doji_bear,
        volume_weak=bool(_g(source, "volume_weak", False)),
        candle_strength=float(_g(source, "candle_strength", 1.0)),
    )


# ============================================================
# SCORING
# ============================================================

def score_pa(signals: PASignals, direction: str) -> PAScore:
    """
    Score signals against a trade `direction` ("BUY" | "SELL").

    Bullish score = sum of weights of active bullish patterns.
    Bearish score = sum of weights of active bearish patterns.
    Strength is the (signed) advantage of the dominant side.

    For BUY:  favor = bullish patterns,  adverse = bearish patterns
    For SELL: favor = bearish patterns,  adverse = bullish patterns

    Direction is case-insensitive.
    """
    bull = sum(PATTERN_WEIGHTS[k] for k in BULLISH_KEYS if getattr(signals, k))
    bear = sum(PATTERN_WEIGHTS[k] for k in BEARISH_KEYS if getattr(signals, k))

    if bull > bear:
        dominant = "BULLISH"
    elif bear > bull:
        dominant = "BEARISH"
    else:
        dominant = "NEUTRAL"

    strength = abs(bull - bear)

    is_buy = direction.upper() == "BUY"
    favor = signals.bullish_names() if is_buy else signals.bearish_names()
    adverse = signals.bearish_names() if is_buy else signals.bullish_names()

    return PAScore(
        bullish_score=round(bull, 2),
        bearish_score=round(bear, 2),
        dominant=dominant,
        strength=round(strength, 2),
        favor_direction=favor,
        adverse_direction=adverse,
    )
