"""Streaming-Runner: führt Subprozesse aus, streamt stdout in einen RichLog
innerhalb eines Modals. Kein TUI-suspend — der user sieht alles live."""
from __future__ import annotations

import asyncio
import shlex
from dataclasses import dataclass

from textual.app import ComposeResult
from textual.binding import Binding
from textual.containers import Vertical
from textual.screen import ModalScreen
from textual.widgets import RichLog, Static

from taninux.core import theme as T


@dataclass
class StreamCommand:
    label: str
    argv: list[str]


class RunnerScreen(ModalScreen[list[int]]):
    """Modal das Commands sequenziell ausführt + stdout/stderr streamt.

    Returns list[int] — ein exit-code pro Command in Reihenfolge."""

    BINDINGS = [
        Binding("ctrl+c", "cancel", "abbruch"),
        Binding("escape", "close", "schliessen", show=True),
        Binding("q", "close", "quit", show=False),
    ]

    CSS = f"""
    RunnerScreen {{
        align: center middle;
        background: rgba(0, 0, 0, 0.85);
    }}
    RunnerScreen #runner-dialog {{
        width: 90%;
        height: 85%;
        padding: 1 2;
        background: {T.YORU};
        border: round {T.BASE3};
    }}
    RunnerScreen #runner-title {{
        height: 1;
        background: {T.YORU};
        color: {T.UME};
    }}
    RunnerScreen #runner-log {{
        height: 1fr;
        background: {T.YORU};
        color: {T.FG_ALT};
        border: none;
        scrollbar-color: {T.BASE2} {T.YORU};
        scrollbar-background: {T.YORU};
        scrollbar-corner-color: {T.YORU};
        scrollbar-size-vertical: 1;
    }}
    RunnerScreen #runner-hint {{
        height: 1;
        background: {T.YORU};
        color: {T.BASE4};
        margin-top: 1;
    }}
    """

    def __init__(self, commands: list[StreamCommand]) -> None:
        super().__init__()
        self.commands = commands
        self.codes: list[int] = []
        self.done = False
        self._current_proc: asyncio.subprocess.Process | None = None
        self._cancelled = False

    def compose(self) -> ComposeResult:
        with Vertical(id="runner-dialog"):
            yield Static("", id="runner-title", markup=True)
            yield RichLog(id="runner-log", highlight=False, markup=True, wrap=True)
            yield Static("", id="runner-hint", markup=True)

    def on_mount(self) -> None:
        self._set_title(f"running {len(self.commands)} command(s)…")
        self._set_hint(f"[{T.BASE5}]ctrl+c[/] [{T.BASE4}]abbruch[/]")
        self.run_worker(self._run_all(), exclusive=True)

    def _set_title(self, msg: str) -> None:
        self.query_one("#runner-title", Static).update(f"[{T.UME}]▲ {msg}[/]")

    def _set_hint(self, msg: str) -> None:
        self.query_one("#runner-hint", Static).update(msg)

    def _log(self, line: str) -> None:
        self.query_one("#runner-log", RichLog).write(line)

    async def _run_all(self) -> None:
        log = self.query_one("#runner-log", RichLog)
        for i, cmd in enumerate(self.commands, 1):
            if self._cancelled:
                self._log(f"[{T.RED}]abgebrochen[/]")
                break
            log.write("")
            log.write(f"[{T.SHINKAI}]┌── [{i}/{len(self.commands)}] {cmd.label}[/]")
            log.write(f"[{T.BASE5}]│   $ {shlex.join(cmd.argv)}[/]")
            log.write(f"[{T.BASE3}]└────────────────[/]")
            code = await self._run_one(cmd)
            self.codes.append(code)
            color = T.TAKE if code == 0 else T.RED
            log.write(f"[{color}]→ exit {code}[/]")

        self.done = True
        self._current_proc = None
        ok = sum(1 for c in self.codes if c == 0)
        if self._cancelled:
            self._set_title(f"abgebrochen ({ok}/{len(self.codes)} ok)")
        elif ok == len(self.codes):
            self._set_title(f"fertig — {ok}/{len(self.codes)} ok")
        else:
            self._set_title(f"fertig mit fehlern — {ok}/{len(self.codes)} ok")
        self._set_hint(
            f"[{T.BASE5}]ESC[/] [{T.BASE4}]schliessen + zurück[/]"
        )

    async def _run_one(self, cmd: StreamCommand) -> int:
        try:
            self._current_proc = await asyncio.create_subprocess_exec(
                *cmd.argv,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.STDOUT,
                stdin=asyncio.subprocess.DEVNULL,
            )
        except FileNotFoundError as e:
            self._log(f"[{T.RED}]fehler: {e}[/]")
            return 127
        except Exception as e:  # noqa: BLE001
            self._log(f"[{T.RED}]start fehlgeschlagen: {e}[/]")
            return 1

        assert self._current_proc.stdout is not None
        try:
            async for raw in self._current_proc.stdout:
                line = raw.decode("utf-8", errors="replace").rstrip("\n").rstrip("\r")
                if line:
                    # Escape rich markup chars so program output isn't parsed
                    self._log(line.replace("[", "\\["))
        except asyncio.CancelledError:
            self._current_proc.terminate()
            raise

        return await self._current_proc.wait()

    def action_cancel(self) -> None:
        if self.done:
            self.dismiss(self.codes)
            return
        self._cancelled = True
        if self._current_proc is not None:
            try:
                self._current_proc.terminate()
            except ProcessLookupError:
                pass
        self._set_title("abbruch läuft…")

    def action_close(self) -> None:
        if self.done:
            self.dismiss(self.codes)
        # Wenn noch running: ignore (use ctrl+c)
