"""
AlphaX FORGE - Pattern Detection Engine
========================================
Translasi dari Pine Script (alphaX.pine) ke Python.
Detect bullish reversal chart patterns dengan confluence scoring.

Patterns yang didetect:
- Double Bottom, Triple Bottom, Inv Head & Shoulders
- Bullish Flag/Pennant, Falling Wedge, Bullish Triangle
- Cup & Handle
"""

from dataclasses import dataclass
from typing import Optional, Tuple, List
import pandas as pd
import numpy as np
import yfinance as yf


# ═══════════════════════════════════════════════════════════════════════════════
#  DATA CLASSES
# ═══════════════════════════════════════════════════════════════════════════════

@dataclass
class Pivot:
    """Represents a pivot point (high or low)"""
    price: float
    index: int


@dataclass
class PatternResult:
    """Result of pattern detection"""
    detected: bool = False
    name: str = ""
    is_bullish: bool = False
    entry_price: float = np.nan
    stop_price: float = np.nan
    target_price: float = np.nan
    start_idx: int = -1
    breakout_idx: int = -1

    # Geometry coordinates untuk rendering (optional)
    fux1: int = -1
    fuy1: float = np.nan
    fux2: int = -1
    fuy2: float = np.nan
    flx1: int = -1
    fly1: float = np.nan
    flx2: int = -1
    fly2: float = np.nan
    lux1: int = -1
    luy1: float = np.nan
    lux2: int = -1
    luy2: float = np.nan
    llx1: int = -1
    lly1: float = np.nan
    llx2: int = -1
    lly2: float = np.nan


# ═══════════════════════════════════════════════════════════════════════════════
#  CORE CALCULATIONS
# ═══════════════════════════════════════════════════════════════════════════════

def calc_atr(df: pd.DataFrame, period: int = 14) -> pd.Series:
    """Average True Range calculation"""
    high = df['High']
    low = df['Low']
    close = df['Close']

    prev_close = close.shift(1)

    tr1 = high - low
    tr2 = (high - prev_close).abs()
    tr3 = (low - prev_close).abs()

    tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
    atr = tr.rolling(window=period).mean()

    return atr


def calc_choppiness(df: pd.DataFrame, period: int = 14) -> float:
    """
    Choppiness Index - measure market trending vs ranging
    Return 0-100, di atas 62 = choppy (avoid)
    """
    atr_series = calc_atr(df, 1)
    ci_sum = atr_series.rolling(window=period).sum().iloc[-1]

    high_lookback = df['High'].rolling(window=period).max().iloc[-1]
    low_lookback = df['Low'].rolling(window=period).min().iloc[-1]
    ci_hl = high_lookback - low_lookback

    if ci_hl <= 0:
        return 50.0

    chop_idx = 100.0 * np.log10(ci_sum / ci_hl) / np.log10(period)

    return max(0, min(100, chop_idx))


def calc_ema(series: pd.Series, period: int) -> pd.Series:
    """Exponential Moving Average"""
    return series.ewm(span=period, adjust=False).mean()


def calc_htf_bias(df: pd.DataFrame, htf_timeframe: str = '1wk',
                  fast_ema: int = 21, slow_ema: int = 55) -> dict:
    """
    Calculate HTF (Higher Timeframe) bias
    Return dict with {bullish: bool, bearish: bool, trend: str}
    """
    try:
        # Download HTF data
        ticker = df.attrs.get('ticker', 'UNKNOWN')
        htf_data = yf.download(ticker, period='5y', interval=htf_timeframe,
                              progress=False, auto_adjust=True)

        if len(htf_data) < slow_ema:
            return {'bullish': False, 'bearish': False, 'trend': 'unknown'}

        # Normalize yfinance MultiIndex columns
        if isinstance(htf_data.columns, pd.MultiIndex):
            htf_data.columns = htf_data.columns.droplevel(1)

        close = htf_data['Close']
        fast = float(calc_ema(close, fast_ema).iloc[-1])
        slow = float(calc_ema(close, slow_ema).iloc[-1])
        current_close = float(close.iloc[-1])

        bullish = fast > slow and current_close > fast
        bearish = fast < slow and current_close < fast

        trend = 'bullish' if bullish else 'bearish' if bearish else 'neutral'

        return {'bullish': bullish, 'bearish': bearish, 'trend': trend}

    except Exception as e:
        print(f"[WARN] HTF bias calculation failed: {e}")
        return {'bullish': False, 'bearish': False, 'trend': 'error'}


