#!/usr/bin/env python
"""
Extract Results from Qlib Workspace to ResultsDatabase

This script parses existing Qlib workspace results (from Docker backtests)
and imports them into the ResultsDatabase for querying and dashboard display.

It can:
1. Parse qlib_res.csv files from workspace directories
2. Parse ret.pkl files for portfolio analysis
3. Import results into the SQLite database
4. Handle both new and existing workspace structures

Usage:
    python scripts/extract_results.py [--workspace-dir PATH] [--dry-run] [--verbose]
"""

import argparse
import pickle
import sys
import traceback
from pathlib import Path
from datetime import datetime
from typing import Dict, Optional, List, Tuple

import pandas as pd

# Add project root to path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))


class WorkspaceResultExtractor:
    """
    Extract backtest results from Qlib workspace directories.

    Scans workspace directories for qlib_res.csv and ret.pkl files,
    parses metrics, and optionally imports them into ResultsDatabase.
    """

    def __init__(self, workspace_dir: Path, dry_run: bool = False, verbose: bool = False):
        """
        Initialize the extractor.

        Parameters
        ----------
        workspace_dir : Path
            Root directory of the Qlib workspace
        dry_run : bool
            If True, only scan and display results without importing to DB
        verbose : bool
            If True, print detailed extraction logs
        """
        self.workspace_dir = workspace_dir
        self.dry_run = dry_run
        self.verbose = verbose
        self.extracted_results: List[Dict] = []

    def scan_workspace(self) -> List[Path]:
        """
        Scan workspace for qlib_res.csv files.

        Returns
        -------
        List[Path]
            List of paths to qlib_res.csv files found
        """
        csv_files = list(self.workspace_dir.rglob("qlib_res.csv"))
        pkl_files = list(self.workspace_dir.rglob("ret.pkl"))

        if self.verbose:
            print(f"\nScanning workspace: {self.workspace_dir}")
            print(f"  Found {len(csv_files)} qlib_res.csv files")
            print(f"  Found {len(pkl_files)} ret.pkl files")

        return csv_files

    def extract_from_csv(self, csv_path: Path) -> Optional[Dict]:
        """
        Extract metrics from a qlib_res.csv file.

        Parameters
        ----------
        csv_path : Path
            Path to the qlib_res.csv file

        Returns
        -------
        Optional[Dict]
            Dictionary of metrics, or None if extraction failed
        """
        try:
            # Read CSV - format is: ,value (first column is metric name, second is value)
            # The CSV has no header, with index column 0 and value column 1
            df = pd.read_csv(csv_path, header=None)
            
            # Convert to dictionary {metric_name: value}
            # Column 0 is metric name, column 1 is value
            metrics = {}
            for _, row in df.iterrows():
                if len(row) >= 2:
                    metric_name = str(row[0]).strip()
                    metric_value = row[1]
                    # Only include non-empty metrics with valid names
                    if metric_name and metric_name.lower() != 'nan' and pd.notna(metric_value) and str(metric_value).strip() != '':
                        try:
                            metrics[metric_name] = float(metric_value)
                        except (ValueError, TypeError):
                            metrics[metric_name] = str(metric_value)

            # Skip if no meaningful metrics found
            if not metrics or len(metrics) < 2:
                if self.verbose:
                    print(f"\n  SKIPPING {csv_path}: No meaningful metrics found (empty or failed backtest)")
                return None

            # Parse important metrics - Qlib uses various naming conventions
            result = {
                'ic': self._safe_float(metrics.get('IC', None)),
                'sharpe_ratio': self._safe_float(
                    metrics.get('1day.excess_return_with_cost.shar', 
                    metrics.get('1day.excess_return_with_cost.sharpe', None))
                ),
                'annualized_return': self._safe_float(
                    metrics.get('1day.excess_return_with_cost.annualized_return', None)
                ),
                'max_drawdown': self._safe_float(
                    metrics.get('1day.excess_return_with_cost.max_drawdown', None)
                ),
                'win_rate': self._safe_float(metrics.get('win_rate', None)),
                'information_ratio': self._safe_float(
                    metrics.get('1day.excess_return_with_cost.information_ratio', None)
                ),
                'volatility': self._safe_float(
                    metrics.get('1day.excess_return_with_cost.std', 
                    metrics.get('1day.excess_return_with_cost.volatility', None))
                ),
                'raw_metrics': metrics,
                'source_file': str(csv_path),
            }

            if self.verbose:
                print(f"\n  Extracted from {csv_path.name}:")
                print(f"    IC: {result['ic']}")
                print(f"    Sharpe: {result['sharpe_ratio']}")
                print(f"    Annual Return: {result['annualized_return']}")
                print(f"    Max Drawdown: {result['max_drawdown']}")
                print(f"    Information Ratio: {result['information_ratio']}")
                print(f"    Volatility: {result['volatility']}")
                print(f"    Total metrics: {len(metrics)}")

            return result

        except Exception as e:
            if self.verbose:
                print(f"\n  ERROR extracting from {csv_path}: {e}")
                traceback.print_exc()
            return None

    def extract_from_pkl(self, pkl_path: Path) -> Optional[pd.DataFrame]:
        """
        Extract portfolio analysis from ret.pkl file.

        Parameters
        ----------
        pkl_path : Path
            Path to the ret.pkl file

        Returns
        -------
        Optional[pd.DataFrame]
            DataFrame with portfolio analysis, or None if failed
        """
        try:
            df = pd.read_pickle(pkl_path)
            if self.verbose:
                print(f"\n  Extracted ret.pkl from {pkl_path}:")
                print(f"    Shape: {df.shape}")
                print(f"    Columns: {list(df.columns)}")
            return df
        except Exception as e:
            if self.verbose:
                print(f"\n  ERROR extracting ret.pkl from {pkl_path}: {e}")
            return None

    def extract_factor_name_from_path(self, csv_path: Path) -> str:
        """
        Attempt to extract factor name from the file path.

        Parameters
        ----------
        csv_path : Path
            Path to qlib_res.csv

        Returns
        -------
        str
            Extracted factor name or 'unknown'
        """
        # Try to find factor name in directory structure
        # Common pattern: workspace/factor_name/qlib_res.csv
        parent_dir = csv_path.parent.name
        if parent_dir and parent_dir not in ['.', '..']:
            return parent_dir

        # Fallback to parent's parent
        grandparent = csv_path.parent.parent.name
        if grandparent:
            return grandparent

        return 'unknown'

    def extract_all(self) -> List[Dict]:
        """
        Extract all results from the workspace.

        Returns
        -------
        List[Dict]
            List of extracted result dictionaries
        """
        csv_files = self.scan_workspace()
        self.failed_extractions: List[Dict] = []

        total = len(csv_files)
        print(f"\nProcessing {total} qlib_res.csv files...")

        for i, csv_path in enumerate(csv_files, 1):
            # Progress indicator for large workspaces
            if i % 100 == 0 or i == total:
                print(f"  Progress: {i}/{total} ({i*100//max(total,1)}%)")

            result = self.extract_from_csv(csv_path)
            if result is not None:
                result['factor_name'] = self.extract_factor_name_from_path(csv_path)
                result['extraction_time'] = datetime.now().isoformat()
                self.extracted_results.append(result)
            else:
                # Track failed extraction
                self.failed_extractions.append({
                    "timestamp": datetime.now().isoformat(),
                    "factor_name": self.extract_factor_name_from_path(csv_path),
                    "source_file": str(csv_path),
                    "error_type": "empty_or_failed_backtest",
                    "stdout": "No meaningful metrics found in qlib_res.csv",
                })

        print(f"\nExtracted {len(self.extracted_results)} valid results from {total} files")
        print(f"  Skipped/Failed: {total - len(self.extracted_results)} empty/failed backtests")

        # Save failed extractions to failed_runs.json
        if self.failed_extractions:
            self._save_failed_extractions()

        return self.extracted_results

    def _save_failed_extractions(self) -> None:
        """Save failed extractions to results/failed_runs/failed_runs.json."""
        import json
        from pathlib import Path

        try:
            failed_dir = project_root / "results" / "failed_runs"
            failed_dir.mkdir(parents=True, exist_ok=True)
            failed_file = failed_dir / "failed_runs.json"

            existing_records = []
            if failed_file.exists():
                try:
                    existing_records = json.loads(failed_file.read_text(encoding="utf-8"))
                    if not isinstance(existing_records, list):
                        existing_records = [existing_records]
                except (json.JSONDecodeError, Exception):
                    existing_records = []

            existing_records.extend(self.failed_extractions)
            if len(existing_records) > 500:
                existing_records = existing_records[-500:]

            failed_file.write_text(
                json.dumps(existing_records, indent=2, default=str, ensure_ascii=False),
                encoding="utf-8"
            )
            print(f"  Failed runs tracked: {len(self.failed_extractions)} entries → {failed_file}")

        except Exception as e:
            print(f"  WARNING: Could not save failed extractions: {e}")

    def import_to_database(self) -> int:
        """
        Import extracted results to the ResultsDatabase.

        Returns
        -------
        int
            Number of results successfully imported
        """
        if self.dry_run:
            print("\n[DRY RUN] Skipping database import")
            return 0

        if not self.extracted_results:
            print("\nNo results to import")
            return 0

        try:
            from rdagent.components.backtesting import ResultsDatabase

            db = ResultsDatabase()
            imported = 0
            failed = 0
            total = len(self.extracted_results)

            print(f"\nImporting {total} results to database...")

            for i, result in enumerate(self.extracted_results, 1):
                # Progress indicator
                if i % 100 == 0 or i == total:
                    print(f"  DB Progress: {i}/{total} ({i*100//max(total,1)}%) - Imported: {imported}, Failed: {failed}")

                try:
                    factor_name = result.get('factor_name', 'unknown')[:100]
                    metrics = {
                        'ic': result.get('ic'),
                        'sharpe_ratio': result.get('sharpe_ratio'),
                        'annualized_return': result.get('annualized_return'),
                        'max_drawdown': result.get('max_drawdown'),
                        'win_rate': result.get('win_rate'),
                        'information_ratio': result.get('information_ratio'),
                        'volatility': result.get('volatility'),
                        'raw_metrics': result.get('raw_metrics'),
                    }

                    run_id = db.add_backtest(factor_name=factor_name, metrics=metrics)
                    if run_id > 0:
                        imported += 1
                        if self.verbose:
                            print(f"  Imported: {factor_name} (IC={metrics['ic']}, Sharpe={metrics['sharpe_ratio']})")
                    else:
                        failed += 1
                        if self.verbose:
                            print(f"  WARNING: Failed to import {factor_name}")

                except Exception as e:
                    failed += 1
                    if self.verbose:
                        print(f"  ERROR importing {result.get('factor_name', 'unknown')}: {e}")
                        traceback.print_exc()

            db.close()
            print(f"\n{'=' * 60}")
            print(f"IMPORT COMPLETE")
            print(f"{'=' * 60}")
            print(f"  Total processed: {total}")
            print(f"  Successfully imported: {imported}")
            print(f"  Failed: {failed}")
            print(f"  Database: {db.db_path}")
            print(f"{'=' * 60}")
            return imported

        except Exception as e:
            print(f"\nERROR: Failed to connect to database: {e}")
            traceback.print_exc()
            return 0

    def _safe_float(self, value) -> Optional[float]:
        """Safely convert value to float."""
        if value is None:
            return None
        try:
            f = float(value)
            if pd.isna(f) or f == float('inf') or f == float('-inf'):
                return None
            return f
        except (ValueError, TypeError):
            return None

    def display_summary(self):
        """Display a summary of extracted results."""
        if not self.extracted_results:
            print("\nNo results extracted")
            return

        print("\n" + "=" * 80)
        print("EXTRACTED RESULTS SUMMARY")
        print("=" * 80)

        df = pd.DataFrame([
            {
                'Factor': r.get('factor_name', 'unknown'),
                'IC': r.get('ic'),
                'Sharpe': r.get('sharpe_ratio'),
                'Ann. Return': r.get('annualized_return'),
                'Max DD': r.get('max_drawdown'),
                'Win Rate': r.get('win_rate'),
            }
            for r in self.extracted_results
        ])

        # Sort by Sharpe ratio
        if 'Sharpe' in df.columns:
            df = df.dropna(subset=['Sharpe']).sort_values('Sharpe', ascending=False)

        print(df.to_string(index=False))
        print(f"\nTotal results: {len(self.extracted_results)}")

        if len(df) > 0 and df['IC'].notna().any():
            print(f"Average IC: {df['IC'].mean():.4f}")
        if len(df) > 0 and df['Sharpe'].notna().any():
            print(f"Best Sharpe: {df['Sharpe'].max():.4f}")
        print("=" * 80)


