"""Strategy base class for OpenClaw Trading.

This module provides the abstract Strategy base class that defines the interface
for all trading strategies in the system.
"""

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any, Optional

import pandas as pd
from pydantic import BaseModel

from openclaw.utils.logging import get_logger


class SignalType(str, Enum):
    """Types of trading signals."""

    BUY = "buy"
    SELL = "sell"
    HOLD = "hold"
    SELECT = "select"
    SKIP = "skip"


@dataclass
class Signal:
    """Trading signal generated by a strategy.

    Attributes:
        signal_type: Type of signal (buy, sell, hold, etc.)
        symbol: Trading symbol
        timestamp: Signal generation timestamp
        price: Suggested price for execution
        quantity: Suggested quantity
        confidence: Confidence level (0.0 to 1.0)
        metadata: Additional signal metadata
    """

    signal_type: SignalType
    symbol: str
    timestamp: datetime = field(default_factory=datetime.now)
    price: float | None = None
    quantity: float | None = None
    confidence: float = 0.5
    metadata: dict[str, Any] = field(default_factory=dict)

    def __post_init__(self) -> None:
        """Validate signal parameters."""
        if not 0.0 <= self.confidence <= 1.0:
            raise ValueError("Confidence must be between 0.0 and 1.0")


@dataclass
class StrategyContext:
    """Context passed to strategy callbacks.

    Attributes:
        symbol: Current trading symbol
        equity: Current equity value
        positions: Current positions dictionary
        trades: List of completed trades
        equity_curve: Historical equity values
        bar_index: Current bar index
        market_data: Additional market data
        custom_data: Strategy-specific custom data
    """

    symbol: str = ""
    equity: float = 0.0
    positions: dict[str, Any] = field(default_factory=dict)
    trades: list[Any] = field(default_factory=list)
    equity_curve: list[float] = field(default_factory=list)
    bar_index: int = 0
    market_data: dict[str, Any] = field(default_factory=dict)
    custom_data: dict[str, Any] = field(default_factory=dict)


class StrategyParameters(BaseModel):
    """Base class for strategy parameters.

    All strategy parameters should inherit from this class
    to ensure consistent parameter handling and validation.
    """

    class Config:
        """Pydantic config."""

        extra = "forbid"
        validate_assignment = True


