"""
NexQuant Simple Factor Evaluator - Direct IC/Sharpe computation.

Evaluates existing factor results by computing IC and Sharpe directly
from factor values and forward returns, without Qlib infrastructure.

Usage:
    python nexquant_simple_eval.py --top 100    # Evaluate top 100 factors
    python nexquant_simple_eval.py --all        # Evaluate all
    python nexquant_simple_eval.py --parallel 4 # 4 parallel workers
"""

import json
import os
import sys
import time
import warnings
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import List, Optional

import numpy as np
import pandas as pd
from rich.console import Console
from rich.progress import (
    Progress,
    SpinnerColumn,
    TextColumn,
    BarColumn,
    TaskProgressColumn,
    TimeElapsedColumn,
)
from rich.table import Table
from rich.panel import Panel

console = Console()

# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
PROJECT_ROOT = Path(__file__).resolve().parent
WORKSPACE_DIR = PROJECT_ROOT / "git_ignore_folder" / "RD-Agent_workspace"
RESULTS_DIR = PROJECT_ROOT / "results"
BACKTESTS_DIR = RESULTS_DIR / "backtests"
DB_DIR = RESULTS_DIR / "db"
DB_PATH = DB_DIR / "backtest_results.db"
EVAL_SUMMARY_PATH = RESULTS_DIR / "eval_summary.json"

# Ensure directories exist
BACKTESTS_DIR.mkdir(parents=True, exist_ok=True)
DB_DIR.mkdir(parents=True, exist_ok=True)


# ---------------------------------------------------------------------------
# Data structures
# ---------------------------------------------------------------------------
@dataclass
class FactorWorkspace:
    """Represents a factor workspace with code and results."""
    workspace_hash: str
    factor_name: str
    workspace_path: Path
    result_path: Optional[Path] = None
    data_path: Optional[Path] = None


@dataclass
class EvalResult:
    """Evaluation result for a single factor."""
    factor_name: str
    workspace_hash: str
    status: str  # success, failed
    ic: Optional[float] = None
    rank_ic: Optional[float] = None
    sharpe: Optional[float] = None
    annualized_return: Optional[float] = None
    max_drawdown: Optional[float] = None
    win_rate: Optional[float] = None
    non_null_count: int = 0
    total_count: int = 0
    error_message: Optional[str] = None
    timestamp: str = field(default_factory=lambda: datetime.now().isoformat())

    def to_dict(self) -> dict:
        return {k: v for k, v in self.__dict__.items()}


# ---------------------------------------------------------------------------
# Workspace scanner
# ---------------------------------------------------------------------------
def scan_workspaces(workspace_dir: Path) -> List[FactorWorkspace]:
    """Scan workspace directories for factors with results."""
    workspaces = []
    for ws in workspace_dir.iterdir():
        if not ws.is_dir():
            continue
        result_file = ws / "result.h5"
        data_file = ws / "intraday_pv.h5"
        if not result_file.exists() or not data_file.exists():
            continue

        # Read factor name from result.h5
        try:
            result = pd.read_hdf(str(result_file), key="data")
            if result is not None and len(result.columns) > 0:
                factor_name = result.columns[0]
                workspaces.append(FactorWorkspace(
                    workspace_hash=ws.name,
                    factor_name=factor_name,
                    workspace_path=ws,
                    result_path=result_file,
                    data_path=data_file,
                ))
        except Exception:
            continue

    return workspaces


