"""Klira evaluation runner - orchestrates eval execution with trace-based model."""

import time
import logging
from typing import Callable, Optional, Any, List
from datetime import datetime
from pathlib import Path

from deepeval.test_case import LLMTestCase

from klira.sdk.evals.types import KliraEvalResult
from klira.sdk.evals.dataset_loader import DatasetLoader
from klira.sdk.evals.dataset_http_client import DatasetHTTPClient
from klira.sdk.config import get_config
from klira.sdk.tracing import set_hierarchy_context

logger = logging.getLogger(__name__)


def evaluate(
    target: Callable[[str], str],
    data: Optional[str] = None,
    experiment_id: Optional[str] = None,
    organization_id: Optional[str] = None,
    project_id: Optional[str] = None,
    user_id: Optional[str] = None,
    **kwargs: Any,
) -> KliraEvalResult:
    """
    Run evaluation on target function with trace-based model.

    **TRACE-BASED EVALUATION MODEL:**
    The SDK has migrated from SDK-side evaluation (using DeepEval metrics) to
    a simplified trace-based model. The platform now evaluates execution traces
    using an LLM judge, providing more robust and comprehensive analysis.

    **Execution Flow:**
    1. Loads test cases from a dataset (CSV/JSON)
    2. Runs the target function on each test case
    3. Captures execution traces automatically via OTLP decorators
    4. Routes traces to /evals when evals_run is configured
    5. Returns basic metadata with test cases and outputs

    **Platform Evaluation:**
    The Klira platform analyzes the captured traces asynchronously using an
    LLM judge to generate evaluation metrics, compliance reports, and insights.
    Results are available via the platform API and dashboard.

    **Migration Note:**
    If you were using the old evaluate() with DeepEval metrics (evaluators
    parameter), see the migration guide:
    https://docs.getklira.com/migration/trace-based-evals

    Args:
        target: Function to evaluate (decorated with @workflow, @agent, etc.)
        data: Path to local CSV or JSON dataset file containing test questions
        experiment_id: Optional experiment ID for context and tracing
        organization_id: Optional organization context for tracing hierarchy
        project_id: Optional project context for tracing hierarchy
        user_id: Optional user identifier for trace attribution. If not provided,
                 auto-generated from API key. Required for platform trace ingestion.
        **kwargs: Additional arguments (for future extensibility)

    Returns:
        KliraEvalResult containing:
        - test_cases: List of test cases with actual_output populated
        - dataset_path: Path to the dataset file
        - total_test_cases: Number of test cases
        - evals_run: Eval run ID from config (if trace-based)
        - created_at: Timestamp of evaluation start
        - duration_seconds: Total execution time
        - experiment_id, organization_id, project_id: Passed-through context

        Deprecated fields (maintained for backward compatibility):
        - pass_rate, passed_test_cases, failed_test_cases: Always 0
        - avg_scores, metric_results: Always empty
        - compliance_report: Always None

        Note: Evaluation metrics are generated by the platform from traces,
        not computed by the SDK. Fetch detailed results from the platform API.

    Example:
        >>> from klira import Klira
        >>> from klira.sdk.evals import evaluate
        >>>
        >>> Klira.init(api_key="klira_...", evals_run="eval_123")
        >>>
        >>> @Klira.agent
        >>> def my_agent(question: str) -> str:
        ...     return "answer"
        >>>
        >>> result = evaluate(target=my_agent, data="test_cases.csv")
        >>> print(f"Evaluated {result.total_test_cases} test cases")
        >>> # Platform automatically evaluates traces sent to /evals
    """
    start_time = time.time()

    # Generate user_id if not provided (required by platform for trace ingestion)
    if not user_id:
        config = get_config()
        if config and config.api_key:
            # Extract stable user ID from API key (first 10 chars after 'klira_')
            api_key_parts = config.api_key.split("_", 1)
            user_id = (
                f"eval_{api_key_parts[1][:10]}"
                if len(api_key_parts) > 1
                else "eval_default"
            )
        else:
            user_id = "eval_default"

    # Set hierarchy context for trace attribution
    if organization_id or project_id or experiment_id or user_id:
        set_hierarchy_context(
            organization_id=organization_id,
            project_id=project_id,
            conversation_id=experiment_id,
            user_id=user_id,
        )

    # 1. Load dataset (remote API takes precedence over local file)
    test_cases, resolved_dataset_path = _resolve_dataset(data)

    # 2. Run target function on each test case (traces captured automatically via OTLP)
    test_cases = _run_target_function(target, test_cases)

    # 3. Optional: Flush OTLP to ensure traces are sent before returning
    # This is best-effort and non-blocking
    try:
        from opentelemetry import trace

        tracer = trace.get_tracer(__name__)
        if hasattr(tracer, "force_flush"):
            # Try to flush with timeout to avoid hanging
            tracer.force_flush(timeout_millis=5000)
    except Exception:
        # Flush failure is non-critical and shouldn't fail evaluate()
        pass

    # 4. Return simple result with test cases
    config = get_config()
    return KliraEvalResult(
        test_cases=test_cases,
        dataset_path=resolved_dataset_path,
        total_test_cases=len(test_cases),
        evals_run=config.evals_run if config else None,
        created_at=datetime.utcnow(),
        duration_seconds=time.time() - start_time,
        experiment_id=experiment_id,
        organization_id=organization_id,
        project_id=project_id,
    )


