/******************************************************************************
 * Remote Debugging Module - Thread Functions
 *
 * This file contains functions for iterating threads and determining
 * thread status in remote process memory.
 ******************************************************************************/

#include "_remote_debugging.h"

#ifndef MS_WINDOWS
#include <unistd.h>
#endif

#ifdef __linux__
#include <dirent.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#endif

/* ============================================================================
 * THREAD ITERATION FUNCTIONS
 * ============================================================================ */

int
iterate_threads(
    RemoteUnwinderObject *unwinder,
    thread_processor_func processor,
    void *context
) {
    uintptr_t thread_state_addr;
    unsigned long tid = 0;
    const size_t MAX_THREADS = 8192;
    size_t thread_count = 0;

    if (0 > _Py_RemoteDebug_PagedReadRemoteMemory(
                &unwinder->handle,
                unwinder->interpreter_addr + (uintptr_t)unwinder->debug_offsets.interpreter_state.threads_head,
                sizeof(void*),
                &thread_state_addr))
    {
        set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read threads head");
        return -1;
    }

    while (thread_state_addr != 0 && thread_count < MAX_THREADS) {
        thread_count++;
        if (0 > _Py_RemoteDebug_PagedReadRemoteMemory(
                    &unwinder->handle,
                    thread_state_addr + (uintptr_t)unwinder->debug_offsets.thread_state.native_thread_id,
                    sizeof(tid),
                    &tid))
        {
            set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read thread ID");
            return -1;
        }

        // Call the processor function for this thread
        if (processor(unwinder, thread_state_addr, tid, context) < 0) {
            return -1;
        }

        // Move to next thread
        if (0 > _Py_RemoteDebug_PagedReadRemoteMemory(
                    &unwinder->handle,
                    thread_state_addr + (uintptr_t)unwinder->debug_offsets.thread_state.next,
                    sizeof(void*),
                    &thread_state_addr))
        {
            set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read next thread state");
            return -1;
        }
    }

    return 0;
}

/* ============================================================================
 * INTERPRETER STATE AND THREAD DISCOVERY FUNCTIONS
 * ============================================================================ */

int
populate_initial_state_data(
    int all_threads,
    RemoteUnwinderObject *unwinder,
    uintptr_t runtime_start_address,
    uintptr_t *interpreter_state,
    uintptr_t *tstate
) {
    uintptr_t interpreter_state_list_head =
        (uintptr_t)unwinder->debug_offsets.runtime_state.interpreters_head;

    uintptr_t address_of_interpreter_state;
    int bytes_read = _Py_RemoteDebug_PagedReadRemoteMemory(
            &unwinder->handle,
            runtime_start_address + interpreter_state_list_head,
            sizeof(void*),
            &address_of_interpreter_state);
    if (bytes_read < 0) {
        set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read interpreter state address");
        return -1;
    }

    if (address_of_interpreter_state == 0) {
        PyErr_SetString(PyExc_RuntimeError, "No interpreter state found");
        set_exception_cause(unwinder, PyExc_RuntimeError, "Interpreter state is NULL");
        return -1;
    }

    *interpreter_state = address_of_interpreter_state;

    if (all_threads) {
        *tstate = 0;
        return 0;
    }

    uintptr_t address_of_thread = address_of_interpreter_state +
            (uintptr_t)unwinder->debug_offsets.interpreter_state.threads_main;

    if (_Py_RemoteDebug_PagedReadRemoteMemory(
            &unwinder->handle,
            address_of_thread,
            sizeof(void*),
            tstate) < 0) {
        set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read main thread state address");
        return -1;
    }

    return 0;
}

int
find_running_frame(
    RemoteUnwinderObject *unwinder,
    uintptr_t address_of_thread,
    uintptr_t *frame
) {
    if ((void*)address_of_thread != NULL) {
        int err = read_ptr(
            unwinder,
            address_of_thread + (uintptr_t)unwinder->debug_offsets.thread_state.current_frame,
            frame);
        if (err) {
            set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read current frame pointer");
            return -1;
        }
        return 0;
    }

    *frame = (uintptr_t)NULL;
    return 0;
}

/* ============================================================================
 * THREAD STATUS FUNCTIONS
 * ============================================================================ */

