import numpy as np
import pandas as pd
from numba import njit

@njit
def calculate_bearish_engulfing(open_arr, close_arr, high_arr, low_arr, quality_ratio, scaling_factor):
    """
    Calculates a score for Bearish Engulfing patterns.
    
    Score is based on:
    1. Pattern validity (Green candle followed by Red engulfing candle)
    2. Body Quality (Body / Range >= quality_ratio)
    3. Confidence (Size of engulfing body relative to previous body)
    """
    n = len(close_arr)
    scores = np.zeros(n)
    
    for i in range(1, n):
        # Previous candle (must be green)
        prev_open = open_arr[i-1]
        prev_close = close_arr[i-1]
        prev_body = abs(prev_close - prev_open)
        
        # Current candle (must be red)
        curr_open = open_arr[i]
        curr_close = close_arr[i]
        curr_high = high_arr[i]
        curr_low = low_arr[i]
        curr_range = curr_high - curr_low
        curr_body = abs(curr_close - curr_open)
        
        # Basic Engulfing Conditions
        is_prev_green = prev_close > prev_open
        is_curr_red = curr_close < curr_open
        # Strict engulfing: Open > Prev Close AND Close < Prev Open
        engulfs = (curr_open > prev_close) and (curr_close < prev_open)
        
        if is_prev_green and is_curr_red and engulfs:
            # Quality Check
            quality = 0.0
            if curr_range > 0:
                quality = curr_body / curr_range
                
            if quality >= quality_ratio:
                # Confidence Calculation
                # Ratio of current body to previous body
                if prev_body > 0:
                    ratio = curr_body / prev_body
                else:
                    ratio = scaling_factor # Max confidence if prev body was doji
                
                # Scale confidence: if ratio >= scaling_factor, score is 1.0
                confidence = min(ratio / scaling_factor, 1.0)
                scores[i] = confidence
                
    return scores

@njit
def calculate_bullish_engulfing(open_arr, close_arr, high_arr, low_arr, quality_ratio, scaling_factor):
    """Mirror of calculate_bearish_engulfing for bullish reversals."""
    n = len(close_arr)
    scores = np.zeros(n)
    for i in range(1, n):
        prev_open  = open_arr[i-1]
        prev_close = close_arr[i-1]
        curr_open  = open_arr[i]
        curr_close = close_arr[i]
        curr_high  = high_arr[i]
        curr_low   = low_arr[i]
        curr_range = curr_high - curr_low
        curr_body  = abs(curr_close - curr_open)
        prev_body  = abs(prev_close - prev_open)

        is_prev_bearish = prev_close < prev_open
        is_curr_bullish = curr_close > curr_open
        # Pine: close > open[1]  (current close above prior bearish open)
        is_engulfing    = curr_close > prev_open

        if is_prev_bearish and is_curr_bullish and is_engulfing:
            prev_range = high_arr[i-1] - low_arr[i-1]
            curr_quality = (curr_body / curr_range) if curr_range > 0 else 0.0
            prev_quality = (prev_body / prev_range) if prev_range > 0 else 0.0
            if curr_quality >= quality_ratio and prev_quality >= quality_ratio:
                body_ratio = (curr_body / prev_body) if prev_body > 0 else 999.0
                score = (body_ratio - 1.0) / (scaling_factor - 1.0)
                scores[i] = max(0.0, min(1.0, score))
    return scores


@njit
def calculate_bullish_hammer(open_arr, close_arr, high_arr, low_arr,
                              min_lower_shadow_ratio, max_body_ratio, max_upper_shadow_ratio):
    """Detects Bullish Hammer / Pin Bar patterns."""
    n = len(close_arr)
    scores = np.zeros(n)
    for i in range(n):
        candle_range = high_arr[i] - low_arr[i]
        if candle_range <= 0.0:
            continue
        body         = abs(close_arr[i] - open_arr[i])
        lower_shadow = min(close_arr[i], open_arr[i]) - low_arr[i]
        upper_shadow = high_arr[i] - max(close_arr[i], open_arr[i])
        lower_ratio  = lower_shadow / candle_range
        body_ratio   = body         / candle_range
        upper_ratio  = upper_shadow / candle_range
        if (lower_ratio >= min_lower_shadow_ratio and
                body_ratio  <= max_body_ratio and
                upper_ratio <= max_upper_shadow_ratio):
            excess     = lower_ratio - min_lower_shadow_ratio
            max_excess = 1.0 - min_lower_shadow_ratio
            scores[i]  = max(0.0, min(1.0, excess / max_excess))
    return scores


