"""
Risk management utilities for backtesting.

Handles funding rate calculations and liquidation detection.
"""
from dataclasses import dataclass

import pandas as pd

from engine.logging_config import get_logger
from engine.market import MarketConfig, calculate_liquidation_price

logger = get_logger(__name__)


@dataclass
class LiquidationEvent:
    """
    Record of a liquidation event during backtesting.
    
    Attributes:
        entry_time: Timestamp when position was opened
        entry_price: Price at position entry
        liquidation_time: Timestamp when liquidation occurred
        liquidation_price: Calculated liquidation price
        actual_price: Actual price that triggered liquidation (high/low)
        direction: 'long' or 'short'
        margin_lost_pct: Percentage of margin lost (typically 100%)
    """
    entry_time: pd.Timestamp
    entry_price: float
    liquidation_time: pd.Timestamp
    liquidation_price: float
    actual_price: float
    direction: str
    margin_lost_pct: float = 1.0


def calculate_funding(
    close: pd.Series,
    long_entries: pd.DataFrame,
    long_exits: pd.DataFrame,
    short_entries: pd.DataFrame,
    short_exits: pd.DataFrame,
    market_config: MarketConfig,
    leverage: int
) -> float:
    """
    Calculate total funding paid/received for perpetual positions.
    
    Simplified model: applies funding rate every 8 hours to open positions.
    Positive rate means longs pay shorts.
    
    Args:
        close: Price series
        long_entries: Long entry signals
        long_exits: Long exit signals
        short_entries: Short entry signals
        short_exits: Short exit signals
        market_config: Market configuration with funding parameters
        leverage: Position leverage
        
    Returns:
        Total funding paid (positive) or received (negative)
    """
    if market_config.funding_interval_hours == 0:
        return 0.0
        
    funding_rate = market_config.funding_rate
    interval_hours = market_config.funding_interval_hours
    
    # Determine position state at each bar
    long_position = long_entries.cumsum() - long_exits.cumsum()
    short_position = short_entries.cumsum() - short_exits.cumsum()
    
    # Clamp to 0/1 (either in position or not)
    long_position = (long_position > 0).astype(int)
    short_position = (short_position > 0).astype(int)
    
    # Find funding timestamps (every 8 hours: 00:00, 08:00, 16:00 UTC)
    funding_times = close.index[close.index.hour % interval_hours == 0]
    
    total_funding = 0.0
    for ts in funding_times:
        if ts not in close.index:
            continue
        price = close.loc[ts]
        
        # Long pays funding, short receives (when rate > 0)
        if isinstance(long_position, pd.DataFrame):
            long_open = long_position.loc[ts].any()
            short_open = short_position.loc[ts].any()
        else:
            long_open = long_position.loc[ts] > 0
            short_open = short_position.loc[ts] > 0
        
        position_value = price * leverage
        if long_open:
            total_funding += position_value * funding_rate
        if short_open:
            total_funding -= position_value * funding_rate
            
    return total_funding


