"""Terminal capture using PTY.

Records terminal sessions in asciinema v2 format (.cast files).
Format spec: https://docs.asciinema.org/manual/asciicast/v2/
"""

import asyncio
import json
import os
import pty
import select
import struct
import sys
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import IO

# Terminal control codes for resize
TIOCGWINSZ = 0x40087468 if sys.platform == "darwin" else 0x5413


@dataclass
class TerminalSession:
    """Active terminal recording session."""

    session_id: str
    output_path: Path
    shell: str = field(default_factory=lambda: os.environ.get("SHELL", "/bin/bash"))
    width: int = 120
    height: int = 40
    recording_start_ms: int | None = None

    # Runtime state
    master_fd: int | None = None
    pid: int | None = None
    start_time: float | None = None
    cast_file: IO[str] | None = None
    _running: bool = False

    def _write_header(self) -> None:
        """Write asciinema v2 header."""
        header = {
            "version": 2,
            "width": self.width,
            "height": self.height,
            "timestamp": int(time.time()),
            "env": {
                "SHELL": self.shell,
                "TERM": os.environ.get("TERM", "xterm-256color"),
            },
            "title": f"ShowRunner Session {self.session_id}",
        }
        if self.recording_start_ms is not None:
            header["recording_start_ms"] = self.recording_start_ms
        if self.cast_file:
            self.cast_file.write(json.dumps(header) + "\n")
            self.cast_file.flush()

    def _write_event(self, event_type: str, data: str) -> None:
        """Write an output event to the cast file.

        Format: [time, event_type, data]
        - time: float seconds since start
        - event_type: "o" for output, "i" for input
        - data: the actual terminal data
        """
        if self.cast_file and self.start_time:
            elapsed = time.time() - self.start_time
            event = [elapsed, event_type, data]
            self.cast_file.write(json.dumps(event) + "\n")
            self.cast_file.flush()

    def start(self) -> None:
        """Start the terminal recording session."""
        # Ensure output directory exists
        self.output_path.parent.mkdir(parents=True, exist_ok=True)

        # Open cast file for writing
        self.cast_file = open(self.output_path, "w", encoding="utf-8")
        self._write_header()

        # Fork a PTY
        self.pid, self.master_fd = pty.fork()

        if self.pid == 0:
            # Child process - exec the shell
            os.environ["TERM"] = "xterm-256color"
            os.execlp(self.shell, self.shell)
        else:
            # Parent process - record output
            self.start_time = time.time()
            self._running = True

            # Set initial terminal size
            self._set_winsize(self.width, self.height)

    def _set_winsize(self, cols: int, rows: int) -> None:
        """Set the PTY window size."""
        if self.master_fd is not None:
            import fcntl

            winsize = struct.pack("HHHH", rows, cols, 0, 0)
            fcntl.ioctl(self.master_fd, TIOCGWINSZ, winsize)

    def write_input(self, data: bytes) -> None:
        """Write input to the terminal."""
        if self.master_fd is not None and self._running:
            os.write(self.master_fd, data)
            # Optionally record input events
            # self._write_event("i", data.decode("utf-8", errors="replace"))

    def read_output(self, timeout: float = 0.1) -> bytes | None:
        """Read available output from the terminal."""
        if self.master_fd is None or not self._running:
            return None

        ready, _, _ = select.select([self.master_fd], [], [], timeout)
        if ready:
            try:
                data = os.read(self.master_fd, 65536)
                if data:
                    # Record output event
                    self._write_event("o", data.decode("utf-8", errors="replace"))
                    return data
            except OSError:
                # PTY closed
                self._running = False
        return None

    def resize(self, cols: int, rows: int) -> None:
        """Resize the terminal."""
        self.width = cols
        self.height = rows
        self._set_winsize(cols, rows)
        # Record resize event (asciinema extension)
        if self.cast_file and self.start_time:
            # Resize events use a special format
            self._write_event("r", f"{cols}x{rows}")

    def stop(self) -> dict:
        """Stop the terminal recording and return metadata."""
        self._running = False

        duration_ms = 0
        if self.start_time:
            duration_ms = int((time.time() - self.start_time) * 1000)

        # Close the PTY
        if self.master_fd is not None:
            try:
                os.close(self.master_fd)
            except OSError:
                pass

        # Wait for child process
        if self.pid is not None and self.pid > 0:
            try:
                os.waitpid(self.pid, os.WNOHANG)
            except ChildProcessError:
                pass

        # Close cast file
        if self.cast_file:
            self.cast_file.close()
            self.cast_file = None

        return {
            "session_id": self.session_id,
            "output_path": str(self.output_path),
            "duration_ms": duration_ms,
            "width": self.width,
            "height": self.height,
        }

    @property
    def is_running(self) -> bool:
        """Check if the session is still running."""
        if not self._running:
            return False
        # Check if child process is still alive
        if self.pid is not None and self.pid > 0:
            try:
                pid, _status = os.waitpid(self.pid, os.WNOHANG)
                if pid != 0:
                    self._running = False
                    return False
            except ChildProcessError:
                self._running = False
                return False
        return True


class TerminalManager:
    """Manages multiple terminal recording sessions."""

    def __init__(self) -> None:
        self._sessions: dict[str, TerminalSession] = {}
        self._tasks: dict[str, asyncio.Task] = {}

    def create_session(
        self,
        session_id: str,
        output_path: Path,
        width: int = 120,
        height: int = 40,
        recording_start_ms: int | None = None,
    ) -> TerminalSession:
        """Create a new terminal session."""
        if session_id in self._sessions:
            raise ValueError(f"Session {session_id} already exists")

        session = TerminalSession(
            session_id=session_id,
            output_path=output_path,
            width=width,
            height=height,
            recording_start_ms=recording_start_ms,
        )
        self._sessions[session_id] = session
        return session

    def get_session(self, session_id: str) -> TerminalSession | None:
        """Get an existing session."""
        return self._sessions.get(session_id)

    def stop_session(self, session_id: str) -> dict | None:
        """Stop and remove a session."""
        session = self._sessions.pop(session_id, None)
        if session:
            # Cancel any running task
            task = self._tasks.pop(session_id, None)
            if task:
                task.cancel()
            return session.stop()
        return None

    def list_sessions(self) -> list[str]:
        """List all active session IDs."""
        return list(self._sessions.keys())


# Global manager instance
_manager: TerminalManager | None = None


def get_manager() -> TerminalManager:
    """Get the global terminal manager."""
    global _manager
    if _manager is None:
        _manager = TerminalManager()
    return _manager
