"""
Predix Strategy Builder - Systematically combine factors into trading strategies.

This module:
1. Loads evaluated factors with time-series values
2. Generates systematic combinations (pairs, triplets, etc.)
3. Evaluates using walk-forward validation
4. Ranks and saves best strategies

Usage:
    predix build-strategies              # Build strategies from top factors
    predix build-strategies --top 50     # Use top 50 factors
    predix build-strategies --max-combo 3  # Allow up to 3-factor combinations
"""

import json
import os
from datetime import datetime
from itertools import combinations
from pathlib import Path
from typing import Dict, List, Optional, Tuple

import numpy as np
import pandas as pd

from rdagent.log import rdagent_logger as logger


class StrategyCombinator:
    """
    Generate systematic factor combinations.

    Types:
    - Pairs: 2-factor combinations
    - Triplets: 3-factor combinations
    - Category-based: Combine best from each category
    """

    def __init__(self, factors: List[Dict], max_combo_size: int = 2):
        """
        Parameters
        ----------
        factors : List[Dict]
            List of factor info dicts (with factor_name, ic, category, etc.)
        max_combo_size : int
            Maximum combination size (2 = pairs, 3 = triplets)
        """
        self.factors = factors
        self.max_combo_size = max_combo_size

    def generate_all(self) -> List[Dict]:
        """Generate all valid combinations up to max_combo_size."""
        combos = []

        for size in range(2, self.max_combo_size + 1):
            for combo in combinations(self.factors, size):
                # Filter: Skip if all factors are from same category
                categories = [f.get("category", "Unknown") for f in combo]
                if len(set(categories)) == 1 and len(categories) > 2:
                    continue  # Skip homogeneous combos > 2

                combos.append({
                    "factors": [f["factor_name"] for f in combo],
                    "categories": categories,
                    "size": size,
                    "avg_ic": np.mean([abs(f.get("ic", 0)) for f in combo]),
                })

        # Sort by average IC
        combos.sort(key=lambda x: x["avg_ic"], reverse=True)
        return combos

    def generate_diversified(self, target_size: int = 20) -> List[Dict]:
        """Generate diversified combinations (one from each category)."""
        # Group by category
        by_cat = {}
        for f in self.factors:
            cat = f.get("category", "Other")
            if cat not in by_cat:
                by_cat[cat] = []
            by_cat[cat].append(f)

        # Sort each category by IC
        for cat in by_cat:
            by_cat[cat].sort(key=lambda x: abs(x.get("ic", 0)), reverse=True)

        # Generate cross-category pairs
        combos = []
        cats = list(by_cat.keys())

        for i, cat1 in enumerate(cats):
            for cat2 in cats[i+1:]:
                # Take best from each category
                f1 = by_cat[cat1][0]
                f2 = by_cat[cat2][0]

                combos.append({
                    "factors": [f1["factor_name"], f2["factor_name"]],
                    "categories": [cat1, cat2],
                    "size": 2,
                    "avg_ic": np.mean([abs(f1.get("ic", 0)), abs(f2.get("ic", 0))]),
                })

        combos.sort(key=lambda x: x["avg_ic"], reverse=True)
        return combos[:target_size]