int
get_thread_status(RemoteUnwinderObject *unwinder, uint64_t tid, uint64_t pthread_id) {
#if defined(__APPLE__) && TARGET_OS_OSX
   if (!unwinder->thread_id_offset_initialized) {
        uint64_t *tids = (uint64_t *)PyMem_Malloc(MAX_NATIVE_THREADS * sizeof(uint64_t));
        if (!tids) {
            // Non-fatal: thread status is best-effort
            return THREAD_STATE_UNKNOWN;
        }
        int n = proc_pidinfo(unwinder->handle.pid, PROC_PIDLISTTHREADS, 0, tids, MAX_NATIVE_THREADS * sizeof(uint64_t)) / sizeof(uint64_t);
        if (n <= 0) {
            PyMem_Free(tids);
            return THREAD_STATE_UNKNOWN;
        }
        uint64_t min_offset = UINT64_MAX;
        for (int i = 0; i < n; i++) {
            uint64_t offset = tids[i] - pthread_id;
            if (offset < min_offset) {
                min_offset = offset;
            }
        }
        unwinder->thread_id_offset = min_offset;
        unwinder->thread_id_offset_initialized = 1;
        PyMem_Free(tids);
    }
    struct proc_threadinfo ti;
    uint64_t tid_with_offset = pthread_id + unwinder->thread_id_offset;
    if (proc_pidinfo(unwinder->handle.pid, PROC_PIDTHREADINFO, tid_with_offset, &ti, sizeof(ti)) != sizeof(ti)) {
        return THREAD_STATE_UNKNOWN;
    }
    if (ti.pth_run_state == TH_STATE_RUNNING) {
        return THREAD_STATE_RUNNING;
    }
    return THREAD_STATE_IDLE;
#elif defined(__linux__)
    char stat_path[256];
    char buffer[2048] = "";

    snprintf(stat_path, sizeof(stat_path), "/proc/%d/task/%" PRIu64 "/stat", unwinder->handle.pid, tid);

    int fd = open(stat_path, O_RDONLY);
    if (fd == -1) {
        return THREAD_STATE_UNKNOWN;
    }

    if (read(fd, buffer, 2047) == 0) {
        close(fd);
        return THREAD_STATE_UNKNOWN;
    }
    close(fd);

    char *p = strchr(buffer, ')');
    if (!p) {
        return THREAD_STATE_UNKNOWN;
    }

    p += 2;  // Skip ") "
    if (*p == ' ') {
        p++;
    }

    switch (*p) {
        case 'R':  // Running
            return THREAD_STATE_RUNNING;
        case 'S':  // Interruptible sleep
        case 'D':  // Uninterruptible sleep
        case 'T':  // Stopped
        case 'Z':  // Zombie
        case 'I':  // Idle kernel thread
            return THREAD_STATE_IDLE;
        default:
            return THREAD_STATE_UNKNOWN;
    }
#elif defined(MS_WINDOWS)
    ULONG n;
    NTSTATUS status = NtQuerySystemInformation(
        SystemProcessInformation,
        unwinder->win_process_buffer,
        unwinder->win_process_buffer_size,
        &n
    );
    if (status == STATUS_INFO_LENGTH_MISMATCH) {
        // Buffer was too small so we reallocate a larger one and try again.
        unwinder->win_process_buffer_size = n;
        PVOID new_buffer = PyMem_Realloc(unwinder->win_process_buffer, n);
        if (!new_buffer) {
            // Match Linux/macOS: degrade gracefully on alloc failure
            return THREAD_STATE_UNKNOWN;
        }
        unwinder->win_process_buffer = new_buffer;
        return get_thread_status(unwinder, tid, pthread_id);
    }
    if (status != STATUS_SUCCESS) {
        return THREAD_STATE_UNKNOWN;
    }

    SYSTEM_PROCESS_INFORMATION *pi = (SYSTEM_PROCESS_INFORMATION *)unwinder->win_process_buffer;
    while ((ULONG)(ULONG_PTR)pi->UniqueProcessId != unwinder->handle.pid) {
        if (pi->NextEntryOffset == 0) {
            // Process not found (may have exited)
            return THREAD_STATE_UNKNOWN;
        }
        pi = (SYSTEM_PROCESS_INFORMATION *)(((BYTE *)pi) + pi->NextEntryOffset);
    }

    SYSTEM_THREAD_INFORMATION *ti = (SYSTEM_THREAD_INFORMATION *)((char *)pi + sizeof(SYSTEM_PROCESS_INFORMATION));
    for (size_t i = 0; i < pi->NumberOfThreads; i++, ti++) {
        if (ti->ClientId.UniqueThread == (HANDLE)tid) {
            return ti->ThreadState != WIN32_THREADSTATE_RUNNING ? THREAD_STATE_IDLE : THREAD_STATE_RUNNING;
        }
    }

    // Thread not found (may have exited)
    return THREAD_STATE_UNKNOWN;
#else
    return THREAD_STATE_UNKNOWN;
#endif
}

