"""
Factor base classes for FactorDB

This module defines the base classes for factor management, including:
- BaseFactor: Abstract base class for all factors
- MinedFactor: Factor wrapper for mining framework results
"""

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Optional, Dict, Any, List
from datetime import datetime
import pandas as pd
import numpy as np

from .config import FactorDBConfig, AssetType, FactorType
from .expression import ExpressionCalculator, ExpressionNormalizer


@dataclass
class FactorMetadata:
    """Metadata for a factor"""
    factor_id: str
    name: Optional[str] = None
    expression: Optional[str] = None
    explanation: Optional[str] = None
    other_info: Optional[Dict[str, Any]] = None
    register_datetime: datetime = field(default_factory=datetime.now)

    # Factor type information
    asset_type: AssetType = AssetType.STOCK
    factor_type: FactorType = FactorType.DAILY

    # Source framework
    source_framework: Optional[str] = None

    def to_dict(self) -> Dict[str, Any]:
        """Convert metadata to dictionary for database storage"""
        return {
            "factor_id": self.factor_id,
            "name": self.name,
            "expression": self.expression,
            "explanation": self.explanation,
            "other_info": str(self.other_info) if self.other_info else None,
            "register_datetime": self.register_datetime,
        }


class BaseFactor(ABC):
    """
    Abstract base class for all factors.

    Users can inherit from this class to implement custom factor calculation logic.
    """

    def __init__(
        self,
        asset_type: AssetType = AssetType.STOCK,
        factor_type: FactorType = FactorType.DAILY,
        config: Optional[FactorDBConfig] = None
    ):
        self.asset_type = asset_type
        self.factor_type = factor_type
        self.config = config or FactorDBConfig()
        self._factor_id: Optional[str] = None
        self._metadata: Optional[FactorMetadata] = None
        self._calculator = ExpressionCalculator()

    @property
    def factor_id(self) -> Optional[str]:
        """Get the factor ID (assigned after registration)"""
        return self._factor_id

    @factor_id.setter
    def factor_id(self, value: str):
        """Set the factor ID"""
        self._factor_id = value

    @property
    def metadata(self) -> Optional[FactorMetadata]:
        """Get factor metadata"""
        return self._metadata

    @abstractmethod
    def get_name(self) -> Optional[str]:
        """Return the factor name"""
        pass

    @abstractmethod
    def get_expression(self) -> Optional[str]:
        """Return the factor expression (if applicable)"""
        pass

    @abstractmethod
    def get_explanation(self) -> Optional[str]:
        """Return the factor explanation"""
        pass

    @abstractmethod
    def calculate(self, data: pd.DataFrame) -> pd.Series:
        """
        Calculate factor values from market data.

        Args:
            data: DataFrame with market data (must have 'code' and 'date' columns)

        Returns:
            Series of factor values with MultiIndex (code, date)
        """
        pass

    def create_metadata(self, factor_id: str) -> FactorMetadata:
        """Create metadata for this factor"""
        self._factor_id = factor_id
        self._metadata = FactorMetadata(
            factor_id=factor_id,
            name=self.get_name(),
            expression=self.get_expression(),
            explanation=self.get_explanation(),
            asset_type=self.asset_type,
            factor_type=self.factor_type,
        )
        return self._metadata

    def validate(self, data: pd.DataFrame) -> bool:
        """
        Validate that the factor can be calculated with the given data.

        Args:
            data: DataFrame with market data

        Returns:
            True if factor can be calculated
        """
        required_cols = ['code', 'date']
        missing = [col for col in required_cols if col not in data.columns]
        if missing:
            raise ValueError(f"Missing required columns: {missing}")
        return True


