"""Buy strategy base class for OpenClaw Trading.

This module provides the BuyStrategy abstract class that defines the interface
for buy-side trading strategies.
"""

from abc import abstractmethod
from typing import Any, Dict, List, Optional

import pandas as pd
from pydantic import BaseModel, Field, field_validator

from openclaw.strategy.base import Signal, SignalType, Strategy, StrategyContext, StrategyParameters


class BuyParameters(StrategyParameters):
    """Parameters for buy strategies.

    Attributes:
        max_position_size: Maximum position size as percentage of equity
        min_confidence: Minimum confidence level to generate signal
        max_hold_bars: Maximum bars to hold position (0 = unlimited)
        entry_threshold: Threshold for entry signal generation
    """

    max_position_size: float = Field(default=0.1, gt=0, le=1.0, description="Max position size as % of equity")
    min_confidence: float = Field(default=0.5, ge=0.0, le=1.0, description="Minimum confidence to generate signal")
    max_hold_bars: int = Field(default=0, ge=0, description="Maximum bars to hold position")
    entry_threshold: float = Field(default=0.0, description="Threshold for entry signal")

    @field_validator("max_position_size")
    @classmethod
    def validate_position_size(cls, v: float) -> float:
        """Validate position size is reasonable."""
        if v > 0.5:
            return v  # Allow but don't restrict - strategies can be aggressive
        return v


class BuyStrategy(Strategy):
    """Abstract base class for buy-side trading strategies.

    Buy strategies generate buy signals based on market conditions
    and strategy-specific logic.

    Args:
        name: Strategy name identifier
        parameters: Buy strategy parameters
        description: Strategy description
    """

    def __init__(
        self,
        name: str,
        parameters: Optional[BuyParameters] = None,
        description: str = "",
    ):
        super().__init__(name, parameters or BuyParameters(), description)
        self.parameters: BuyParameters = self.parameters  # Type hint for IDE

        # Track buy-specific state
        self._buy_signals_generated = 0
        self._positions_entered: List[Dict[str, Any]] = []

    @property
    def buy_signals_generated(self) -> int:
        """Get count of buy signals generated."""
        return self._buy_signals_generated

    def on_bar(self, data: pd.Series, context: StrategyContext) -> Optional[Signal]:
        """Process a new bar and generate buy signal if conditions met.

        Args:
            data: Current bar data (OHLCV)
            context: Strategy context with positions, equity, etc.

        Returns:
            Buy signal if conditions are met, None otherwise
        """
        # Check if we should generate a buy signal
        if not self._should_buy(data, context):
            return None

        # Calculate buy parameters
        confidence = self._calculate_buy_confidence(data, context)

        # Check minimum confidence
        if confidence < self.parameters.min_confidence:
            return None

        # Calculate position size
        quantity = self._calculate_position_size(data, context)
        if quantity <= 0:
            return None

        # Generate buy signal
        signal = Signal(
            signal_type=SignalType.BUY,
            symbol=context.symbol,
            price=self._get_entry_price(data),
            quantity=quantity,
            confidence=confidence,
            metadata={
                "strategy": self.name,
                "threshold": self.parameters.entry_threshold,
                "indicators": self._get_signal_indicators(data),
            },
        )

        self._buy_signals_generated += 1
        return signal

    def _generate_signal_impl(self, context: StrategyContext) -> Optional[Signal]:
        """Generate buy signal based on current context.

        For buy strategies, this checks if current conditions warrant a buy.

        Args:
            context: Strategy context

        Returns:
            Buy signal if conditions are met, None otherwise
        """
        # This is a simplified implementation
        # Subclasses can override for more sophisticated logic
        if not context.market_data:
            return None

        # Check if we already have a position
        if context.symbol in context.positions:
            return None

        # Generate buy signal with default values
        signal = Signal(
            signal_type=SignalType.BUY,
            symbol=context.symbol,
            confidence=self.parameters.min_confidence,
            metadata={"strategy": self.name, "generated_from": "context"},
        )

        self._buy_signals_generated += 1
        return signal

    @abstractmethod
    def _should_buy(self, data: pd.Series, context: StrategyContext) -> bool:
        """Determine if strategy should generate a buy signal.

        Subclasses must implement this method to define their buy logic.

        Args:
            data: Current bar data
            context: Strategy context

        Returns:
            True if buy conditions are met, False otherwise
        """
        pass

    @abstractmethod
    def _calculate_buy_confidence(self, data: pd.Series, context: StrategyContext) -> float:
        """Calculate confidence level for buy signal.

        Subclasses must implement this to provide confidence scoring.

        Args:
            data: Current bar data
            context: Strategy context

        Returns:
            Confidence level between 0.0 and 1.0
        """
        pass

    def _calculate_position_size(self, data: pd.Series, context: StrategyContext) -> float:
        """Calculate position size for buy order.

        Args:
            data: Current bar data
            context: Strategy context

        Returns:
            Position quantity
        """
        if context.equity <= 0:
            return 0.0

        price = data.get("close", 0)
        if price <= 0:
            return 0.0

        # Calculate position value based on max_position_size
        position_value = context.equity * self.parameters.max_position_size
        quantity = position_value / price

        return float(quantity)

    def _get_entry_price(self, data: pd.Series) -> Optional[float]:
        """Get entry price for buy order.

        Args:
            data: Current bar data

        Returns:
            Entry price or None
        """
        price = data.get("close")
        return float(price) if price is not None else None

    def _get_signal_indicators(self, data: pd.Series) -> Dict[str, Any]:
        """Get indicator values for signal metadata.

        Args:
            data: Current bar data

        Returns:
            Dictionary of indicator values
        """
        return {
            "close": data.get("close"),
            "volume": data.get("volume"),
        }

    def on_trade(self, trade: Any, context: StrategyContext) -> None:
        """Called when a trade is completed.

        Args:
            trade: Completed trade record
            context: Strategy context
        """
        # Track completed trades
        if hasattr(trade, "symbol"):
            self._positions_entered.append({
                "symbol": trade.symbol,
                "entry_time": getattr(trade, "entry_time", None),
                "exit_time": getattr(trade, "exit_time", None),
                "pnl": getattr(trade, "pnl", 0),
            })

    def get_buy_stats(self) -> Dict[str, Any]:
        """Get buy strategy statistics.

        Returns:
            Dictionary of buy statistics
        """
        return {
            "buy_signals_generated": self._buy_signals_generated,
            "positions_entered": len(self._positions_entered),
            "avg_position_pnl": (
                sum(p["pnl"] for p in self._positions_entered) / len(self._positions_entered)
                if self._positions_entered else 0.0
            ),
        }

    def reset(self) -> None:
        """Reset buy strategy state."""
        super().reset()
        self._buy_signals_generated = 0
        self._positions_entered.clear()