/* ============================================================================
 * STACK UNWINDING FUNCTIONS
 * ============================================================================ */

typedef struct {
    unsigned int initialized:1;
    unsigned int bound:1;
    unsigned int unbound:1;
    unsigned int bound_gilstate:1;
    unsigned int active:1;
    unsigned int finalizing:1;
    unsigned int cleared:1;
    unsigned int finalized:1;
    unsigned int :24;
} _thread_status;

static int
read_thread_state_and_maybe_frame(
    RemoteUnwinderObject *unwinder,
    uintptr_t tstate_addr,
    size_t tstate_size,
    char *tstate_buffer,
    uintptr_t predicted_frame_addr,
    char *frame_buffer,
    int *frame_read)
{
    *frame_read = 0;
    if (predicted_frame_addr != 0) {
        _Py_RemoteReadSegment segments[2] = {
            {tstate_addr, tstate_buffer, tstate_size},
            {predicted_frame_addr, frame_buffer, SIZEOF_INTERP_FRAME},
        };
        Py_ssize_t nread = _Py_RemoteDebug_BatchedReadRemoteMemory(
            &unwinder->handle, segments, 2);
        int completed = 0;
        if (nread >= (Py_ssize_t)tstate_size) {
            completed = 1;
            if (nread == (Py_ssize_t)(tstate_size + SIZEOF_INTERP_FRAME)) {
                completed = 2;
            }
        }
        STATS_BATCHED_READ(unwinder, 2, completed);
        if (completed >= 1) {
            *frame_read = completed == 2;
            return 0;
        }
    }
    return _Py_RemoteDebug_ReadRemoteMemory(
        &unwinder->handle, tstate_addr, tstate_size, tstate_buffer);
}