# ---------------------------------------------------------------------------
# Factor evaluator
# ---------------------------------------------------------------------------
def evaluate_factor(ws: FactorWorkspace, forward_return_bars: int = 96) -> EvalResult:
    """
    Evaluate a factor by computing IC and Sharpe from factor values.

    Parameters
    ----------
    ws : FactorWorkspace
        Workspace with result.h5 and intraday_pv.h5
    forward_return_bars : int
        Number of bars for forward return calculation (96 = 96 minutes for 1min data)

    Returns
    -------
    EvalResult
    """
    try:
        # Load data
        df = pd.read_hdf(str(ws.data_path), key="data")
        result = pd.read_hdf(str(ws.result_path), key="data")

        total_count = len(result)
        factor_val = result.iloc[:, 0]
        non_null_count = factor_val.notna().sum()

        # Skip if too few valid values
        if non_null_count < 100:
            return EvalResult(
                factor_name=ws.factor_name,
                workspace_hash=ws.workspace_hash,
                status="failed",
                non_null_count=non_null_count,
                total_count=total_count,
                error_message=f"Too few valid values: {non_null_count}",
            )

        # Compute forward returns
        # Handle column name escaping
        col_close = "$close"
        if col_close not in df.columns:
            # Try alternative column name
            col_close = next((c for c in df.columns if "close" in c.lower()), None)
            if col_close is None:
                return EvalResult(
                    factor_name=ws.factor_name,
                    workspace_hash=ws.workspace_hash,
                    status="failed",
                    non_null_count=non_null_count,
                    total_count=total_count,
                    error_message=f"No close column found. Columns: {list(df.columns)}",
                )

        close = df[col_close]
        forward_ret = close.groupby(level="instrument").shift(-forward_return_bars) / close - 1

        # Compute IC (Information Coefficient)
        valid_idx = factor_val.dropna().index.intersection(forward_ret.dropna().index)
        if len(valid_idx) < 100:
            return EvalResult(
                factor_name=ws.factor_name,
                workspace_hash=ws.workspace_hash,
                status="failed",
                non_null_count=non_null_count,
                total_count=total_count,
                error_message=f"Too little overlap: {len(valid_idx)}",
            )

        ic = factor_val.loc[valid_idx].corr(forward_ret.loc[valid_idx])
        rank_ic = factor_val.loc[valid_idx].corr(forward_ret.loc[valid_idx], method="spearman")

        # Compute factor-level Sharpe (mean/std of factor values)
        factor_mean = factor_val.loc[valid_idx].mean()
        factor_std = factor_val.loc[valid_idx].std()
        sharpe = factor_mean / factor_std if factor_std > 0 else 0

        # Annualized return (assuming 252 trading days, 1440 minutes per day)
        ann_factor = np.sqrt(252 * 1440 / forward_return_bars)
        annualized_return = float(factor_mean * ann_factor * 100)  # in percent

        # Max drawdown approximation (cumulative factor performance)
        cum_perf = factor_val.loc[valid_idx].cumsum()
        running_max = cum_perf.expanding().max()
        drawdown = (cum_perf - running_max) / running_max.replace(0, np.nan)
        max_drawdown = float(drawdown.min()) if len(drawdown) > 0 else 0

        # Win rate (percentage of positive factor values)
        win_rate = float((factor_val.loc[valid_idx] > 0).sum()) / len(valid_idx)

        return EvalResult(
            factor_name=ws.factor_name,
            workspace_hash=ws.workspace_hash,
            status="success",
            ic=float(ic) if ic is not None else None,
            rank_ic=float(rank_ic) if rank_ic is not None else None,
            sharpe=float(sharpe),
            annualized_return=annualized_return,
            max_drawdown=max_drawdown,
            win_rate=win_rate,
            non_null_count=non_null_count,
            total_count=total_count,
        )

    except Exception as e:
        return EvalResult(
            factor_name=ws.factor_name,
            workspace_hash=ws.workspace_hash,
            status="failed",
            error_message=str(e)[:500],
        )


# ---------------------------------------------------------------------------
# Parallel evaluation
# ---------------------------------------------------------------------------
def run_evaluation(
    workspaces: List[FactorWorkspace],
    n_workers: int = 4,
) -> List[EvalResult]:
    """Run factor evaluation in parallel using threads."""
    results = []

    with Progress(
        SpinnerColumn(),
        TextColumn("[progress.description]{task.description}"),
        BarColumn(),
        TaskProgressColumn(),
        TimeElapsedColumn(),
        console=console,
    ) as progress:
        task = progress.add_task(f"Evaluating {len(workspaces)} factors...", total=len(workspaces))

        with ThreadPoolExecutor(max_workers=n_workers) as executor:
            futures = {executor.submit(evaluate_factor, ws): ws for ws in workspaces}

            for future in as_completed(futures):
                ws = futures[future]
                try:
                    result = future.result(timeout=300)
                    results.append(result)
                except Exception as e:
                    results.append(EvalResult(
                        factor_name=ws.factor_name,
                        workspace_hash=ws.workspace_hash,
                        status="failed",
                        error_message=f"Timeout/Exception: {str(e)[:300]}",
                    ))

                n_success = sum(1 for r in results if r.status == "success")
                n_fail = sum(1 for r in results if r.status == "failed")
                progress.update(
                    task,
                    advance=1,
                    description=f"Evaluating: {n_success}✅ {n_fail}❌ | {ws.factor_name[:40]}",
                )

    return results


