import 'dart:async';
import 'dart:convert';
import 'dart:ffi' as ffi;
import 'dart:io';
import 'package:ffi/ffi.dart';

// FFI Signatures for Linux libc
typedef _posix_openpt_c = ffi.Int32 Function(ffi.Int32 flags);
typedef _posix_openpt_dart = int Function(int flags);

typedef _grantpt_c = ffi.Int32 Function(ffi.Int32 fd);
typedef _grantpt_dart = int Function(int fd);

typedef _unlockpt_c = ffi.Int32 Function(ffi.Int32 fd);
typedef _unlockpt_dart = int Function(int fd);

typedef _ptsname_c = ffi.Pointer<ffi.Char> Function(ffi.Int32 fd);
typedef _ptsname_dart = ffi.Pointer<ffi.Char> Function(int fd);

typedef _ioctl_c = ffi.Int32 Function(ffi.Int32 fd, ffi.UnsignedLong request, ffi.Pointer<ffi.Void> argp);
typedef _ioctl_dart = int Function(int fd, int request, ffi.Pointer<ffi.Void> argp);

typedef _fork_c = ffi.Int32 Function();
typedef _fork_dart = int Function();

typedef _setsid_c = ffi.Int32 Function();
typedef _setsid_dart = int Function();

typedef _open_c = ffi.Int32 Function(ffi.Pointer<ffi.Char> pathname, ffi.Int32 flags);
typedef _open_dart = int Function(ffi.Pointer<ffi.Char> pathname, int flags);

typedef _close_c = ffi.Int32 Function(ffi.Int32 fd);
typedef _close_dart = int Function(int fd);

typedef _dup2_c = ffi.Int32 Function(ffi.Int32 oldfd, ffi.Int32 newfd);
typedef _dup2_dart = int Function(int oldfd, int newfd);

typedef _execvp_c = ffi.Int32 Function(ffi.Pointer<ffi.Char> file, ffi.Pointer<ffi.Pointer<ffi.Char>> argv);
typedef _execvp_dart = int Function(ffi.Pointer<ffi.Char> file, ffi.Pointer<ffi.Pointer<ffi.Char>> argv);

typedef _read_c = ffi.IntPtr Function(ffi.Int32 fd, ffi.Pointer<ffi.Void> buf, ffi.IntPtr count);
typedef _read_dart = int Function(int fd, ffi.Pointer<ffi.Void> buf, int count);

typedef _write_c = ffi.IntPtr Function(ffi.Int32 fd, ffi.Pointer<ffi.Void> buf, ffi.IntPtr count);
typedef _write_dart = int Function(int fd, ffi.Pointer<ffi.Void> buf, int count);

typedef _waitpid_c = ffi.Int32 Function(ffi.Int32 pid, ffi.Pointer<ffi.Int32> status, ffi.Int32 options);
typedef _waitpid_dart = int Function(int pid, ffi.Pointer<ffi.Int32> status, int options);

typedef _chdir_c = ffi.Int32 Function(ffi.Pointer<ffi.Char> path);
typedef _chdir_dart = int Function(ffi.Pointer<ffi.Char> path);

typedef _fcntl_c = ffi.Int32 Function(ffi.Int32 fd, ffi.Int32 cmd, ffi.Int32 arg);
typedef _fcntl_dart = int Function(int fd, int cmd, int arg);

typedef _unsetenv_c = ffi.Int32 Function(ffi.Pointer<ffi.Char> name);
typedef _unsetenv_dart = int Function(ffi.Pointer<ffi.Char> name);

typedef _exit_c = ffi.Void Function(ffi.Int32 status);
typedef _exit_dart = void Function(int status);

base class Winsize extends ffi.Struct {
  @ffi.Uint16()
  external int ws_row;
  @ffi.Uint16()
  external int ws_col;
  @ffi.Uint16()
  external int ws_xpixel;
  @ffi.Uint16()
  external int ws_ypixel;
}

class PtySession {
  final int masterFd;
  final String slaveName;
  final int childPid;
  
  final _stdoutController = StreamController<List<int>>();
  bool _isClosed = false;

  PtySession._({
    required this.masterFd,
    required this.slaveName,
    required this.childPid,
  }) {
    _startReading();
  }

  Stream<List<int>> get stdout => _stdoutController.stream;