PyObject*
unwind_stack_for_thread(
    RemoteUnwinderObject *unwinder,
    uintptr_t *current_tstate,
    uintptr_t gil_holder_tstate,
    uintptr_t gc_frame,
    uintptr_t main_thread_tstate,
    const RemoteReadPrefetch *prefetch
) {
    PyObject *frame_info = NULL;
    PyObject *thread_id = NULL;
    PyObject *result = NULL;
    StackChunkList chunks = {0};

    char local_ts[SIZEOF_THREAD_STATE];
    char local_prefetched_frame[SIZEOF_INTERP_FRAME];
    const char *ts;
    RemoteReadPrefetch ctx_prefetch = {0};
    if (prefetch->tstate && prefetch->tstate_addr == *current_tstate) {
        ts = prefetch->tstate;
        if (prefetch->frame) {
            ctx_prefetch.frame = prefetch->frame;
            ctx_prefetch.frame_addr = prefetch->frame_addr;
        }
    }
    else if (unwinder->cache_frames) {
        uintptr_t predicted_frame_addr = 0;
        int have_prefetched_frame = 0;
        FrameCacheEntry *entry = frame_cache_find_by_tstate(unwinder, *current_tstate);
        if (entry && entry->num_addrs > 0) {
            predicted_frame_addr = entry->addrs[0];
        }

        int rc = read_thread_state_and_maybe_frame(
            unwinder,
            *current_tstate,
            (size_t)unwinder->debug_offsets.thread_state.size,
            local_ts,
            predicted_frame_addr,
            local_prefetched_frame,
            &have_prefetched_frame);
        if (rc < 0) {
            set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read thread state");
            goto error;
        }
        ts = local_ts;
        if (have_prefetched_frame) {
            ctx_prefetch.frame = local_prefetched_frame;
            ctx_prefetch.frame_addr = predicted_frame_addr;
        }
    }
    else {
        int rc = _Py_RemoteDebug_ReadRemoteMemory(
            &unwinder->handle,
            *current_tstate,
            (size_t)unwinder->debug_offsets.thread_state.size,
            local_ts);
        if (rc < 0) {
            set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read thread state");
            goto error;
        }
        ts = local_ts;
    }
    STATS_INC(unwinder, memory_reads);
    STATS_ADD(unwinder, memory_bytes_read, unwinder->debug_offsets.thread_state.size);
    if (ctx_prefetch.frame) {
        STATS_INC(unwinder, memory_reads);
        STATS_ADD(unwinder, memory_bytes_read, SIZEOF_INTERP_FRAME);
    }

    long tid = GET_MEMBER(long, ts, unwinder->debug_offsets.thread_state.native_thread_id);

    // Calculate thread status using flags (always)
    int status_flags = 0;

    // Check GIL status
    int has_gil = 0;
    int gil_requested = 0;
#ifdef Py_GIL_DISABLED
    int active = GET_MEMBER(_thread_status, ts, unwinder->debug_offsets.thread_state.status).active;
    has_gil = active;
    (void)gil_requested;  // unused
#else
    // Read holds_gil directly from thread state
    has_gil = GET_MEMBER(int, ts, unwinder->debug_offsets.thread_state.holds_gil);

    // Check if thread is actively requesting the GIL
    if (unwinder->debug_offsets.thread_state.gil_requested != 0) {
        gil_requested = GET_MEMBER(int, ts, unwinder->debug_offsets.thread_state.gil_requested);
    }

    // Set GIL_REQUESTED flag if thread is waiting
    if (!has_gil && gil_requested) {
        status_flags |= THREAD_STATUS_GIL_REQUESTED;
    }
#endif
    if (has_gil) {
        status_flags |= THREAD_STATUS_HAS_GIL;
        // gh-142207 for remote debugging.
        gil_requested = 0;
    }

    // Check exception state (both raised and handled exceptions)
    int has_exception = 0;

    // Check current_exception (exception being raised/propagated)
    uintptr_t current_exception = GET_MEMBER(uintptr_t, ts,
        unwinder->debug_offsets.thread_state.current_exception);
    if (current_exception != 0) {
        has_exception = 1;
    }

    // Check exc_state.exc_value (exception being handled in except block)
    // exc_state is embedded in PyThreadState, so we read it directly from
    // the thread state buffer. This catches most cases; nested exception
    // handlers where exc_info points elsewhere are rare.
    if (!has_exception) {
        uintptr_t exc_value = GET_MEMBER(uintptr_t, ts,
            unwinder->debug_offsets.thread_state.exc_state +
            unwinder->debug_offsets.err_stackitem.exc_value);
        if (exc_value != 0) {
            has_exception = 1;
        }
    }

    if (has_exception) {
        status_flags |= THREAD_STATUS_HAS_EXCEPTION;
    }

    // Check CPU status
    long pthread_id = GET_MEMBER(long, ts, unwinder->debug_offsets.thread_state.thread_id);

    // Optimization: only check CPU status if needed by mode because it's expensive
    int cpu_status = THREAD_STATE_UNKNOWN;
    if (unwinder->mode == PROFILING_MODE_CPU || unwinder->mode == PROFILING_MODE_ALL) {
        cpu_status = get_thread_status(unwinder, tid, pthread_id);
    }

    if (cpu_status == THREAD_STATE_UNKNOWN) {
        status_flags |= THREAD_STATUS_UNKNOWN;
    } else if (cpu_status == THREAD_STATE_RUNNING) {
        status_flags |= THREAD_STATUS_ON_CPU;
    }

    if (*current_tstate == main_thread_tstate) {
        status_flags |= THREAD_STATUS_MAIN_THREAD;
    }

    // Check if we should skip this thread based on mode
    int should_skip = 0;
    if (unwinder->skip_non_matching_threads) {
        if (unwinder->mode == PROFILING_MODE_CPU) {
            // Skip if not on CPU
            should_skip = !(status_flags & THREAD_STATUS_ON_CPU);
        } else if (unwinder->mode == PROFILING_MODE_GIL) {
            // Skip if doesn't have GIL
            should_skip = !(status_flags & THREAD_STATUS_HAS_GIL);
        } else if (unwinder->mode == PROFILING_MODE_EXCEPTION) {
            // Skip if thread doesn't have an exception active
            should_skip = !(status_flags & THREAD_STATUS_HAS_EXCEPTION);
        }
        // PROFILING_MODE_WALL and PROFILING_MODE_ALL never skip
    }

    if (should_skip) {
        // Advance to next thread and return NULL to skip processing
        *current_tstate = GET_MEMBER(uintptr_t, ts, unwinder->debug_offsets.thread_state.next);
        return NULL;
    }

    uintptr_t frame_addr = GET_MEMBER(uintptr_t, ts, unwinder->debug_offsets.thread_state.current_frame);
    uintptr_t base_frame_addr = GET_MEMBER(uintptr_t, ts, unwinder->debug_offsets.thread_state.base_frame);

    frame_info = PyList_New(0);
    if (!frame_info) {
        set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create frame info list");
        goto error;
    }

    // Cache mode skips this for full hits, but cache misses copy chunks before
    // walking so newly stored cache entries come from a stable stack snapshot.
    if (!unwinder->cache_frames) {
        if (copy_stack_chunks(unwinder, *current_tstate, &chunks) < 0) {
            set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to copy stack chunks");
            goto error;
        }
    }

    uintptr_t addrs[FRAME_CACHE_MAX_FRAMES];
    FrameWalkContext ctx = {
        .frame_addr = frame_addr,
        .thread_state_addr = *current_tstate,
        .base_frame_addr = base_frame_addr,
        .gc_frame = gc_frame,
        .chunks = &chunks,
        .prefetch = ctx_prefetch,
        .frame_info = frame_info,
        .frame_addrs = addrs,
        .num_addrs = 0,
        .max_addrs = FRAME_CACHE_MAX_FRAMES,
    };
    assert(ctx.max_addrs == FRAME_CACHE_MAX_FRAMES);

    if (unwinder->cache_frames) {
        // Use cache to avoid re-reading unchanged parent frames
        ctx.last_profiled.frame = GET_MEMBER(uintptr_t, ts,
            unwinder->debug_offsets.thread_state.last_profiled_frame);
        ctx.last_profiled.seq = GET_MEMBER(uintptr_t, ts,
            unwinder->debug_offsets.thread_state.last_profiled_frame_seq);
        if (collect_frames_with_cache(unwinder, &ctx, tid) < 0) {
            set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to collect frames");
            goto error;
        }
        // Update last_profiled_frame for next sample if it changed
        if (frame_addr != ctx.last_profiled.frame) {
            if (set_last_profiled_frame(unwinder, *current_tstate, frame_addr) < 0) {
                PyErr_Clear();  // Non-fatal
            }
        }
    } else {
        // No caching - process entire frame chain with base_frame validation
        if (process_frame_chain(unwinder, &ctx) < 0) {
            set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to process frame chain");
            goto error;
        }
    }

    *current_tstate = GET_MEMBER(uintptr_t, ts, unwinder->debug_offsets.thread_state.next);

    if (unwinder->cache_frames) {
        FrameCacheEntry *entry = frame_cache_find(unwinder, (uint64_t)tid);
        if (entry && entry->thread_id_obj) {
            thread_id = Py_NewRef(entry->thread_id_obj);
        }
    }
    if (thread_id == NULL) {
        thread_id = PyLong_FromLongLong(tid);
        if (thread_id == NULL) {
            set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to create thread ID");
            goto error;
        }
    }

    RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)unwinder);
    result = PyStructSequence_New(state->ThreadInfo_Type);
    if (result == NULL) {
        set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create ThreadInfo");
        goto error;
    }

    // Always use status_flags
    PyObject *py_status = PyLong_FromLong(status_flags);
    if (py_status == NULL) {
        set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to create thread status");
        goto error;
    }

    // py_status contains status flags (bitfield)
    PyStructSequence_SetItem(result, 0, thread_id);
    PyStructSequence_SetItem(result, 1, py_status);  // Steals reference
    PyStructSequence_SetItem(result, 2, frame_info); // Steals reference

    cleanup_stack_chunks(&chunks);
    return result;

