#!/usr/bin/env python3
"""
Terminal class for Gtk4 + Vte 3.91
State‑machine based command runner for interactive shells.
"""

import os
import re
import shlex
import gettext
from enum import Enum
from app_shared import DebSoftwareApp

import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Vte", "3.91")
from gi.repository import GObject, GLib, Gtk, Vte, Gdk

_ = gettext.translation('iso-constructor', fallback=True).gettext


# ---------------------------------------------------------------------------
# Sentinel detection
# ---------------------------------------------------------------------------

# Clean, unambiguous sentinel format:
# @@CMD_DONE idx=0 exit=0@@
_SENTINEL_RE = re.compile(
    r'@@CMD_DONE idx=(\d+) exit=(\d+)@@'
)

# Strip ANSI CSI sequences (colors, cursor moves, etc.)
_ANSI_RE = re.compile(r'\x1b\[[0-9;?]*[A-Za-z]')


# ---------------------------------------------------------------------------
# State machine definition
# ---------------------------------------------------------------------------

class RunnerState(Enum):
    """Explicit states for the command runner."""
    IDLE = 0            # No commands running
    RUNNING = 1         # A command is being executed
    WAITING = 2         # Command finished, waiting for sentinel
    DONE = 3            # All commands completed


# ---------------------------------------------------------------------------
# Terminal widget
# ---------------------------------------------------------------------------