# ═══════════════════════════════════════════════════════════════════════════════
#  PIVOT DETECTION
# ═══════════════════════════════════════════════════════════════════════════════

def detect_pivot_high(series: pd.Series, lookback_left: int, lookback_right: int) -> pd.Series:
    """
    Detect pivot high points - local maxima
    Return series dengan pivot high price, NaN untuk non-pivot bars
    """
    pivots = pd.Series([np.nan] * len(series), index=series.index)

    for i in range(lookback_left, len(series) - lookback_right):
        candidate = float(series.iloc[i])
        is_pivot = True

        # Check left side
        for j in range(i - lookback_left, i):
            if float(series.iloc[j]) >= candidate:
                is_pivot = False
                break

        # Check right side
        if is_pivot:
            for j in range(i + 1, i + lookback_right + 1):
                if float(series.iloc[j]) >= candidate:
                    is_pivot = False
                    break

        if is_pivot:
            pivots.iloc[i] = candidate

    return pivots


def detect_pivot_low(series: pd.Series, lookback_left: int, lookback_right: int) -> pd.Series:
    """
    Detect pivot low points - local minima
    Return series dengan pivot low price, NaN untuk non-pivot bars
    """
    pivots = pd.Series([np.nan] * len(series), index=series.index)

    for i in range(lookback_left, len(series) - lookback_right):
        candidate = float(series.iloc[i])
        is_pivot = True

        # Check left side
        for j in range(i - lookback_left, i):
            if float(series.iloc[j]) <= candidate:
                is_pivot = False
                break

        # Check right side
        if is_pivot:
            for j in range(i + 1, i + lookback_right + 1):
                if float(series.iloc[j]) <= candidate:
                    is_pivot = False
                    break

        if is_pivot:
            pivots.iloc[i] = candidate

    return pivots


def collect_pivots(df: pd.DataFrame, left_lb: int = 10, right_lb: int = 10,
                   max_history: int = 500) -> Tuple[List[Pivot], List[Pivot]]:
    """
    Collect pivot points from price data
    Return (pivot_highs, pivot_lows) sebagai list of Pivot objects
    """
    # Detect pivots
    high_pivots = detect_pivot_high(df['High'], left_lb, right_lb)
    low_pivots = detect_pivot_low(df['Low'], left_lb, right_lb)

    # Convert to Pivot objects, from newest to oldest (unshift behavior)
    pivot_highs = []
    pivot_lows = []

    for i in range(len(df) - 1, -1, -1):
        if not pd.isna(high_pivots.iloc[i]):
            pivot_highs.append(Pivot(price=high_pivots.iloc[i], index=i))
            if len(pivot_highs) >= max_history:
                break

    for i in range(len(df) - 1, -1, -1):
        if not pd.isna(low_pivots.iloc[i]):
            pivot_lows.append(Pivot(price=low_pivots.iloc[i], index=i))
            if len(pivot_lows) >= max_history:
                break

    return pivot_highs, pivot_lows


# ═══════════════════════════════════════════════════════════════════════════════
#  HELPER FUNCTIONS
# ═══════════════════════════════════════════════════════════════════════════════

def project_line(x1: float, y1: float, x2: float, y2: float, target_x: float) -> float:
    """Project a line to find y value at target_x"""
    if x2 == x1:
        return y1
    return y1 + ((y2 - y1) / (x2 - x1)) * (target_x - x1)


def is_near(v1: float, v2: float, tol_pct: float) -> bool:
    """Check if two values are near each other within tolerance"""
    avg = (v1 + v2) / 2
    if avg == 0:
        return v1 == v2
    return abs(v1 - v2) <= (avg * tol_pct)


def neckline_at_break(nl_i: int, nl_p: float, nr_i: int, nr_p: float, b_idx: int) -> float:
    """Calculate neckline price at breakout index"""
    if pd.isna(nl_i) and pd.isna(nr_i):
        return np.nan
    if pd.isna(nl_i):
        return nr_p
    if pd.isna(nr_i):
        return nl_p
    if nl_i == nr_i:
        return nl_p
    return project_line(nl_i, nl_p, nr_i, nr_p, b_idx)


def is_valid_size(height: float, price: float, atr: float,
                  min_size_pct: float = 0.005, min_atr_mult: float = 1.0) -> bool:
    """Check if pattern height is valid (not too small)"""
    min_height = max(price * min_size_pct, atr * min_atr_mult)
    return height > min_height


def apply_buffer(price: float, atr: float, is_bullish: bool, buffer_mult: float = 0.5) -> float:
    """Apply ATR buffer to price for stop loss calculation"""
    buffer = atr * buffer_mult
    return price - buffer if is_bullish else price + buffer


