"""Run service: persisted test run management."""

from datetime import datetime
import logging

from voicetest.engine.session import ConversationRunner
from voicetest.models.agent import AgentGraph
from voicetest.models.results import Message
from voicetest.models.results import MetricResult
from voicetest.models.results import TestResult
from voicetest.models.test_case import RunOptions
from voicetest.models.test_case import TestCase
from voicetest.services.agents import AgentService
from voicetest.services.settings import SettingsService
from voicetest.services.testing.execution import TestExecutionService
from voicetest.services.testing.execution import resolve_run_options
from voicetest.simulator.scripted import ScriptedUserSimulator
from voicetest.storage.repositories import AgentRepository
from voicetest.storage.repositories import RunRepository
from voicetest.storage.repositories import TestCaseRepository


_logger = logging.getLogger(__name__)


class RunService:
    """Manages persisted test runs (CRUD, result tracking)."""

    def __init__(
        self,
        run_repo: RunRepository,
        agent_repo: AgentRepository,
        test_case_repo: TestCaseRepository,
        agent_service: AgentService,
        test_execution_service: TestExecutionService,
        settings_service: SettingsService,
    ):
        self._runs = run_repo
        self._agents = agent_repo
        self._tests = test_case_repo
        self._agent_service = agent_service
        self._test_execution = test_execution_service
        self._settings = settings_service

    def create_run(self, agent_id: str) -> dict:
        """Create a new run."""
        return self._runs.create(agent_id)

    def list_runs(self, agent_id: str, limit: int = 50) -> list[dict]:
        """List runs for an agent with result summary counts."""
        return self._runs.list_for_agent_with_summary(agent_id, limit)

    def get_run(self, run_id: str) -> dict | None:
        """Get a run with all results, enriched with dynamic variables."""
        run = self._runs.get_with_results(run_id)
        if not run:
            return None

        # Enrich results with dynamic_variables from test cases (DB + file-based)
        tc_ids = {r.get("test_case_id") for r in run["results"] if r.get("test_case_id")}
        if tc_ids:
            agent_id = run["agent_id"]
            agent = self._agents.get(agent_id)
            tests_paths = agent.get("tests_paths") if agent else None
            all_tests = self._tests.list_for_agent_with_linked(agent_id, tests_paths)
            dv_map = {}
            for tc in all_tests:
                if tc["id"] in tc_ids and tc.get("dynamic_variables"):
                    dv_map[tc["id"]] = tc["dynamic_variables"]
            for r in run["results"]:
                dv = dv_map.get(r.get("test_case_id"))
                if dv:
                    r["dynamic_variables"] = dv

        return run

    def add_result(
        self,
        run_id: str,
        result,
        *,
        test_case_id: str | None = None,
        call_id: str | None = None,
    ) -> str:
        """Add a completed result to a run.

        Pass `test_case_id` for test runs, `call_id` for live calls, or neither
        for imported transcripts. Returns the new result id.
        """
        return self._runs.add_result(run_id, result, test_case_id=test_case_id, call_id=call_id)

    def delete_run(self, run_id: str) -> None:
        """Delete a run and all its results."""
        self._runs.delete(run_id)

    def create_pending_result(self, run_id: str, test_case_id: str, test_name: str) -> str:
        """Create a pending result placeholder for a test."""
        return self._runs.create_pending_result(run_id, test_case_id, test_name)

    def complete_result(self, result_id: str, result) -> None:
        """Mark a result as complete with test data."""
        self._runs.complete_result(result_id, result)

    def mark_result_error(self, result_id: str, error: str) -> None:
        """Mark a result as errored."""
        self._runs.mark_result_error(result_id, error)

    def mark_result_cancelled(self, result_id: str) -> None:
        """Mark a result as cancelled."""
        self._runs.mark_result_cancelled(result_id)

    def complete(self, run_id: str) -> None:
        """Mark a run as completed."""
        self._runs.complete(run_id)

    def update_transcript(self, result_id: str, transcript) -> None:
        """Update a result's transcript."""
        self._runs.update_transcript(result_id, transcript)

    def result_to_dict(self, db_result) -> dict:
        """Render an ORM Result row as a plain dict (transport-shaped)."""
        return self._runs._result_to_dict(db_result)

    def update_audio_eval(self, result_id: str, transformed, audio_metrics) -> None:
        """Update a result with audio eval data."""
        self._runs.update_audio_eval(result_id, transformed, audio_metrics)

    async def save_call_as_run(self, call: dict) -> str | None:
        """Convert a completed call into a Run with a single Result.

        Evaluates the agent's configured global metrics against the transcript.
        Returns the new run_id, or None if the call has no transcript.
        """
        transcript_data = call.get("transcript_json") or []
        if not transcript_data:
            return None

        agent_id = call["agent_id"]
        call_id = call["id"]
        transcript = [Message(**m) for m in transcript_data]

        duration_ms = None
        if call.get("started_at") and call.get("ended_at"):
            started = datetime.fromisoformat(call["started_at"])
            ended = datetime.fromisoformat(call["ended_at"])
            duration_ms = int((ended - started).total_seconds() * 1000)

        turn_count = len(transcript) // 2

        metrics_config = self._agent_service.get_metrics_config(agent_id)
        metric_results: list[MetricResult] = []
        if metrics_config and metrics_config.global_metrics:
            try:
                metric_results = await self._test_execution.evaluate_global_metrics(
                    transcript, metrics_config
                )
            except Exception:
                _logger.exception("Failed to evaluate global metrics for call %s", call_id)

        status = "pass" if all(r.passed for r in metric_results) else "fail"

        test_result = TestResult(
            test_name="Live Call",
            status=status,
            transcript=transcript,
            metric_results=metric_results,
            turn_count=turn_count,
            duration_ms=duration_ms,
            end_reason="user_ended",
        )

        run = self.create_run(agent_id)
        self.add_result(run["id"], test_result, call_id=call_id)
        self.complete(run["id"])
        return run["id"]

    def add_result_from_call(self, run_id: str, call_id: str, test_result) -> str:
        """Back-compat alias — prefer add_result(run_id, result, call_id=...).

        Retained because external integrators may depend on this entry point.
        """
        return self.add_result(run_id, test_result, call_id=call_id)

    def import_calls(self, agent_id: str, results: list[TestResult]) -> dict:
        """Persist a batch of imported call transcripts as a single Run.

        Creates one Run with N Results — each Result holds one call's transcript
        with status="imported" and no test_case_id/call_id linkage. The Run is
        marked complete immediately since imports are historical, not running.

        Returns the created Run record.
        """
        run = self.create_run(agent_id)
        for result in results:
            self.add_result(run["id"], result)
        self.complete(run["id"])
        return self.get_run(run["id"])

    async def replay_run(
        self,
        source_run_id: str,
        graph: AgentGraph,
        options: RunOptions | None = None,
    ) -> dict:
        """Replay a source Run against the agent's current graph.

        For each Result in the source Run, drives a fresh conversation against
        `graph` using a ScriptedUserSimulator that yields the source's recorded
        user turns in order. The live agent's responses replace the recorded
        ones. Each replay produces a new Result inside a new Run.

        Status is "pass" by default — replay results are passive captures of
        live behavior; judging happens later when metrics are configured. The
        diff against the source is implicit (same source, different agent
        responses) and consumed by the diff view.

        Returns the new Run record.

        Raises:
            ValueError: source run not found, or has no replayable results.
        """
        source = self.get_run(source_run_id)
        if not source:
            raise ValueError(f"Source run not found: {source_run_id}")
        if not source["results"]:
            raise ValueError(f"Source run has no results to replay: {source_run_id}")

        options = resolve_run_options(options, self._settings)
        new_run = self.create_run(source["agent_id"])

        for source_result in source["results"]:
            transcript_data = source_result.get("transcript_json") or []
            messages = [Message(**m) for m in transcript_data]

            simulator = ScriptedUserSimulator(messages)
            runner = ConversationRunner(graph, options)

            # Runner takes a TestCase but only uses it in mock mode (which we
            # aren't using). A minimal placeholder satisfies the type.
            placeholder = TestCase(
                name=f"Replay of {source_result.get('test_name', 'call')}",
                user_prompt="",
            )

            state = await runner.run(placeholder, simulator)

            replay_result = TestResult(
                test_id=source_result.get("id"),
                test_name=f"Replay of {source_result.get('test_name', 'call')}",
                status="pass",
                transcript=state.transcript,
                turn_count=state.turn_count,
                duration_ms=0,
                end_reason=state.end_reason,
                nodes_visited=state.nodes_visited,
                tools_called=state.tools_called,
            )

            self.add_result(new_run["id"], replay_result)

        self.complete(new_run["id"])
        return self.get_run(new_run["id"])