class StrategyEvaluator:
    """
    Evaluate strategy combinations using walk-forward validation.
    """

    def __init__(self, values_dir: Path, cost_bps: float = 1.5):
        """
        Parameters
        ----------
        values_dir : Path
            Directory containing factor value parquet files
        cost_bps : float
            Transaction cost in basis points
        """
        self.values_dir = values_dir
        self.cost_bps = cost_bps
        self.cost_pct = cost_bps / 10000

    def load_factor_values(self, factor_name: str) -> Optional[pd.Series]:
        """Load factor time-series values from parquet."""
        safe_name = factor_name.replace("/", "_").replace("\\", "_").replace(" ", "_")[:100]
        parquet_path = self.values_dir / f"{safe_name}.parquet"

        if not parquet_path.exists():
            return None

        try:
            series = pd.read_parquet(str(parquet_path))
            return series
        except Exception as e:
            logger.warning(f"Failed to load {factor_name}: {e}")
            return None

    def evaluate_combo(self, combo: Dict) -> Dict:
        """
        Evaluate a factor combination.

        Uses simple weighted sum signal and calculates:
        - Sharpe ratio
        - Max drawdown
        - Win rate
        - Annualized return
        """
        factor_names = combo["factors"]

        # Load all factor values
        values = {}
        for fname in factor_names:
            series = self.load_factor_values(fname)
            if series is not None:
                values[fname] = series

        if len(values) < len(factor_names):
            return {**combo, "status": "failed", "reason": "Missing factor values"}

        # Combine into DataFrame
        df = pd.DataFrame(values)

        # Align and drop NaN
        df = df.dropna()
        if len(df) < 100:
            return {**combo, "status": "failed", "reason": "Not enough valid data"}

        # Calculate combined signal (equal weight for now)
        # Normalize each factor to zero mean, unit variance
        df_norm = (df - df.mean()) / df.std()
        signal = df_norm.mean(axis=1)

        # Calculate returns (forward returns approximation)
        # Use factor values as proxy for returns
        returns = signal.diff().fillna(0)

        # Apply transaction costs
        trades = (signal.diff().abs() > 0.1).sum()  # Rough trade count
        total_cost = trades * self.cost_pct
        returns = returns - (total_cost / len(returns))

        # Calculate metrics
        total_return = returns.sum()
        ann_factor = np.sqrt(252 * 1440 / 96)  # Annualization for 1min data
        ann_return = total_return * ann_factor
        volatility = returns.std() * np.sqrt(252 * 1440 / 96)
        sharpe = ann_return / volatility if volatility > 0 else 0

        # Max drawdown
        cum = returns.cumsum()
        running_max = cum.expanding().max()
        drawdown = (cum - running_max) / running_max.replace(0, np.nan)
        max_dd = drawdown.min() if len(drawdown) > 0 else 0

        # Win rate
        win_rate = (returns > 0).sum() / len(returns) if len(returns) > 0 else 0

        return {
            **combo,
            "status": "success",
            "sharpe": float(sharpe),
            "annualized_return": float(ann_return),
            "max_drawdown": float(max_dd),
            "win_rate": float(win_rate),
            "volatility": float(volatility),
            "num_trades": int(trades),
            "calmar_ratio": float(ann_return / abs(max_dd)) if max_dd != 0 else 0,
        }


class StrategyBuilder:
    """
    Main orchestrator for building strategies from factors.
    """

    def __init__(self, results_dir: Optional[Path] = None):
        if results_dir is None:
            self.project_root = Path(__file__).parent.parent.parent.parent.parent
            self.results_dir = self.project_root / "results"
        else:
            self.results_dir = results_dir

        self.factors_dir = self.results_dir / "factors"
        self.values_dir = self.factors_dir / "values"
        self.strategies_dir = self.results_dir / "strategies"
        self.strategies_dir.mkdir(parents=True, exist_ok=True)

    def load_evaluated_factors(self, top_n: int = 50) -> List[Dict]:
        """Load top factors from evaluation results."""
        if not self.factors_dir.exists():
            return []

        factors = []
        for f in self.factors_dir.glob("*.json"):
            try:
                with open(f) as fh:
                    data = json.load(fh)
                if data.get("status") == "success" and data.get("ic") is not None:
                    factors.append(data)
            except Exception:
                logger.warning("Failed to load factor file %s", f, exc_info=True)
                continue

        # Sort by absolute IC
        factors.sort(key=lambda x: abs(x.get("ic", 0) or 0), reverse=True)
        return factors[:top_n]

    def build_strategies(
        self,
        top_n: int = 50,
        max_combo_size: int = 2,
        diversified_only: bool = False,
    ) -> List[Dict]:
        """
        Build strategies from factor combinations.

        Parameters
        ----------
        top_n : int
            Number of top factors to consider
        max_combo_size : int
            Maximum combination size
        diversified_only : bool
            If True, only generate cross-category combinations

        Returns
        -------
        List[Dict]
            List of evaluated strategies
        """
        # 1. Load factors
        factors = self.load_evaluated_factors(top_n)
        if not factors:
            logger.warning("No evaluated factors found.")
            return []

        logger.info(f"Loaded {len(factors)} top factors.")

        # 2. Generate combinations
        combinator = StrategyCombinator(factors, max_combo_size)

        if diversified_only:
            combos = combinator.generate_diversified()
        else:
            combos = combinator.generate_all()

        logger.info(f"Generated {len(combos)} combinations.")

        # 3. Evaluate combinations
        evaluator = StrategyEvaluator(self.values_dir)
        results = []

        for combo in combos:
            result = evaluator.evaluate_combo(combo)
            results.append(result)

        # 4. Rank and save
        results.sort(key=lambda x: x.get("sharpe", 0), reverse=True)

        # 5. Save strategies
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        strategies_file = self.strategies_dir / f"strategies_{timestamp}.json"

        with open(strategies_file, "w") as f:
            json.dump(results, f, indent=2, default=str)

        logger.info(f"Saved {len(results)} strategies to {strategies_file}")

        return results