def calc_stop_loss(anchor: float, is_bullish: bool, entry_price: float,
                   target_price: float, atr: float, use_pct: bool = True,
                   sl_pct_tgt: float = 0.25, buffer_mult: float = 0.5) -> float:
    """Calculate stop loss price"""
    if use_pct and not pd.isna(entry_price) and not pd.isna(target_price):
        dist = abs(target_price - entry_price)
        return entry_price - dist * sl_pct_tgt if is_bullish else entry_price + dist * sl_pct_tgt
    else:
        return apply_buffer(anchor, atr, is_bullish, buffer_mult)


def calc_entry_price(df: pd.DataFrame, breakout_idx: int) -> float:
    """Get entry price at breakout (open of next bar)"""
    if breakout_idx < 0 or breakout_idx >= len(df) - 1:
        return np.nan
    return df['Open'].iloc[breakout_idx + 1]


def find_breakout_index(df: pd.DataFrame, x1: int, y1: float, x2: int, y2: float,
                        is_bullish: bool, atr: float, use_atr_break: bool = True,
                        break_atr_mult: float = 1.0, break_pct: float = 0.003,
                        min_lookback: int = 1) -> int:
    """
    Find breakout index where price crosses a line
    Return bar index or -1 if not found
    """
    scan_limit = min(200, len(df))

    for i in range(scan_limit - 1, 0, -1):
        bar_idx = len(df) - i
        if bar_idx < x2:
            continue

        # Project line to this bar
        proj = project_line(x1, y1, x2, y2, bar_idx)

        # Calculate breakout threshold
        margin = atr.iloc[bar_idx] * break_atr_mult if use_atr_break else proj * break_pct
        up_threshold = proj + margin
        down_threshold = proj - margin

        # Check for breakout
        if is_bullish:
            broken = df['High'].iloc[bar_idx] > up_threshold
        else:
            broken = df['Low'].iloc[bar_idx] < down_threshold

        if broken:
            return bar_idx

    return -1


# ═══════════════════════════════════════════════════════════════════════════════
#  PATTERN DETECTORS (Bullish Reversal Only)
# ═══════════════════════════════════════════════════════════════════════════════

def detect_double_bottom(pivot_highs: List[Pivot], pivot_lows: List[Pivot],
                        df: pd.DataFrame, atr: pd.Series,
                        tol_lvl: float = 0.03, sym_tol: float = 0.10) -> PatternResult:
    """Detect Double Bottom pattern (W shape at lows)"""
    if len(pivot_highs) < 1 or len(pivot_lows) < 2:
        return PatternResult()

    p1 = pivot_lows[0]  # First bottom (recent)
    p2 = pivot_lows[1]  # Second bottom
    mid = pivot_highs[0]  # Peak between bottoms

    # Check structure: mid > p1 and mid > p2
    if not (mid.index > p1.index > p2.index):
        return PatternResult()

    # Check symmetry: two bottoms are near same price level
    if not is_near(p1.price, p2.price, tol_lvl):
        return PatternResult()

    # Calculate pattern height
    avg_bottom = (p1.price + p2.price) / 2
    height = mid.price - avg_bottom
    current_price = df['Close'].iloc[-1]

    if not is_valid_size(height, current_price, atr.iloc[-1]):
        return PatternResult()

    # Find breakout above the peak
    breakout_idx = find_breakout_index(df, mid.index, mid.price, len(df)-1, mid.price,
                                       is_bullish=True, atr=atr)

    if breakout_idx < 0:
        return PatternResult()

    # Calculate targets
    entry_price = calc_entry_price(df, breakout_idx)
    target_price = avg_bottom + height  # Measure rule: project height upward
    stop_price = calc_stop_loss(p1.price, is_bullish=True, entry_price=entry_price,
                               target_price=target_price, atr=atr.iloc[-1])

    result = PatternResult(
        detected=True,
        name="Double Bottom",
        is_bullish=True,
        entry_price=entry_price,
        stop_price=stop_price,
        target_price=target_price,
        start_idx=p2.index,
        breakout_idx=breakout_idx,
        lux1=p2.index, luy1=avg_bottom, lux2=len(df)-1, luy2=avg_bottom,
        llx1=mid.index, lly1=mid.price, llx2=len(df)-1, lly2=mid.price
    )

    return result


