"""
Benchmark Evaluation using OpenCompass

Evaluator that runs OpenCompass in Docker to evaluate fine-tuned models on standard benchmarks.

Configure benchmark behavior via editting .env to cover default settings in conf.py:
```
FT_BENCHMARK_DATASETS='["aime25", "gsm8k"]'
FT_BENCHMARK_NUM_RUNS=4
FT_JUDGE_MODEL="gpt-4"
FT_JUDGE_API_KEY="sk-xxx"
FT_JUDGE_API_BASE="https://api.openai.com/v1"
```
"""

import json
import random
import shutil
import subprocess
from pathlib import Path
from typing import Any, Dict, List, Optional

import pandas as pd
import yaml

from rdagent.app.finetune.llm.conf import FT_RD_SETTING
from rdagent.components.coder.finetune.conf import (
    FT_MODEL_PATH,
    get_benchmark_env,
    get_ft_env,
    get_workspace_prefix,
    is_docker_env,
)
from rdagent.core.experiment import FBWorkspace, Task
from rdagent.log import rdagent_logger as logger
from rdagent.oai.llm_conf import LLM_SETTINGS
from rdagent.scenarios.finetune.benchmark.data.adaptor import (
    BENCHMARK_CONFIG_DICT,
    BenchmarkConfig,
)
from rdagent.scenarios.finetune.benchmark.data.default import extract_error_samples
from rdagent.scenarios.finetune.benchmark.merge.merge import (
    check_if_merging_needed,
    merge_model,
)
from rdagent.utils.agent.tpl import T


def get_model_inference_config(base_model_name: str, gpu_count: int) -> dict:
    """
    Load model inference configuration from YAML file.

    Args:
        base_model_name: HuggingFace model name (e.g., "Qwen/Qwen3-8B")
        gpu_count: GPU count for tensor_parallel_size (from scenario.device_info)

    Returns:
        dict: Merged configuration (model-specific overrides default)
              Uses exact match first, then longest prefix match, finally default only.
    """
    config_data = yaml.safe_load(open(Path(__file__).parent / "configs" / "models.yaml", "r"))

    default_config = config_data.get("default", {})
    models_config = config_data.get("models", {})

    # 1. Exact match
    if base_model_name in models_config:
        model_specific = models_config[base_model_name]
    else:
        # 2. Prefix match - find longest matching prefix
        model_specific = {}
        best_match_len = 5
        for configured_model in models_config:
            if base_model_name.startswith(configured_model) and len(configured_model) > best_match_len:
                model_specific = models_config[configured_model]
                best_match_len = len(configured_model)

    final_config = {**default_config, **model_specific}

    # Handle auto tensor_parallel_size
    if final_config.get("tensor_parallel_size") == "auto":
        if gpu_count <= 0:
            final_config["tensor_parallel_size"] = 1
        else:
            # Round down to nearest power of 2
            power = 0
            while (1 << (power + 1)) <= gpu_count:
                power += 1
            final_config["tensor_parallel_size"] = 1 << power

    return final_config


def detect_model_type(model_path: str) -> bool:
    """
    Detect whether the given model path corresponds to a LoRA adapter.

    Returns:
        True if LoRA adapter, False otherwise.
    """
    model_dir = Path(model_path)

    # LoRA (llama-factory style)
    if (model_dir / "adapter_config.json").exists():
        return True

    # Alternate LoRA file indicators
    for fname in ("adapter_model.bin", "adapter_model.safetensors"):
        if (model_dir / fname).exists():
            return True

    return False