class ExpressionFactor(BaseFactor):
    """
    Factor defined by an expression string.

    This class calculates factor values by evaluating an expression on market data.
    """

    def __init__(
        self,
        expression: str,
        name: Optional[str] = None,
        explanation: Optional[str] = None,
        source_framework: str = "auto",
        asset_type: AssetType = AssetType.STOCK,
        factor_type: FactorType = FactorType.DAILY,
        config: Optional[FactorDBConfig] = None
    ):
        super().__init__(asset_type, factor_type, config)
        self._expression = expression
        self._name = name
        self._explanation = explanation
        self._source_framework = source_framework
        self._normalizer = ExpressionNormalizer()

        # Normalize expression
        self._normalized_expression = self._normalizer.normalize_expression(
            expression, source_framework
        )

    @property
    def expression(self) -> str:
        """Get the original expression"""
        return self._expression

    @property
    def normalized_expression(self) -> str:
        """Get the normalized expression"""
        return self._normalized_expression

    def get_name(self) -> Optional[str]:
        return self._name

    def get_expression(self) -> Optional[str]:
        return self._normalized_expression

    def get_explanation(self) -> Optional[str]:
        return self._explanation

    def calculate(self, data: pd.DataFrame) -> pd.Series:
        """Calculate factor values from expression"""
        self.validate(data)
        return self._calculator.calculate(
            self._normalized_expression,
            data,
            normalize=False,  # Already normalized
            source_framework=self._source_framework
        )


class MinedFactor(ExpressionFactor):
    """
    Factor wrapper for mining framework results.

    This class wraps factor expressions from different mining frameworks
    (gpfactor, masfactorMiner) and provides a unified interface.
    """

    def __init__(
        self,
        expression: str,
        source_framework: str,
        name: Optional[str] = None,
        explanation: Optional[str] = None,
        metrics: Optional[Dict[str, Any]] = None,
        original_data: Optional[Dict[str, Any]] = None,
        asset_type: AssetType = AssetType.STOCK,
        factor_type: FactorType = FactorType.DAILY,
        config: Optional[FactorDBConfig] = None
    ):
        super().__init__(
            expression=expression,
            name=name,
            explanation=explanation,
            source_framework=source_framework,
            asset_type=asset_type,
            factor_type=factor_type,
            config=config
        )
        self._metrics = metrics or {}
        self._original_data = original_data or {}

    @property
    def metrics(self) -> Dict[str, Any]:
        """Get the metrics from mining results"""
        return self._metrics

    @property
    def original_data(self) -> Dict[str, Any]:
        """Get the original data from mining results"""
        return self._original_data

    def get_metric(self, key: str, default: Any = None) -> Any:
        """Get a specific metric value"""
        return self._metrics.get(key, default)

    def create_metadata(self, factor_id: str) -> FactorMetadata:
        """Create metadata including mining metrics as other_info"""
        metadata = super().create_metadata(factor_id)
        metadata.source_framework = self._source_framework
        metadata.other_info = {
            "source_framework": self._source_framework,
            "original_metrics": self._metrics,
        }
        return metadata

    @classmethod
    def from_gpfactor(
        cls,
        data: Dict[str, Any],
        asset_type: AssetType = AssetType.STOCK,
        factor_type: FactorType = FactorType.DAILY
    ) -> "MinedFactor":
        """
        Create MinedFactor from gpfactor mining result.

        Expected data format:
        {
            "expression": "sub(close)(low)",
            "fitness": 0.096,
            "ic_mean": -0.142,
            "ic_ir": -0.612,
            "rank_ic_mean": -0.098,
            ...
        }
        """
        expression = data.get("expression", "")
        metrics = {k: v for k, v in data.items() if k != "expression"}

        return cls(
            expression=expression,
            source_framework="gpfactor",
            name=None,
            explanation=None,
            metrics=metrics,
            original_data=data,
            asset_type=asset_type,
            factor_type=factor_type
        )

    @classmethod
    def from_masfactor_miner(
        cls,
        data: Dict[str, Any],
        asset_type: AssetType = AssetType.STOCK,
        factor_type: FactorType = FactorType.DAILY
    ) -> "MinedFactor":
        """
        Create MinedFactor from masfactorMiner mining result.

        Expected data format:
        {
            "name": "turnover_volatility_reverse",
            "expression": "mul(-1, cs_rank(...))",
            "logic": "手率波动率...",
            "score": 76.58,
            "direction": 1,
            "metrics": {...},
            ...
        }
        """
        expression = data.get("expression", "")
        name = data.get("name")
        explanation = data.get("logic")

        # Extract metrics (may be nested under different period keys)
        raw_metrics = data.get("metrics", {})
        metrics = {
            "score": data.get("score"),
            "direction": data.get("direction"),
            "is_valid": data.get("is_valid"),
        }

        # Flatten period-based metrics
        for period_key, period_metrics in raw_metrics.items():
            if isinstance(period_metrics, dict):
                for metric_key, metric_value in period_metrics.items():
                    if metric_key != "ic_series":  # Skip large arrays
                        metrics[f"{period_key}_{metric_key}"] = metric_value

        return cls(
            expression=expression,
            source_framework="masfactorMiner",
            name=name,
            explanation=explanation,
            metrics=metrics,
            original_data=data,
            asset_type=asset_type,
            factor_type=factor_type
        )

    @classmethod
    def from_mas2(
        cls,
        data: Dict[str, Any],
        asset_type: AssetType = AssetType.STOCK,
        factor_type: FactorType = FactorType.DAILY
    ) -> "MinedFactor":
        """Backward-compatible alias for deprecated mas2 framework."""
        return cls.from_masfactor_miner(data, asset_type=asset_type, factor_type=factor_type)