def detect_triple_bottom(pivot_highs: List[Pivot], pivot_lows: List[Pivot],
                         df: pd.DataFrame, atr: pd.Series,
                         tol_lvl: float = 0.03) -> PatternResult:
    """Detect Triple Bottom pattern (three lows at same level)"""
    if len(pivot_highs) < 2 or len(pivot_lows) < 3:
        return PatternResult()

    l1 = pivot_lows[0]  # First bottom
    l2 = pivot_lows[1]  # Second bottom
    l3 = pivot_lows[2]  # Third bottom (oldest)
    h1 = pivot_highs[0]  # First peak
    h2 = pivot_highs[1]  # Second peak

    # Check structure
    if not (h1.index > l1.index > h2.index > l2.index > l3.index):
        return PatternResult()

    # Check three lows are near same level
    if not (is_near(l1.price, l2.price, tol_lvl) and is_near(l2.price, l3.price, tol_lvl)):
        return PatternResult()

    # Calculate pattern
    neck_level = min(h1.price, h2.price)
    height = neck_level - max(l1.price, l2.price, l3.price)
    current_price = df['Close'].iloc[-1]

    if not is_valid_size(height, current_price, atr.iloc[-1]):
        return PatternResult()

    # Find breakout
    breakout_idx = find_breakout_index(df, h2.index, neck_level, h1.index, neck_level,
                                       is_bullish=True, atr=atr)

    if breakout_idx < 0:
        return PatternResult()

    entry_price = calc_entry_price(df, breakout_idx)
    target_price = neck_level + height
    stop_price = calc_stop_loss(l3.price, is_bullish=True, entry_price=entry_price,
                               target_price=target_price, atr=atr.iloc[-1])

    result = PatternResult(
        detected=True,
        name="Triple Bottom",
        is_bullish=True,
        entry_price=entry_price,
        stop_price=stop_price,
        target_price=target_price,
        start_idx=l3.index,
        breakout_idx=breakout_idx,
        lux1=l3.index, luy1=neck_level, lux2=len(df)-1, luy2=neck_level,
        llx1=h2.index, lly1=neck_level, llx2=len(df)-1, lly2=neck_level
    )

    return result


def detect_inv_head_shoulders(pivot_highs: List[Pivot], pivot_lows: List[Pivot],
                             df: pd.DataFrame, atr: pd.Series,
                             sym_tol: float = 0.10) -> PatternResult:
    """Detect Inverted Head & Shoulders (bottoming pattern)"""
    if len(pivot_highs) < 2 or len(pivot_lows) < 3:
        return PatternResult()

    # Inv H&S: Head is lowest, shoulders are higher
    rs = pivot_lows[0]  # Right shoulder
    head = pivot_lows[1]  # Head (lowest point)
    ls = pivot_lows[2]  # Left shoulder
    neck_r = pivot_highs[0]  # Right neckline point
    neck_l = pivot_highs[1]  # Left neckline point

    # Check structure
    if not (rs.index > neck_r.index > head.index > neck_l.index > ls.index):
        return PatternResult()

    # Head must be lowest, shoulders near same level
    if not (head.price < rs.price and head.price < ls.price):
        return PatternResult()

    if not is_near(ls.price, rs.price, sym_tol):
        return PatternResult()

    # Calculate
    neck_avg = (neck_r.price + neck_l.price) / 2
    height = neck_avg - head.price
    current_price = df['Close'].iloc[-1]

    if not is_valid_size(height, current_price, atr.iloc[-1]):
        return PatternResult()

    # Find neckline breakout
    breakout_idx = find_breakout_index(df, neck_l.index, neck_l.price, neck_r.index, neck_r.price,
                                       is_bullish=True, atr=atr)

    if breakout_idx < 0:
        return PatternResult()

    entry_price = calc_entry_price(df, breakout_idx)
    neck_at_break = neckline_at_break(neck_l.index, neck_l.price, neck_r.index, neck_r.price, breakout_idx)
    target_price = neck_at_break + height
    stop_price = calc_stop_loss(rs.price, is_bullish=True, entry_price=entry_price,
                               target_price=target_price, atr=atr.iloc[-1])

    result = PatternResult(
        detected=True,
        name="Inv Head & Shoulders",
        is_bullish=True,
        entry_price=entry_price,
        stop_price=stop_price,
        target_price=target_price,
        start_idx=ls.index,
        breakout_idx=breakout_idx,
        lux1=neck_l.index, luy1=neck_l.price, lux2=len(df)-1, luy2=neck_at_break,
        llx1=ls.index, lly1=ls.price, llx2=len(df)-1, lly2=rs.price
    )

    return result


