// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

// Package conpty implements support for Windows pseudo-consoles.
package conpty

import (
	"errors"
	"fmt"
	"io"
	"os"

	"github.com/dblohm7/wingoes"
	"golang.org/x/sys/windows"
	"tailscale.com/util/winutil"
)

var (
	// ErrUnsupported is returned by NewPseudoConsole if the current Windows
	// build does not support this package's API.
	ErrUnsupported = errors.New("conpty unsupported on this version of Windows")
)

// PseudoConsole encapsulates a Windows pseudo-console. Use NewPseudoConsole
// to create a new instance.
type PseudoConsole struct {
	outputRead io.ReadCloser
	inputWrite io.WriteCloser
	console    windows.Handle
}

// NewPseudoConsole creates a new PseudoConsole using size for its initial
// width and height. It requires Windows 10 1809 or newer, and will return
// ErrUnsupported if that requirement is not met.
func NewPseudoConsole(size windows.Coord) (pty *PseudoConsole, err error) {
	if !wingoes.IsWin10BuildOrGreater(wingoes.Win10Build1809) {
		return nil, ErrUnsupported
	}
	if size.X <= 0 || size.Y <= 0 {
		return nil, fmt.Errorf("%w: size must contain positive values", os.ErrInvalid)
	}

	var inputRead, inputWrite windows.Handle
	if err := windows.CreatePipe(&inputRead, &inputWrite, nil, 0); err != nil {
		return nil, err
	}
	defer func() {
		windows.CloseHandle(inputRead)
		if err != nil {
			windows.CloseHandle(inputWrite)
		}
	}()

	var outputRead, outputWrite windows.Handle
	if err := windows.CreatePipe(&outputRead, &outputWrite, nil, 0); err != nil {
		return nil, err
	}
	defer func() {
		windows.CloseHandle(outputWrite)
		if err != nil {
			windows.CloseHandle(outputRead)
		}
	}()

	var console windows.Handle
	if err := windows.CreatePseudoConsole(size, inputRead, outputWrite, 0, &console); err != nil {
		return nil, err
	}

	pty = &PseudoConsole{
		outputRead: os.NewFile(uintptr(outputRead), "ptyOutputRead"),
		inputWrite: os.NewFile(uintptr(inputWrite), "ptyInputWrite"),
		console:    console,
	}
	return pty, nil
}

// Resize sets the width and height of pty to size.
func (pty *PseudoConsole) Resize(size windows.Coord) error {
	if pty.console == 0 {
		return fmt.Errorf("PseudoConsole is closed")
	}
	if size.X <= 0 || size.Y <= 0 {
		return fmt.Errorf("%w: size must contain positive values", os.ErrInvalid)
	}

	return windows.ResizePseudoConsole(pty.console, size)
}

// Close shuts down the pty. The caller must continue reading from the
// ReadCloser returned by Output until either EOF is reached or Close returns;
// failure to adequately drain the ReadCloser may result in Close deadlocking.
func (pty *PseudoConsole) Close() error {
	if pty.console != 0 {
		windows.ClosePseudoConsole(pty.console)
		pty.console = 0
	}

	// now we can stop these
	if pty.outputRead != nil {
		pty.outputRead.Close()
		pty.outputRead = nil
	}
	if pty.inputWrite != nil {
		pty.inputWrite.Close()
		pty.inputWrite = nil
	}
	return nil
}

// ConfigureStartupInfo associates pty with the process to be started using sib.
func (pty *PseudoConsole) ConfigureStartupInfo(sib *winutil.StartupInfoBuilder) error {
	if sib == nil {
		return os.ErrInvalid
	}
	// We need to explicitly set null std handles.
	// Failure to do so causes interference between the pty and the console
	// handles that are implicitly inherited from the parent.
	// This isn't explicitly documented anywhere. Windows Terminal does this too.
	if err := sib.SetStdHandles(0, 0, 0); err != nil {
		return err
	}
	return sib.SetPseudoConsole(pty.console)
}

// OutputPipe returns the ReadCloser for reading pty's output.
func (pty *PseudoConsole) OutputPipe() io.ReadCloser {
	return pty.outputRead
}

// InputPipe returns the WriteCloser for writing pty's output.
func (pty *PseudoConsole) InputPipe() io.WriteCloser {
	return pty.inputWrite
}
