//go:build windows

package api

// Windows ConPTY (Pseudo Console) backend for the terminal tab.
//
// creack/pty/v2 returns "operating system not supported" on Windows, so the
// previous fallback was plain stdin/stdout pipes (terminal_session.go::
// pipeSession). Pipes don't carry TTY signals, so cmd.exe / pwsh / wsl
// detect "no TTY" and either go silent or wait forever — the user sees a
// black screen. This file implements a real pseudo console using the
// kernel32 ConPTY API, so the spawned shell behaves as if it were attached
// to a real terminal: prompts render, ANSI escapes are honoured, resize
// events propagate.
//
// Requires Windows 10 v1809 (build 17763) or newer. On older hosts
// CreatePseudoConsole returns an error and startTermSession_windows falls
// back to pipeSession.

import (
	"fmt"
	"io"
	"os/exec"
	"sync"
	"syscall"
	"unsafe"

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

const (
	procThreadAttributePseudoconsole = 0x00020016
	extendedStartupinfoPresent       = 0x00080000
	createUnicodeEnvironment         = 0x00000400
)

// conptySession drives a Windows pseudo console.
type conptySession struct {
	hPC      windows.Handle
	inWrite  windows.Handle
	outRead  windows.Handle
	procInfo windows.ProcessInformation
	closed   bool
	mu       sync.Mutex
}

// startConptySession spins up the pseudo console, plumbs the pipes, and
// CreateProcessW's the child with the PC attached via STARTUPINFOEX.
func startConptySession(cmd *exec.Cmd) (termSession, error) {
	// 1. Two pipe pairs: in (we write → child stdin) and out (child stdout → we read).
	var inRead, inWrite, outRead, outWrite windows.Handle
	if err := windows.CreatePipe(&inRead, &inWrite, nil, 0); err != nil {
		return nil, fmt.Errorf("create stdin pipe: %w", err)
	}
	if err := windows.CreatePipe(&outRead, &outWrite, nil, 0); err != nil {
		windows.CloseHandle(inRead)
		windows.CloseHandle(inWrite)
		return nil, fmt.Errorf("create stdout pipe: %w", err)
	}

	// 2. Create the pseudo console. After this call ConPTY effectively owns
	//    the child-facing pipe ends (inRead, outWrite); we close our copy.
	var hPC windows.Handle
	sz := windows.Coord{X: 120, Y: 30}
	if err := windows.CreatePseudoConsole(sz, inRead, outWrite, 0, &hPC); err != nil {
		windows.CloseHandle(inRead)
		windows.CloseHandle(inWrite)
		windows.CloseHandle(outRead)
		windows.CloseHandle(outWrite)
		return nil, fmt.Errorf("CreatePseudoConsole: %w", err)
	}
	windows.CloseHandle(inRead)
	windows.CloseHandle(outWrite)

	// 3. Allocate an attribute list with one slot for the PC attribute.
	attrList, err := windows.NewProcThreadAttributeList(1)
	if err != nil {
		windows.ClosePseudoConsole(hPC)
		windows.CloseHandle(inWrite)
		windows.CloseHandle(outRead)
		return nil, fmt.Errorf("NewProcThreadAttributeList: %w", err)
	}
	// PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE is a quirk of the Win32 API: lpValue
	// is the HPCON *value* (cast to PVOID), not a pointer to the handle. If
	// we pass &hPC the kernel reads garbage, the PC attribute is silently
	// ignored, and cmd/pwsh get their own external console window — which is
	// exactly the regression v0.7.6 introduced. The cbSize stays the size of
	// the handle (8 bytes on amd64). Reference: Microsoft EchoCon sample.
	if err := attrList.Update(
		procThreadAttributePseudoconsole,
		unsafe.Pointer(uintptr(hPC)),
		unsafe.Sizeof(hPC),
	); err != nil {
		attrList.Delete()
		windows.ClosePseudoConsole(hPC)
		windows.CloseHandle(inWrite)
		windows.CloseHandle(outRead)
		return nil, fmt.Errorf("attrList.Update: %w", err)
	}

	// 4. Build command line.
	cmdLine, err := buildCommandLine(cmd)
	if err != nil {
		attrList.Delete()
		windows.ClosePseudoConsole(hPC)
		windows.CloseHandle(inWrite)
		windows.CloseHandle(outRead)
		return nil, err
	}
	cmdLineUTF16, err := windows.UTF16PtrFromString(cmdLine)
	if err != nil {
		attrList.Delete()
		windows.ClosePseudoConsole(hPC)
		windows.CloseHandle(inWrite)
		windows.CloseHandle(outRead)
		return nil, err
	}

	// 5. Build the env block (key=value\0...\0\0).
	var envBlock *uint16
	if cmd.Env != nil {
		eb, err := makeEnvBlock(cmd.Env)
		if err != nil {
			attrList.Delete()
			windows.ClosePseudoConsole(hPC)
			windows.CloseHandle(inWrite)
			windows.CloseHandle(outRead)
			return nil, err
		}
		envBlock = eb
	}

	si := windows.StartupInfoEx{}
	si.StartupInfo.Cb = uint32(unsafe.Sizeof(si))
	si.ProcThreadAttributeList = attrList.List()

	flags := uint32(extendedStartupinfoPresent)
	if envBlock != nil {
		flags |= createUnicodeEnvironment
	}

	var pi windows.ProcessInformation
	err = windows.CreateProcess(
		nil, // application name (null = parse from cmdline)
		cmdLineUTF16,
		nil,   // process security attrs
		nil,   // thread security attrs
		false, // inherit handles (ConPTY hands handles via attribute list)
		flags,
		envBlock,
		nil, // working dir
		&si.StartupInfo,
		&pi,
	)
	attrList.Delete()
	if err != nil {
		windows.ClosePseudoConsole(hPC)
		windows.CloseHandle(inWrite)
		windows.CloseHandle(outRead)
		return nil, fmt.Errorf("CreateProcess: %w", err)
	}

	return &conptySession{
		hPC:      hPC,
		inWrite:  inWrite,
		outRead:  outRead,
		procInfo: pi,
	}, nil
}

func (s *conptySession) Read(p []byte) (int, error) {
	var n uint32
	err := windows.ReadFile(s.outRead, p, &n, nil)
	if err != nil {
		if n > 0 {
			return int(n), nil
		}
		return 0, io.EOF
	}
	return int(n), nil
}

func (s *conptySession) Write(p []byte) (int, error) {
	var n uint32
	err := windows.WriteFile(s.inWrite, p, &n, nil)
	if err != nil {
		return int(n), err
	}
	return int(n), nil
}

func (s *conptySession) Resize(rows, cols uint16) error {
	return windows.ResizePseudoConsole(s.hPC, windows.Coord{X: int16(cols), Y: int16(rows)})
}

func (s *conptySession) Close() error {
	s.mu.Lock()
	if s.closed {
		s.mu.Unlock()
		return nil
	}
	s.closed = true
	s.mu.Unlock()

	// Order matters: close the pseudo console first so the child sees EOF,
	// then close our pipe ends, then terminate / close handles.
	windows.ClosePseudoConsole(s.hPC)
	windows.CloseHandle(s.inWrite)
	windows.CloseHandle(s.outRead)
	if s.procInfo.Process != 0 {
		windows.TerminateProcess(s.procInfo.Process, 0)
		windows.CloseHandle(s.procInfo.Process)
	}
	if s.procInfo.Thread != 0 {
		windows.CloseHandle(s.procInfo.Thread)
	}
	return nil
}

func (s *conptySession) Wait() error {
	if s.procInfo.Process == 0 {
		return nil
	}
	_, err := windows.WaitForSingleObject(s.procInfo.Process, windows.INFINITE)
	return err
}

func (s *conptySession) Pid() int {
	return int(s.procInfo.ProcessId)
}

// --- helpers -----------------------------------------------------------

// buildCommandLine produces the Windows command-line string for an
// *exec.Cmd, mirroring what os/exec uses internally (escaping spaces and
// quotes per Windows convention).
func buildCommandLine(cmd *exec.Cmd) (string, error) {
	if cmd.Path == "" {
		return "", fmt.Errorf("empty cmd.Path")
	}
	parts := []string{cmd.Path}
	if len(cmd.Args) > 1 {
		parts = append(parts, cmd.Args[1:]...)
	}
	out := syscall.EscapeArg(parts[0])
	for _, a := range parts[1:] {
		out += " " + syscall.EscapeArg(a)
	}
	return out, nil
}

// makeEnvBlock packs a Go environ slice into the Windows UTF-16 env block
// format: key=value\0key=value\0\0.
func makeEnvBlock(env []string) (*uint16, error) {
	var buf []uint16
	for _, kv := range env {
		s, err := syscall.UTF16FromString(kv)
		if err != nil {
			return nil, err
		}
		buf = append(buf, s...) // includes trailing NUL
	}
	buf = append(buf, 0) // final terminator
	if len(buf) == 0 {
		return nil, nil
	}
	return &buf[0], nil
}

// Compile-time interface assertion.
var _ termSession = (*conptySession)(nil)