def detect_bullish_flag_pennant(pivot_highs: List[Pivot], pivot_lows: List[Pivot],
                                df: pd.DataFrame, atr: pd.Series,
                                min_atr_mult: float = 3.0) -> PatternResult:
    """Detect Bullish Flag or Pennant (continuation pattern after pole)"""
    if len(pivot_highs) < 2 or len(pivot_lows) < 2:
        return PatternResult()

    h1 = pivot_highs[0]
    h2 = pivot_highs[1]
    l1 = pivot_lows[0]
    l2 = pivot_lows[1]

    start_bar = min(h2.index, l2.index)
    pole_len = min(200, len(df) - start_bar)

    # Calculate pole (prior uptrend)
    pole_high = df['High'].iloc[start_bar:start_bar+pole_len].max()
    pole_low = df['Low'].iloc[start_bar:start_bar+pole_len].min()
    pole_move = pole_high - pole_low

    if pole_move < atr.iloc[-1] * min_atr_mult:
        return PatternResult()

    # Calculate slopes
    slope_u = (h1.price - h2.price) / max(1, h1.index - h2.index) if h1.index != h2.index else 0
    slope_l = (l1.price - l2.price) / max(1, l1.index - l2.index) if l1.index != l2.index else 0

    # Bull flag/pennant: both slopes negative (consolidation after uptrend)
    if slope_u >= 0 or slope_l >= 0:
        return PatternResult()

    # Check if parallel (flag) or converging (pennant)
    is_parallel = is_near(slope_u, slope_l, 0.2)
    pattern_name = "Bull Flag" if is_parallel else "Bull Pennant"

    # Find breakout
    breakout_idx = find_breakout_index(df, h2.index, h2.price, h1.index, h1.price,
                                       is_bullish=True, atr=atr)

    if breakout_idx < 0:
        return PatternResult()

    # Calculate targets
    upper_break = project_line(h2.index, h2.price, h1.index, h1.price, breakout_idx)
    lower_break = project_line(l2.index, l2.price, l1.index, l1.price, breakout_idx)

    entry_price = calc_entry_price(df, breakout_idx)
    target_price = upper_break + pole_move  # Measure rule
    stop_price = calc_stop_loss(lower_break, is_bullish=True, entry_price=entry_price,
                               target_price=target_price, atr=atr.iloc[-1])

    result = PatternResult(
        detected=True,
        name=pattern_name,
        is_bullish=True,
        entry_price=entry_price,
        stop_price=stop_price,
        target_price=target_price,
        start_idx=start_bar,
        breakout_idx=breakout_idx,
        fux1=start_bar, fuy1=project_line(h2.index, h2.price, h1.index, h1.price, start_bar),
        fux2=breakout_idx, fuy2=upper_break,
        flx1=start_bar, fly1=project_line(l2.index, l2.price, l1.index, l1.price, start_bar),
        flx2=breakout_idx, fly2=lower_break,
        lux1=start_bar, luy1=upper_break, lux2=breakout_idx, luy2=upper_break,
        llx1=start_bar, lly1=lower_break, llx2=breakout_idx, lly2=lower_break
    )

    return result


def detect_falling_wedge(pivot_highs: List[Pivot], pivot_lows: List[Pivot],
                         df: pd.DataFrame, atr: pd.Series) -> PatternResult:
    """Detect Falling Wedge (bullish reversal within downtrend)"""
    if len(pivot_highs) < 2 or len(pivot_lows) < 2:
        return PatternResult()

    h1 = pivot_highs[0]
    h2 = pivot_highs[1]
    l1 = pivot_lows[0]
    l2 = pivot_lows[1]

    # Calculate slopes
    slope_u = (h1.price - h2.price) / max(1, h1.index - h2.index) if h1.index != h2.index else 0
    slope_l = (l1.price - l2.price) / max(1, l1.index - l2.index) if l1.index != l2.index else 0

    # Falling wedge: both slopes negative, lower slope steeper than upper
    if slope_u >= 0 or slope_l >= 0 or slope_l >= slope_u:
        return PatternResult()

    # Current wedge height
    proj_u_now = project_line(h2.index, h2.price, h1.index, h1.price, len(df)-1)
    proj_l_now = project_line(l2.index, l2.price, l1.index, l1.price, len(df)-1)
    height_now = proj_u_now - proj_l_now

    if height_now <= 0:
        return PatternResult()

    # Find breakout (upward through upper line)
    breakout_idx = find_breakout_index(df, h2.index, h2.price, h1.index, h1.price,
                                       is_bullish=True, atr=atr)

    if breakout_idx < 0:
        return PatternResult()

    # Calculate targets
    lower_break = project_line(l2.index, l2.price, l1.index, l1.price, breakout_idx)

    entry_price = calc_entry_price(df, breakout_idx)
    target_price = h2.price  # Target: back to pattern high
    stop_price = calc_stop_loss(lower_break, is_bullish=True, entry_price=entry_price,
                               target_price=target_price, atr=atr.iloc[-1])

    result = PatternResult(
        detected=True,
        name="Falling Wedge",
        is_bullish=True,
        entry_price=entry_price,
        stop_price=stop_price,
        target_price=target_price,
        start_idx=h2.index,
        breakout_idx=breakout_idx,
        fux1=h2.index, fuy1=h2.price, fux2=breakout_idx,
        fuy2=project_line(h2.index, h2.price, h1.index, h1.price, breakout_idx),
        flx1=l2.index, fly1=l2.price, flx2=breakout_idx,
        fly2=project_line(l2.index, l2.price, l1.index, l1.price, breakout_idx),
        lux1=min(h2.index, l2.index), luy1=proj_u_now, lux2=breakout_idx, luy2=proj_u_now,
        llx1=min(h2.index, l2.index), lly1=proj_l_now, llx2=breakout_idx, lly2=proj_l_now
    )

    return result