class CustomFactor(BaseFactor):
    """
    Base class for user-defined custom factors.

    Users should inherit from this class and implement the _compute method
    to define custom factor calculation logic.

    Example:
        class MyFactor(CustomFactor):
            def __init__(self, window: int = 20):
                super().__init__()
                self.window = window
                self._name = f"my_factor_{window}"
                self._explanation = "Custom momentum factor"

            def get_name(self):
                return self._name

            def get_expression(self):
                return None  # No expression for custom factors

            def get_explanation(self):
                return self._explanation

            def _compute(self, group_data: pd.DataFrame) -> pd.Series:
                return group_data['close'].pct_change(self.window)
    """

    def __init__(
        self,
        asset_type: AssetType = AssetType.STOCK,
        factor_type: FactorType = FactorType.DAILY,
        config: Optional[FactorDBConfig] = None
    ):
        super().__init__(asset_type, factor_type, config)
        self._name: Optional[str] = None
        self._explanation: Optional[str] = None

    def get_name(self) -> Optional[str]:
        return self._name

    def get_expression(self) -> Optional[str]:
        return None  # Custom factors typically don't have expressions

    def get_explanation(self) -> Optional[str]:
        return self._explanation

    @abstractmethod
    def _compute(self, group_data: pd.DataFrame) -> pd.Series:
        """
        Compute factor values for a single code group.

        Args:
            group_data: DataFrame for a single code, sorted by date

        Returns:
            Series of factor values
        """
        pass

    def calculate(self, data: pd.DataFrame) -> pd.Series:
        """Calculate factor values by applying _compute to each code group"""
        self.validate(data)

        # Sort by code and date
        data = data.sort_values(['code', 'date']).copy()

        results = []
        for code, group in data.groupby('code'):
            group_result = self._compute(group)
            if group_result is not None and len(group_result) > 0:
                if isinstance(group_result, pd.Series):
                    group_result = group_result.values
                result_series = pd.Series(
                    group_result,
                    index=pd.MultiIndex.from_arrays(
                        [group['code'].values, group['date'].values],
                        names=['code', 'date']
                    )
                )
                results.append(result_series)

        if not results:
            raise ValueError("No valid results from factor calculation")

        return pd.concat(results).astype(np.float32)
