"""Full-screen PTY terminal view with proper VT100 emulation via pyte."""

import curses
import os
import pty
import select
import signal
import struct
import fcntl
import termios

import pyte

from muva.views.base import BaseView
from muva import theme

# Map pyte's named colors to curses 256-color indices
_PYTE_COLOR_MAP = {
    "black": 0, "red": 1, "green": 2, "brown": 3,
    "blue": 4, "magenta": 5, "cyan": 6, "white": 7,
    "default": -1,
}

# Cache for dynamically allocated curses color pairs
_dyn_pairs = {}  # (fg_idx, bg_idx) -> pair_id
_next_dyn_pair = [50]  # start after theme pairs


def _get_color_idx(color):
    """Convert a pyte color value to a curses color index."""
    if color == "default" or color is None:
        return -1
    if isinstance(color, str):
        return _PYTE_COLOR_MAP.get(color, -1)
    if isinstance(color, int):
        return color
    return -1


def _get_pair(fg, bg):
    """Get or allocate a curses color pair for (fg, bg)."""
    key = (fg, bg)
    if key in _dyn_pairs:
        return _dyn_pairs[key]
    pair_id = _next_dyn_pair[0]
    _next_dyn_pair[0] += 1
    try:
        curses.init_pair(pair_id, fg if fg != -1 else -1, bg if bg != -1 else -1)
        _dyn_pairs[key] = pair_id
    except (curses.error, ValueError):
        return 0
    return pair_id