def detect_bullish_triangle(pivot_highs: List[Pivot], pivot_lows: List[Pivot],
                           df: pd.DataFrame, atr: pd.Series) -> PatternResult:
    """Detect Bullish Triangle (ascending or symmetrical)"""
    if len(pivot_highs) < 2 or len(pivot_lows) < 2:
        return PatternResult()

    h1 = pivot_highs[0]
    h2 = pivot_highs[1]
    l1 = pivot_lows[0]
    l2 = pivot_lows[1]

    # Calculate slopes
    slope_u = (h1.price - h2.price) / max(1, h1.index - h2.index) if h1.index != h2.index else 0
    slope_l = (l1.price - l2.price) / max(1, l1.index - l2.index) if l1.index != l2.index else 0

    start_tri = min(h2.index, l2.index)
    base_height = abs(project_line(h2.index, h2.price, h1.index, h1.price, start_tri) -
                      project_line(l2.index, l2.price, l1.index, l1.price, start_tri))

    # Check convergence and minimum size
    proj_u_end = project_line(h2.index, h2.price, h1.index, h1.price, len(df)-1)
    proj_l_end = project_line(l2.index, l2.price, l1.index, l1.price, len(df)-1)

    if proj_u_end <= proj_l_end:
        return PatternResult()

    current_price = df['Close'].iloc[-1]
    if not is_valid_size(base_height, current_price, atr.iloc[-1]):
        return PatternResult()

    # Determine triangle type
    flat_tol = current_price * 0.0005
    is_ascending = abs(slope_u) < flat_tol
    is_symmetrical = (slope_u < 0 and slope_l > 0)

    if not (is_ascending or is_symmetrical):
        return PatternResult()

    pattern_name = "Ascending Triangle" if is_ascending else "Sym Triangle"

    # Find breakout
    breakout_idx = find_breakout_index(df, h2.index, h2.price, h1.index, h1.price,
                                       is_bullish=True, atr=atr)

    if breakout_idx < 0:
        return PatternResult()

    # Calculate targets
    upper_break = project_line(h2.index, h2.price, h1.index, h1.price, breakout_idx)
    lower_break = project_line(l2.index, l2.price, l1.index, l1.price, breakout_idx)

    entry_price = calc_entry_price(df, breakout_idx)
    target_price = upper_break + base_height
    stop_price = calc_stop_loss(lower_break, is_bullish=True, entry_price=entry_price,
                               target_price=target_price, atr=atr.iloc[-1])

    result = PatternResult(
        detected=True,
        name=pattern_name,
        is_bullish=True,
        entry_price=entry_price,
        stop_price=stop_price,
        target_price=target_price,
        start_idx=start_tri,
        breakout_idx=breakout_idx,
        fux1=h2.index, fuy1=h2.price, fux2=breakout_idx, fuy2=upper_break,
        flx1=l2.index, fly1=l2.price, flx2=breakout_idx, fly2=lower_break,
        lux1=start_tri, luy1=project_line(h2.index, h2.price, h1.index, h1.price, start_tri),
        lux2=breakout_idx, luy2=upper_break,
        llx1=start_tri, lly1=project_line(l2.index, l2.price, l1.index, l1.price, start_tri),
        llx2=breakout_idx, lly2=lower_break
    )

    return result