def run_benchmark(
    workspace_path: str,
    model_path: str,
    model_name: str,
    benchmark_name: str,
    gpu_count: int,
    test_range: Optional[str] = "[:100]",
    num_runs: int = 1,
    pass_k: Optional[List[int]] = None,
    max_error_samples: int = 10,
    result_subdir: str = "",
) -> Dict[str, Any]:
    """
    Run benchmark evaluation on a fine-tuned model.

    Args:
        workspace_path: Path to workspace directory
        model_path: Path to fine-tuned model (supports full/LoRA auto-detection)
        model_name: HuggingFace model name
        benchmark_name: Benchmark dataset name (e.g., "aime25", "gsm8k")
        gpu_count: GPU count for tensor_parallel_size (from scenario.device_info)
        test_range: Python slice string for dataset sampling (e.g., "[:100]", "[-100:]").
                    Negative indexing allows automatic adaptation to varying subset sizes.
        num_runs: Number of times to run each sample (default: 1)
        pass_k: Optional list of k values for pass@k evaluation (e.g., [1, 5, 10])
        max_error_samples: Maximum number of error samples to extract for feedback
        result_subdir: Subdirectory for results (e.g., "validation", "test")

    Returns:
        Dict containing:
        - accuracy_summary: Dict mapping dataset -> {metric: value}, grouped by dataset
        - error_samples: List of error samples for feedback analysis
    """
    # Load configurations
    benchmark_cfg: BenchmarkConfig = BENCHMARK_CONFIG_DICT[benchmark_name]
    dataset_imports = benchmark_cfg.dataset

    # Auto download dependent data if configured on this benchmark
    if benchmark_cfg.download is not None:
        benchmark_cfg.download()

    model_is_lora = detect_model_type(model_path)
    inference_config = get_model_inference_config(model_name, gpu_count)
    workspace_path = Path(workspace_path)

    # Get environment first to determine path prefix
    env = get_benchmark_env()
    ws_prefix = get_workspace_prefix(env)
    is_docker = is_docker_env(env)

    # Determine model paths based on environment type
    model_rel_path = Path(model_path).relative_to(workspace_path)
    adapter_path_in_env = Path(ws_prefix) / model_rel_path

    if model_is_lora:
        if is_docker:
            # Docker: use /assets/models mount
            model_path_in_env = Path(FT_MODEL_PATH) / model_name
        else:
            # Conda: use actual file path
            model_path_in_env = Path(FT_RD_SETTING.file_path) / "models" / model_name
        lora_path_in_env = adapter_path_in_env

        # Check if we need to merge the model (e.g. vLLM doesn't support LoRA with modules_to_save)
        if check_if_merging_needed(model_path):
            merged_model_dir_inside_env = Path(ws_prefix) / "merged_model"

            # Create a temporary environment for merging (use FT env as it has peft/transformers)
            merge_env = get_ft_env()

            merge_model(
                env=merge_env,
                workspace_path=workspace_path,
                base_model_path=str(model_path_in_env),
                adapter_path=str(lora_path_in_env),
                output_path=str(merged_model_dir_inside_env),
            )

            # Switch to using the merged model
            model_path_in_env = merged_model_dir_inside_env
            model_is_lora = False
            lora_path_in_env = ""
            adapter_path_in_env = merged_model_dir_inside_env
    else:
        model_path_in_env = adapter_path_in_env
        lora_path_in_env = ""

    # Prepare template variables (merge inference config from models.yaml)
    template_vars = {
        # Model configuration
        "model_abbr": f"ft-{benchmark_name}",
        "model_path": model_path_in_env,
        "is_lora": model_is_lora,
        "lora_path": lora_path_in_env,
        # Dataset configuration
        "dataset_imports": [dataset_imports],
        "test_range": test_range,
        "num_runs": num_runs,
        "pass_k": pass_k,
        "work_dir": adapter_path_in_env,
        # Merge all inference parameters from models.yaml (default + model-specific)
        **inference_config,
    }

    # Override use_cot_postprocessor based on force_think_token setting
    # When force_think_token=false, we don't need the CoT postprocessor to extract answers
    if not FT_RD_SETTING.force_think_token:
        template_vars["use_cot_postprocessor"] = False

    # Render Jinja2 template
    config_content = T("rdagent.scenarios.finetune.benchmark.configs.opencompass_template:template").r(**template_vars)

    # Note: env was already created above via get_benchmark_env()

    (workspace_path / "config.py").write_text(config_content)
    # Use result_subdir for validation/test separation
    if result_subdir:
        benchmark_work_dir = f"{ws_prefix}/benchmark_results/{result_subdir}"
    else:
        benchmark_work_dir = f"{ws_prefix}/benchmark_results"

    # Logging
    logger.info(f"Running benchmark '{benchmark_name}' on model: {model_path}")
    logger.info(f"Base model: {model_name}, LoRA?: {model_is_lora}")
    logger.info(f"Workspace: {workspace_path}")
    logger.info(f"Benchmark work_dir: {benchmark_work_dir}")
    if test_range:
        logger.info(f"Dataset range: {test_range}")

    # Environment variables
    env_vars = {
        "OC_JUDGE_MODEL": FT_RD_SETTING.judge_model or LLM_SETTINGS.chat_model,
        "OC_JUDGE_API_KEY": FT_RD_SETTING.judge_api_key or LLM_SETTINGS.openai_api_key,
        "OC_JUDGE_API_BASE": FT_RD_SETTING.judge_api_base or LLM_SETTINGS.openai_api_base,
        "OC_JUDGE_RETRY": str(FT_RD_SETTING.judge_retry),
    }

    # Check if results already exist (skip re-running if cached)
    results_base = workspace_path / "benchmark_results"
    if result_subdir:
        results_base = results_base / result_subdir
    timestamped_dirs = sorted([d for d in results_base.glob("202*_*") if d.is_dir()], reverse=True)

    if timestamped_dirs:
        logger.info(f"Found existing results in {timestamped_dirs[0].name}, skipping benchmark execution")
    else:
        # Run OpenCompass
        entry_cmd = f"opencompass {ws_prefix}/config.py --work-dir {benchmark_work_dir}"

        result = env.run(
            entry=entry_cmd,
            local_path=str(workspace_path),
            env=env_vars,
        )

        # Log execution immediately (for UI display)
        tag_prefix = "docker_run" if is_docker else "conda_run"
        logger.log_object(
            {
                "exit_code": result.exit_code,
                "stdout": (result.stdout or ""),
                "benchmark_name": benchmark_name,
                "model_path": str(model_path),
                "workspace_path": str(workspace_path),
            },
            tag=f"{tag_prefix}.Benchmark",
        )

        # Check execution status
        if result.exit_code != 0:
            error_msg = result.stdout[-2000:] if result.stdout else "No output"
            raise RuntimeError(f"Benchmark execution failed (exit_code={result.exit_code})\n{error_msg}")

        # Re-scan for timestamped directories after execution
        timestamped_dirs = sorted([d for d in results_base.glob("202*_*") if d.is_dir()], reverse=True)

    # OpenCompass stores results in results/<model_name>/<dataset>.json
    results_subdir = timestamped_dirs[0] / "summary"

    results_csv_path = sorted([f for f in results_subdir.rglob("*.csv")], reverse=True)[0]
    logger.info(f"Detailed results CSV: {results_csv_path.relative_to(results_base)}")

    # Read CSV content for accuracy summary (grouped by dataset)
    df = pd.read_csv(results_csv_path)
    # Get score column (the model name column, e.g., 'api-chemcotbench')
    score_col = [c for c in df.columns if c not in ["dataset", "version", "metric", "mode"]][0]
    # Pivot to group by dataset, with metrics as columns (use pivot_table to handle duplicates)
    pivoted = df.pivot_table(index="dataset", columns="metric", values=score_col, aggfunc="first").to_dict("index")
    # Filter out NaN values (different datasets have different metrics)
    accuracy_summary = {ds: {k: v for k, v in metrics.items() if pd.notna(v)} for ds, metrics in pivoted.items()}

    # Extract error samples for feedback
    error_samples = extract_error_samples(
        timestamped_dirs[0],
        max_samples=max_error_samples,
    )

    # Log benchmark result for UI display
    # Use result_subdir to distinguish validation vs test in tag
    log_tag = f"benchmark_result.{result_subdir}" if result_subdir else "benchmark_result"
    logger.log_object(
        {
            "accuracy_summary": accuracy_summary,
            "error_samples": error_samples,
            "benchmark_name": benchmark_name,
            "split": result_subdir or "default",  # validation, test, or default
        },
        tag=log_tag,
    )

    return {
        "accuracy_summary": accuracy_summary,
        "error_samples": error_samples,
    }


