from __future__ import annotations

import sys
from contextlib import contextmanager
from dataclasses import dataclass
from enum import IntEnum
from typing import Iterator, Optional

from rich.console import Console
from rich.status import Status
from rich.table import Table

# ANSI escape helpers for in-place step title updates.
_CSI = "\033["
_CLEAR_LINE = f"{_CSI}K"
_GREEN = f"{_CSI}32m"
_RED = f"{_CSI}31m"
_CYAN = f"{_CSI}36m"
_RESET = f"{_CSI}0m"


class Verbosity(IntEnum):
    """User-facing verbosity levels (cumulative).

    Each level includes everything from the previous one:

    - quiet:   only final results and errors
    - normal:  single-line step results (spinner -> checkmark/cross)
    - logs:    + streaming high-level operational logs below each step
    - debug:   + streaming low-level command details (commands, rc, stdout/stderr)
    """

    quiet = 0
    normal = 1
    logs = 2
    debug = 3


@dataclass(frozen=True)
class UiOptions:
    verbosity: Verbosity = Verbosity.normal


class Ui:
    """Console UI renderer.

    Output patterns per verbosity level
    ------------------------------------

    **default** (no flag)::

        ✔ Step title          ← spinner replaced by checkmark

    **-v** (logs)::

        ✔ Step title          ← → replaced by ✔ in-place
          Operational log...  ← streamed in real-time below title

    **-vv** (debug)::

        ✔ Step title          ← → replaced by ✔ in-place
          Operational log...  ← streamed in real-time below title
          $ command           ← command details streamed below title
          rc=0
          stdout: ...
    """

    def __init__(self, options: UiOptions):
        self.options = options
        self.console = Console(file=sys.stderr)

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def debug(self, text: str) -> None:
        """Print low-level debug text (visible at -vv). Always streams."""
        if self.options.verbosity >= Verbosity.debug:
            self.console.print(f"[dim]  {text}[/dim]")

    def log(self, text: str) -> None:
        """Print indented operational log text (visible at -v and above). Always streams."""
        if self.options.verbosity >= Verbosity.logs:
            self.console.print(f"  {text}")

    def info(self, text: str) -> None:
        if self.options.verbosity >= Verbosity.normal:
            self.console.print(text)

    def success(self, text: str) -> None:
        if self.options.verbosity >= Verbosity.quiet:
            self.console.print(f"[green]✔[/green] {text}")

    def warning(self, text: str) -> None:
        if self.options.verbosity >= Verbosity.normal:
            self.console.print(f"[yellow]⚠[/yellow] {text}")

    def error(self, text: str) -> None:
        self.console.print(f"[red]✖[/red] {text}")

    def table(self, *, title: Optional[str], columns: list[str], rows: list[list[str]]) -> None:
        if self.options.verbosity == Verbosity.quiet:
            return
        t = Table(title=title, show_header=True, header_style="bold")
        for c in columns:
            t.add_column(c)
        for r in rows:
            t.add_row(*r)
        self.console.print(t)

    # ------------------------------------------------------------------
    # Step context manager
    # ------------------------------------------------------------------

    @contextmanager
    def step(self, title: str) -> Iterator[None]:
        """Render a step with contextual output per verbosity level.

        - quiet:   no output, just run.
        - normal:  animated spinner ``→ title``, replaced by ``✔``/``✖``.
        - logs / debug (TTY):
              Static ``→ title`` printed first, verbose output streams
              below it in real-time, then the ``→`` is updated to ``✔``
              or ``✖`` in-place via ANSI cursor movement.
        - logs / debug (non-TTY):
              ``→ title`` printed, output streams, then ``✔``/``✖ title``
              printed at the end (no ANSI codes).
        """
        if self.options.verbosity == Verbosity.quiet:
            yield
            return

        # --- Verbose modes (-v / -vv) ------------------------------------
        if self.options.verbosity >= Verbosity.logs:
            is_tty = self.console.is_terminal
            f = self.console.file

            # Print step header
            self.console.print(f"[cyan]→[/cyan] {title}")

            if is_tty:
                # Count newlines written during step body so we can
                # jump back to the title line and update it in-place.
                original_write = f.write
                newline_count = [0]

                def counting_write(s: str) -> int:
                    newline_count[0] += s.count("\n")
                    return original_write(s)

                f.write = counting_write  # type: ignore[assignment]

                ok = False
                try:
                    yield
                    ok = True
                finally:
                    f.write = original_write  # type: ignore[assignment]
                    n = newline_count[0]

                    # Move cursor up to the → title line.
                    up = n + 1
                    f.write(f"{_CSI}{up}A\r{_CLEAR_LINE}")

                    # Overwrite with result icon.
                    if ok:
                        f.write(f"{_GREEN}✔{_RESET} {title}\n")
                    else:
                        f.write(f"{_RED}✖{_RESET} {title}\n")

                    # Move cursor back to end of output.
                    if n > 0:
                        f.write(f"{_CSI}{n}B")
                    f.flush()
            else:
                # Non-TTY fallback: no ANSI cursor movement.
                try:
                    yield
                    self.success(title)
                except Exception:
                    self.error(title)
                    raise
            return

        # --- Normal mode: animated spinner --------------------------------
        status = Status(
            f"[cyan]→[/cyan] {title}",
            spinner="dots",
            spinner_style="cyan",
            console=self.console,
        )
        try:
            status.start()
            yield
            status.stop()
            self.success(title)
        except Exception:
            status.stop()
            self.error(title)
            raise
