"""
EvalRunner: orchestrates eval suite execution against a bundle.

Flow: load suite -> load bundle -> render prompts -> call LLM -> run judges -> produce EvalReport
"""

from __future__ import annotations

import concurrent.futures
import logging
import time
from datetime import datetime, timezone
from typing import Any, Callable, Dict, List, Optional

from llmhq_releaseops.eval.judges.base import create_judge
from llmhq_releaseops.models.bundle import BundleManifest
from llmhq_releaseops.models.eval_result import (
    AssertionResult,
    CaseResult,
    EvalReport,
    ScoreCard,
)
from llmhq_releaseops.models.eval_suite import EvalCase, EvalSuite
from llmhq_releaseops.storage.git_store import GitStore

logger = logging.getLogger(__name__)


class EvalRunner:
    """
    Orchestrates eval suite execution against a bundle.

    Args:
        store: GitStore for loading suites, bundles, saving reports.
        llm_caller: Optional callable(rendered_prompt, bundle) -> output_text.
                    If None, uses a default caller based on bundle.model_config.
    """

    DEFAULT_CASE_TIMEOUT = 300  # seconds

    def __init__(
        self,
        store: GitStore,
        llm_caller: Optional[Callable[[str, BundleManifest], str]] = None,
        case_timeout: int = DEFAULT_CASE_TIMEOUT,
    ) -> None:
        self.store = store
        self._llm_caller = llm_caller
        self._case_timeout = case_timeout

    def run(
        self,
        suite_id: str,
        bundle_id: str,
        version: str,
        prompt_renderer: Optional[Callable[[Dict[str, Any], BundleManifest], str]] = None,
        save_report: bool = True,
    ) -> EvalReport:
        """
        Run an eval suite against a specific bundle version.

        Args:
            suite_id: ID of the eval suite to run.
            bundle_id: ID of the bundle to evaluate.
            version: Version of the bundle.
            prompt_renderer: Optional callable(input_vars, bundle) -> rendered_prompt.
            save_report: Whether to persist the report to disk.

        Returns:
            EvalReport with all results.
        """
        suite = self.store.load_eval_suite(suite_id)
        bundle = self.store.load_bundle(bundle_id, version)

        case_results: List[CaseResult] = []
        for case in suite.cases:
            result = self._run_case_with_timeout(
                case, bundle, suite, prompt_renderer
            )
            case_results.append(result)

        score_card = ScoreCard.compute(case_results, suite.pass_threshold)

        report = EvalReport(
            bundle_id=bundle_id,
            bundle_version=version,
            suite_id=suite_id,
            timestamp=datetime.now(timezone.utc).isoformat(),
            summary=score_card,
            results=case_results,
            bundle_hash=bundle.bundle_hash,
        )

        if save_report:
            self.store.save_eval_report(report)
            logger.info(
                "Eval report saved: %s on %s v%s -> %s",
                suite_id,
                bundle_id,
                version,
                score_card.gate_decision.value,
            )

        return report

    def _run_case_with_timeout(
        self,
        case: EvalCase,
        bundle: BundleManifest,
        suite: EvalSuite,
        prompt_renderer: Optional[Callable] = None,
    ) -> CaseResult:
        """Run a single eval case with a timeout."""
        with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
            future = executor.submit(
                self._run_case, case, bundle, suite, prompt_renderer
            )
            try:
                return future.result(timeout=self._case_timeout)
            except concurrent.futures.TimeoutError:
                logger.error(
                    "Case '%s' timed out after %ds", case.id, self._case_timeout
                )
                return CaseResult(
                    case_id=case.id,
                    passed=False,
                    assertions=[],
                    error=f"Timed out after {self._case_timeout}s",
                )

    def _run_case(
        self,
        case: EvalCase,
        bundle: BundleManifest,
        suite: EvalSuite,
        prompt_renderer: Optional[Callable] = None,
    ) -> CaseResult:
        """Run a single eval case."""
        try:
            # Render prompt
            if prompt_renderer:
                rendered = prompt_renderer(case.input, bundle)
            else:
                rendered = self._default_render(case.input, bundle)

            # Call LLM to get output
            start = time.time()
            output = self._call_llm(rendered, bundle)
            latency_ms = (time.time() - start) * 1000

            # Run each assertion's judge
            assertion_results: List[AssertionResult] = []
            for assertion in case.assertions:
                judge = create_judge(assertion)
                ar = judge.evaluate(
                    output=output,
                    assertion=assertion,
                    expected=case.expected_output,
                    context=case.input,
                )
                assertion_results.append(ar)

            all_passed = all(ar.passed for ar in assertion_results)

            return CaseResult(
                case_id=case.id,
                passed=all_passed,
                assertions=assertion_results,
                output=output,
                latency_ms=latency_ms,
            )
        except Exception as e:
            logger.error("Case '%s' failed with error: %s", case.id, e)
            return CaseResult(
                case_id=case.id,
                passed=False,
                assertions=[],
                error=str(e),
            )

    def _default_render(
        self, input_vars: Dict[str, Any], bundle: BundleManifest
    ) -> str:
        """Default prompt rendering from input variables."""
        parts = [f"{k}: {v}" for k, v in input_vars.items()]
        return "\n".join(parts)

    def _call_llm(self, prompt: str, bundle: BundleManifest) -> str:
        """Call the LLM using the injected caller or bundle's model_config."""
        if self._llm_caller:
            return self._llm_caller(prompt, bundle)
        return self._default_llm_call(prompt, bundle)

    def _default_llm_call(self, prompt: str, bundle: BundleManifest) -> str:
        """Default LLM call using bundle.model_config."""
        if not bundle.model_config:
            raise ValueError(
                "Bundle has no model_config and no llm_caller was provided"
            )

        provider = bundle.model_config.provider
        model = bundle.model_config.model
        temperature = bundle.model_config.temperature
        max_tokens = bundle.model_config.max_tokens

        if provider == "openai":
            try:
                from openai import OpenAI
            except ImportError:
                raise ImportError(
                    "Install 'llmhq-releaseops[eval]' for OpenAI support"
                )
            client = OpenAI()
            response = client.chat.completions.create(
                model=model,
                messages=[{"role": "user", "content": prompt}],
                temperature=temperature,
                max_tokens=max_tokens,
            )
            return response.choices[0].message.content

        elif provider == "anthropic":
            try:
                from anthropic import Anthropic
            except ImportError:
                raise ImportError(
                    "Install 'llmhq-releaseops[eval]' for Anthropic support"
                )
            client = Anthropic()
            response = client.messages.create(
                model=model,
                max_tokens=max_tokens,
                temperature=temperature,
                messages=[{"role": "user", "content": prompt}],
            )
            return response.content[0].text

        else:
            raise ValueError(f"Unsupported provider: {provider}")
