"""Select strategy base class for OpenClaw Trading.

This module provides the SelectStrategy abstract class that defines the interface
for stock/asset selection strategies.
"""

from abc import abstractmethod
from dataclasses import dataclass, field
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


@dataclass
class SelectResult:
    """Result of a selection operation.

    Attributes:
        symbol: Selected symbol
        score: Selection score (higher is better)
        selected: Whether this symbol passed selection
        rank: Rank among all candidates (1 = best)
        metadata: Additional selection metadata
    """

    symbol: str
    score: float = 0.0
    selected: bool = False
    rank: int = 0
    metadata: Dict[str, Any] = field(default_factory=dict)

    def __post_init__(self) -> None:
        """Validate selection result."""
        if not self.symbol:
            raise ValueError("Symbol cannot be empty")


class SelectParameters(StrategyParameters):
    """Parameters for select strategies.

    Attributes:
        max_selections: Maximum number of symbols to select
        min_score: Minimum score for selection
        top_n: Select top N symbols by score
        filter_volume: Minimum average volume filter
        filter_price: Minimum price filter
    """

    max_selections: int = Field(default=10, ge=1, description="Maximum symbols to select")
    min_score: float = Field(default=0.0, description="Minimum score for selection")
    top_n: Optional[int] = Field(default=None, ge=1, description="Select top N by score")
    filter_volume: Optional[float] = Field(default=None, ge=0, description="Minimum average volume")
    filter_price: Optional[float] = Field(default=None, ge=0, description="Minimum price")

    @field_validator("max_selections")
    @classmethod
    def validate_max_selections(cls, v: int) -> int:
        """Validate max selections is reasonable."""
        if v > 1000:
            return 1000  # Cap at 1000
        return v


class SelectStrategy(Strategy):
    """Abstract base class for stock/asset selection strategies.

    Selection strategies filter and rank symbols based on criteria
    to identify the best trading candidates.

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

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

        # Track select-specific state
        self._selections_made = 0
        self._selection_history: List[Dict[str, Any]] = []
        self._current_universe: List[str] = []

    @property
    def selections_made(self) -> int:
        """Get count of selections made."""
        return self._selections_made

    def on_bar(self, data: pd.Series, context: StrategyContext) -> Optional[Signal]:
        """Process a new bar - not typically used for selection strategies.

        Selection strategies usually work on the entire universe at once
        via the select() method. This method returns None by default.

        Args:
            data: Current bar data (OHLCV)
            context: Strategy context

        Returns:
            None (selection strategies don't generate bar-based signals)
        """
        # Selection strategies typically don't generate signals on individual bars
        # They work on the entire universe via select()
        return None

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

        For select strategies, this can signal selection availability.

        Args:
            context: Strategy context

        Returns:
            Select signal if selection is active, None otherwise
        """
        if not self._current_universe:
            return None

        return Signal(
            signal_type=SignalType.SELECT,
            symbol=context.symbol or "universe",
            confidence=1.0,
            metadata={
                "strategy": self.name,
                "universe_size": len(self._current_universe),
            },
        )

    @abstractmethod
    def calculate_score(self, symbol: str, data: pd.DataFrame) -> float:
        """Calculate selection score for a symbol.

        Subclasses must implement this method to define their scoring logic.
        Higher scores indicate better candidates.

        Args:
            symbol: Trading symbol
            data: Historical data for the symbol

        Returns:
            Selection score (higher is better)
        """
        pass

    def select(self, universe: Dict[str, pd.DataFrame]) -> List[SelectResult]:
        """Select symbols from the given universe.

        This is the main method for selection strategies. It scores all symbols
        in the universe and returns the selected ones.

        Args:
            universe: Dictionary mapping symbols to their historical data

        Returns:
            List of selection results
        """
        self._current_universe = list(universe.keys())
        results: List[SelectResult] = []

        # Score all symbols
        for symbol, data in universe.items():
            try:
                # Apply filters first
                if not self._passes_filters(symbol, data):
                    results.append(SelectResult(symbol=symbol, selected=False, score=0.0))
                    continue

                # Calculate score
                score = self.calculate_score(symbol, data)

                results.append(SelectResult(
                    symbol=symbol,
                    score=score,
                    selected=score >= self.parameters.min_score,
                    metadata={"data_points": len(data)},
                ))
            except Exception as e:
                self.logger.warning(f"Error scoring {symbol}: {e}")
                results.append(SelectResult(symbol=symbol, selected=False, score=0.0))

        # Sort by score (descending)
        results.sort(key=lambda x: x.score, reverse=True)

        # Assign ranks
        for i, result in enumerate(results, 1):
            result.rank = i

        # Apply selection limits
        selected_count = 0
        for result in results:
            if result.selected:
                if selected_count >= self.parameters.max_selections:
                    result.selected = False
                else:
                    selected_count += 1

        # Apply top_n filter if specified
        if self.parameters.top_n is not None:
            for i, result in enumerate(results):
                result.selected = i < self.parameters.top_n and result.score >= self.parameters.min_score

        # Record selection
        self._record_selection(results)
        self._selections_made += 1

        selected = [r for r in results if r.selected]
        self.logger.info(f"Selected {len(selected)} symbols from {len(results)} candidates")

        return results

    def _passes_filters(self, symbol: str, data: pd.DataFrame) -> bool:
        """Check if symbol passes basic filters.

        Args:
            symbol: Trading symbol
            data: Historical data

        Returns:
            True if symbol passes all filters
        """
        if data.empty:
            return False

        # Volume filter
        if self.parameters.filter_volume is not None:
            if "volume" in data.columns:
                avg_volume = data["volume"].mean()
                if avg_volume < self.parameters.filter_volume:
                    return False

        # Price filter
        if self.parameters.filter_price is not None:
            if "close" in data.columns:
                last_price = data["close"].iloc[-1]
                if last_price < self.parameters.filter_price:
                    return False

        return True

    def _record_selection(self, results: List[SelectResult]) -> None:
        """Record selection results to history.

        Args:
            results: List of selection results
        """
        selected = [r for r in results if r.selected]
        self._selection_history.append({
            "timestamp": pd.Timestamp.now(),
            "total_candidates": len(results),
            "selected_count": len(selected),
            "selected_symbols": [r.symbol for r in selected],
            "avg_score": sum(r.score for r in selected) / len(selected) if selected else 0.0,
        })

    def get_top_selections(self, results: List[SelectResult], n: Optional[int] = None) -> List[SelectResult]:
        """Get top N selections from results.

        Args:
            results: List of selection results
            n: Number of top selections to return (default: max_selections)

        Returns:
            List of top selection results
        """
        n = n or self.parameters.max_selections
        selected = [r for r in results if r.selected]
        return selected[:n]

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

        Returns:
            Dictionary of selection statistics
        """
        if not self._selection_history:
            return {
                "selections_made": 0,
                "avg_candidates": 0.0,
                "avg_selected": 0.0,
            }

        return {
            "selections_made": self._selections_made,
            "avg_candidates": sum(h["total_candidates"] for h in self._selection_history) / len(self._selection_history),
            "avg_selected": sum(h["selected_count"] for h in self._selection_history) / len(self._selection_history),
            "last_selection": self._selection_history[-1] if self._selection_history else None,
        }

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

        Selection strategies typically don't track individual trades,
        but this method is available if needed.

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

    def reset(self) -> None:
        """Reset select strategy state."""
        super().reset()
        self._selections_made = 0
        self._selection_history.clear()
        self._current_universe.clear()
