"""
Candlestick pattern detectors — port fedele da V15.

Each function scans the last 1-3 bars and returns boolean flags.
Scope: hammer/shooting_star, engulfing (bull/bear), doji (+ type),
piercing/dark_cloud, morning_star/evening_star, volume_weak.

These flags become the input to analysis.price_action which
computes the weighted PA score consumed by both Brains.
"""

from __future__ import annotations

from typing import Optional

import pandas as pd


# ============================================================
# HAMMER / SHOOTING STAR
# ============================================================

def calc_hammer(df: pd.DataFrame) -> tuple[bool, bool]:
    """
    Returns (hammer, shooting_star). Scans last 2 bars.

    Hammer: body small near top, lower shadow >= 2x body, close in
            top 60% of range.
    Shooting star: mirrored — upper shadow >= 2x body, close in bottom 60%.
    """
    for i in [-1, -2]:
        o = df["open"].iloc[i]
        h = df["high"].iloc[i]
        l = df["low"].iloc[i]
        c = df["close"].iloc[i]
        body = abs(c - o) + 1e-9
        lower_shadow = min(o, c) - l
        upper_shadow = h - max(o, c)
        candle_range = h - l + 1e-9
        hammer = (lower_shadow >= 2 * body and (c - l) / candle_range >= 0.6)
        shooting_star = (upper_shadow >= 2 * body and (h - c) / candle_range >= 0.6)
        if hammer or shooting_star:
            return bool(hammer), bool(shooting_star)
    return False, False


# ============================================================
# ENGULFING
# ============================================================

def calc_engulfing(df: pd.DataFrame) -> tuple[bool, bool]:
    """
    Returns (bull_engulfing, bear_engulfing). Scans last 3 bar-pairs.
    """
    bull_eng = False
    bear_eng = False
    for i in [-1, -2, -3]:
        o_prev = df["open"].iloc[i - 1]
        c_prev = df["close"].iloc[i - 1]
        o_curr = df["open"].iloc[i]
        c_curr = df["close"].iloc[i]
        prev_bearish = c_prev < o_prev
        curr_bullish = c_curr > o_curr
        covers = o_curr <= c_prev and c_curr >= o_prev
        if prev_bearish and curr_bullish and covers:
            bull_eng = True
        prev_bullish = c_prev > o_prev
        curr_bearish = c_curr < o_curr
        covers_bear = o_curr >= c_prev and c_curr <= o_prev
        if prev_bullish and curr_bearish and covers_bear:
            bear_eng = True
    return bull_eng, bear_eng


# ============================================================
# DOJI
# ============================================================

def calc_doji(df: pd.DataFrame) -> tuple[Optional[str], bool]:
    """
    Returns (doji_type, has_doji). doji_type ∈ {"dragonfly",
    "gravestone", "standard"} — body < 10% of range.

    NOTE: the price-action engine maps "dragonfly" -> doji_bull and
    "gravestone" -> doji_bear; "standard" doji has no directional bias
    so it is ignored by the PA score.
    """
    for i in [-1, -2]:
        o = df["open"].iloc[i]
        h = df["high"].iloc[i]
        l = df["low"].iloc[i]
        c = df["close"].iloc[i]
        body = abs(c - o)
        candle_range = h - l + 1e-9
        lower_shadow = min(o, c) - l
        upper_shadow = h - max(o, c)
        is_doji = body < candle_range * 0.1
        if is_doji:
            if lower_shadow > upper_shadow * 2:
                return "dragonfly", True
            if upper_shadow > lower_shadow * 2:
                return "gravestone", True
            return "standard", True
    return None, False


# ============================================================
# PIERCING / DARK CLOUD
# ============================================================

def calc_piercing_dark(df: pd.DataFrame) -> tuple[bool, bool]:
    """
    Returns (piercing, dark_cloud). Scans last 3 bar-pairs.

    Piercing: prev bearish, curr bullish, closes above prev midpoint
              but below prev open.
    Dark cloud: mirrored.
    """
    piercing = False
    dark_cloud = False
    for i in [-1, -2, -3]:
        o_prev = df["open"].iloc[i - 1]
        c_prev = df["close"].iloc[i - 1]
        o_curr = df["open"].iloc[i]
        c_curr = df["close"].iloc[i]
        prev_mid = (o_prev + c_prev) / 2
        prev_bearish = c_prev < o_prev
        curr_bullish = c_curr > o_curr
        if prev_bearish and curr_bullish and c_curr > prev_mid and c_curr < o_prev:
            piercing = True
        prev_bullish = c_prev > o_prev
        curr_bearish = c_curr < o_curr
        if prev_bullish and curr_bearish and c_curr < prev_mid and c_curr > o_prev:
            dark_cloud = True
    return piercing, dark_cloud


# ============================================================
# MORNING / EVENING STAR
# ============================================================

def calc_star_patterns(df: pd.DataFrame) -> tuple[bool, bool]:
    """
    Returns (morning_star, evening_star). 3-bar pattern.

    Morning: bearish | small | bullish closing above midpoint of bar1.
    Evening: mirrored.
    """
    morning_star = False
    evening_star = False
    for i in [-1, -2]:
        o1 = df["open"].iloc[i - 2]
        c1 = df["close"].iloc[i - 2]
        o2 = df["open"].iloc[i - 1]
        c2 = df["close"].iloc[i - 1]
        o3 = df["open"].iloc[i]
        c3 = df["close"].iloc[i]
        second_small = abs(c2 - o2) < abs(c1 - o1) * 0.3
        if (c1 < o1 and second_small and c3 > o3 and c3 > (o1 + c1) / 2):
            morning_star = True
        if (c1 > o1 and second_small and c3 < o3 and c3 < (o1 + c1) / 2):
            evening_star = True
    return morning_star, evening_star


# ============================================================
# VOLUME WEAK
# ============================================================

def calc_volume_weak(df: pd.DataFrame) -> bool:
    """
    True when last bar volume < 85% of 20-bar avg.

    V14 used 'tick_volume'; V15+ uses real CME 'volume' from
    TopstepX. Logic identical, data quality much better.
    """
    col = "volume" if "volume" in df.columns else "tick_volume"
    if col not in df.columns:
        return False
    avg_vol = df[col].rolling(20).mean().iloc[-1]
    last_vol = df[col].iloc[-1]
    return bool(last_vol < avg_vol * 0.85)
