"""
Unix/Linux/macOS PTY implementation.
"""

from __future__ import annotations

import os
import pty
import termios
import struct
import fcntl
import signal
import asyncio
import subprocess
import logging
from typing import Optional, Dict

from .base import PTYBase
from .. import constants

logger = logging.getLogger(__name__)


class UnixPTY(PTYBase):
    """Unix/Linux/macOS PTY implementation."""

    def __init__(self, rows: int = constants.DEFAULT_TERMINAL_HEIGHT, cols: int = constants.DEFAULT_TERMINAL_WIDTH):
        super().__init__(rows, cols)
        self.master_fd, self.slave_fd = pty.openpty()
        logger.info(f"Created PTY: master_fd={self.master_fd}, slave_fd={self.slave_fd}")
        self.resize(rows, cols)

    def read(self, size: int = constants.DEFAULT_PTY_BUFFER_SIZE) -> str:
        """Read data from the PTY."""
        if self._closed:
            return ""
        try:
            data = os.read(self.master_fd, size)
            return data.decode("utf-8", errors="replace")
        except OSError as e:
            if e.errno in (constants.EBADF, constants.EINVAL):
                self._closed = True
                raise
            return ""

    def write(self, data: str) -> int:
        """Write data to the PTY."""
        if self._closed:
            return 0
        try:
            return os.write(self.master_fd, data.encode("utf-8"))
        except OSError:
            return 0

    def resize(self, rows: int, cols: int) -> None:
        """Resize the terminal using TIOCSWINSZ ioctl."""
        self.rows = rows
        self.cols = cols
        if self._closed:
            return

        try:
            winsize = struct.pack("HHHH", rows, cols, 0, 0)
            fcntl.ioctl(self.master_fd, termios.TIOCSWINSZ, winsize)
        except OSError:
            pass

    def close(self) -> None:
        """Close the PTY file descriptors."""
        if not self._closed:
            logger.info(f"Closing PTY: master_fd={self.master_fd}, slave_fd={self.slave_fd}")

            # Send SIGHUP to process group (like a shell would)
            if self._process is not None:
                try:
                    os.killpg(os.getpgid(self._process.pid), signal.SIGHUP)
                    logger.info(f"Sent SIGHUP to process group {os.getpgid(self._process.pid)}")
                except (OSError, AttributeError) as e:
                    logger.info(f"Could not send SIGHUP to process group: {e}")

            # Remove from asyncio event loop first
            try:
                loop = asyncio.get_event_loop()
                if self.master_fd and isinstance(self.master_fd, int):
                    loop.remove_reader(self.master_fd)
                    logger.info(f"Removed master_fd {self.master_fd} from event loop")
            except (RuntimeError, ValueError, OSError):
                # Event loop not running or fd not registered
                pass

            try:
                if self.master_fd and isinstance(self.master_fd, int):
                    os.close(self.master_fd)
            except OSError:
                pass
            try:
                if self.slave_fd and isinstance(self.slave_fd, int):
                    os.close(self.slave_fd)
            except OSError:
                pass
            self._closed = True

    def spawn_process(self, command: str, env: Optional[Dict[str, str]] = None) -> subprocess.Popen:
        """Spawn a process attached to this PTY."""
        process_env = dict(os.environ)
        if env:
            process_env.update(env)

        # Add terminal environment variables
        process_env.update(
            {
                "TERM": "xterm-256color",
                # Don't set LINES/COLUMNS - let process discover size via ioctl
                # Setting these prevents ncurses from responding to SIGWINCH properly
            }
        )

        # Ensure UTF-8 locale if not already set
        if "LANG" not in process_env or "UTF-8" not in process_env.get("LANG", ""):
            process_env["LANG"] = "en_US.UTF-8"
        if "LC_ALL" not in process_env:
            process_env["LC_ALL"] = process_env.get("LANG", "en_US.UTF-8")

        def preexec_fn():
            """Set up the child process to use PTY as controlling terminal."""
            # Create new session and become process group leader
            os.setsid()

            # Make the PTY the controlling terminal
            fcntl.ioctl(0, termios.TIOCSCTTY, 0)

        process = subprocess.Popen(
            command if isinstance(command, list) else [command],
            shell=False,
            stdin=self.slave_fd,
            stdout=self.slave_fd,
            stderr=self.slave_fd,
            preexec_fn=preexec_fn,
            env=process_env,
        )

        # Close slave fd in parent (child has its own copy)
        os.close(self.slave_fd)
        self.slave_fd = None

        # Store process reference for cleanup
        self._process = process

        return process

    def set_nonblocking(self) -> None:
        """Set the PTY to non-blocking mode for async operations."""
        if self._closed:
            return
        flags = fcntl.fcntl(self.master_fd, fcntl.F_GETFL)
        fcntl.fcntl(self.master_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)

    async def read_async(self, size: int = constants.DEFAULT_PTY_BUFFER_SIZE) -> str:
        """Async read from PTY. Returns empty string when no data available."""
        if self._closed:
            return ""

        loop = asyncio.get_event_loop()
        try:
            # Use asyncio's add_reader for efficient async I/O
            future = loop.create_future()

            def read_ready():
                try:
                    data = os.read(self.master_fd, size)
                    loop.remove_reader(self.master_fd)
                    future.set_result(data.decode("utf-8", errors="replace"))
                except BlockingIOError:
                    loop.remove_reader(self.master_fd)
                    future.set_result("")
                except OSError as e:
                    loop.remove_reader(self.master_fd)
                    if e.errno in (constants.EBADF, constants.EINVAL):
                        self._closed = True
                    future.set_result("")

            loop.add_reader(self.master_fd, read_ready)
            return await future
        except Exception:
            return ""

    def flush(self) -> None:
        """Flush any buffered output."""
        if self._closed:
            return
        try:
            os.fsync(self.master_fd)
        except OSError:
            pass
