# Copyright (C) 2026 GoodData Corporation

"""Shared validation runner utilities for step-based tooling."""

from __future__ import annotations

from collections.abc import Awaitable, Callable
from contextlib import suppress
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any

from aida_mcp.tools.core.results import ValidationStepDetail
from aida_mcp.utils.progress import ProgressTracker
from aida_mcp.utils.timing import StepTimer, format_step_result, time_step

if TYPE_CHECKING:
    from mcp.server.fastmcp import Context as MCPContext

    Context = MCPContext[Any, Any, Any]


StepExecutor = Callable[[], Awaitable["StepResult"]]


def _new_line_list() -> list[str]:
    return []


def _new_step_detail_list() -> list[ValidationStepDetail]:
    return []


@dataclass(slots=True)
class StepResult:
    """Outcome of a single validation step."""

    output: str
    success: bool = True
    skipped_reason: str | None = None
    detail: ValidationStepDetail | None = None

    @property
    def skipped(self) -> bool:
        return self.skipped_reason is not None


@dataclass(slots=True)
class ValidationStep:
    """Descriptor for executing and reporting a validation step."""

    name: str
    executor: StepExecutor
    stop_on_failure: bool = True
    visible: bool = True
    announce: bool = True
    weight: float = 1.0


@dataclass(slots=True)
class RunnerResult:
    """Aggregated output of running validation steps."""

    lines: list[str] = field(default_factory=_new_line_list)
    success: bool = True
    step_details: list[ValidationStepDetail] = field(default_factory=_new_step_detail_list)


class ValidationRunner:
    """Utility for executing ordered validation steps with consistent UX."""

    def __init__(self, ctx: Context | None = None, tracker: ProgressTracker | None = None):
        self._ctx = ctx
        self._steps: list[ValidationStep] = []
        self._timer = StepTimer()
        self._tracker = tracker

    def add_step(self, step: ValidationStep) -> None:
        self._steps.append(step)

    @property
    def timer(self) -> StepTimer:
        return self._timer

    async def run(self) -> RunnerResult:
        report = RunnerResult()

        for step in self._steps:
            if self._ctx and step.announce and not self._tracker:
                # Non-visible steps still announce start/end for progress clarity.
                with suppress(Exception):  # Context may be closed/invalid
                    await self._ctx.info(f"▶️  Starting {step.name}...")

            with time_step(step.name, self._timer):
                try:
                    result = await step.executor()
                except Exception:
                    if self._tracker:
                        await self._tracker.advance(step.name, step.weight)
                    raise

            if self._tracker:
                await self._tracker.advance(step.name, step.weight)

            duration = self._timer.step_timings[-1][1] if self._timer.step_timings else 0.0

            if result.skipped:
                message = f"⚠️  {step.name} skipped: {result.skipped_reason}"
                report.lines.append(message)
                report.lines.append("")
                if self._ctx:
                    with suppress(Exception):  # Context may be closed/invalid
                        await self._ctx.info(message)
                continue

            if result.detail is not None:
                report.step_details.append(result.detail)

            if step.visible:
                formatted = format_step_result(result.output, step.name, duration)
                report.lines.append(formatted)
                report.lines.append("")
            else:
                if result.output:
                    report.lines.append(result.output)
                    report.lines.append("")

            if self._ctx and step.announce and not self._tracker:
                icon = "✅" if result.success else "❌"
                status_word = "completed" if result.success else "failed"
                with suppress(Exception):  # Context may be closed/invalid
                    await self._ctx.info(f"{icon} {step.name} {status_word} ({duration:.1f}s)")

            if not result.success and step.stop_on_failure:
                report.success = False
                break

        return report