  static Future<PtySession> start(String executable, List<String> arguments, {
    String? workingDirectory,
    Map<String, String>? environment,
  }) async {
    final libc = ffi.DynamicLibrary.open('libc.so.6');

    final posixOpenpt = libc.lookupFunction<_posix_openpt_c, _posix_openpt_dart>('posix_openpt');
    final grantpt = libc.lookupFunction<_grantpt_c, _grantpt_dart>('grantpt');
    final unlockpt = libc.lookupFunction<_unlockpt_c, _unlockpt_dart>('unlockpt');
    final ptsname = libc.lookupFunction<_ptsname_c, _ptsname_dart>('ptsname');
    final fork = libc.lookupFunction<_fork_c, _fork_dart>('fork');
    final setsid = libc.lookupFunction<_setsid_c, _setsid_dart>('setsid');
    final open = libc.lookupFunction<_open_c, _open_dart>('open');
    final close = libc.lookupFunction<_close_c, _close_dart>('close');
    final dup2 = libc.lookupFunction<_dup2_c, _dup2_dart>('dup2');
    final execvp = libc.lookupFunction<_execvp_c, _execvp_dart>('execvp');
    final ioctl = libc.lookupFunction<_ioctl_c, _ioctl_dart>('ioctl');
    final chdir = libc.lookupFunction<_chdir_c, _chdir_dart>('chdir');
    final fcntl = libc.lookupFunction<_fcntl_c, _fcntl_dart>('fcntl');
    final unsetenv = libc.lookupFunction<_unsetenv_c, _unsetenv_dart>('unsetenv');
    final cExit = libc.lookupFunction<_exit_c, _exit_dart>('_exit');

    // 1. Prepare master PTY
    final masterFd = posixOpenpt(2 | 0x00000400);
    if (masterFd < 0) {
      throw OSError('Failed to open pseudo-terminal master');
    }

    if (grantpt(masterFd) != 0) {
      close(masterFd);
      throw OSError('Failed to grant pseudo-terminal slave permissions');
    }

    if (unlockpt(masterFd) != 0) {
      close(masterFd);
      throw OSError('Failed to unlock pseudo-terminal slave descriptor');
    }

    final slavePtr = ptsname(masterFd);
    if (slavePtr == ffi.Pointer.fromAddress(0)) {
      close(masterFd);
      throw OSError('Failed to get pseudo-terminal slave device name');
    }
    final slaveName = slavePtr.cast<Utf8>().toDartString();

    // Set master fd to non-blocking mode to prevent freezing the Dart event loop (§6.7)
    // F_GETFL = 3, F_SETFL = 4, O_NONBLOCK = 2048 (0x800)
    final flags = fcntl(masterFd, 3, 0);
    if (flags >= 0) {
      fcntl(masterFd, 4, flags | 2048);
    }

    // 2. Pre-allocate ALL structures on the heap before fork() for async-signal-safety (§6.7)
    final pathPtr = slaveName.toNativeUtf8();
    final exePtr = executable.toNativeUtf8();
    final workDirPtr = workingDirectory?.toNativeUtf8();
    
    // Environment isolation variables to be cleared in child context
    final tmuxNamePtr = 'TMUX'.toNativeUtf8();
    final tmuxPaneNamePtr = 'TMUX_PANE'.toNativeUtf8();

    final argsCount = arguments.length;
    final argv = malloc<ffi.Pointer<ffi.Char>>(argsCount + 2);
    argv[0] = exePtr.cast<ffi.Char>();
    
    final allocatedArgs = <ffi.Pointer<Utf8>>[];
    for (var i = 0; i < argsCount; i++) {
      final ptr = arguments[i].toNativeUtf8();
      allocatedArgs.add(ptr);
      argv[i + 1] = ptr.cast<ffi.Char>();
    }
    argv[argsCount + 1] = ffi.Pointer.fromAddress(0);

    // 3. Fork child process
    final pid = fork();
    if (pid < 0) {
      close(masterFd);
      malloc.free(pathPtr);
      malloc.free(exePtr);
      malloc.free(tmuxNamePtr);
      malloc.free(tmuxPaneNamePtr);
      if (workDirPtr != null) malloc.free(workDirPtr);
      malloc.free(argv);
      for (final p in allocatedArgs) {
        malloc.free(p);
      }
      throw OSError('Failed to fork child process');
    }

    if (pid == 0) {
      // --- CHILD PROCESS BRANCH (Strictly async-signal-safe OS calls only!) ---
      // A. Disassociate controlling terminal
      setsid();

      // B. Isolate from nested TMUX environments (§6.7)
      unsetenv(tmuxNamePtr.cast<ffi.Char>());
      unsetenv(tmuxPaneNamePtr.cast<ffi.Char>());

      // C. Open slave and redirect stdio
      final slaveFd = open(pathPtr.cast<ffi.Char>(), 2);
      if (slaveFd < 0) {
        cExit(-1); // direct libc _exit to prevent async-signal-unsafe Dart runtime exit
      }

      ioctl(slaveFd, 0x540E, ffi.Pointer.fromAddress(0));

      dup2(slaveFd, 0);
      dup2(slaveFd, 1);
      dup2(slaveFd, 2);

      close(masterFd);
      if (slaveFd > 2) {
        close(slaveFd);
      }

      if (workDirPtr != null) {
        chdir(workDirPtr.cast<ffi.Char>());
      }

      execvp(exePtr.cast<ffi.Char>(), argv);
      cExit(-2); // direct libc _exit on exec failure
    }

    // --- PARENT PROCESS BRANCH ---
    malloc.free(pathPtr);
    malloc.free(exePtr);
    malloc.free(tmuxNamePtr);
    malloc.free(tmuxPaneNamePtr);
    if (workDirPtr != null) malloc.free(workDirPtr);
    malloc.free(argv);
    for (final p in allocatedArgs) {
      malloc.free(p);
    }

    return PtySession._(
      masterFd: masterFd,
      slaveName: slaveName,
      childPid: pid,
    );
  }