def detect_cup_handle(pivot_highs: List[Pivot], pivot_lows: List[Pivot],
                     df: pd.DataFrame, atr: pd.Series,
                     sym_tol: float = 0.10) -> PatternResult:
    """Detect Cup & Handle pattern (bullish continuation)"""
    if len(pivot_highs) < 2 or len(pivot_lows) < 2:
        return PatternResult()

    # Cup: U-shaped bottom with handle
    h_rim = pivot_highs[0]  # Right rim of cup
    h_left = pivot_highs[1]  # Left rim of cup
    l_handle = pivot_lows[0]  # Handle low
    l_bottom = pivot_lows[1]  # Cup bottom

    # Check structure
    if not (l_handle.index > h_rim.index > l_bottom.index > h_left.index):
        return PatternResult()

    # Rims should be near same level
    if not is_near(h_rim.price, h_left.price, sym_tol):
        return PatternResult()

    # Handle should be above bottom, below rim
    if not (l_handle.price > l_bottom.price and l_handle.price < h_rim.price):
        return PatternResult()

    # Calculate
    cup_height = h_rim.price - l_bottom.price
    current_price = df['Close'].iloc[-1]

    if not is_valid_size(cup_height, current_price, atr.iloc[-1]):
        return PatternResult()

    # Find breakout (through right rim)
    breakout_idx = find_breakout_index(df, h_left.index, h_left.price, h_rim.index, h_rim.price,
                                       is_bullish=True, atr=atr)

    if breakout_idx < 0:
        return PatternResult()

    entry_price = calc_entry_price(df, breakout_idx)
    neckline_at_break = neckline_at_break(h_left.index, h_left.price, h_rim.index, h_rim.price, breakout_idx)
    target_price = neckline_at_break + cup_height
    stop_price = calc_stop_loss(l_handle.price, is_bullish=True, entry_price=entry_price,
                               target_price=target_price, atr=atr.iloc[-1])

    result = PatternResult(
        detected=True,
        name="Cup & Handle",
        is_bullish=True,
        entry_price=entry_price,
        stop_price=stop_price,
        target_price=target_price,
        start_idx=h_left.index,
        breakout_idx=breakout_idx,
        lux1=h_left.index, luy1=h_left.price, lux2=breakout_idx, luy2=neckline_at_break,
        llx1=h_left.index, lly1=l_bottom.price, llx2=breakout_idx, lly2=l_handle.price
    )

    return result


# ═══════════════════════════════════════════════════════════════════════════════
#  MAIN DETECTION FUNCTION
# ═══════════════════════════════════════════════════════════════════════════════

def detect_all_bullish_patterns(df: pd.DataFrame,
                               left_lb: int = 10, right_lb: int = 10,
                               max_pivots: int = 500,
                               min_confluence: int = 5,
                               htf_timeframe: str = '1wk',
                               htf_fast: int = 21, htf_slow: int = 55,
                               chop_max: float = 62.0,
                               require_htf: bool = False,
                               min_rr: float = 1.5) -> PatternResult:
    """
    Run all bullish pattern detectors and return best pattern by confluence score

    Parameters:
    - df: Price data with OHLC columns
    - left_lb, right_lb: Pivot lookback periods
    - max_pivots: Maximum pivot history to store
    - min_confluence: Minimum confluence score (0-10)
    - htf_timeframe: Higher timeframe for bias check
    - htf_fast, htf_slow: HTF EMA periods
    - chop_max: Maximum choppiness index (avoid choppy markets)
    - require_htf: Block counter-HTF patterns
    - min_rr: Minimum risk:reward ratio

    Returns:
    - PatternResult dengan pattern terbaik atau empty result
    """
    # Need minimum data
    if len(df) < 100:
        return PatternResult()

    # Calculate ATR
    atr = calc_atr(df, 14)

    # Collect pivots
    pivot_highs, pivot_lows = collect_pivots(df, left_lb, right_lb, max_pivots)

    if len(pivot_highs) < 2 or len(pivot_lows) < 2:
        return PatternResult()

    # Run all pattern detectors
    patterns = []

    try:
        patterns.append(detect_double_bottom(pivot_highs, pivot_lows, df, atr))
    except Exception as e:
        print(f"[WARN] Double Bottom detection failed: {e}")

    try:
        patterns.append(detect_triple_bottom(pivot_highs, pivot_lows, df, atr))
    except Exception as e:
        print(f"[WARN] Triple Bottom detection failed: {e}")

    try:
        patterns.append(detect_inv_head_shoulders(pivot_highs, pivot_lows, df, atr))
    except Exception as e:
        print(f"[WARN] Inv H&S detection failed: {e}")

    try:
        patterns.append(detect_bullish_flag_pennant(pivot_highs, pivot_lows, df, atr))
    except Exception as e:
        print(f"[WARN] Flag/Pennant detection failed: {e}")

    try:
        patterns.append(detect_falling_wedge(pivot_highs, pivot_lows, df, atr))
    except Exception as e:
        print(f"[WARN] Falling Wedge detection failed: {e}")

    try:
        patterns.append(detect_bullish_triangle(pivot_highs, pivot_lows, df, atr))
    except Exception as e:
        print(f"[WARN] Triangle detection failed: {e}")

    try:
        patterns.append(detect_cup_handle(pivot_highs, pivot_lows, df, atr))
    except Exception as e:
        print(f"[WARN] Cup & Handle detection failed: {e}")

    # Filter detected patterns and score by confluence
    valid_patterns = [p for p in patterns if p.detected]

    if not valid_patterns:
        return PatternResult()

    # Score each pattern by confluence
    best_pattern = PatternResult()
    best_score = 0

    for pattern in valid_patterns:
        score = calc_confluence_score(pattern, df, atr, htf_timeframe, htf_fast, htf_slow,
                                     chop_max, require_htf, min_rr, pivot_highs, pivot_lows)
        if score >= min_confluence and score > best_score:
            best_score = score
            best_pattern = pattern

    return best_pattern


