"""
Time-Sequenced Flow Analysis Service
Analyzes flow patterns over time to detect urgency, distribution, and continuation signals
"""
import pandas as pd
import numpy as np
from typing import Dict, Optional
from datetime import timedelta
from utils.logger import logger


class TimeSequencedAnalyzer:
    """
    Analyzes flow patterns over time to detect:
    - Flow acceleration (urgency building)
    - Distribution patterns (flow weakening)
    - Strike laddering (sequential accumulation)
    """
    
    def __init__(self):
        # Configuration
        self.analysis_window_minutes = 60  # Look back window for flow analysis
        self.min_trades_for_analysis = 3  # Minimum trades needed for meaningful analysis
        
    def calculate_flow_acceleration(
        self,
        df: pd.DataFrame,
        row_idx: int
    ) -> Optional[float]:
        """
        Calculate flow acceleration: change in premium per minute.
        Positive = accelerating (urgency building)
        Negative = decelerating (flow weakening)
        
        Returns: Δ premium / minute (premium rate change)
        """
        if row_idx >= len(df):
            return None
        
        current_row = df.iloc[row_idx]
        symbol = current_row.get('symbol_norm')
        direction = current_row.get('direction')
        flow_ts_utc = current_row.get('flow_ts_utc')
        
        if pd.isna(symbol) or pd.isna(direction) or pd.isna(flow_ts_utc):
            return None
        
        # Sort by timestamp
        df_sorted = df.sort_values('flow_ts_utc').reset_index(drop=True)
        
        # Look at recent trades for same symbol and direction
        window_start = flow_ts_utc - timedelta(minutes=self.analysis_window_minutes)
        
        mask = (
            (df_sorted['symbol_norm'] == symbol) &
            (df_sorted['direction'] == direction) &
            (df_sorted['flow_ts_utc'] >= window_start) &
            (df_sorted['flow_ts_utc'] <= flow_ts_utc)
        )
        
        recent_trades = df_sorted[mask]
        
        if len(recent_trades) < self.min_trades_for_analysis:
            return None
        
        # Calculate premium accumulation over time
        recent_trades = recent_trades.sort_values('flow_ts_utc')
        recent_trades = recent_trades.copy()
        recent_trades['cumulative_premium'] = recent_trades['premium_num'].cumsum()
        
        # Fit linear trend to cumulative premium vs time
        # Convert timestamps to minutes since window start
        recent_trades['minutes_since_start'] = (
            recent_trades['flow_ts_utc'] - window_start
        ).dt.total_seconds() / 60.0
        
        # Calculate slope (premium per minute)
        if len(recent_trades) >= 2:
            x = recent_trades['minutes_since_start'].values
            y = recent_trades['cumulative_premium'].values
            
            # Linear regression: y = mx + b, we want m (slope)
            # Calculate acceleration as change in slope (second derivative approximation)
            if len(recent_trades) >= 3:
                # Split into two halves
                mid_idx = len(recent_trades) // 2
                first_half = recent_trades.iloc[:mid_idx]
                second_half = recent_trades.iloc[mid_idx:]
                
                if len(first_half) >= 2 and len(second_half) >= 2:
                    # Calculate slopes for each half
                    x1 = first_half['minutes_since_start'].values
                    y1 = first_half['cumulative_premium'].values
                    slope1 = np.polyfit(x1, y1, 1)[0] if len(x1) > 1 else 0
                    
                    x2 = second_half['minutes_since_start'].values
                    y2 = second_half['cumulative_premium'].values
                    slope2 = np.polyfit(x2, y2, 1)[0] if len(x2) > 1 else 0
                    
                    # Acceleration = change in slope
                    acceleration = slope2 - slope1
                    return float(acceleration)
        
        return None
    
    def calculate_time_between_hits(
        self,
        df: pd.DataFrame,
        row_idx: int
    ) -> Optional[float]:
        """
        Calculate average time between consecutive trades (in minutes).
        Lower = faster pace, higher urgency.
        """
        if row_idx >= len(df):
            return None
        
        current_row = df.iloc[row_idx]
        symbol = current_row.get('symbol_norm')
        direction = current_row.get('direction')
        flow_ts_utc = current_row.get('flow_ts_utc')
        
        if pd.isna(symbol) or pd.isna(direction) or pd.isna(flow_ts_utc):
            return None
        
        # Sort by timestamp
        df_sorted = df.sort_values('flow_ts_utc').reset_index(drop=True)
        
        # Look at recent trades
        window_start = flow_ts_utc - timedelta(minutes=self.analysis_window_minutes)
        
        mask = (
            (df_sorted['symbol_norm'] == symbol) &
            (df_sorted['direction'] == direction) &
            (df_sorted['flow_ts_utc'] >= window_start) &
            (df_sorted['flow_ts_utc'] <= flow_ts_utc)
        )
        
        recent_trades = df_sorted[mask].sort_values('flow_ts_utc')
        
        if len(recent_trades) < 2:
            return None
        
        # Calculate time gaps between consecutive trades
        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) == 0:
            return None
        
        avg_time_between = np.mean(time_gaps)
        return float(avg_time_between)
    
    def calculate_follow_on_ratio(
        self,
        df: pd.DataFrame,
        row_idx: int
    ) -> Optional[float]:
        """
        Calculate follow-on ratio: fraction of trades in same direction after initial trade.
        Higher = continuation, Lower = reversal/distribution.
        
        Returns: ratio of same-direction trades / total trades (0-1)
        """
        if row_idx >= len(df):
            return None
        
        current_row = df.iloc[row_idx]
        symbol = current_row.get('symbol_norm')
        direction = current_row.get('direction')
        flow_ts_utc = current_row.get('flow_ts_utc')
        
        if pd.isna(symbol) or pd.isna(direction) or pd.isna(flow_ts_utc):
            return None
        
        # Sort by timestamp
        df_sorted = df.sort_values('flow_ts_utc').reset_index(drop=True)
        
        # Look at trades after this one (within window)
        window_end = flow_ts_utc + timedelta(minutes=self.analysis_window_minutes)
        
        mask = (
            (df_sorted['symbol_norm'] == symbol) &
            (df_sorted['flow_ts_utc'] > flow_ts_utc) &
            (df_sorted['flow_ts_utc'] <= window_end)
        )
        
        follow_on_trades = df_sorted[mask]
        
        if len(follow_on_trades) == 0:
            return None
        
        # Count same-direction vs opposite-direction
        same_direction = follow_on_trades[follow_on_trades['direction'] == direction]
        follow_on_ratio = len(same_direction) / len(follow_on_trades) if len(follow_on_trades) > 0 else 0.0
        
        return float(follow_on_ratio)
    
    def detect_strike_laddering(
        self,
        df: pd.DataFrame,
        row_idx: int
    ) -> bool:
        """
        Detect strike laddering: sequential strikes in same direction.
        Returns True if laddering pattern detected.
        """
        if row_idx >= len(df):
            return False
        
        current_row = df.iloc[row_idx]
        symbol = current_row.get('symbol_norm')
        direction = current_row.get('direction')
        cp_norm = current_row.get('cp_norm')
        flow_ts_utc = current_row.get('flow_ts_utc')
        exp_date = current_row.get('exp_date')
        
        if pd.isna(symbol) or pd.isna(direction) or pd.isna(cp_norm):
            return False
        
        # Sort by timestamp
        df_sorted = df.sort_values('flow_ts_utc').reset_index(drop=True)
        
        # Look at recent trades
        window_start = flow_ts_utc - timedelta(minutes=self.analysis_window_minutes)
        
        mask = (
            (df_sorted['symbol_norm'] == symbol) &
            (df_sorted['direction'] == direction) &
            (df_sorted['cp_norm'] == cp_norm) &
            (df_sorted['flow_ts_utc'] >= window_start) &
            (df_sorted['flow_ts_utc'] <= flow_ts_utc)
        )
        
        if pd.notna(exp_date):
            mask = mask & (df_sorted['exp_date'] == exp_date)
        
        recent_trades = df_sorted[mask].sort_values('flow_ts_utc')
        
        if len(recent_trades) < 3:
            return False
        
        # Get unique strikes in order
        strikes = recent_trades['strike_num'].dropna().unique()
        strikes = sorted(strikes)
        
        if len(strikes) < 3:
            return False
        
        # Check if strikes are sequential (laddering)
        # Look for consistent spacing (e.g., 100, 105, 110 or 50, 55, 60)
        strike_diffs = [strikes[i+1] - strikes[i] for i in range(len(strikes)-1)]
        
        if len(strike_diffs) >= 2:
            # Check if differences are roughly equal (within 20% variance)
            avg_diff = np.mean(strike_diffs)
            if avg_diff > 0:
                std_diff = np.std(strike_diffs)
                cv = std_diff / avg_diff if avg_diff > 0 else float('inf')  # Coefficient of variation
                
                # Low coefficient of variation = regular spacing = laddering
                if cv < 0.2:  # Less than 20% variation
                    return True
        
        return False
    
    def enrich_with_time_sequenced_metrics(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        Add time-sequenced flow metrics to DataFrame.
        Adds: flow_acceleration, time_between_hits, follow_on_ratio, strike_laddering_detected
        """
        if df.empty:
            return df
        
        df = df.copy()
        
        # Initialize columns
        df['flow_acceleration'] = None
        df['time_between_hits'] = None
        df['follow_on_ratio'] = None
        df['strike_laddering_detected'] = False
        
        # Sort by timestamp for proper analysis
        df_sorted = df.sort_values('flow_ts_utc').reset_index(drop=True)
        
        # Calculate metrics for each row
        for idx in df_sorted.index:
            original_idx = df_sorted.index[idx]
            
            # Flow acceleration
            acceleration = self.calculate_flow_acceleration(df_sorted, idx)
            if acceleration is not None:
                df.at[original_idx, 'flow_acceleration'] = float(acceleration)
            
            # Time between hits
            time_between = self.calculate_time_between_hits(df_sorted, idx)
            if time_between is not None:
                df.at[original_idx, 'time_between_hits'] = float(time_between)
            
            # Follow-on ratio
            follow_on = self.calculate_follow_on_ratio(df_sorted, idx)
            if follow_on is not None:
                df.at[original_idx, 'follow_on_ratio'] = float(follow_on)
            
            # Strike laddering
            laddering = self.detect_strike_laddering(df_sorted, idx)
            df.at[original_idx, 'strike_laddering_detected'] = bool(laddering)
        
        logger.info(f"Time-sequenced analysis complete. Strike laddering detected: {df['strike_laddering_detected'].sum()}")
        
        return df