def _resolve_dataset(
    data: Optional[str],
) -> tuple:
    """Resolve dataset from remote API or local file.

    Priority: dataset_id (config) > data (local file path)

    Args:
        data: Optional local file path for the dataset

    Returns:
        Tuple of (test_cases, dataset_path_string)

    Raises:
        ValueError: If neither dataset_id nor data is provided
    """
    # Check config for dataset_id first — remote takes precedence
    config = get_config()
    if config and config.dataset_id:
        if not config.api_key:
            raise ValueError(
                "API key is required to fetch remote datasets. "
                "Configure 'api_key' in Klira.init()."
            )
        logger.info(
            f"Fetching remote dataset: {config.dataset_id}"
        )
        client = DatasetHTTPClient(api_key=config.api_key)
        items = client.fetch_dataset_items(config.dataset_id)
        test_cases = DatasetLoader.load_from_api(items)
        return test_cases, f"remote:{config.dataset_id}"

    # Fallback to local file path
    if data is not None:
        return _load_dataset(data), data

    raise ValueError(
        "No dataset provided. Either configure 'dataset_id' in Klira.init() "
        "to fetch from platform, or pass 'data' parameter with a file path."
    )


def _load_dataset(file_path: str) -> List[LLMTestCase]:
    """Load dataset from CSV or JSON file."""
    path = Path(file_path)
    suffix = path.suffix.lower()

    if suffix == ".csv":
        return DatasetLoader.load_csv(file_path)
    elif suffix == ".json":
        return DatasetLoader.load_json(file_path)
    else:
        raise ValueError(f"Unsupported dataset format: {suffix}. Use .csv or .json")


def _run_target_function(
    target: Callable, test_cases: List[LLMTestCase]
) -> List[LLMTestCase]:
    """
    Run target function on each test case.

    Executes the target function (decorated with @workflow, @agent, etc.)
    on each test case's input. Traces are captured automatically via OTLP
    based on the function's decorators.

    This modifies test_cases in place, adding:
    - actual_output: The response from the target function
    - additional_metadata["error"]: Error details if execution failed
    """
    for test_case in test_cases:
        try:
            # Run target function (traces created automatically via decorators)
            output = target(test_case.input)
            test_case.actual_output = output

        except Exception as e:
            # Store error but continue evaluation
            test_case.actual_output = ""
            if test_case.additional_metadata is None:
                test_case.additional_metadata = {}
            test_case.additional_metadata["error"] = str(e)

    return test_cases