def inject_liquidation_exits(
    close: pd.Series,
    high: pd.Series,
    low: pd.Series,
    long_entries: pd.DataFrame | pd.Series,
    long_exits: pd.DataFrame | pd.Series,
    short_entries: pd.DataFrame | pd.Series,
    short_exits: pd.DataFrame | pd.Series,
    leverage: int,
    maintenance_margin_rate: float
) -> tuple[pd.DataFrame | pd.Series, pd.DataFrame | pd.Series, list[LiquidationEvent]]:
    """
    Modify exit signals to force position closure at liquidation points.
    
    This function simulates realistic liquidation behavior by:
    1. Finding positions that would be liquidated before their normal exit
    2. Injecting forced exit signals at the liquidation bar
    3. Recording all liquidation events
    
    Args:
        close: Close price series
        high: High price series
        low: Low price series
        long_entries: Long entry signals
        long_exits: Long exit signals
        short_entries: Short entry signals
        short_exits: Short exit signals
        leverage: Position leverage
        maintenance_margin_rate: Maintenance margin rate for liquidation
        
    Returns:
        Tuple of (modified_long_exits, modified_short_exits, liquidation_events)
    """
    if leverage <= 1:
        return long_exits, short_exits, []
    
    liquidation_events: list[LiquidationEvent] = []
    
    # Convert to DataFrame if Series for consistent handling
    is_series = isinstance(long_entries, pd.Series)
    if is_series:
        long_entries_df = long_entries.to_frame()
        long_exits_df = long_exits.to_frame()
        short_entries_df = short_entries.to_frame()
        short_exits_df = short_exits.to_frame()
    else:
        long_entries_df = long_entries
        long_exits_df = long_exits.copy()
        short_entries_df = short_entries
        short_exits_df = short_exits.copy()
    
    modified_long_exits = long_exits_df.copy()
    modified_short_exits = short_exits_df.copy()
    
    # Process long positions
    long_mask = long_entries_df.any(axis=1)
    for entry_idx in close.index[long_mask]:
        entry_price = close.loc[entry_idx]
        liq_price = calculate_liquidation_price(
            entry_price, leverage, is_long=True,
            maintenance_margin_rate=maintenance_margin_rate
        )
        
        # Find the normal exit for this entry
        subsequent_exits = long_exits_df.loc[entry_idx:].any(axis=1)
        exit_indices = subsequent_exits[subsequent_exits].index
        normal_exit_idx = exit_indices[0] if len(exit_indices) > 0 else close.index[-1]
        
        # Check if liquidation occurs before normal exit
        price_range = low.loc[entry_idx:normal_exit_idx]
        if (price_range < liq_price).any():
            liq_bar = price_range[price_range < liq_price].index[0]
            
            # Inject forced exit at liquidation bar
            for col in modified_long_exits.columns:
                modified_long_exits.loc[liq_bar, col] = True
            
            # Record the liquidation event
            liquidation_events.append(LiquidationEvent(
                entry_time=entry_idx,
                entry_price=entry_price,
                liquidation_time=liq_bar,
                liquidation_price=liq_price,
                actual_price=low.loc[liq_bar],
                direction='long',
                margin_lost_pct=1.0
            ))
            
            logger.warning(
                "LIQUIDATION (Long): Entry %s ($%.2f) -> Liquidated %s "
                "(liq=$%.2f, low=$%.2f)",
                entry_idx.strftime('%Y-%m-%d'), entry_price,
                liq_bar.strftime('%Y-%m-%d'), liq_price, low.loc[liq_bar]
            )
    
    # Process short positions
    short_mask = short_entries_df.any(axis=1)
    for entry_idx in close.index[short_mask]:
        entry_price = close.loc[entry_idx]
        liq_price = calculate_liquidation_price(
            entry_price, leverage, is_long=False,
            maintenance_margin_rate=maintenance_margin_rate
        )
        
        # Find the normal exit for this entry
        subsequent_exits = short_exits_df.loc[entry_idx:].any(axis=1)
        exit_indices = subsequent_exits[subsequent_exits].index
        normal_exit_idx = exit_indices[0] if len(exit_indices) > 0 else close.index[-1]
        
        # Check if liquidation occurs before normal exit
        price_range = high.loc[entry_idx:normal_exit_idx]
        if (price_range > liq_price).any():
            liq_bar = price_range[price_range > liq_price].index[0]
            
            # Inject forced exit at liquidation bar
            for col in modified_short_exits.columns:
                modified_short_exits.loc[liq_bar, col] = True
            
            # Record the liquidation event
            liquidation_events.append(LiquidationEvent(
                entry_time=entry_idx,
                entry_price=entry_price,
                liquidation_time=liq_bar,
                liquidation_price=liq_price,
                actual_price=high.loc[liq_bar],
                direction='short',
                margin_lost_pct=1.0
            ))
            
            logger.warning(
                "LIQUIDATION (Short): Entry %s ($%.2f) -> Liquidated %s "
                "(liq=$%.2f, high=$%.2f)",
                entry_idx.strftime('%Y-%m-%d'), entry_price,
                liq_bar.strftime('%Y-%m-%d'), liq_price, high.loc[liq_bar]
            )
    
    # Convert back to Series if input was Series
    if is_series:
        modified_long_exits = modified_long_exits.iloc[:, 0]
        modified_short_exits = modified_short_exits.iloc[:, 0]
    
    return modified_long_exits, modified_short_exits, liquidation_events