error:
    Py_XDECREF(frame_info);
    Py_XDECREF(thread_id);
    Py_XDECREF(result);
    cleanup_stack_chunks(&chunks);
    return NULL;
}

/* ============================================================================
 * PROCESS STOP FUNCTIONS
 * ============================================================================ */

#if defined(__APPLE__) && TARGET_OS_OSX

void
_Py_RemoteDebug_InitThreadsState(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st)
{
    st->task = MACH_PORT_NULL;
    st->suspended = 0;
}

int
_Py_RemoteDebug_StopAllThreads(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st)
{
    kern_return_t kr = task_suspend(unwinder->handle.task);
    if (kr != KERN_SUCCESS) {
        if (kr == MACH_SEND_INVALID_DEST) {
            PyErr_Format(PyExc_ProcessLookupError,
                "Process %d has terminated", unwinder->handle.pid);
        } else {
            PyErr_Format(PyExc_RuntimeError,
                "task_suspend failed for PID %d: kern_return_t %d",
                unwinder->handle.pid, kr);
        }
        return -1;
    }

    st->task = unwinder->handle.task;
    st->suspended = 1;
    _Py_RemoteDebug_ClearCache(&unwinder->handle);
    return 0;
}

void
_Py_RemoteDebug_ResumeAllThreads(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st)
{
    if (!st->suspended || st->task == MACH_PORT_NULL) {
        return;
    }
    task_resume(st->task);
    st->task = MACH_PORT_NULL;
    st->suspended = 0;
}

