#if os(Windows)
import Foundation
import WinSDK

// The meaning behind the POSIX vocabulary declared in `WindowsSignals.swift`
// (docs/WINDOWS_PORT.md §4.3): signal delivery, process-tree enumeration, and the per-child Job
// Object that makes "kill the whole tree" actually true on Windows.
//
// Scope note: this covers **host** children only — `git.exe`, `gh`, MinGit helpers, the Build &
// Run panel's commands, and `nucleic-brokerd` itself. Agent processes run inside WSL containers
// on Windows (D2), and those get real POSIX signals through the broker's `proc.signal` RPC, so
// nothing here is on an agent's Stop path.
//
// Verified on Windows 11 amd64 (2026-07-29): compiles and links, and the Carbon suite exercises
// the `git` spawn path that `kill`/`resolvedSpawnTarget` sit behind. The gate is
// `windows/build.ps1 -Target core -Test` on the dev box — there is no Windows GitHub CI (see
// docs/WINDOWS_PORT.md §14).

// MARK: - Signal delivery

/// Windows stand-in for `kill(2)`, for the signals the shared code actually sends.
///
/// * `SIGKILL` → `TerminateProcess`, the true uncatchable equivalent. The exit code is **137**
///   (`128 + 9`) rather than 1, so `Backend.isSIGKILL` and the "was killed" transcript messaging
///   classify a force-killed host command identically to macOS and Linux.
/// * `SIGTERM` / `SIGINT` → a console control event, the closest Windows analogue of a catchable
///   termination request. This only reaches a child that is a process-group leader sharing our
///   console; `Foundation.Process` does not pass `CREATE_NEW_PROCESS_GROUP`, so in practice it is
///   a no-op today and the caller's grace-then-SIGKILL escalation does the work. It is sent
///   anyway (rather than skipped) because it costs nothing, it cannot hit the wrong process — a
///   non-leader pid is not a valid group id and the call simply fails — and it starts working for
///   free the day a spawn site opts into its own group.
///
/// Returns 0 on a delivered signal, -1 otherwise, matching `kill(2)`'s convention. Every shared
/// call site ignores the result: delivery is best-effort on every platform, and the escalation
/// ladder (`ProcessHandle.terminate`, `GitRunner.signalTree`) is what guarantees the process dies.
@discardableResult
func kill(_ pid: pid_t, _ sig: Int32) -> Int32 {
    guard pid > 0 else { return -1 }
    switch sig {
    case SIGKILL:
        return terminateProcess(pid) ? 0 : -1
    case SIGTERM, SIGINT:
        let event = DWORD(sig == SIGINT ? CTRL_C_EVENT : CTRL_BREAK_EVENT)
        return GenerateConsoleCtrlEvent(event, DWORD(pid)).boolValue ? 0 : -1
    default:
        return -1
    }
}

/// `TerminateProcess` with the POSIX-shaped 137 exit code. Best-effort: a pid that has already
/// exited (the common case in a kill sweep) simply fails to open.
private func terminateProcess(_ pid: pid_t) -> Bool {
    guard let handle = OpenProcess(DWORD(PROCESS_TERMINATE), false, DWORD(pid)) else { return false }
    defer { CloseHandle(handle) }
    return TerminateProcess(handle, 137).boolValue
}

// MARK: - Job Object (kill the tree, and never orphan one)

/// A Job Object holding one host child and, transitively, everything it spawns.
///
/// Two problems this solves, both of which POSIX solves differently and neither of which
/// `Foundation.Process` solves on Windows:
///
/// 1. **Killing a tree.** `TerminateProcess` kills exactly one process. A `git push` that hangs
///    in a credential helper, or a build command that forked a compiler farm, leaves the
///    descendants running — and on Windows those descendants keep the child's stdout pipe handles
///    open, so the reader tasks never see EOF and the wait never settles.
/// 2. **Orphans outliving the host.** `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` means the tree dies
///    when this object is released — including when hostd itself exits, since process teardown
///    closes the handle. That is what §11 asks for on `nucleic-brokerd`: the broker (and with it
///    the wslc session's client) can never survive its parent.
///
/// The child is assigned *after* `Process.run()`, because corelibs-foundation does not expose the
/// creation flags or the process handle. The window between spawn and assignment is microseconds
/// wide; a grandchild forked inside it escapes the job (but is still caught by the snapshot-based
/// ``hostDescendantPIDs(of:)`` sweep that `terminateTree` performs).
final class WindowsJobObject: @unchecked Sendable {
    private let handle: HANDLE

