# Copyright (C) 2026 GoodData Corporation

"""Streaming progress forwarding for unified validate.

This module is intentionally small and generic:
- If a subprocess emits `AIDA_EVENT {json}` lines, we parse and forward them to the MCP Context.
- If it doesn't, we can optionally emit a low-noise tail-style heartbeat based on interesting lines.
"""

from __future__ import annotations

import time
from contextlib import suppress
from dataclasses import dataclass, field
from typing import Any, Protocol

from aida_mcp.output.policy import MAX_PROGRESS_MESSAGE_CHARS
from aida_mcp.output.progress_events import parse_event_line


class ProgressContext(Protocol):
    async def info(self, message: str) -> Any: ...

    async def report_progress(self, progress: float, total: float | None = None, message: str | None = None) -> Any: ...


def _truncate(msg: str) -> str:
    max_chars = MAX_PROGRESS_MESSAGE_CHARS
    if max_chars <= 0:
        return ""
    if len(msg) <= max_chars:
        return msg
    if max_chars <= 3:
        return msg[:max_chars]
    return msg[: max_chars - 3] + "..."


@dataclass(slots=True)
class TailProgress:
    """Tail-style progress for commands without structured progress events."""

    ctx: ProgressContext | None
    current_step: int
    total_steps: int
    step_label: str
    update_interval: float = 2.0
    last_interesting: str = ""
    _last_emit: float = field(default=0.0, init=False)

    async def on_line(self, line: str) -> None:
        if not self.ctx:
            return
        stripped = (line or "").strip()
        if not stripped:
            return
        self.last_interesting = stripped
        await self._emit(force=False)

    async def _emit(self, *, force: bool) -> None:
        if not self.ctx:
            return
        now = time.monotonic()
        if not force and (now - self._last_emit) < self.update_interval:
            return
        self._last_emit = now
        message = _truncate(f"{self.step_label} – {self.last_interesting}".strip())
        with suppress(Exception):
            await self.ctx.report_progress(self.current_step, self.total_steps, message)


@dataclass(slots=True)
class StreamProgressForwarder:
    """Forward `AIDA_EVENT` lines (and optionally tail) to the MCP Context."""

    ctx: ProgressContext | None
    current_step: int
    total_steps: int
    step_label: str
    tail_fallback: TailProgress | None = None

    async def on_stdout_line(self, line: str) -> None:
        await self._handle_line(line)

    async def on_stderr_line(self, line: str) -> None:
        await self._handle_line(line)

    async def _handle_line(self, line: str) -> None:
        if not self.ctx:
            return

        event = parse_event_line(line)
        if isinstance(event, dict):
            kind = str(event.get("kind") or "")
            if kind == "progress":
                msg_any = event.get("message")
                msg = msg_any if isinstance(msg_any, str) else None
                with suppress(Exception):
                    # Engine owns the step X/Y scale. Events are treated as message updates only.
                    await self.ctx.report_progress(
                        float(self.current_step),
                        float(self.total_steps),
                        _truncate(msg) if msg else None,
                    )
                return

            if kind == "info":
                msg_any = event.get("message")
                if isinstance(msg_any, str) and msg_any.strip():
                    with suppress(Exception):
                        await self.ctx.info(_truncate(msg_any.strip()))
                return

            # Unknown event kind -> ignore (future-proofing).
            return

        if self.tail_fallback is not None:
            await self.tail_fallback.on_line(line)


def build_forwarder(
    *,
    ctx: ProgressContext | None,
    current_step: int,
    total_steps: int,
    step_label: str,
    enable_tail_fallback: bool,
) -> StreamProgressForwarder:
    tail = None
    if enable_tail_fallback:
        tail = TailProgress(ctx=ctx, current_step=current_step, total_steps=total_steps, step_label=step_label)
    return StreamProgressForwarder(
        ctx=ctx,
        current_step=current_step,
        total_steps=total_steps,
        step_label=step_label,
        tail_fallback=tail,
    )