class Strategy(ABC):
    """Abstract base class for all trading strategies.

    This class defines the interface that all trading strategies must implement.
    Strategies receive market data through callbacks and generate trading signals.

    Args:
        name: Strategy name identifier
        parameters: Strategy parameters object
        description: Strategy description
    """

    def __init__(
        self,
        name: str,
        parameters: Optional[StrategyParameters] = None,
        description: str = "",
    ):
        self.name = name
        self.parameters = parameters or StrategyParameters()
        self.description = description
        self.logger = get_logger(f"strategy.{name}")

        # State tracking
        self._initialized = False
        self._active = False
        self._signals_generated = 0
        self._start_time: Optional[datetime] = None

        # Strategy data storage
        self._data: dict[str, Any] = {}

    @property
    def is_initialized(self) -> bool:
        """Check if strategy has been initialized."""
        return self._initialized

    @property
    def is_active(self) -> bool:
        """Check if strategy is currently active."""
        return self._active

    @property
    def signals_generated(self) -> int:
        """Get count of signals generated."""
        return self._signals_generated

    def initialize(self) -> None:
        """Initialize the strategy.

        This method should be called before using the strategy.
        It sets up internal state and calls on_init for subclass setup.
        """
        if self._initialized:
            self.logger.warning(f"Strategy {self.name} already initialized")
            return

        self.logger.info(f"Initializing strategy: {self.name}")
        self._start_time = datetime.now()

        # Call subclass initialization
        self.on_init()

        self._initialized = True
        self._active = True
        self.logger.info(f"Strategy {self.name} initialized successfully")

    def shutdown(self) -> None:
        """Shutdown the strategy.

        This method should be called when the strategy is no longer needed.
        It calls on_exit for subclass cleanup.
        """
        if not self._active:
            return

        self.logger.info(f"Shutting down strategy: {self.name}")

        # Call subclass cleanup
        self.on_exit()

        self._active = False

        duration = ""
        if self._start_time:
            duration = f" (runtime: {datetime.now() - self._start_time})"
        self.logger.info(f"Strategy {self.name} shutdown complete{duration}")

    def process_bar(self, data: pd.Series, context: StrategyContext) -> Optional[Signal]:
        """Process a new bar of market data.

        This is the main entry point for strategy execution. It validates
        state, calls on_bar for processing, and tracks signal generation.

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

        Returns:
            Signal object if strategy generates a signal, None otherwise
        """
        if not self._initialized:
            raise RuntimeError(f"Strategy {self.name} not initialized. Call initialize() first.")

        if not self._active:
            self.logger.warning(f"Strategy {self.name} is not active")
            return None

        # Call subclass implementation
        signal = self.on_bar(data, context)

        if signal:
            self._signals_generated += 1
            self.on_signal_generated(signal)

        return signal

    def process_trade(self, trade: Any, context: StrategyContext) -> None:
        """Process a completed trade.

        Args:
            trade: Completed trade record
            context: Strategy context
        """
        if not self._initialized:
            raise RuntimeError(f"Strategy {self.name} not initialized")

        # Call subclass implementation
        self.on_trade(trade, context)

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

        This method can be called independently to get a signal without
        processing a specific bar of data.

        Args:
            context: Strategy context

        Returns:
            Signal object if strategy generates a signal, None otherwise
        """
        if not self._initialized:
            raise RuntimeError(f"Strategy {self.name} not initialized")

        signal = self._generate_signal_impl(context)

        if signal:
            self._signals_generated += 1

        return signal

    # Callback methods for subclasses to override

    def on_init(self) -> None:
        """Called when strategy is initialized.

        Subclasses can override this to perform setup tasks like:
        - Loading historical data
        - Initializing indicators
        - Setting up internal state
        """
        pass

    def on_exit(self) -> None:
        """Called when strategy is shutting down.

        Subclasses can override this to perform cleanup tasks like:
        - Saving state
        - Closing connections
        - Releasing resources
        """
        pass

    @abstractmethod
    def on_bar(self, data: pd.Series, context: StrategyContext) -> Optional[Signal]:
        """Process a new bar of market data.

        This is the main strategy logic method that subclasses must implement.
        It is called for each bar of market data.

        Args:
            data: Current bar data (OHLCV) with keys like 'open', 'high', 'low', 'close', 'volume'
            context: Strategy context with positions, equity, trades, etc.

        Returns:
            Signal object if strategy generates a signal, None otherwise
        """
        pass

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

        Subclasses can override this to react to trade completions,
        such as updating internal state or risk metrics.

        Args:
            trade: Completed trade record
            context: Strategy context
        """
        pass

    def on_signal_generated(self, signal: Signal) -> None:
        """Called when a signal is generated.

        Subclasses can override this to react to signal generation,
        such as logging or additional validation.

        Args:
            signal: The generated signal
        """
        self.logger.debug(f"Signal generated: {signal.signal_type.value} for {signal.symbol}")

    @abstractmethod
    def _generate_signal_impl(self, context: StrategyContext) -> Optional[Signal]:
        """Implementation of signal generation.

        Subclasses must implement this method to provide signal generation logic.

        Args:
            context: Strategy context

        Returns:
            Signal object if strategy generates a signal, None otherwise
        """
        pass

    def get_state(self) -> dict[str, Any]:
        """Get current strategy state.

        Returns:
            Dictionary containing strategy state
        """
        return {
            "name": self.name,
            "description": self.description,
            "initialized": self._initialized,
            "active": self._active,
            "signals_generated": self._signals_generated,
            "start_time": self._start_time.isoformat() if self._start_time else None,
        }

    def reset(self) -> None:
        """Reset strategy state.

        This method resets the strategy to its initial state.
        Subclasses should call super().reset() and then reset their own state.
        """
        self._signals_generated = 0
        self._data.clear()
        self.logger.info(f"Strategy {self.name} state reset")

    def __repr__(self) -> str:
        """String representation of the strategy."""
        return (
            f"{self.__class__.__name__}("
            f"name='{self.name}', "
            f"initialized={self._initialized}, "
            f"active={self._active}"
            f")"
        )

    def __enter__(self) -> "Strategy":
        """Context manager entry."""
        self.initialize()
        return self

    def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        """Context manager exit."""
        self.shutdown()
