-- ffi/pty.lua — forkpty-backed exec.
-- Phase 1: replaces Phase 0's io.popen path so interactive cmds (vim, less,
-- htop) work and so executor's exit-code recovery can use waitpid instead
-- of the §7 sentinel hack.
-- See docs/PHASE1.md §5.

local ffi  = require("ffi")
local libc = require("ffi.libc")

ffi.cdef[[
typedef int pid_t;
pid_t forkpty(int *amaster, char *name, void *termp, void *winp);
int   execvp (const char *file, char *const argv[]);
void  _exit  (int status);
]]

-- libutil-dev's unversioned `libutil.so` symlink isn't assumed; fall back to
-- versioned sonames so a runtime-only host (no -dev installed) works. Same
-- idiom as ffi/readline + ffi/curl.
local function load_util()
    local errs = {}
    for _, name in ipairs({"util", "util.so.1", "util.so.0"}) do
        local ok, lib = pcall(ffi.load, name)
        if ok then return lib end
        errs[#errs + 1] = name .. ": " .. tostring(lib)
    end
    error("libutil not loadable: " .. table.concat(errs, "; "))
end

local util = load_util()
local C    = ffi.C

local M       = {}
local Session = {}
Session.__index = Session

-- Spawn `cmd` (shell-interpreted via /bin/sh -c) under a fresh PTY.
-- Returns:
--   session table : { pid, master_fd, closed } with :read/:write/:close/:wait/:signal
--   nil, errmsg   : on forkpty failure
function M.spawn(cmd)
    local master = ffi.new("int[1]")
    local pid = util.forkpty(master, nil, nil, nil)
    if pid < 0 then
        return nil, "forkpty: " .. libc.strerror(libc.errno())
    end
    if pid == 0 then
        -- child: exec /bin/sh -c cmd. argv must be NULL-terminated.
        local argv = ffi.new("const char *[4]")
        argv[0] = "/bin/sh"
        argv[1] = "-c"
        argv[2] = cmd
        argv[3] = nil
        C.execvp("/bin/sh", ffi.cast("char *const *", argv))
        -- execvp returned -> exec failed; abandon ship with conventional 127
        C._exit(127)
    end
    return setmetatable({
        pid       = pid,
        master_fd = master[0],
        closed    = false,
    }, Session)
end

-- Read up to `count` bytes from the master fd. Blocking.
-- Returns:
--   (string, n)     on success; n == 0 means EOF (child closed its end)
--   (nil, errmsg)   on syscall failure
function Session:read(count)
    return libc.read(self.master_fd, count or 4096)
end

function Session:write(data)
    return libc.write(self.master_fd, data)
end

function Session:close()
    if self.closed then return end
    libc.close(self.master_fd)
    self.closed = true
end

function Session:wait(options)
    return libc.waitpid(self.pid, options)
end

function Session:signal(sig)
    return libc.kill(self.pid, sig)
end

return M