def calculate_liquidation_adjustment(
    liquidation_events: list[LiquidationEvent],
    init_cash: float,
    leverage: int
) -> tuple[float, float]:
    """
    Calculate the return adjustment for liquidated positions.
    
    VectorBT calculates trade P&L using close price at exit bar.
    For liquidations, the actual loss is 100% of the position margin.
    This function calculates the difference between what VectorBT
    recorded and what actually would have happened.
    
    In our portfolio setup:
    - Long/short each get half the capital (init_cash * leverage / 2)
    - Each trade uses 100% of that allocation (size=1.0, percent)
    - On liquidation, the margin for that trade is lost entirely
    
    The adjustment is the DIFFERENCE between:
    - VectorBT's calculated P&L (exit at close price)
    - Actual liquidation P&L (100% margin loss)
    
    Args:
        liquidation_events: List of liquidation events
        init_cash: Initial portfolio cash (before leverage)
        leverage: Position leverage used
        
    Returns:
        Tuple of (total_margin_lost, adjustment_pct)
        - total_margin_lost: Estimated total margin lost from liquidations
        - adjustment_pct: Percentage adjustment to apply to returns
    """
    if not liquidation_events:
        return 0.0, 0.0
    
    # In our setup, each side (long/short) gets half the capital
    # Margin per side = init_cash / 2
    margin_per_side = init_cash / 2
    
    # For each liquidation, VectorBT recorded some P&L based on close price
    # The actual P&L should be -100% of the margin used for that trade
    # 
    # We estimate the adjustment as:
    # - Each liquidation should have resulted in ~-20% loss (at 5x leverage)
    # - VectorBT may have recorded a different value
    # - The margin loss is (1/leverage) per trade that gets liquidated
    
    # Calculate liquidation loss rate based on leverage
    # At 5x leverage, liquidation = ~19.6% adverse move = 100% margin loss
    liq_loss_rate = 1.0 / leverage  # Approximate loss per trade as % of position
    
    # Count liquidations
    n_liquidations = len(liquidation_events)
    
    # Estimate total margin lost:
    # Each liquidation on average loses the margin for that trade
    # Since VectorBT uses half capital per side, and we trade 100% size,
    # each liquidation loses approximately margin_per_side
    # But we cap at available capital
    total_margin_lost = min(n_liquidations * margin_per_side * liq_loss_rate, init_cash)
    
    # Calculate as percentage of initial capital
    adjustment_pct = (total_margin_lost / init_cash) * 100
    
    return total_margin_lost, adjustment_pct


def check_liquidations(
    close: pd.Series,
    high: pd.Series,
    low: pd.Series,
    long_entries: pd.DataFrame,
    long_exits: pd.DataFrame,
    short_entries: pd.DataFrame,
    short_exits: pd.DataFrame,
    leverage: int,
    maintenance_margin_rate: float
) -> int:
    """
    Check for liquidation events and log warnings.
    
    Args:
        close: Close price series
        high: High price series
        low: Low price series
        long_entries: Long entry signals
        long_exits: Long exit signals
        short_entries: Short entry signals
        short_exits: Short exit signals
        leverage: Position leverage
        maintenance_margin_rate: Maintenance margin rate for liquidation
        
    Returns:
        Count of liquidation warnings
    """
    warnings = 0
    
    # For long positions
    long_mask = (
        long_entries.any(axis=1) 
        if isinstance(long_entries, pd.DataFrame) 
        else long_entries
    )
    
    for entry_idx in close.index[long_mask]:
        entry_price = close.loc[entry_idx]
        liq_price = calculate_liquidation_price(
            entry_price, leverage, is_long=True, 
            maintenance_margin_rate=maintenance_margin_rate
        )
        
        subsequent = low.loc[entry_idx:]
        if (subsequent < liq_price).any():
            liq_bar = subsequent[subsequent < liq_price].index[0]
            logger.warning(
                "LIQUIDATION WARNING (Long): Entry at %s ($%.2f), "
                "would liquidate at %s (liq_price=$%.2f, low=$%.2f)",
                entry_idx, entry_price, liq_bar, liq_price, low.loc[liq_bar]
            )
            warnings += 1
    
    # For short positions
    short_mask = (
        short_entries.any(axis=1) 
        if isinstance(short_entries, pd.DataFrame) 
        else short_entries
    )
    
    for entry_idx in close.index[short_mask]:
        entry_price = close.loc[entry_idx]
        liq_price = calculate_liquidation_price(
            entry_price, leverage, is_long=False,
            maintenance_margin_rate=maintenance_margin_rate
        )
        
        subsequent = high.loc[entry_idx:]
        if (subsequent > liq_price).any():
            liq_bar = subsequent[subsequent > liq_price].index[0]
            logger.warning(
                "LIQUIDATION WARNING (Short): Entry at %s ($%.2f), "
                "would liquidate at %s (liq_price=$%.2f, high=$%.2f)",
                entry_idx, entry_price, liq_bar, liq_price, high.loc[liq_bar]
            )
            warnings += 1
            
    return warnings
