//go:build windows

package proc

import (
	"errors"
	"os/exec"
	"sync"
	"time"
	"unsafe"

	"golang.org/x/sys/windows"
)

// windowsGroup 用 Job Object 实现整树终止：把进程纳入一个设置了
// KILL_ON_JOB_CLOSE 的 Job，此后该进程派生的所有子孙自动归入同一 Job，
// 关闭 Job 句柄即整树终止。优于 Process.Kill（只杀单 PID，漏 node 等子进程）。
type windowsGroup struct {
	job       windows.Handle
	closeOnce sync.Once
}

func newGroup() Group { return &windowsGroup{} }

// Prepare 在 Windows 为 no-op：Job 须在进程 Start 之后才能 Assign。
func (g *windowsGroup) Prepare(cmd *exec.Cmd) {}

// Adopt 必须在 cmd.Start() 之后、子进程派生之前调用。竞态窗口：父进程 exec 后
// 到首个子进程派生之间存在微秒级窗口（Go 无 pre-exec hook 无法完全消除），
// 实践风险极小，文档化为已知限制。
func (g *windowsGroup) Adopt(cmd *exec.Cmd) error {
	if cmd.Process == nil {
		return errors.New("proc: Adopt before Start")
	}
	job, err := windows.CreateJobObject(nil, nil)
	if err != nil {
		return err
	}
	var info windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION
	info.BasicLimitInformation.LimitFlags = windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
	if _, err := windows.SetInformationJobObject(
		job,
		windows.JobObjectExtendedLimitInformation,
		uintptr(unsafe.Pointer(&info)),
		uint32(unsafe.Sizeof(info)),
	); err != nil {
		_ = windows.CloseHandle(job)
		return err
	}
	h, err := windows.OpenProcess(windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE, false, uint32(cmd.Process.Pid))
	if err != nil {
		_ = windows.CloseHandle(job)
		return err
	}
	defer windows.CloseHandle(h)
	if err := windows.AssignProcessToJobObject(job, h); err != nil {
		_ = windows.CloseHandle(job)
		return err
	}
	g.job = job
	return nil
}

// TerminateGroup 在 Windows 无可靠 SIGTERM 等价物，直接关闭 Job 句柄触发
// KILL_ON_JOB_CLOSE 整树终止（grace/done 不参与，仅保持接口签名一致）。
func (g *windowsGroup) TerminateGroup(_ <-chan struct{}, _ time.Duration) {
	g.Close()
}

// Close 幂等且并发安全：TerminateGroup 与 monitor goroutine 可能并发调用，
// 用 sync.Once 保证 CloseHandle 至多执行一次，避免重复关闭句柄（可能误关被复用
// 的句柄）及对 g.job 的数据竞争。不改变 KILL_ON_JOB_CLOSE 语义：句柄关闭即整树终止。
func (g *windowsGroup) Close() {
	g.closeOnce.Do(func() {
		if g.job != 0 {
			_ = windows.CloseHandle(g.job)
			g.job = 0
		}
	})
}
