"""
Signal Component Scorer
Converts binary badge logic into continuous numeric signal components
"""
import pandas as pd
import numpy as np
from typing import Dict, Optional
from utils.logger import logger


class SignalComponentScorer:
    """
    Replaces binary badge logic (💎 ⭐ 🟢 🔴) with continuous numeric scores.
    Badges remain display-only, but signal_strength is computed from components.
    """
    
    def __init__(self):
        # Signal strength weights (must sum to 1.0)
        self.aggression_weight = 0.30
        self.size_concentration_weight = 0.30
        self.repeat_velocity_weight = 0.20
        self.strike_clustering_weight = 0.20
        
    def calculate_aggression_score(self, row: pd.Series) -> float:
        """
        Measures trade aggression: premium paid at ask vs bid, ITM vs OTM preference.
        
        Higher score = more aggressive buying:
        - ITM premiums indicate willingness to pay intrinsic value
        - Ask-side trades (AA) indicate market orders
        - Large premium relative to spot price
        """
        score = 0.0
        
        premium_num = row.get('premium_num', 0) or 0
        spot_num = row.get('spot_num', 0) or 0
        cp_norm = row.get('cp_norm', '')
        side = str(row.get('Side', '')).upper()
        
        # ITM premium component (0-40 points)
        bull_prem_itm = row.get('bull_prem_itm', 0) or 0
        bear_prem_itm = row.get('bear_prem_itm', 0) or 0
        total_prem_itm = bull_prem_itm + bear_prem_itm
        bull_total = row.get('bull_total', 0) or 0
        bear_total = row.get('bear_total', 0) or 0
        total_premium = bull_total + bear_total
        
        if total_premium > 0:
            itm_ratio = total_prem_itm / total_premium
            score += itm_ratio * 40.0
        
        # Ask-side aggression component (0-30 points)
        # AA = buying at ask (market orders, urgency)
        has_aa = 'AA' in side or 'AT_ASK' in side
        has_bb = 'BB' in side or 'AT_BID' in side
        
        if has_aa and not has_bb:
            score += 30.0
        elif has_aa:
            score += 15.0
        
        # Premium-to-spot ratio component (0-30 points)
        # Large premium relative to spot indicates aggressive positioning
        if spot_num > 0 and premium_num > 0:
            prem_to_spot_ratio = premium_num / spot_num
            # Normalize: 0.01 (1%) = 10 points, 0.05 (5%) = 30 points
            ratio_score = min(30.0, prem_to_spot_ratio * 600.0)
            score += ratio_score
        
        return min(100.0, max(0.0, score))
    
    def calculate_size_concentration_score(self, row: pd.Series) -> float:
        """
        Measures size concentration: how much premium is concentrated in single strikes/expiries.
        
        Higher score = more concentrated (single large trade vs many small ones):
        - High premium in single trade
        - OI buildup in specific strikes
        - Directional consistency (all calls or all puts)
        """
        score = 0.0
        
        premium_num = row.get('premium_num', 0) or 0
        bull_total = row.get('bull_total', 0) or 0
        bear_total = row.get('bear_total', 0) or 0
        total_premium = bull_total + bear_total
        
        # Single-trade premium concentration (0-40 points)
        # Premium of current trade relative to running total
        if total_premium > 0:
            concentration_ratio = premium_num / total_premium
            # Single trade = 50%+ of total = very concentrated
            if concentration_ratio >= 0.5:
                score += 40.0
            elif concentration_ratio >= 0.3:
                score += 30.0
            elif concentration_ratio >= 0.2:
                score += 20.0
            elif concentration_ratio >= 0.1:
                score += 10.0
        
        # OI concentration component (0-30 points)
        # High OI in OTM strikes indicates concentrated positioning
        oi_cb_otm = row.get('oi_cb_otm', 0) or 0
        oi_pb_otm = row.get('oi_pb_otm', 0) or 0
        oi_all = row.get('oi_all', 0) or 0
        
        if oi_all > 0:
            otm_oi_ratio = (oi_cb_otm + oi_pb_otm) / oi_all
            score += otm_oi_ratio * 30.0
        
        # Directional consistency (0-30 points)
        # Pure directional flow (all bull or all bear) = more concentrated
        if total_premium > 0:
            direction_strength = abs(bull_total - bear_total) / total_premium
            score += direction_strength * 30.0
        
        return min(100.0, max(0.0, score))
    
    def calculate_repeat_trade_velocity(self, df: pd.DataFrame, row_idx: int) -> float:
        """
        Measures repeat trade velocity: frequency of trades in same direction/expiry.
        
        Higher score = faster follow-on trades (urgency building):
        - Time between consecutive trades decreasing
        - Multiple trades in same direction
        - Same expiry date (rolling accumulation)
        """
        if row_idx >= len(df):
            return 0.0
        
        current_row = df.iloc[row_idx]
        symbol = current_row.get('symbol_norm')
        direction = current_row.get('direction')
        exp_date = current_row.get('exp_date')
        flow_ts_utc = current_row.get('flow_ts_utc')
        
        if pd.isna(symbol) or pd.isna(direction) or pd.isna(flow_ts_utc):
            return 0.0
        
        # Look back at recent trades for same symbol
        # Sort by timestamp to find preceding trades
        df_sorted = df.sort_values('flow_ts_utc').reset_index(drop=True)
        current_idx = df_sorted.index[df_sorted.index == row_idx]
        
        if len(current_idx) == 0:
            return 0.0
        
        current_pos = current_idx[0]
        
        # Look at last 10 trades for this symbol
        symbol_mask = (df_sorted['symbol_norm'] == symbol) & \
                      (df_sorted.index < current_pos) & \
                      (df_sorted['flow_ts_utc'] <= flow_ts_utc)
        
        recent_trades = df_sorted[symbol_mask].tail(10)
        
        if len(recent_trades) == 0:
            return 0.0
        
        score = 0.0
        
        # Time compression component (0-40 points)
        # Decreasing time gaps = urgency building
        if len(recent_trades) >= 2:
            time_gaps = []
            prev_ts = None
            for _, trade in recent_trades.iterrows():
                ts = trade.get('flow_ts_utc')
                if pd.notna(ts) and prev_ts:
                    gap_minutes = (ts - prev_ts).total_seconds() / 60.0
                    if gap_minutes > 0:
                        time_gaps.append(gap_minutes)
                prev_ts = ts
            
            if len(time_gaps) >= 2:
                # Check if gaps are decreasing (accelerating)
                recent_gaps = time_gaps[-2:]
                if recent_gaps[1] < recent_gaps[0]:
                    compression_ratio = recent_gaps[1] / recent_gaps[0] if recent_gaps[0] > 0 else 0
                    # 50% compression = 20 points, 80% compression = 40 points
                    score += (1.0 - compression_ratio) * 40.0
        
        # Directional consistency (0-35 points)
        # Same direction trades = building position
        same_direction = recent_trades[recent_trades['direction'] == direction]
        if len(recent_trades) > 0:
            consistency_ratio = len(same_direction) / len(recent_trades)
            score += consistency_ratio * 35.0
        
        # Expiry concentration (0-25 points)
        # Same expiry = rolling accumulation
        if pd.notna(exp_date):
            same_expiry = recent_trades[recent_trades['exp_date'] == exp_date]
            if len(recent_trades) > 0:
                expiry_ratio = len(same_expiry) / len(recent_trades)
                score += expiry_ratio * 25.0
        
        return min(100.0, max(0.0, score))
    
    def calculate_strike_clustering_score(self, df: pd.DataFrame, row_idx: int) -> float:
        """
        Measures strike clustering: trades accumulating at specific strike levels.
        
        Higher score = more clustering (laddering or concentrated strikes):
        - Multiple trades at same strike
        - Sequential strikes (laddering)
        - Strikes near current price (pin risk)
        """
        if row_idx >= len(df):
            return 0.0
        
        current_row = df.iloc[row_idx]
        symbol = current_row.get('symbol_norm')
        strike_num = current_row.get('strike_num')
        spot_num = current_row.get('spot_num', 0) or 0
        flow_ts_utc = current_row.get('flow_ts_utc')
        exp_date = current_row.get('exp_date')
        
        if pd.isna(symbol) or pd.isna(strike_num) or pd.isna(flow_ts_utc):
            return 0.0
        
        # Look at recent trades for same symbol and expiry
        df_sorted = df.sort_values('flow_ts_utc').reset_index(drop=True)
        symbol_mask = (df_sorted['symbol_norm'] == symbol) & \
                      (df_sorted['flow_ts_utc'] <= flow_ts_utc)
        
        if pd.notna(exp_date):
            symbol_mask = symbol_mask & (df_sorted['exp_date'] == exp_date)
        
        recent_trades = df_sorted[symbol_mask].tail(20)
        
        if len(recent_trades) <= 1:
            return 0.0
        
        score = 0.0
        
        # Exact strike clustering (0-40 points)
        # Multiple trades at same strike
        same_strike_count = len(recent_trades[recent_trades['strike_num'] == strike_num])
        if len(recent_trades) > 0:
            clustering_ratio = same_strike_count / len(recent_trades)
            score += clustering_ratio * 40.0
        
        # Strike laddering detection (0-35 points)
        # Sequential strikes (e.g., 100, 105, 110) indicate laddering
        unique_strikes = sorted(recent_trades['strike_num'].dropna().unique())
        if len(unique_strikes) >= 3:
            # Check if strikes are roughly evenly spaced (laddering)
            if spot_num > 0:
                strike_diffs = [abs(unique_strikes[i+1] - unique_strikes[i]) for i in range(len(unique_strikes)-1)]
                if len(strike_diffs) > 0:
                    avg_diff = np.mean(strike_diffs)
                    std_diff = np.std(strike_diffs)
                    # Low std relative to mean = regular spacing (laddering)
                    if avg_diff > 0:
                        regularity = 1.0 - min(1.0, std_diff / avg_diff)
                        score += regularity * 35.0
        
        # Proximity to spot (0-25 points)
        # Strikes near spot price = pin risk, more significant
        if spot_num > 0:
            strike_pct_diff = abs(strike_num - spot_num) / spot_num * 100.0
            # Within 2% = 25 points, within 5% = 15 points, within 10% = 5 points
            if strike_pct_diff <= 2.0:
                score += 25.0
            elif strike_pct_diff <= 5.0:
                score += 15.0
            elif strike_pct_diff <= 10.0:
                score += 5.0
        
        return min(100.0, max(0.0, score))
    
    def calculate_signal_strength(self, row: pd.Series) -> float:
        """
        Calculate composite signal_strength from component scores.
        
        signal_strength = 
            0.30 * aggression_score +
            0.30 * size_concentration_score +
            0.20 * repeat_trade_velocity +
            0.20 * strike_clustering_score
        """
        aggression = row.get('aggression_score', 0) or 0
        size_conc = row.get('size_concentration_score', 0) or 0
        repeat_vel = row.get('repeat_trade_velocity', 0) or 0
        strike_clust = row.get('strike_clustering_score', 0) or 0
        
        signal_strength = (
            self.aggression_weight * aggression +
            self.size_concentration_weight * size_conc +
            self.repeat_velocity_weight * repeat_vel +
            self.strike_clustering_weight * strike_clust
        )
        
        return min(100.0, max(0.0, signal_strength))
    
    def enrich_with_signal_components(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        Add signal component scores to DataFrame.
        Adds: aggression_score, size_concentration_score, repeat_trade_velocity,
              strike_clustering_score, signal_strength
        """
        if df.empty:
            return df
        
        df = df.copy()
        
        # Initialize columns
        df['aggression_score'] = 0.0
        df['size_concentration_score'] = 0.0
        df['repeat_trade_velocity'] = 0.0
        df['strike_clustering_score'] = 0.0
        df['signal_strength'] = 0.0
        
        # Calculate component scores (some require DataFrame context)
        df['aggression_score'] = df.apply(self.calculate_aggression_score, axis=1)
        df['size_concentration_score'] = df.apply(self.calculate_size_concentration_score, axis=1)
        
        # These require DataFrame context, so iterate
        for idx in df.index:
            df.at[idx, 'repeat_trade_velocity'] = self.calculate_repeat_trade_velocity(df, idx)
            df.at[idx, 'strike_clustering_score'] = self.calculate_strike_clustering_score(df, idx)
        
        # Calculate composite signal_strength
        df['signal_strength'] = df.apply(self.calculate_signal_strength, axis=1)
        
        logger.info(f"Signal component scoring complete. Mean signal_strength: {df['signal_strength'].mean():.2f}")
        
        return df

