"""
Candlestick pattern recognition — pure pandas/numpy, rule-based (no TA-Lib).

detect_all(df) returns a DataFrame aligned to df.index where each column is a
pattern name and the value is:
    0   -> pattern not present on that bar
    1   -> bullish signal
   -1   -> bearish signal

Single-candle patterns look only at the current bar. Multi-candle patterns
(engulfing, harami, stars, soldiers/crows) look back 1-2 extra bars, so the
first couple of rows will be all zeros (not enough history yet).
"""

import numpy as np
import pandas as pd


def _body(df):
    return (df["close"] - df["open"]).abs()


def _range(df):
    return (df["high"] - df["low"]).replace(0, np.nan)


def _upper_wick(df):
    return df["high"] - df[["open", "close"]].max(axis=1)


def _lower_wick(df):
    return df[["open", "close"]].min(axis=1) - df["low"]


def _is_bull(df):
    return df["close"] > df["open"]


def _is_bear(df):
    return df["close"] < df["open"]


# ---------------------------------------------------------------------------
# Single-candle patterns
# ---------------------------------------------------------------------------

def doji(df, threshold=0.1):
    return (_body(df) <= threshold * _range(df)).astype(int)


def hammer(df):
    body, rng = _body(df), _range(df)
    lower, upper = _lower_wick(df), _upper_wick(df)
    cond = (lower >= 2 * body) & (upper <= 0.15 * rng) & (body > 0)
    return np.where(cond, 1, 0)


def inverted_hammer(df):
    body, rng = _body(df), _range(df)
    lower, upper = _lower_wick(df), _upper_wick(df)
    cond = (upper >= 2 * body) & (lower <= 0.15 * rng) & (body > 0)
    return np.where(cond, 1, 0)


def hanging_man(df, trend_up):
    """Same shape as hammer but bearish reversal — needs prior uptrend context."""
    cond = (hammer(df) == 1) & trend_up
    return np.where(cond, -1, 0)


def shooting_star(df, trend_up):
    cond = (inverted_hammer(df) == 1) & trend_up
    return np.where(cond, -1, 0)


def dragonfly_doji(df):
    body, rng = _body(df), _range(df)
    lower, upper = _lower_wick(df), _upper_wick(df)
    cond = (body <= 0.1 * rng) & (lower >= 0.6 * rng) & (upper <= 0.1 * rng)
    return np.where(cond, 1, 0)


def gravestone_doji(df):
    body, rng = _body(df), _range(df)
    lower, upper = _lower_wick(df), _upper_wick(df)
    cond = (body <= 0.1 * rng) & (upper >= 0.6 * rng) & (lower <= 0.1 * rng)
    return np.where(cond, -1, 0)


def marubozu(df):
    body, rng = _body(df), _range(df)
    lower, upper = _lower_wick(df), _upper_wick(df)
    strong_body = body >= 0.95 * rng
    bull = strong_body & _is_bull(df)
    bear = strong_body & _is_bear(df)
    return np.where(bull, 1, np.where(bear, -1, 0))


def pin_bar(df):
    body, rng = _body(df), _range(df)
    lower, upper = _lower_wick(df), _upper_wick(df)
    bull_pin = (lower >= 0.66 * rng) & (body <= 0.25 * rng)
    bear_pin = (upper >= 0.66 * rng) & (body <= 0.25 * rng)
    return np.where(bull_pin, 1, np.where(bear_pin, -1, 0))


# ---------------------------------------------------------------------------
# Two/three-candle patterns
# ---------------------------------------------------------------------------

def engulfing(df):
    prev_open, prev_close = df["open"].shift(1), df["close"].shift(1)
    o, c = df["open"], df["close"]

    bull = (prev_close < prev_open) & (c > o) & (c >= prev_open) & (o <= prev_close)
    bear = (prev_close > prev_open) & (c < o) & (o >= prev_close) & (c <= prev_open)
    return np.where(bull, 1, np.where(bear, -1, 0))


def harami(df):
    prev_open, prev_close = df["open"].shift(1), df["close"].shift(1)
    o, c = df["open"], df["close"]
    prev_body_high = pd.concat([prev_open, prev_close], axis=1).max(axis=1)
    prev_body_low = pd.concat([prev_open, prev_close], axis=1).min(axis=1)

    inside = (o < prev_body_high) & (o > prev_body_low) & (c < prev_body_high) & (c > prev_body_low)
    bull = inside & (prev_close < prev_open) & (c > o)
    bear = inside & (prev_close > prev_open) & (c < o)
    return np.where(bull, 1, np.where(bear, -1, 0))