def main():
    parser = argparse.ArgumentParser(
        description="Extract Qlib workspace results to ResultsDatabase"
    )
    parser.add_argument(
        '--workspace-dir',
        type=str,
        default='git_ignore_folder/RD-Agent_workspace',
        help='Path to workspace directory (default: git_ignore_folder/RD-Agent_workspace)'
    )
    parser.add_argument(
        '--dry-run',
        action='store_true',
        help='Scan and display results without importing to database'
    )
    parser.add_argument(
        '--verbose',
        action='store_true',
        help='Enable verbose output with detailed logs'
    )
    parser.add_argument(
        '--summary-only',
        action='store_true',
        help='Only show summary, skip extraction'
    )
    parser.add_argument(
        '--generate-db-summary',
        action='store_true',
        help='Generate RESULTS_SUMMARY.md from the database'
    )

    args = parser.parse_args()

    # Handle DB summary generation separately
    if args.generate_db_summary:
        try:
            from rdagent.components.backtesting import ResultsDatabase
            db = ResultsDatabase()
            summary = db.generate_results_summary(print_to_console=True)
            db.close()
            print(f"\nResults summary written to: results/RESULTS_SUMMARY.md")
        except Exception as e:
            print(f"ERROR: Could not generate DB summary: {e}")
            import traceback
            traceback.print_exc()
        return

    workspace_path = Path(args.workspace_dir).resolve()

    if not workspace_path.exists():
        print(f"ERROR: Workspace directory not found: {workspace_path}")
        print("Please ensure you have run at least one fin_quant loop")
        sys.exit(1)

    print(f"Workspace: {workspace_path}")
    print(f"Dry run: {args.dry_run}")
    print(f"Verbose: {args.verbose}")

    # Extract results
    extractor = WorkspaceResultExtractor(
        workspace_dir=workspace_path,
        dry_run=args.dry_run,
        verbose=args.verbose
    )

    if not args.summary_only:
        extractor.extract_all()
        extractor.import_to_database()

    extractor.display_summary()


if __name__ == "__main__":
    main()
