"""BaseStep ABC — the subprocess execution engine.

Each installation step subclasses BaseStep and implements command().
The execute() method handles:
  - Spawning via asyncio.create_subprocess_shell (non-blocking)
  - Concurrent stdout/stderr line-by-line reading
  - Piping every line to BOTH the UI (via post_message) and the
    rolling log deque (for agent context injection)
  - PID registration in InstallState (for agent interrupt tool)
  - Timeout enforcement with process kill on expiry

This runs on the main Textual async loop. asyncio subprocess I/O
is truly non-blocking so the UI stays responsive.
"""

from __future__ import annotations

import asyncio
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from textual.app import App

    from ..state import InstallState


class BaseStep(ABC):
    """Abstract base for a single installation step."""

    name: str = "Unnamed Step"
    description: str = ""
    timeout: int = 600  # seconds — default 10 minutes
    allow_retry: bool = True
    max_retries: int = 2

    @abstractmethod
    def command(self) -> str:
        """Return the shell command string to execute.

        The command runs inside ``bash -c '...'`` via
        ``asyncio.create_subprocess_shell``.
        """
        ...

    def validate_preconditions(self) -> tuple[bool, str]:
        """Check before running.  Return (ok, reason_if_not_ok)."""
        return True, ""

    async def execute(self, state: InstallState, app: App) -> bool:
        """Run the step's command, streaming output to UI and log buffer.

        Returns True on success (exit code 0), False on failure.
        """
        from ..messages import LogLine

        cmd = self.command()

        # --- Spawn subprocess ---
        try:
            proc = await asyncio.create_subprocess_shell(
                cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
        except OSError as exc:
            app.post_message(
                LogLine(f"[ERROR] Failed to start: {exc}", is_stderr=True)
            )
            return False

        await state.set_active_process(proc)

        # --- Concurrent stream readers ---
        async def _read_stream(
            stream: asyncio.StreamReader, is_stderr: bool
        ) -> None:
            """Read lines until EOF; push each to UI + deque."""
            async for raw_line in stream:
                line = raw_line.decode("utf-8", errors="replace").rstrip()
                state.log_buffer.append(line)
                app.post_message(LogLine(line, is_stderr=is_stderr))

        # --- Execute with timeout ---
        try:
            await asyncio.wait_for(
                asyncio.gather(
                    _read_stream(proc.stdout, False),
                    _read_stream(proc.stderr, True),
                ),
                timeout=self.timeout,
            )
            await proc.wait()
        except asyncio.TimeoutError:
            proc.kill()
            await proc.wait()
            app.post_message(
                LogLine(
                    f"[TIMEOUT] Step '{self.name}' exceeded {self.timeout}s",
                    is_stderr=True,
                )
            )
            return False
        finally:
            await state.clear_active_process()

        return proc.returncode == 0