#elif defined(__linux__)

void
_Py_RemoteDebug_InitThreadsState(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st)
{
    st->tids = NULL;
    st->count = 0;
}

static int
read_thread_ids(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st)
{
    char task_path[64];
    snprintf(task_path, sizeof(task_path), "/proc/%d/task", unwinder->handle.pid);

    DIR *dir = opendir(task_path);
    if (dir == NULL) {
        _Py_RemoteDebug_InitThreadsState(unwinder, st);
        if (errno == ENOENT || errno == ESRCH) {
            PyErr_Format(PyExc_ProcessLookupError,
                "Process %d has terminated", unwinder->handle.pid);
        } else {
            PyErr_SetFromErrnoWithFilename(PyExc_OSError, task_path);
        }
        return -1;
    }

    st->count = 0;

    for (;;) {
        errno = 0;
        struct dirent *entry = readdir(dir);
        if (entry == NULL) {
            if (errno != 0) {
                int err = errno;
                closedir(dir);
                _Py_RemoteDebug_InitThreadsState(unwinder, st);
                _set_debug_oserror_from_errno_with_filename(err, task_path,
                    "Failed to read process task directory '%s': %s",
                    task_path, strerror(err));
                return -1;
            }
            break;
        }
        if (entry->d_name[0] < '1' || entry->d_name[0] > '9') {
            continue;
        }
        char *endptr;
        long tid = strtol(entry->d_name, &endptr, 10);
        if (*endptr != '\0' || tid <= 0) {
            continue;
        }
        if (st->count >= unwinder->thread_tids_capacity) {
            size_t new_cap = unwinder->thread_tids_capacity == 0 ? 64 : unwinder->thread_tids_capacity * 2;
            pid_t *new_tids = PyMem_RawRealloc(unwinder->thread_tids, new_cap * sizeof(pid_t));
            if (new_tids == NULL) {
                closedir(dir);
                _Py_RemoteDebug_InitThreadsState(unwinder, st);
                PyErr_NoMemory();
                return -1;
            }
            unwinder->thread_tids = new_tids;
            unwinder->thread_tids_capacity = new_cap;
        }
        unwinder->thread_tids[st->count++] = (pid_t)tid;
    }

    if (closedir(dir) != 0) {
        int err = errno;
        _Py_RemoteDebug_InitThreadsState(unwinder, st);
        _set_debug_oserror_from_errno_with_filename(err, task_path,
            "Failed to close process task directory '%s': %s",
            task_path, strerror(err));
        return -1;
    }
    st->tids = unwinder->thread_tids;
    return 0;
}

static inline void
detach_threads(_Py_RemoteDebug_ThreadsState *st, size_t up_to)
{
    for (size_t j = 0; j < up_to; j++) {
        ptrace(PTRACE_DETACH, st->tids[j], NULL, NULL);
    }
}

static int
seize_thread(pid_t tid, int *err)
{
    if (ptrace(PTRACE_SEIZE, tid, NULL, 0) == 0) {
        return 0;
    }
    *err = errno;
    if (*err == ESRCH) {
        return 1;  // Thread gone, skip
    }
    if (*err == EPERM) {
        // Thread may have exited, be in a special state, or already be traced.
        // Skip rather than fail - this avoids endless retry loops when
        // threads transiently become inaccessible.
        return 1;
    }
    if (*err == EINVAL || *err == EIO) {
        // Fallback for older kernels
        if (ptrace(PTRACE_ATTACH, tid, NULL, NULL) == 0) {
            int status;
            waitpid(tid, &status, __WALL);
            return 0;
        }
        *err = errno;
        if (*err == ESRCH || *err == EPERM) {
            return 1;  // Thread gone or inaccessible
        }
    }
    return -1;  // Real error
}