    /// Nil when the job couldn't be created or the process couldn't be opened — the caller then
    /// falls back to per-pid termination, which is exactly the POSIX behavior.
    init?(attaching pid: pid_t) {
        guard pid > 0, let job = CreateJobObjectW(nil, nil) else { return nil }
        var limits = JOBOBJECT_EXTENDED_LIMIT_INFORMATION()
        limits.BasicLimitInformation.LimitFlags = DWORD(JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE)
        _ = withUnsafeMutablePointer(to: &limits) { pointer in
            SetInformationJobObject(
                job, JobObjectExtendedLimitInformation, pointer,
                DWORD(MemoryLayout<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>.size))
        }
        guard let process = OpenProcess(
            DWORD(PROCESS_SET_QUOTA | PROCESS_TERMINATE), false, DWORD(pid))
        else {
            CloseHandle(job)
            return nil
        }
        defer { CloseHandle(process) }
        guard AssignProcessToJobObject(job, process).boolValue else {
            CloseHandle(job)
            return nil
        }
        handle = job
    }

    /// Kill every process in the job with the SIGKILL-shaped 137 exit code.
    func terminate() {
        _ = TerminateJobObject(handle, 137)
    }

    deinit { CloseHandle(handle) }
}

// MARK: - Process table / resource sampling

/// `(pid, ppid)` for every process, from a Toolhelp snapshot — the Windows counterpart of
/// Darwin's `sysctl(KERN_PROC_ALL)` and Linux's `/proc` walk. Empty on any failure, which is the
/// documented degradation (callers then signal the root pid alone).
func windowsProcessTable() -> [(pid: pid_t, ppid: pid_t)] {
    let snapshot = CreateToolhelp32Snapshot(DWORD(TH32CS_SNAPPROCESS), 0)
    guard snapshot != INVALID_HANDLE_VALUE else { return [] }
    defer { CloseHandle(snapshot) }

    var entry = PROCESSENTRY32W()
    entry.dwSize = DWORD(MemoryLayout<PROCESSENTRY32W>.size)
    guard Process32FirstW(snapshot, &entry).boolValue else { return [] }
    var table: [(pid: pid_t, ppid: pid_t)] = []
    repeat {
        table.append((pid: pid_t(entry.th32ProcessID), ppid: pid_t(entry.th32ParentProcessID)))
    } while Process32NextW(snapshot, &entry).boolValue
    return table
}

/// Cumulative user+kernel CPU nanoseconds for one process, via `GetProcessTimes` (whose FILETIMEs
/// count 100 ns ticks). `0` on any error — a dead pid contributes nothing to the tree total,
/// matching the Darwin/Linux samplers.
func windowsProcessCPUNanos(_ pid: pid_t) -> UInt64 {
    guard pid > 0,
        let handle = OpenProcess(DWORD(PROCESS_QUERY_LIMITED_INFORMATION), false, DWORD(pid))
    else { return 0 }
    defer { CloseHandle(handle) }
    var creation = FILETIME(), exit = FILETIME(), kernel = FILETIME(), user = FILETIME()
    guard GetProcessTimes(handle, &creation, &exit, &kernel, &user).boolValue else { return 0 }
    return (ticks(kernel) &+ ticks(user)) &* 100
}

/// Cumulative I/O bytes (read + written) for one process, via `GetProcessIoCounters`. The
/// transfer counts include pipe and device traffic, matching Linux's `rchar`/`wchar` more closely
/// than Darwin's disk-only counter — all three are only ever *diffed* by `ProcessStallMonitor` to
/// answer "is this process still moving data", so the exact denominator doesn't matter.
func windowsProcessIOBytes(_ pid: pid_t) -> UInt64 {
    guard pid > 0,
        let handle = OpenProcess(DWORD(PROCESS_QUERY_LIMITED_INFORMATION), false, DWORD(pid))
    else { return 0 }
    defer { CloseHandle(handle) }
    var counters = IO_COUNTERS()
    guard GetProcessIoCounters(handle, &counters).boolValue else { return 0 }
    return counters.ReadTransferCount &+ counters.WriteTransferCount
}

private func ticks(_ time: FILETIME) -> UInt64 {
    UInt64(time.dwHighDateTime) << 32 | UInt64(time.dwLowDateTime)
}
#endif