def get_benchmark_ranges() -> tuple[str, str]:
    """Get validation and test range strings for benchmark evaluation.

    Uses dynamic expressions that adapt to any dataset size:
    - For small datasets (<200): splits 50/50 to avoid overlap
    - For large datasets (>=200): takes 100 samples each

    The expressions use OpenCompass's eval mechanism with index_list variable.

    Returns:
        Tuple of (validation_range, test_range) - guaranteed non-overlapping:
        - validation: first min(100, 50%) samples
        - test: last min(100, 50%) samples
    """
    return "[:min(100, len(index_list)//2)]", "[-min(100, len(index_list)//2):]"


if __name__ == "__main__":
    """Test benchmark evaluation on Qwen3-1.7B with LoRA adapter."""
    # Configuration - Fill in your LoRA adapter path and model name
    LORA_ADAPTER_PATH = "/home/v-qizhengli/workspace/FT_workspace/gitignore_folder/B200/B200_FT_workspace/limo/train/b200_sweep_yamls/saves/qwen3-1.7b/lora_b200_lr1e-4_acc4/checkpoint-100"
    MODEL_NAME = "Qwen/Qwen3-1.7B"
    BENCHMARK = "aime25"
    GPU_COUNT = 1

    print("=" * 80)
    print("Benchmark Evaluation Test")
    print("=" * 80)
    print(f"\nEnvironment: FT_JUDGE_API_KEY={'Set' if FT_RD_SETTING.judge_api_key else 'Not Set'}")
    print(f"Judge API Base: {FT_RD_SETTING.judge_api_base or 'Not Set'}")

    if not Path(LORA_ADAPTER_PATH).exists():
        print(f"\nPlease set LORA_ADAPTER_PATH to a valid checkpoint directory")
        print(f"Current path does not exist: {LORA_ADAPTER_PATH}")
        exit(1)

    print(f"\nModel: {MODEL_NAME}")
    print(f"Adapter: {LORA_ADAPTER_PATH}")
    print(f"Benchmark: {BENCHMARK}")
    print("-" * 80)

    try:
        # Create FBWorkspace for test (auto-generates UUID workspace)
        test_task = Task(name=f"benchmark_test_{BENCHMARK}")
        test_workspace = FBWorkspace(target_task=test_task)
        test_workspace.prepare()

        print(f"\nWorkspace: {test_workspace.workspace_path}")

        result = run_benchmark(
            workspace_path=str(test_workspace.workspace_path),
            model_path=LORA_ADAPTER_PATH,
            model_name=MODEL_NAME,
            benchmark_name=BENCHMARK,
            gpu_count=GPU_COUNT,
        )

        print("\nEvaluation completed!")
        print(f"Accuracy Summary: {result['accuracy_summary']}")
        print(f"Error Samples: {len(result['error_samples'])} samples")
        print(f"\nResults saved to: {test_workspace.workspace_path / 'benchmark_results'}")

    except Exception as e:
        print(f"\nEvaluation failed: {e}")
        import traceback

        traceback.print_exc()