@njit
def calculate_shooting_star(open_arr, close_arr, high_arr, low_arr,
                             min_upper_shadow_ratio, max_body_ratio, max_lower_shadow_ratio):
    """Detects Bearish Shooting Star / Inverted Pin Bar patterns."""
    n = len(close_arr)
    scores = np.zeros(n)
    for i in range(n):
        candle_range = high_arr[i] - low_arr[i]
        if candle_range <= 0.0:
            continue
        body         = abs(close_arr[i] - open_arr[i])
        upper_shadow = high_arr[i] - max(close_arr[i], open_arr[i])
        lower_shadow = min(close_arr[i], open_arr[i]) - low_arr[i]
        upper_ratio  = upper_shadow / candle_range
        body_ratio   = body         / candle_range
        lower_ratio  = lower_shadow / candle_range
        if (upper_ratio >= min_upper_shadow_ratio and
                body_ratio  <= max_body_ratio and
                lower_ratio <= max_lower_shadow_ratio):
            excess     = upper_ratio - min_upper_shadow_ratio
            max_excess = 1.0 - min_upper_shadow_ratio
            scores[i]  = max(0.0, min(1.0, excess / max_excess))
    return scores


@njit
def clip_scalar(value, min_value, max_value):
    if value < min_value:
        return min_value
    elif value > max_value:
        return max_value
    else:
        return value