class Terminal(Gtk.Box):
    """ Terminal widget for Gtk4 + Vte 3.91 """
    __gsignals__ = {
        "commands_done": (GObject.SIGNAL_RUN_FIRST, None, ()),
    }

    def __init__(self, app: DebSoftwareApp):
        super().__init__(orientation=Gtk.Orientation.VERTICAL)

        self.app: DebSoftwareApp = app
        self.enable_copy_paste: bool = True
        self._command_mode: bool = False

        # Button bar (horizontal)
        self.button_bar = self.build_button_bar()

        # Add button bar to the top of the Terminal widget
        self.append(self.button_bar)

        # Create scroll container
        scroll = Gtk.ScrolledWindow()
        scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scroll.set_vexpand(True)
        scroll.set_hexpand(True)
        scroll.set_css_classes(['terminal'])

        # Create VTE terminal widget
        self.terminal: Vte.Terminal = Vte.Terminal()
        self.terminal.set_input_enabled(True)
        self.terminal.set_scroll_on_output(True)

        scroll.set_child(self.terminal)
        self.append(scroll)

        # State machine
        self._state: RunnerState = RunnerState.IDLE

        # Command queue
        self._commands: list[str] = []
        self._current_index: int = 0

        # Cursor tracking for incremental reading
        self._last_row: int = 0
        self._last_col: int = 0

        # Poll timer ID
        self._poll_id: int = None

        # Child pid
        self._child_pid: int = None

        # Key controller for copy/paste
        key_controller = Gtk.EventControllerKey()
        key_controller.connect("key-pressed", self.on_key_press)
        self.terminal.add_controller(key_controller)

        # Child exit handler
        self.terminal.connect("child-exited", self._on_child_exited)


    # -----------------------------------------------------------------------
    # Shell startup
    # -----------------------------------------------------------------------

    def start_shell(self, working_directory: str=None, argv: list[str]=None) -> None:
        """Start an interactive shell inside VTE."""
        if argv is None:
            shell = os.environ.get("SHELL", "/bin/bash")
            argv = [shell, "-i", "-c",
                    "unset HISTFILE; HISTSIZE=0; HISTFILESIZE=0; set +o history; exec bash -i"]

        if working_directory is None:
            working_directory = "/"

        def on_spawn(_terminal, pid, error, _user_data=None):
            if error:
                self.app.logging.error(f"Spawn error: {error.message}")
                return

            self._child_pid = pid
            self.app.logging.info("Spawned shell pid=%s", pid)

            # Focus the terminal a little later so PTY and widget settle
            def _focus_once():
                self.terminal.set_input_enabled(True)
                self.terminal.grab_focus()
                return False
            GLib.timeout_add(50, _focus_once)

            # Start polling output
            if self._poll_id is None:
                self._poll_id = GLib.timeout_add(200, self._poll_output)

        # Spawn shell
        self.terminal.spawn_async(
            Vte.PtyFlags.DEFAULT,
            working_directory,
            argv,
            None,
            GLib.SpawnFlags.DEFAULT,
            None,
            None,
            -1,
            None,
            on_spawn,
            None,
        )

    # -----------------------------------------------------------------------
    # Command runner API
    # -----------------------------------------------------------------------

    def run_commands(self, commands: list[str]) -> None:
        """Public API: run a list of commands sequentially."""
        if not commands:
            GLib.idle_add(self.emit, "commands_done")
            return

        self._commands = list(commands)
        self._current_index = 0
        self._state = RunnerState.RUNNING

        # Start the first command
        GLib.idle_add(self._dispatch_next_command)

    # -----------------------------------------------------------------------
    # State machine: dispatch next command
    # -----------------------------------------------------------------------

    def _dispatch_next_command(self) -> bool:
        """Send the next command to the shell."""
        if self._state != RunnerState.RUNNING:
            return False

        if self._current_index >= len(self._commands):
            # No more commands → DONE
            self._state = RunnerState.DONE
            GLib.idle_add(self.emit, "commands_done")
            return False

        cmd = self._commands[self._current_index]

        # Reset incremental reader to the *current* cursor position
        cur_col, cur_row = self.terminal.get_cursor_position()
        self._last_row = cur_row
        self._last_col = cur_col

        self.app.logging.info("Dispatching command [%d]: %s",
                            self._current_index, cmd)

        # Spawn the command in its own non-interactive child attached to the terminal.
        # When it exits, _on_child_exited will run; we also want to continue the state machine.
        def on_command_exit(_terminal, status):
            self.app.logging.info(
                "Command child finished, scheduling interactive shell restart (status=%s)", status
            )
            # status is exit status; keep existing sentinel-based flow for robustness.
            # Ensure we mark that command-mode ended so we can respawn the interactive shell.
            self._command_mode = False
            # After the command child exits, respawn the interactive shell so the user can continue.
            # Small delay to let terminal settle
            GLib.idle_add(self.start_shell)

        self._spawn_command_in_terminal(cmd, on_command_exit)

        # Now waiting for sentinel (the spawned command prints it)
        self._state = RunnerState.WAITING
        return False

    def _spawn_command_in_terminal(self, cmd: str, on_exit_cb):
        """
        Spawn a non-interactive child in the terminal to run `cmd` (string).
        on_exit_cb will be called with (terminal, status) when the child exits.
        """
        disable_history = (
            "unset HISTFILE; "
            "HISTSIZE=0; "
            "HISTFILESIZE=0; "
            "set +o history; "
        )

        sentinel = f'@@CMD_DONE idx={self._current_index} exit=$?@@'
        concealed = f'echo -e "\\033[8m{sentinel}\\033[0m"'

        inner = disable_history + cmd + "; " + concealed

        shell = os.environ.get("SHELL", "/bin/bash")
        argv = [shell, "-c", inner]  # non-interactive; -c runs the command and exits

        def on_spawn(_terminal, pid, error, _user_data=None):
            if error:
                self.app.logging.error("Spawn error for command: %s", error.message)
                # ensure state is consistent and call the exit callback
                self._command_mode = False
                self._child_pid = None
                on_exit_cb(_terminal, 1)
                return

            self._child_pid = pid
            self.app.logging.info("Spawned command pid=%s", pid)

            # Ensure polling is active so sentinel detection works
            if self._poll_id is None:
                self._poll_id = GLib.timeout_add(200, self._poll_output)

            # Mark command-mode so global handler can behave accordingly
            self._command_mode = True

            # Focus the terminal a little later so PTY and widget settle
            def _focus_once():
                self.terminal.set_input_enabled(True)
                self.terminal.grab_focus()
                return False
            GLib.timeout_add(50, _focus_once)

            # Attach a one-shot child-exited handler to call on_exit_cb exactly once.
            # Initialize handler_id in the outer scope so the closure can reference it.
            handler_id = None

            def _one_shot_child_exited(_t, status):
                nonlocal handler_id
                try:
                    # clear local state immediately
                    self._child_pid = None
                    self._command_mode = False
                    self.app.logging.info("One shot child exited with status %s", status)
                    on_exit_cb(_t, status)
                finally:
                    # disconnect this handler if it was connected
                    if handler_id is not None:
                        try:
                            GObject.signal_handler_disconnect(self.terminal, handler_id)
                        except Exception:
                            pass

            handler_id = self.terminal.connect("child-exited", _one_shot_child_exited)

        # Spawn the command directly in the terminal (replaces the interactive shell)
        self.terminal.spawn_async(
            Vte.PtyFlags.DEFAULT,
            None,           # working directory: inherit
            argv,
            None,
            GLib.SpawnFlags.DEFAULT,
            None,
            None,
            -1,
            None,
            on_spawn,
            None,
        )

    # -----------------------------------------------------------------------
    # Polling: read new output incrementally
    # -----------------------------------------------------------------------

    def _poll_output(self) -> bool:
        """Read only new output since last cursor position."""
        cur_col, cur_row = self.terminal.get_cursor_position()

        # No new output
        if (cur_row, cur_col) == (self._last_row, self._last_col):
            return True

        try:
            # Read only the new region
            text, _attrs = self.terminal.get_text_range_format(
                Vte.Format.TEXT,
                self._last_row,
                self._last_col,
                cur_row,
                cur_col
            )
        except Exception as e:
            self.app.logging.exception("Cannot read terminal text: %s", e)
            return True

        # Update cursor tracking
        self._last_row = cur_row
        self._last_col = cur_col

        if not text:
            return True

        # Log clean lines
        for line in text.splitlines():
            clean = line.strip()
            if not clean:
                continue

            # Skip concealed sentinel lines
            if clean.startswith("@@CMD_DONE"):
                continue

            self.app.logging.info(clean)

        # Strip ANSI and normalize
        normalized = text.replace('\r\n', '\n').replace('\r', '\n')
        normalized_no_ansi = _ANSI_RE.sub('', normalized)

        # If waiting for sentinel → check it
        if self._state == RunnerState.WAITING:
            self._handle_waiting_state(normalized_no_ansi)

        return True

    # -----------------------------------------------------------------------
    # State machine: sentinel detection
    # -----------------------------------------------------------------------

    def _handle_waiting_state(self, text: str) -> None:
        """Check for sentinel in the new output chunk."""
        for m in _SENTINEL_RE.finditer(text):
            idx = int(m.group(1))
            exit_code = int(m.group(2))

            # Only accept sentinel for the current command
            if idx == self._current_index:
                self.app.logging.info(
                    "Command %d finished exit=%d", idx, exit_code
                )

                # Move to next command
                self._current_index += 1
                self._state = RunnerState.RUNNING

                # Dispatch next command
                GLib.idle_add(self._dispatch_next_command)

    # -----------------------------------------------------------------------
    # Global methods
    # -----------------------------------------------------------------------

    def build_button_bar(self) -> Gtk.Box:
        """Build the button bar."""
        button_bar: Gtk.Box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
        button_bar.set_css_classes(['terminal_button_bar'])

        # Exit button
        btn_exit: Gtk.Button = Gtk.Button()
        btn_exit.set_icon_name("application-exit")
        btn_exit.set_tooltip_text(_("Exit shell"))
        btn_exit.set_css_name("button")
        btn_exit.connect("clicked", lambda _b: self.exit_shell())
        button_bar.append(btn_exit)

        # Copy button
        btn_copy: Gtk.Button = Gtk.Button()
        btn_copy.set_icon_name("edit-copy")
        btn_copy.set_tooltip_text(_("Copy"))
        btn_copy.set_css_name("button")
        btn_copy.connect("clicked", lambda _b: self.copy_clipboard())
        button_bar.append(btn_copy)

        # Paste button
        btn_paste: Gtk.Button = Gtk.Button()
        btn_paste.set_icon_name("edit-paste")
        btn_paste.set_tooltip_text(_("Paste"))
        btn_paste.set_css_name("button")
        btn_paste.connect("clicked", lambda _b: self.paste_clipboard())
        button_bar.append(btn_paste)

        # Clear button
        btn_clear: Gtk.Button = Gtk.Button()
        btn_clear.set_icon_name("edit-clear")
        btn_clear.set_tooltip_text(_("Clear terminal"))
        btn_clear.set_css_name("button")
        btn_clear.connect("clicked", lambda _b: self.clear_terminal())
        button_bar.append(btn_clear)

        return button_bar

    def enable_input(self, enable: bool) -> None:
        """ Enable or disable input to the terminal. """
        self.terminal.set_input_enabled(enable)

    def show_button_bar(self, show: bool) -> None:
        """ Show/hide the button bar. """
        self.button_bar.set_visible(show)

    def clear_terminal(self) -> None:
        """Clear the terminal screen and scrollback."""
        # Reset VTE: clear screen + history, keep shell alive
        self.terminal.reset(True, True)

        # Reset incremental reader cursor tracking
        cur_col, cur_row = self.terminal.get_cursor_position()
        self._last_row = cur_row
        self._last_col = cur_col

        # Force shell to redraw prompt
        try:
            self.terminal.feed_child(b"\n", 1)
        except TypeError:
            self.terminal.feed_child(b"\n")

        self.app.logging.info("Terminal cleared")

    # -----------------------------------------------------------------------
    # Shell methods
    # -----------------------------------------------------------------------

    def exit_shell(self) -> None:
        """ Exit the running shell only if one exists. """
        if self._child_pid is None:
            self.app.logging.info("Exit requested but no shell is active.")
            return

        s = "exit\n".encode("utf-8")
        try:
            self.terminal.feed_child(s, len(s))
        except TypeError:
            self.terminal.feed_child(s)

    def _on_child_exited(self, _terminal, status: int) -> None:
        self.app.logging.info("Child exited with status %s", status)
        # If a one‑off command was running, the one‑shot handler already handled cleanup.
        if self._command_mode:
            return

        # Normal interactive shell exit: stop polling
        if self._poll_id:
            GLib.source_remove(self._poll_id)
            self._poll_id = None

    def copy_clipboard(self) -> None:
        """Copy selection, or entire terminal contents if no selection."""
        if self.terminal.get_has_selection():
            self.terminal.copy_clipboard_format(Vte.Format.TEXT)
            return

        # No selection > copy entire terminal buffer
        text = self.terminal.get_text_format(Vte.Format.TEXT)
        if text:
            clipboard = self.get_display().get_clipboard()

            # Create a GValue containing the text
            value = GObject.Value()
            value.init(str)
            value.set_string(text)

            provider = Gdk.ContentProvider.new_for_value(value)
            clipboard.set_content(provider)

    def paste_clipboard(self) -> None:
        """Paste clipboard contents."""
        self.terminal.paste_clipboard()

    def on_key_press(self, _controller, keyval: int, _keycode, state: int) -> bool:
        """ Handle Ctrl‑Shift‑C and Ctrl‑Shift‑V. """
        if not self.enable_copy_paste:
            return False

        ctrl = state & Gdk.ModifierType.CONTROL_MASK
        shift = state & Gdk.ModifierType.SHIFT_MASK

        if ctrl and shift:
            if keyval == Gdk.KEY_C and self.terminal.get_has_selection():
                self.terminal.copy_clipboard_format(Vte.Format.TEXT)
                return True
            if keyval == Gdk.KEY_V:
                self.paste_clipboard()
                return True

        return False