def inside_bar(df):
    prev_high, prev_low = df["high"].shift(1), df["low"].shift(1)
    cond = (df["high"] < prev_high) & (df["low"] > prev_low)
    return np.where(cond, 1, 0)  # direction-neutral, context decides bias


def outside_bar(df):
    prev_high, prev_low = df["high"].shift(1), df["low"].shift(1)
    cond = (df["high"] > prev_high) & (df["low"] < prev_low)
    bull = cond & _is_bull(df)
    bear = cond & _is_bear(df)
    return np.where(bull, 1, np.where(bear, -1, 0))


def morning_star(df):
    c1_open, c1_close = df["open"].shift(2), df["close"].shift(2)
    c2_open, c2_close = df["open"].shift(1), df["close"].shift(1)
    c3_open, c3_close = df["open"], df["close"]

    c1_bear = c1_close < c1_open
    c2_small = (c2_close - c2_open).abs() < (c1_open - c1_close).abs() * 0.5
    c3_bull = c3_close > c3_open
    c3_recovers = c3_close > (c1_open + c1_close) / 2

    cond = c1_bear & c2_small & c3_bull & c3_recovers
    return np.where(cond, 1, 0)


def evening_star(df):
    c1_open, c1_close = df["open"].shift(2), df["close"].shift(2)
    c2_open, c2_close = df["open"].shift(1), df["close"].shift(1)
    c3_open, c3_close = df["open"], df["close"]

    c1_bull = c1_close > c1_open
    c2_small = (c2_close - c2_open).abs() < (c1_close - c1_open).abs() * 0.5
    c3_bear = c3_close < c3_open
    c3_falls = c3_close < (c1_open + c1_close) / 2

    cond = c1_bull & c2_small & c3_bear & c3_falls
    return np.where(cond, -1, 0)


def three_white_soldiers(df):
    bulls = _is_bull(df) & _is_bull(df.shift(1)) & _is_bull(df.shift(2))
    rising = (df["close"] > df["close"].shift(1)) & (df["close"].shift(1) > df["close"].shift(2))
    small_wicks = (_upper_wick(df) < _body(df) * 0.3)
    cond = bulls & rising & small_wicks
    return np.where(cond, 1, 0)


def three_black_crows(df):
    bears = _is_bear(df) & _is_bear(df.shift(1)) & _is_bear(df.shift(2))
    falling = (df["close"] < df["close"].shift(1)) & (df["close"].shift(1) < df["close"].shift(2))
    small_wicks = (_lower_wick(df) < _body(df) * 0.3)
    cond = bears & falling & small_wicks
    return np.where(cond, -1, 0)


# ---------------------------------------------------------------------------
# Run all
# ---------------------------------------------------------------------------

def detect_all(df: pd.DataFrame) -> pd.DataFrame:
    trend_up = df["close"] > df["close"].rolling(10).mean()

    patterns = {
        "doji": doji(df) * 0,  # doji itself is neutral; flag only, no directional signal
        "hammer": hammer(df),
        "inverted_hammer": inverted_hammer(df),
        "hanging_man": hanging_man(df, trend_up),
        "shooting_star": shooting_star(df, trend_up),
        "dragonfly_doji": dragonfly_doji(df),
        "gravestone_doji": gravestone_doji(df),
        "marubozu": marubozu(df),
        "pin_bar": pin_bar(df),
        "engulfing": engulfing(df),
        "harami": harami(df),
        "inside_bar": inside_bar(df),
        "outside_bar": outside_bar(df),
        "morning_star": morning_star(df),
        "evening_star": evening_star(df),
        "three_white_soldiers": three_white_soldiers(df),
        "three_black_crows": three_black_crows(df),
    }
    result = pd.DataFrame(patterns, index=df.index).fillna(0).astype(int)
    return result


def latest_signal(df: pd.DataFrame) -> dict:
    """Convenience: patterns firing on the most recent completed candle only."""
    all_patterns = detect_all(df)
    last_row = all_patterns.iloc[-1]
    fired = {k: int(v) for k, v in last_row.items() if v != 0}
    return fired