  void _startReading() {
    final libc = ffi.DynamicLibrary.open('libc.so.6');
    final cRead = libc.lookupFunction<_read_c, _read_dart>('read');
    final buffer = malloc<ffi.Uint8>(1024);

    Timer.periodic(const Duration(milliseconds: 10), (timer) {
      if (_isClosed) {
        timer.cancel();
        malloc.free(buffer);
        return;
      }

      final bytesRead = cRead(masterFd, buffer.cast<ffi.Void>(), 1024);
      if (bytesRead > 0) {
        final bytes = List<int>.generate(bytesRead, (i) => buffer[i]);
        _stdoutController.add(bytes);
      } else if (bytesRead < 0) {
        final errno = ffi.DynamicLibrary.open('libc.so.6').lookup<ffi.Int32>('errno').value;
        if (errno != 4 && errno != 11) {
          close();
        }
      } else {
        close();
      }
    });
  }

  void write(List<int> data) {
    if (_isClosed) return;
    final libc = ffi.DynamicLibrary.open('libc.so.6');
    final cWrite = libc.lookupFunction<_write_c, _write_dart>('write');
    
    final buffer = malloc<ffi.Uint8>(data.length);
    for (var i = 0; i < data.length; i++) {
      buffer[i] = data[i];
    }
    
    cWrite(masterFd, buffer.cast<ffi.Void>(), data.length);
    malloc.free(buffer);
  }

  void writeString(String str) {
    write(utf8.encode(str));
  }

  void resize(int cols, int rows) {
    if (_isClosed) return;
    final libc = ffi.DynamicLibrary.open('libc.so.6');
    final ioctl = libc.lookupFunction<_ioctl_c, _ioctl_dart>('ioctl');

    final size = calloc<Winsize>();
    size.ref.ws_col = cols;
    size.ref.ws_row = rows;
    
    ioctl(masterFd, 0x5414, size.cast<ffi.Void>());
    calloc.free(size);
  }

  void close() {
    if (_isClosed) return;
    _isClosed = true;
    _stdoutController.close();
    
    final libc = ffi.DynamicLibrary.open('libc.so.6');
    final close = libc.lookupFunction<_close_c, _close_dart>('close');
    close(masterFd);

    // Send SIGTERM to terminate child process
    final kill = libc.lookupFunction<
      ffi.Int32 Function(ffi.Int32 pid, ffi.Int32 sig),
      int Function(int pid, int sig)
    >('kill');
    kill(childPid, 15); // SIGTERM = 15

    // Reap child zombie process (blocking waitpid options = 0 to guarantee reaping)
    final waitpid = libc.lookupFunction<_waitpid_c, _waitpid_dart>('waitpid');
    final statusPtr = calloc<ffi.Int32>();
    waitpid(childPid, statusPtr, 0); // Blocking wait guarantees process resource removal
    calloc.free(statusPtr);
  }
}