class TerminalView(BaseView):
    """Full-screen terminal emulator using PTY + pyte VT100."""

    def __init__(self, cwd=None, shell=None, tab_id=1):
        self.cwd = cwd or os.getcwd()
        self.shell = shell or self._detect_shell()
        self.tab_id = tab_id
        self.master_fd = None
        self.pid = None
        self.alive = False
        self._screen_h = 24
        self._screen_w = 80

        # pyte virtual terminal
        self.screen = pyte.Screen(self._screen_w, self._screen_h)
        self.screen.set_mode(pyte.modes.LNM)
        self.stream = pyte.Stream(self.screen)

        # Scrollback history
        self.history = pyte.HistoryScreen(self._screen_w, self._screen_h, history=10000)
        self.history.set_mode(pyte.modes.LNM)
        self.hist_stream = pyte.Stream(self.history)

        # Use history screen as the primary
        self.screen = self.history
        self.stream = self.hist_stream

        self.scroll_offset = 0  # how many lines scrolled back

    @property
    def title(self):
        return f"\ue795 T:{self.tab_id}"

    def _detect_shell(self):
        for sh in ["/bin/zsh", "/usr/bin/zsh", "/bin/bash", "/usr/bin/bash"]:
            if os.path.exists(sh):
                return sh
        return "/bin/sh"

    def start(self):
        """Fork a PTY and start the shell."""
        if self.alive:
            return

        pid, fd = pty.openpty()
        if pid == 0:
            # Child process
            os.chdir(self.cwd)
            env = os.environ.copy()
            env["TERM"] = "xterm-256color"
            env["COLORTERM"] = "truecolor"
            os.execvpe(self.shell, [self.shell], env)
        else:
            # Parent
            self.master_fd = fd
            self.pid = pid
            self.alive = True
            # Set non-blocking
            flags = fcntl.fcntl(fd, fcntl.F_GETFL)
            fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
            # Set initial PTY size
            self.resize_pty(self._screen_h, self._screen_w)

    def stop(self):
        """Kill the shell process."""
        if self.pid:
            try:
                os.kill(self.pid, signal.SIGTERM)
            except (ProcessLookupError, PermissionError, OSError):
                pass
            try:
                os.waitpid(self.pid, os.WNOHANG)
            except ChildProcessError:
                pass
        if self.master_fd is not None:
            try:
                os.close(self.master_fd)
            except OSError:
                pass
        self.alive = False
        self.master_fd = None
        self.pid = None

    def resize_pty(self, rows, cols):
        """Inform the PTY and pyte screen of a terminal size change."""
        if rows == self._screen_h and cols == self._screen_w:
            return
        self._screen_h = rows
        self._screen_w = cols
        if self.master_fd is not None:
            try:
                winsize = struct.pack("HHHH", rows, cols, 0, 0)
                fcntl.ioctl(self.master_fd, termios.TIOCSWINSZ, winsize)
            except OSError:
                pass
        try:
            self.screen.resize(rows, cols)
        except Exception:
            pass

    def _read_output(self):
        """Non-blocking read from PTY master, feed to pyte."""
        if self.master_fd is None:
            return
        try:
            # Read multiple times per frame to keep up with fast output
            for _ in range(10):
                ready, _, _ = select.select([self.master_fd], [], [], 0)
                if not ready:
                    break
                data = os.read(self.master_fd, 65536)
                if not data:
                    self.alive = False
                    return
                self.stream.feed(data.decode("utf-8", errors="replace"))
        except (OSError, ValueError):
            self.alive = False

    def on_enter(self):
        if not self.alive:
            self.start()

    def on_leave(self):
        pass

    def draw(self, stdscr, h, w, y_offset):
        term_h = h - 1  # reserve 1 row for status bar
        term_w = w

        self._read_output()

        # Resize if needed
        if self.alive:
            self.resize_pty(term_h, term_w)

        # Get the default bg from our theme for empty space
        default_bg = theme.cp("default")

        # Draw the pyte screen content
        screen = self.screen
        for row in range(term_h):
            if row >= screen.lines:
                # Clear extra rows
                try:
                    stdscr.addnstr(y_offset + row, 0, " " * w, w, default_bg)
                except curses.error:
                    pass
                continue

            line = screen.buffer[row]
            x = 0
            for col in range(min(term_w, screen.columns)):
                char = line[col]
                ch = char.data or " "

                # Resolve colors
                fg_idx = _get_color_idx(char.fg)
                bg_idx = _get_color_idx(char.bg)

                # Get curses attributes
                attr = 0
                if char.bold:
                    attr |= curses.A_BOLD
                if char.italics:
                    attr |= curses.A_ITALIC if hasattr(curses, "A_ITALIC") else 0
                if char.underscore:
                    attr |= curses.A_UNDERLINE
                if char.reverse:
                    attr |= curses.A_REVERSE
                if char.blink:
                    attr |= curses.A_BLINK

                # Apply color pair
                if fg_idx != -1 or bg_idx != -1:
                    pair = _get_pair(fg_idx, bg_idx)
                    attr |= curses.color_pair(pair)
                else:
                    attr |= default_bg

                try:
                    stdscr.addstr(y_offset + row, col, ch, attr)
                except curses.error:
                    pass

        # Draw cursor
        if self.alive and self.scroll_offset == 0:
            cy = screen.cursor.y
            cx = screen.cursor.x
            if 0 <= cy < term_h and 0 <= cx < term_w:
                try:
                    char = screen.buffer[cy][cx]
                    ch = char.data or " "
                    stdscr.addstr(y_offset + cy, cx, ch,
                                  curses.A_REVERSE | curses.A_BOLD)
                except curses.error:
                    pass

        # Status bar
        status_row = y_offset + h - 1
        if self.alive:
            status = f" \ue795 Terminal {self.tab_id} | {self.shell} | {self.cwd} "
        else:
            status = f" \ue795 Terminal {self.tab_id} [exited] "
        try:
            stdscr.addnstr(status_row, 0, status[:w].ljust(w), w,
                           theme.cp("statusbar") | curses.A_BOLD)
        except curses.error:
            pass

    def handle_key(self, key):
        """Pass keys through to PTY. Leader key is handled by app before this."""
        if not self.alive:
            return "back"

        if self.master_fd is None:
            return None

        # Convert curses key to bytes and send to PTY
        try:
            if key == curses.KEY_ENTER:
                os.write(self.master_fd, b"\r")
            elif key == curses.KEY_BACKSPACE or key == 127:
                os.write(self.master_fd, b"\x7f")
            elif key == curses.KEY_UP:
                os.write(self.master_fd, b"\x1b[A")
            elif key == curses.KEY_DOWN:
                os.write(self.master_fd, b"\x1b[B")
            elif key == curses.KEY_RIGHT:
                os.write(self.master_fd, b"\x1b[C")
            elif key == curses.KEY_LEFT:
                os.write(self.master_fd, b"\x1b[D")
            elif key == curses.KEY_HOME:
                os.write(self.master_fd, b"\x1b[H")
            elif key == curses.KEY_END:
                os.write(self.master_fd, b"\x1b[F")
            elif key == curses.KEY_DC:
                os.write(self.master_fd, b"\x1b[3~")
            elif key == curses.KEY_IC:
                os.write(self.master_fd, b"\x1b[2~")
            elif key == curses.KEY_PPAGE:
                os.write(self.master_fd, b"\x1b[5~")
            elif key == curses.KEY_NPAGE:
                os.write(self.master_fd, b"\x1b[6~")
            elif key == curses.KEY_F1:
                os.write(self.master_fd, b"\x1bOP")
            elif key == curses.KEY_F2:
                os.write(self.master_fd, b"\x1bOQ")
            elif key == curses.KEY_F3:
                os.write(self.master_fd, b"\x1bOR")
            elif key == curses.KEY_F4:
                os.write(self.master_fd, b"\x1bOS")
            elif key == curses.KEY_F5:
                os.write(self.master_fd, b"\x1b[15~")
            elif key == curses.KEY_F6:
                os.write(self.master_fd, b"\x1b[17~")
            elif key == curses.KEY_F7:
                os.write(self.master_fd, b"\x1b[18~")
            elif key == curses.KEY_F8:
                os.write(self.master_fd, b"\x1b[19~")
            elif key == curses.KEY_F9:
                os.write(self.master_fd, b"\x1b[20~")
            elif key == curses.KEY_F10:
                os.write(self.master_fd, b"\x1b[21~")
            elif key == curses.KEY_F11:
                os.write(self.master_fd, b"\x1b[23~")
            elif key == curses.KEY_F12:
                os.write(self.master_fd, b"\x1b[24~")
            elif key == curses.KEY_BTAB:  # Shift+Tab
                os.write(self.master_fd, b"\x1b[Z")
            elif key == 9:  # Tab
                os.write(self.master_fd, b"\t")
            elif key == 27:  # Escape
                os.write(self.master_fd, b"\x1b")
            elif 0 <= key <= 26:  # Ctrl+A through Ctrl+Z
                os.write(self.master_fd, bytes([key]))
            elif 32 <= key <= 126:
                os.write(self.master_fd, chr(key).encode("utf-8"))
            elif key >= 128:
                try:
                    os.write(self.master_fd, chr(key).encode("utf-8"))
                except (ValueError, OverflowError):
                    pass
        except OSError:
            self.alive = False

        self.scroll_offset = 0
        return None

    def __del__(self):
        self.stop()