# ---------------------------------------------------------------------------
# Results storage
# ---------------------------------------------------------------------------
def save_results(results: List[EvalResult]) -> None:
    """Save evaluation results to JSON and SQLite."""
    # Save as JSON
    successful = [r for r in results if r.status == "success"]
    failed = [r for r in results if r.status == "failed"]

    # Sort by IC
    successful.sort(key=lambda r: abs(r.ic) if r.ic is not None else 0, reverse=True)

    # Save individual results
    for r in successful[:50]:  # Top 50
        json_path = BACKTESTS_DIR / f"{r.factor_name}_{r.workspace_hash}.json"
        with open(json_path, "w", encoding="utf-8") as f:
            json.dump(r.to_dict(), f, indent=2, default=str)

    # Save summary
    valid_ic = [r.ic for r in results if r.ic is not None]
    valid_sharpe = [r.sharpe for r in results if r.sharpe is not None]

    summary = {
        "generated_at": datetime.now().isoformat(),
        "total_evaluated": len(results),
        "successful": len(successful),
        "failed": len(failed),
        "success_rate": len(successful) / len(results) if results else 0,
        "avg_ic": float(np.mean(valid_ic)) if valid_ic else 0,
        "best_ic": float(max(valid_ic, key=abs)) if valid_ic else 0,
        "avg_sharpe": float(np.mean(valid_sharpe)) if valid_sharpe else 0,
        "best_sharpe": float(max(valid_sharpe)) if valid_sharpe else 0,
        "top_20_by_ic": [r.to_dict() for r in successful[:20]],
        "all_results": [r.to_dict() for r in results],
    }

    with open(EVAL_SUMMARY_PATH, "w", encoding="utf-8") as f:
        json.dump(summary, f, indent=2, default=str)

    # Save to SQLite
    try:
        import sqlite3
        conn = sqlite3.connect(str(DB_PATH))
        c = conn.cursor()
        c.execute("""CREATE TABLE IF NOT EXISTS factor_evaluations (
            id INTEGER PRIMARY KEY,
            factor_name TEXT,
            workspace_hash TEXT,
            ic REAL,
            rank_ic REAL,
            sharpe REAL,
            annualized_return REAL,
            max_drawdown REAL,
            win_rate REAL,
            non_null_count INTEGER,
            total_count INTEGER,
            status TEXT,
            timestamp TEXT
        )""")

        for r in results:
            c.execute("""INSERT INTO factor_evaluations
                (factor_name, workspace_hash, ic, rank_ic, sharpe,
                 annualized_return, max_drawdown, win_rate,
                 non_null_count, total_count, status, timestamp)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
                (r.factor_name, r.workspace_hash, r.ic, r.rank_ic, r.sharpe,
                 r.annualized_return, r.max_drawdown, r.win_rate,
                 r.non_null_count, r.total_count, r.status, r.timestamp))

        conn.commit()
        conn.close()
    except Exception as e:
        console.print(f"[yellow]SQLite save warning: {e}[/yellow]")


# ---------------------------------------------------------------------------
# Display
# ---------------------------------------------------------------------------
def display_results(results: List[EvalResult]) -> None:
    """Display evaluation results as a table."""
    successful = [r for r in results if r.status == "success"]
    successful.sort(key=lambda r: abs(r.ic) if r.ic is not None else 0, reverse=True)

    table = Table(
        title="Factor Evaluation Results",
        show_header=True,
        header_style="bold cyan",
    )
    table.add_column("#", justify="center", width=4)
    table.add_column("Factor", width=40)
    table.add_column("IC", justify="right", width=10)
    table.add_column("Rank IC", justify="right", width=10)
    table.add_column("Sharpe", justify="right", width=10)
    table.add_column("Ann. Ret %", justify="right", width=10)
    table.add_column("Max DD", justify="right", width=10)
    table.add_column("Win Rate", justify="right", width=10)

    for i, r in enumerate(successful[:20], 1):
        table.add_row(
            str(i),
            r.factor_name[:38],
            f"{r.ic:.6f}" if r.ic is not None else "N/A",
            f"{r.rank_ic:.6f}" if r.rank_ic is not None else "N/A",
            f"{r.sharpe:.4f}" if r.sharpe is not None else "N/A",
            f"{r.annualized_return:.4f}" if r.annualized_return is not None else "N/A",
            f"{r.max_drawdown:.4f}" if r.max_drawdown is not None else "N/A",
            f"{r.win_rate:.2%}" if r.win_rate is not None else "N/A",
        )

    console.print()
    console.print(table)

    # Summary
    valid_ic = [r.ic for r in results if r.ic is not None]
    valid_sharpe = [r.sharpe for r in results if r.sharpe is not None]

    console.print(Panel(
        f"[bold]Evaluation Summary[/bold]\n"
        f"Total evaluated: {len(results)}\n"
        f"Successful: {len(successful)} ✅\n"
        f"Failed: {len(results) - len(successful)} ❌\n"
        f"Avg IC: {np.mean(valid_ic):.6f} (n={len(valid_ic)})\n"
        f"Best IC: {max(valid_ic, key=abs, default=0):.6f}\n"
        f"Avg Sharpe: {np.mean(valid_sharpe):.4f} (n={len(valid_sharpe)})\n"
        f"Best Sharpe: {max(valid_sharpe, default=0):.4f}\n"
        f"Saved to: {EVAL_SUMMARY_PATH}\n"
        f"Database: {DB_PATH}",
        border_style="green",
    ))


# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def main(
    top: int = 100,
    all_factors: bool = False,
    parallel: int = 4,
) -> None:
    """Main entry point."""
    console.print(Panel(
        "[bold cyan]NexQuant Simple Factor Evaluator[/bold cyan]\n"
        f"Scanning workspaces for generated factors...",
        border_style="cyan",
    ))

    # Scan workspaces
    workspaces = scan_workspaces(WORKSPACE_DIR)
    console.print(f"\n[bold]Total workspaces with results: {len(workspaces)}[/bold]")

    if not workspaces:
        console.print("[red]No factors found![/red]")
        return

    # Select factors to evaluate
    if all_factors:
        to_evaluate = workspaces
    else:
        # Deduplicate by factor name, keep first occurrence
        seen = set()
        unique = []
        for ws in workspaces:
            if ws.factor_name not in seen:
                seen.add(ws.factor_name)
                unique.append(ws)

        # Sort by non-null count (prefer factors with more valid values)
        to_evaluate = sorted(unique, key=lambda ws: 0, reverse=True)[:top]

    console.print(f"[bold green]Selected {len(to_evaluate)} factors for evaluation[/bold green]")
    console.print(f"  Using {parallel} parallel workers")

    # Run evaluation
    results = run_evaluation(to_evaluate, n_workers=parallel)

    # Save results
    console.print(f"\n[bold cyan]Saving results...[/bold cyan]")
    save_results(results)

    # Display
    display_results(results)


if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(
        description="NexQuant Simple Factor Evaluator - Direct IC/Sharpe computation"
    )
    parser.add_argument(
        "--top", "-n",
        type=int,
        default=100,
        help="Number of factors to evaluate (default: 100)",
    )
    parser.add_argument(
        "--all", "-a",
        action="store_true",
        help="Evaluate all discovered factors",
    )
    parser.add_argument(
        "--parallel", "-p",
        type=int,
        default=4,
        help="Number of parallel workers (default: 4)",
    )

    args = parser.parse_args()

    main(
        top=args.top,
        all_factors=args.all,
        parallel=args.parallel,
    )