@njit
def calculate_activation_score_poc(
    stoch_value, macd_prediction, osc, macd_flipped_bullish, m3_momentum,
    m2_diff_tiny, rsid_osc, stoch_div_osc, vwap_div_osc,
    stoch_is_peaking, stoch_is_bottoming, m3_div_osc,
    m2_div_osc_tiny, m2_div_osc_noOffset, bearish_engulfing_score,
    bullish_hammer_score, bullish_engulfing_score, shooting_star_score,
    w_stoch, w_macd_pred, w_osc, w_macd_bullish, w_m3_momentum,
    w_m2_tiny, w_rsid_osc, w_stoch_div_osc, w_vwap_div_osc,
    w_stoch_peaking, w_stoch_bottoming, w_m3_div_osc,
    w_m2_div_osc, w_m2_div_osc_noOffset, w_bearish_engulfing,
    w_bullish_hammer, w_bullish_engulfing, w_shooting_star
):
    n = len(stoch_value)
    score = np.zeros(n)
    for i in range(n):
        # Normalization
        stoch_norm = (stoch_value[i] - 50) / 50 if not np.isnan(stoch_value[i]) else 0
        macd_pred_norm = 1 if macd_prediction[i] > 0 else -1
        osc_norm = (osc[i] - 50) / 50 if not np.isnan(osc[i]) else 0
        macd_bullish_norm = 1 if macd_flipped_bullish[i] else 0
        m3_momentum_norm = 1 if m3_momentum[i] > 0 else -1
        # m2_diff_tiny: use epsilon threshold because the CSV column
        # (`m2_diff_abs_tinyoffset_to_future`) occasionally contains machine-epsilon
        # values (~1e-14) that Pine Script rounds to 0 rather than treating as signed.
        m2_tiny_momentum_norm = 1 if m2_diff_tiny[i] > 1e-9 else (-1 if m2_diff_tiny[i] < -1e-9 else 0)
        rsid_osc_norm = clip_scalar(-(rsid_osc[i] - 42) / 28, -1.0, 1.0) if not np.isnan(rsid_osc[i]) else 0
        stoch_div_osc_norm = 1 if stoch_div_osc[i] > 0 else (-1 if stoch_div_osc[i] < 0 else 0)
        vwap_div_osc_norm = clip_scalar(vwap_div_osc[i], -1.0, 1.0) if not np.isnan(vwap_div_osc[i]) else 0
        stoch_peaking_norm = -1 if stoch_is_peaking[i] else 0
        stoch_bottoming_norm = 1 if stoch_is_bottoming[i] else 0
        m3_div_osc_norm = 1 if m3_div_osc[i] > 0 else (-1 if m3_div_osc[i] < 0 else 0)
        # Pine v48 library uses 3-way: m2_div_osc_tiny > 0 ? 1 : (m2_div_osc_tiny < 0 ? -1 : 0)
        # 14 residual outlier rows exist where CSV precision truncates a tiny negative to 0.0,
        # so Python returns 0 here while Pine sees the actual <0 value and returns -1.
        m2_div_osc_norm = 1 if m2_div_osc_tiny[i] > 0 else (-1 if m2_div_osc_tiny[i] < 0 else 0)
        m2_div_osc_noOffset_norm = 1 if m2_div_osc_noOffset[i] > 0 else (-1 if m2_div_osc_noOffset[i] < 0 else 0)

        s = (
            (macd_pred_norm * w_macd_pred) +
            (stoch_norm * w_stoch) +
            (osc_norm * w_osc) +
            (macd_bullish_norm * w_macd_bullish) +
            (m3_momentum_norm * w_m3_momentum) +
            (m2_tiny_momentum_norm * w_m2_tiny) +
            (rsid_osc_norm * w_rsid_osc) +
            (stoch_div_osc_norm * w_stoch_div_osc) +
            (vwap_div_osc_norm * w_vwap_div_osc) +
            (stoch_peaking_norm * w_stoch_peaking) +
            (stoch_bottoming_norm * w_stoch_bottoming) +
            (m3_div_osc_norm * w_m3_div_osc) +
            (m2_div_osc_norm * w_m2_div_osc) +
            (m2_div_osc_noOffset_norm * w_m2_div_osc_noOffset) +
            (bearish_engulfing_score[i] * w_bearish_engulfing) +
            (bullish_hammer_score[i] * w_bullish_hammer) +
            (bullish_engulfing_score[i] * w_bullish_engulfing) +
            (shooting_star_score[i] * w_shooting_star)
        )
        score[i] = s
    return score

def scale_to_range(series, lookback, min_val=0, max_val=100):
    """
    Scales a pandas Series to a given range [min_val, max_val] based on its rolling min/max.
    Equivalent to Pine Script's scale_ToRange function.
    """
    rolling_min = series.rolling(window=lookback, min_periods=1).min()
    rolling_max = series.rolling(window=lookback, min_periods=1).max()
    denom = rolling_max - rolling_min
    
    # Handle division by zero (when max == min)
    # If denom is 0, we return the midpoint of the range
    mid_val = (max_val + min_val) / 2
    
    scaled = np.where(denom == 0, mid_val, (series - rolling_min) / denom * (max_val - min_val) + min_val)
    scaled = pd.Series(scaled, index=series.index)
    return scaled.fillna(0)

@njit
def calculate_positions(entries, exits):
    """
    Determines executed trades based on position state.
    Entry only if flat, Exit only if long.
    """
    n = len(entries)
    in_pos = np.zeros(n, dtype=np.bool_)
    exec_entry = np.zeros(n, dtype=np.bool_)
    exec_exit = np.zeros(n, dtype=np.bool_)
    
    currently_in_pos = False
    
    for i in range(n):
        if currently_in_pos:
            if exits[i]:
                exec_exit[i] = True
                currently_in_pos = False
        else:
            if entries[i]:
                exec_entry[i] = True
                currently_in_pos = True
        
        in_pos[i] = currently_in_pos
        
    return in_pos, exec_entry, exec_exit