def calc_confluence_score(pattern: PatternResult, df: pd.DataFrame, atr: pd.Series,
                         htf_timeframe: str = '1wk', htf_fast: int = 21, htf_slow: int = 55,
                         chop_max: float = 62.0, require_htf: bool = False,
                         min_rr: float = 1.5, pivot_highs: List[Pivot] = None,
                         pivot_lows: List[Pivot] = None) -> int:
    """
    Calculate confluence score for a pattern (0-10)

    Score components:
    - R:R Ratio (2 pts): >= minRR gets 2, >= 1.0 gets 1
    - HTF Bias (2 pts): Bullish pattern + HTF bullish = 2, else 0-1
    - Chop Filter (1 pt): Chop index < chopMax
    - Pivot Depth (2 pts): >= 12 pivots gets 2, >= 6 gets 1
    - Volume Confirmation (1 pt): Volume spike at breakout
    - Pattern Size (1 pt): Valid size vs price/ATR
    - Additional (1 pt): Reserved
    """
    if not pattern.detected or pd.isna(pattern.entry_price) or \
       pd.isna(pattern.stop_price) or pd.isna(pattern.target_price):
        return 0

    score = 0

    # 1. R:R Ratio
    risk = abs(pattern.entry_price - pattern.stop_price)
    if risk > 0:
        rr = abs(pattern.target_price - pattern.entry_price) / risk
        if rr >= min_rr:
            score += 2
        elif rr >= 1.0:
            score += 1

    # 2. HTF Bias
    try:
        htf_bias = calc_htf_bias(df, htf_timeframe, htf_fast, htf_slow)
        if pattern.is_bullish and htf_bias['bullish']:
            score += 2
        elif not require_htf:
            score += 1  # Partial points if HTF check disabled
    except:
        if not require_htf:
            score += 1

    # 3. Chop Filter
    try:
        chop_idx = calc_choppiness(df, 14)
        if chop_idx < chop_max:
            score += 1
    except:
        pass

    # 4. Pivot Depth
    if pivot_highs and pivot_lows:
        piv_total = len(pivot_highs) + len(pivot_lows)
        if piv_total >= 12:
            score += 2
        elif piv_total >= 6:
            score += 1

    # 5. Volume Confirmation
    if pattern.breakout_idx >= 0 and pattern.breakout_idx < len(df):
        vol_at_break = df['Volume'].iloc[pattern.breakout_idx]
        vol_sma = df['Volume'].rolling(20).mean().iloc[pattern.breakout_idx]
        if vol_sma > 0 and vol_at_break >= vol_sma * 1.5:
            score += 1

    # 6. Pattern Size
    try:
        pattern_height = abs(pattern.target_price - pattern.entry_price)
        if is_valid_size(pattern_height, pattern.entry_price, atr.iloc[-1]):
            score += 1
    except:
        pass

    # 7. Additional (reserved for future)
    # Can add: RSI oversold, stochastic confirmation, etc.
    score += 0

    return min(score, 10)
