"""Concurrent check executor."""

from __future__ import annotations

import hashlib
import json
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import TYPE_CHECKING

from ai_qa.core.config import get_config
from ai_qa.core.result import CheckResult, ValidationResult

if TYPE_CHECKING:
    from ai_qa.core.check import BaseCheck
    from ai_qa.core.context import Context


class Runner:
    """Executes multiple checks concurrently against a context.

    Separates deterministic checks from LLM-powered checks and runs
    them in order, caching results to avoid re-computation.
    """

    def __init__(
        self,
        parallelism: int | None = None,
        cache: object | None = None,
    ) -> None:
        """Initialize Runner.

        Args:
            parallelism: Max concurrent LLM checks. If None, uses config.
            cache: Cache instance. If None, uses config.
        """
        from ai_qa.cache import Cache

        config = get_config()
        self.parallelism = parallelism or config.parallelism or 4
        self.cache = cache or (Cache() if config.cache_enabled else None)

    def run(
        self,
        ctx: Context,
        checks: list[BaseCheck],
    ) -> ValidationResult:
        """Execute checks against context.

        Args:
            ctx: Context to validate.
            checks: Checks to run.

        Returns:
            ValidationResult with all check outcomes.
        """
        results: list[CheckResult] = []

        # Separate checks by LLM requirement
        deterministic_checks = [c for c in checks if not c.requires_llm]
        llm_checks = [c for c in checks if c.requires_llm]

        # Compute context hash once
        context_hash = self._hash_context(ctx)

        # Run deterministic checks first (fast, free)
        for check in deterministic_checks:
            result = self._run_check(check, ctx, context_hash)
            results.append(result)

        # Run LLM checks concurrently
        with ThreadPoolExecutor(max_workers=self.parallelism) as executor:
            futures = {
                executor.submit(self._run_check, check, ctx, context_hash): check
                for check in llm_checks
            }
            for future in as_completed(futures):
                result = future.result()
                results.append(result)

        return ValidationResult(checks=results, context=ctx)

    def _run_check(
        self,
        check: BaseCheck,
        ctx: Context,
        context_hash: str,
    ) -> CheckResult:
        """Run a single check, using cache if available.

        Args:
            check: Check to run.
            ctx: Context to validate.
            context_hash: Pre-computed hash of context.

        Returns:
            CheckResult.
        """
        # Check cache first
        if self.cache and check.requires_llm:
            cached = self.cache.get(
                context_hash,
                check.name,
                check.version,
            )
            if cached:
                cached.cached = True
                return cached

        # Run check
        try:
            result = check.evaluate(ctx)
        except Exception as e:
            result = CheckResult(
                name=check.name,
                score=0.0,
                passed=False,
                threshold=check.threshold,
                evidence=f"Check failed: {str(e)}",
                cost_usd=0.0,
                tokens_used={},
                check_version=check.version,
            )

        # Cache result if LLM-powered
        if self.cache and check.requires_llm:
            self.cache.put(
                context_hash,
                check.name,
                check.version,
                result,
            )

        return result

    @staticmethod
    def _hash_context(ctx: Context) -> str:
        """Hash context for cache key.

        Args:
            ctx: Context to hash.

        Returns:
            Hex hash string.
        """
        context_json = json.dumps(
            ctx.model_dump(exclude_none=True),
            sort_keys=True,
        )
        return hashlib.sha256(context_json.encode()).hexdigest()
