"""
Type definitions for jbqlab.

This module contains all dataclasses, type aliases, and protocol definitions
used throughout the package. Keeping types centralized ensures consistency
and makes the public API clearer.
"""

from collections.abc import Callable
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Protocol

import pandas as pd

# =============================================================================
# Type Aliases
# =============================================================================

# A strategy function takes a DataFrame with 'close' column and returns signals
# Signals: 1 = long, 0 = flat (no position)
StrategyFunc = Callable[..., pd.Series]


# =============================================================================
# Dataclasses
# =============================================================================


@dataclass(frozen=True)
class StrategyParam:
    """Description of a single strategy parameter."""

    name: str
    description: str
    param_type: type
    default: Any
    required: bool = False


@dataclass(frozen=True)
class StrategyInfo:
    """Metadata about a registered strategy."""

    name: str
    description: str
    params: tuple[StrategyParam, ...]

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for JSON serialization."""
        return {
            "name": self.name,
            "description": self.description,
            "params": [
                {
                    "name": p.name,
                    "description": p.description,
                    "type": p.param_type.__name__,
                    "default": p.default,
                    "required": p.required,
                }
                for p in self.params
            ],
        }


@dataclass
class BacktestConfig:
    """Configuration for a backtest run.

    Attributes:
        strategy: Name of the strategy to use.
        strategy_params: Parameters to pass to the strategy function.
        transaction_cost: Proportional transaction cost (e.g., 0.001 = 0.1%).
        slippage: Proportional slippage cost (e.g., 0.0005 = 0.05%).
        initial_capital: Starting capital (used for reporting, not position sizing).
        risk_free_rate: Annual risk-free rate for Sharpe calculation (default 0).
    """

    strategy: str
    strategy_params: dict[str, Any] = field(default_factory=dict)
    transaction_cost: float = 0.001  # 0.1% per trade (10 bps)
    slippage: float = 0.0005  # 0.05% slippage (5 bps)
    initial_capital: float = 100_000.0
    risk_free_rate: float = 0.0

    def __post_init__(self) -> None:
        """Validate configuration values."""
        if self.transaction_cost < 0:
            raise ValueError("transaction_cost must be non-negative")
        if self.slippage < 0:
            raise ValueError("slippage must be non-negative")
        if self.initial_capital <= 0:
            raise ValueError("initial_capital must be positive")


@dataclass
class BacktestResult:
    """Results from a backtest run.

    Attributes:
        equity_curve: Series of portfolio values indexed by date.
        returns: Series of daily returns (after costs).
        positions: Series of position signals (1 = long, 0 = flat).
        metrics: Dictionary of computed performance metrics.
        metadata: Additional information about the backtest run.
    """

    equity_curve: pd.Series
    returns: pd.Series
    positions: pd.Series
    metrics: dict[str, float]
    metadata: dict[str, Any]

    def to_dataframes(self) -> dict[str, pd.DataFrame]:
        """Convert results to DataFrames for export.

        Returns:
            Dictionary with 'equity_curve' and 'positions' DataFrames.
        """
        return {
            "equity_curve": pd.DataFrame(
                {"date": self.equity_curve.index, "equity": self.equity_curve.values}
            ),
            "positions": pd.DataFrame(
                {"date": self.positions.index, "position": self.positions.values}
            ),
        }


@dataclass
class ValidationResult:
    """Result of data validation."""

    is_valid: bool
    errors: list[str]
    warnings: list[str]
    row_count: int
    date_range: tuple[datetime, datetime] | None

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for JSON serialization."""
        return {
            "is_valid": self.is_valid,
            "errors": self.errors,
            "warnings": self.warnings,
            "row_count": self.row_count,
            "date_range": (
                [self.date_range[0].isoformat(), self.date_range[1].isoformat()]
                if self.date_range
                else None
            ),
        }


# =============================================================================
# Protocols (for type checking extensibility)
# =============================================================================


class StrategyProtocol(Protocol):
    """Protocol that all strategy functions must implement."""

    def __call__(self, df: pd.DataFrame, **kwargs: Any) -> pd.Series:
        """Generate trading signals from price data.

        Args:
            df: DataFrame with at least 'date' and 'close' columns.
            **kwargs: Strategy-specific parameters.

        Returns:
            Series of signals indexed by date. Values: 1 = long, 0 = flat.
        """
        ...