int
_Py_RemoteDebug_StopAllThreads(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st)
{
    if (read_thread_ids(unwinder, st) < 0) {
        return -1;
    }

    size_t n_tids = st->count;
    size_t seized = 0;
    for (size_t i = 0; i < n_tids; i++) {
        pid_t tid = st->tids[i];

        int err = 0;
        int ret = seize_thread(tid, &err);
        if (ret == 1) {
            continue;  // Thread gone, skip
        }
        if (ret < 0) {
            detach_threads(st, seized);
            _set_debug_oserror_from_errno(err,
                "Failed to seize thread %d: %s", tid, strerror(err));
            _Py_RemoteDebug_InitThreadsState(unwinder, st);
            return -1;
        }
        st->tids[seized++] = tid;

        if (ptrace(PTRACE_INTERRUPT, tid, NULL, NULL) == -1) {
            err = errno;
            if (err != ESRCH) {
                detach_threads(st, seized);
                _set_debug_oserror_from_errno(err,
                    "Failed to interrupt thread %d: %s", tid, strerror(err));
                _Py_RemoteDebug_InitThreadsState(unwinder, st);
                return -1;
            }
        }

        int status;
        if (waitpid(tid, &status, __WALL) == -1) {
            err = errno;
            if (err != ECHILD && err != ESRCH) {
                detach_threads(st, seized);
                _set_debug_oserror_from_errno(err,
                    "waitpid failed for thread %d: %s", tid, strerror(err));
                _Py_RemoteDebug_InitThreadsState(unwinder, st);
                return -1;
            }
        }
    }

    st->count = seized;
    return 0;
}

void
_Py_RemoteDebug_ResumeAllThreads(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st)
{
    if (st->tids == NULL || st->count == 0) {
        return;
    }
    detach_threads(st, st->count);
    st->tids = NULL;
    st->count = 0;
}

#elif defined(MS_WINDOWS)

void
_Py_RemoteDebug_InitThreadsState(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st)
{
    st->hProcess = NULL;
    st->suspended = 0;
}

int
_Py_RemoteDebug_StopAllThreads(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st)
{
    static NtSuspendProcessFunc pNtSuspendProcess = NULL;
    static int tried_load = 0;

    if (!tried_load) {
        HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll");
        if (hNtdll) {
            pNtSuspendProcess = (NtSuspendProcessFunc)GetProcAddress(hNtdll, "NtSuspendProcess");
        }
        tried_load = 1;
    }

    if (pNtSuspendProcess == NULL) {
        PyErr_SetString(PyExc_RuntimeError, "NtSuspendProcess not available");
        return -1;
    }

    NTSTATUS status = pNtSuspendProcess(unwinder->handle.hProcess);
    if (status >= 0) {
        st->hProcess = unwinder->handle.hProcess;
        st->suspended = 1;
        _Py_RemoteDebug_ClearCache(&unwinder->handle);
        return 0;
    }

    if (!is_process_alive(unwinder->handle.hProcess)) {
        PyErr_Format(PyExc_ProcessLookupError,
            "Process %d has terminated", unwinder->handle.pid);
        return -1;
    }

    PyErr_Format(PyExc_RuntimeError, "NtSuspendProcess failed: 0x%lx", status);
    return -1;
}

void
_Py_RemoteDebug_ResumeAllThreads(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st)
{
    if (!st->suspended || st->hProcess == NULL) {
        return;
    }

    static NtResumeProcessFunc pNtResumeProcess = NULL;
    static int tried_load = 0;

    if (!tried_load) {
        HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll");
        if (hNtdll) {
            pNtResumeProcess = (NtResumeProcessFunc)GetProcAddress(hNtdll, "NtResumeProcess");
        }
        tried_load = 1;
    }

    if (pNtResumeProcess != NULL) {
        pNtResumeProcess(st->hProcess);
    }
    st->hProcess = NULL;
    st->suspended = 0;
}

#else

void
_Py_RemoteDebug_InitThreadsState(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st)
{
    (void)unwinder;
    (void)st;
}

int
_Py_RemoteDebug_StopAllThreads(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st)
{
    (void)unwinder;
    (void)st;
    return 0;
}

void
_Py_RemoteDebug_ResumeAllThreads(RemoteUnwinderObject *unwinder, _Py_RemoteDebug_ThreadsState *st)
{
    (void)unwinder;
    (void)st;
}

#endif
