package main

import (
	"bufio"
	"bytes"
	"context"
	"encoding/binary"
	"errors"
	"flag"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"syscall"
	"time"

	"github.com/fxamacker/cbor/v2"
)

// Canonical POSIX errno numbers. The C++ client always decodes an "Errno"
// error via std::generic_category(), regardless of which OS the bridge
// runs on, so these must not be replaced with syscall.ENOENT and friends:
// on Windows those are either raw Win32 error codes or Go-invented
// sentinel values, neither of which are POSIX numbers.
const (
	posixENOENT    = 2
	posixEACCES    = 13
	posixEEXIST    = 17
	posixEINVAL    = 22
	posixENOTEMPTY = 39
)

// Win32 codes with no equivalent among syscall's portable Errno values
// (see isErrno), needed to recognize a condition on Windows that a POSIX
// syscall would report through a different errno than syscall.EINVAL/
// ENOTEMPTY, e.g. os.Readlink on a plain file.
const (
	errorNotAReparsePoint = 4390 // os.Readlink on a non-symlink
	errorDirNotEmpty      = 145  // rmdir on a non-empty directory
)

// True if err carries posixErrno on a POSIX system, or the equivalent
// winCode on Windows - the two are unrelated numbers for the same
// condition. errors.Is matches posixErrno directly, by plain equality
// down err's unwrap chain; win32Errno is compared the same way, since
// syscall.Errno on Windows is just the raw GetLastError() value.
func isErrno(err error, posixErrno syscall.Errno, winCode int) bool {
	return errors.Is(err, posixErrno) || errors.Is(err, syscall.Errno(winCode))
}

// Can be changed from CMakeLists.txt (-X main.MagicPacketMarker=...)
var MagicPacketMarker = "-magic-packet-marker-"

var globalWaitGroup sync.WaitGroup

// Registry of cancel functions for long-running, cancelable jobs (e.g. find),
// keyed by command Id. A "cancel" command looks the Id up here and stops the job
// early so it does not keep streaming results to a client that stopped listening.
var jobCancels = struct {
	sync.Mutex
	funcs map[int]context.CancelFunc
}{funcs: make(map[int]context.CancelFunc)}

func registerCancelable(id int) context.Context {
	ctx, cancel := context.WithCancel(context.Background())
	jobCancels.Lock()
	jobCancels.funcs[id] = cancel
	jobCancels.Unlock()
	return ctx
}

func unregisterCancelable(id int) {
	jobCancels.Lock()
	if cancel, ok := jobCancels.funcs[id]; ok {
		cancel()
		delete(jobCancels.funcs, id)
	}
	jobCancels.Unlock()
}

func cancelJob(id int) {
	jobCancels.Lock()
	if cancel, ok := jobCancels.funcs[id]; ok {
		cancel()
	}
	jobCancels.Unlock()
}

type issamefile struct {
	Path1 string
	Path2 string
}

type socketdatacmd struct {
	Data []byte
}

type command struct {
	Type   string
	Id     int
	ConnId int // identifies a specific connection within a forward (socketdata, socketclose)

	Stat stat
	Exec execute
	Find find
	Is   is
	ReadFile readfile
	WriteFile writefile

	Path string

	Error string

	CopyFile copyfile
	CreateSymLink createsymlink
	RenameFile renamefile
	SetPermissions setpermissions

	Signal signal

	IsSameFile issamefile

	SocketData socketdatacmd
}

type errorresult struct {
	Type  string
	Id    int
	Error string
	ErrorType string
	Errno int
}

type readlinkresult struct {
	Type   string
	Id     int
	Target string
}

type fileidresult struct {
	Type string
	Id   int
	FileId string
}

type freespaceresult struct {
	Type string
	Id   int
	FreeSpace uint64
}

type groupresult struct {
	Type string
	Id   int
	Group string
}

type groupidresult struct {
	Type string
	Id   int
	GroupId int
}

type ownerresult struct {
	Type string
	Id   int
	Owner string
}

type owneridresult struct {
	Type string
	Id   int
	OwnerId int
}

type voidresult struct {
	Type string
	Id   int
}

type environment struct {
	Type string
	Id int
	OsType string
	Env  []string
}

type copyfile struct {
	Source string
	Target string
}

type createsymlink struct {
        Source string
        SymLink string
}

type renamefile struct {
	Source string
	Target string
}

type setpermissions struct {
	Path string
	Mode uint32
}

type createtempfileresult struct {
	Type string
	Id   int
	Path string
}

type createtempdirresult struct {
	Type string
	Id   int
	Path string
}

type signal struct {
	Type string
	Id int
	Pid int
	Signal string
}

type signalsuccess struct {
	Type string
	Id int
}

type issamefileresult struct {
	Type string
	Id   int
	Result bool
}

func readPacket(decoder *cbor.Decoder) (*command, error) {
	cmd := new(command)
	err := decoder.Decode(&cmd)
	return cmd, err
}

func hasBufferedData(decoder *cbor.Decoder) bool {
	n, _ := decoder.Buffered().Read(make([]byte, 1))
	return n > 0
}

func sendError(out chan<- []byte, cmd command, err error) {
	errMsg := err.Error()
	if e, ok := err.(*os.PathError); ok {
		errMsg = e.Err.Error()
	}

	errType := "Error"
	errno := 0
	switch {
	// More specific than fs.ErrExist/fs.ErrNotExist below, which also
	// match on these: syscall.Errno.Is() treats ENOTEMPTY as a kind of
	// "exists" (rmdir on a directory that already has entries in it),
	// and ENOTDIR/ENOENT are otherwise indistinguishable through fs.Err*.
	// fs.ErrInvalid is not one of these: unlike the other three,
	// syscall.Errno never matches it, on any platform, so it can only
	// ever classify errors this function does not itself construct.
	case isErrno(err, syscall.EINVAL, errorNotAReparsePoint):
		errType, errno = "Errno", posixEINVAL
	case isErrno(err, syscall.ENOTEMPTY, errorDirNotEmpty):
		errType, errno = "Errno", posixENOTEMPTY
	case errors.Is(err, fs.ErrPermission):
		errType, errno = "Errno", posixEACCES
	case errors.Is(err, fs.ErrExist):
		errType, errno = "Errno", posixEEXIST
	case errors.Is(err, fs.ErrNotExist):
		errType, errno = "Errno", posixENOENT
	}

	result, _ := cbor.Marshal(errorresult{
		Type:  "error",
		Id:    cmd.Id,
		Error: errMsg,
		ErrorType: errType,
		Errno: errno,
	})
	out <- result
}

func processReadLink(cmd command, out chan<- []byte) {
	linkTarget, err := os.Readlink(cmd.Path)
	if err != nil {
		sendError(out, cmd, err)
		return
	}

	result, _ := cbor.Marshal(readlinkresult{
		Type:   "readlinkresult",
		Id:     cmd.Id,
		Target: linkTarget,
	})
	out <- result
}

func processFileId(cmd command, out chan<- []byte) {
	result, _ := cbor.Marshal(fileidresult{
		Type:         "fileidresult",
		Id:           cmd.Id,
		FileId:       fileId(cmd.Path),
	})
	out <- result
}

func processFreespace(cmd command, out chan<- []byte) {
	result, _ := cbor.Marshal(freespaceresult{
		Type:         "freespaceresult",
		Id:           cmd.Id,
		FreeSpace:    freeSpace(cmd.Path),
	})
	out <- result
}

func processGroup(cmd command, out chan<- []byte) {
	result, _ := cbor.Marshal(groupresult{
		Type:         "groupresult",
		Id:           cmd.Id,
		Group:        group(cmd.Path),
	})
	out <- result
}

func processGroupId(cmd command, out chan<- []byte) {
	result, _ := cbor.Marshal(groupidresult{
		Type:         "groupidresult",
		Id:           cmd.Id,
		GroupId:      groupId(cmd.Path),
	})
	out <- result
}

func processOwner(cmd command, out chan<- []byte) {
	result, _ := cbor.Marshal(ownerresult{
		Type:         "ownerresult",
		Id:           cmd.Id,
		Owner:        owner(cmd.Path),
	})
	out <- result
}

func processOwnerId(cmd command, out chan<- []byte) {
	result, _ := cbor.Marshal(owneridresult{
		Type:         "owneridresult",
		Id:           cmd.Id,
		OwnerId:      ownerId(cmd.Path),
	})
	out <- result
}

func processRemove(cmd command, out chan<- []byte) {
	err := os.Remove(cmd.Path)
	if err != nil {
		sendError(out, cmd, err)
		return
	}

	result, _ := cbor.Marshal(voidresult{
		Type: "removeresult",
		Id:   cmd.Id,
	})
	out <- result
}

func processRemoveAll(cmd command, out chan<- []byte) {
	err := os.RemoveAll(cmd.Path)
	if err != nil {
		sendError(out, cmd, err)
		return
	}

	result, _ := cbor.Marshal(voidresult{
		Type: "removeallresult",
		Id:   cmd.Id,
	})
	out <- result
}

func processEnsureExistingFile(cmd command, out chan<- []byte) {
	_, err := os.Stat(cmd.Path)
	if err != nil {
		file, err := os.Create(cmd.Path)
		if err != nil {
			sendError(out, cmd, err)
			return
		}
		file.Close()
	}

	result, _ := cbor.Marshal(voidresult{
		Type: "ensureexistingfileresult",
		Id:   cmd.Id,
	})
	out <- result
}

func processCreateDir(cmd command, out chan<- []byte) {
	err := os.MkdirAll(cmd.Path, 0755)
	if err != nil {
		sendError(out, cmd, err)
		return
	}

	result, _ := cbor.Marshal(voidresult{
		Type: "createdirresult",
		Id:   cmd.Id,
	})
	out <- result
}

func processCopyFile(cmd command, out chan<- []byte) {
	src, err := os.Open(cmd.CopyFile.Source)
	if err != nil {
		sendError(out, cmd, err)
		return
	}
	defer src.Close()

	dst, err := os.Create(cmd.CopyFile.Target)
	if err != nil {
		sendError(out, cmd, err)
		return
	}
	defer dst.Close()

	_, err = io.Copy(dst, src)
	if err != nil {
		sendError(out, cmd, err)
		return
	}

        // Copy over the execute permissions from the source.
        // This emulates the behavior of CopyFS.
        sourceStat, err := os.Stat(cmd.CopyFile.Source)
        if err != nil {
                sendError(out, cmd, err)
                return
        }
        targetStat, err := os.Stat(cmd.CopyFile.Target)
        if err != nil {
                sendError(out, cmd, err)
                return
        }
        const executeBits = os.FileMode(0111)
        newMode := (targetStat.Mode() &^ executeBits) | (sourceStat.Mode() & executeBits)
        err = os.Chmod(cmd.CopyFile.Target, newMode)
        if err != nil {
                sendError(out, cmd, err)
                return
        }

	result, _ := cbor.Marshal(voidresult{
		Type: "copyfileresult",
		Id:   cmd.Id,
	})
	out <- result
}

func processCreateSymLink(cmd command, out chan<- []byte) {
        err := os.Symlink(cmd.CreateSymLink.Source, cmd.CreateSymLink.SymLink)
        if err != nil {
                sendError(out, cmd, err)
                return
        }

        result, _ := cbor.Marshal(voidresult{
                Type: "createsymlinkresult",
                Id:   cmd.Id,
        })
        out <- result
}

func processRenameFile(cmd command, out chan<- []byte) {
	err := os.Rename(cmd.RenameFile.Source, cmd.RenameFile.Target)
	if err != nil {
		sendError(out, cmd, err)
		return
	}

	result, _ := cbor.Marshal(voidresult{
		Type: "renamefileresult",
		Id:   cmd.Id,
	})
	out <- result
}

func processCreateTempDir(cmd command, out chan<- []byte) {
	dir := filepath.Dir(cmd.Path)
	template := filepath.Base(cmd.Path)

	tempDir, err := os.MkdirTemp(dir, template)
	if err != nil {
		sendError(out, cmd, err)
		return
	}

	result, _ := cbor.Marshal(createtempdirresult{
		Type: "createtempdirresult",
		Id:   cmd.Id,
		Path: tempDir,
	})
	out <- result
}

func processCreateTempFile(cmd command, out chan<- []byte) {
	dir := filepath.Dir(cmd.Path)
	template := filepath.Base(cmd.Path)

	file, err := os.CreateTemp(dir, template)
	if err != nil {
		sendError(out, cmd, err)
		return
	}
	file.Close()

	result, _ := cbor.Marshal(createtempfileresult{
		Type: "createtempfileresult",
		Id:   cmd.Id,
		Path: file.Name(),
	})
	out <- result
}

func processSetPermissions(cmd command, out chan<- []byte) {
	err := os.Chmod(cmd.SetPermissions.Path, os.FileMode(cmd.SetPermissions.Mode))
	if err != nil {
		sendError(out, cmd, err)
		return
	}

	result, _ := cbor.Marshal(voidresult{
		Type: "setpermissionsresult",
		Id:   cmd.Id,
	})
	out <- result
}

func processSignal(cmd command, out chan<- []byte) {
	proc, err := os.FindProcess(cmd.Signal.Pid)
	if err != nil {
		sendError(out, cmd, err)
		return
	}

	switch(cmd.Signal.Signal) {
	case "terminate":
		err = proc.Signal(syscall.SIGTERM)
	case "kill":
		err = proc.Kill()
	case "interrupt":
		err = proc.Signal(syscall.SIGINT)
	}

	if err != nil {
		sendError(out, cmd, err)
		return
	}

	data, _ := cbor.Marshal(signalsuccess{
		Type: "signalsuccess",
		Id: cmd.Id,
	})
	out <- data
}

func processIsSameFile(cmd command, out chan<- []byte) {
	fileInfo1, err1 := os.Stat(cmd.IsSameFile.Path1)
	if err1 != nil {
		sendError(out, cmd, err1)
		return
	}

	fileInfo2, err2 := os.Stat(cmd.IsSameFile.Path2)
	if err2 != nil {
		sendError(out, cmd, err2)
		return
	}
	same := os.SameFile(fileInfo1, fileInfo2)

	result, _ := cbor.Marshal(issamefileresult{
		Type: "issamefileresult",
		Id:   cmd.Id,
		Result: same,
	})
	out <- result
}

func processCommand(watcher *WatcherHandler, socketHandler *SocketForwardHandler, cmd command, out chan<- []byte) {
	defer globalWaitGroup.Done()

	switch cmd.Type {
	case "ping":
		// just a keepalive
	case "cancel":
		// Ask a running cancelable job to stop early. If no such job is
		// registered it has already finished, so its terminal packet has been
		// (or will be) delivered through the normal path and there is nothing
		// more to do.
		cancelJob(cmd.Id)
	case "copyfile":
		processCopyFile(cmd, out)
        case "createsymlink":
                processCreateSymLink(cmd, out)
	case "createdir":
		processCreateDir(cmd, out)
	case "createtempfile":
		processCreateTempFile(cmd, out)
	case "createtempdir":
		processCreateTempDir(cmd, out)
	case "ensureexistingfile":
		processEnsureExistingFile(cmd, out)
	case "exec":
		processExec(cmd, out)
	case "fileid":
		processFileId(cmd, out)
	case "find":
		processFind(cmd, out)
	case "freespace":
		processFreespace(cmd, out)
	case "group":
		processGroup(cmd, out)
	case "groupId":
		processGroupId(cmd, out)
	case "is":
		processIs(cmd, out)
	case "signal":
		processSignal(cmd, out)
	case "readfile":
		processReadFile(cmd, out)
	case "owner":
		processOwner(cmd, out)
	case "ownerid":
		processOwnerId(cmd, out)
	case "readlink":
		processReadLink(cmd, out)
	case "remove":
		processRemove(cmd, out)
	case "removeall":
		processRemoveAll(cmd, out)
	case "renamefile":
		processRenameFile(cmd, out)
	case "setpermissions":
		processSetPermissions(cmd, out)
	case "stat":
		processStat(cmd, out)
	case "stopwatch":
		watcher.processStop(cmd, out)
	case "watch":
		watcher.processAdd(cmd, out)
	case "writefile":
		processWriteFile(cmd, out)
	case "issamefile":
		processIsSameFile(cmd, out)
	case "forwardlocalsocketserver":
		socketHandler.processForward(cmd, out)
	case "socketdata":
		socketHandler.processData(cmd)
	case "socketclose":
		socketHandler.processClose(cmd)
	case "stopforwardserver":
		socketHandler.processStopForward(cmd, out)
	case "error":
		result, _ := cbor.Marshal(errorresult{
			Type:  "error",
			Id:    cmd.Id,
			Error: cmd.Error,
		})
		out <- result
	case "exit":
		os.Exit(0)
	}
}

func executor(watcher *WatcherHandler, socketHandler *SocketForwardHandler, commands <-chan command, out chan<- []byte) {
	for cmd := range commands {
		globalWaitGroup.Add(1)
		go processCommand(watcher, socketHandler, cmd, out)
	}
}

func outputter(out <-chan []byte) {
	writer := bufio.NewWriter(os.Stderr)
	for data := range out {
		if len(data) == 0 {
			break
		}
		writer.Write([]byte(MagicPacketMarker))
		binary.Write(writer, binary.BigEndian, uint32(len(data)))
		writer.Write(data)
		writer.Flush()
	}
}

func prettyOutputter(out <-chan []byte) {
	for data := range out {
		if len(data) == 0 {
			break
		}

		reader := bytes.NewReader(data)
		// As long as reader has data ...
		decoder := cbor.NewDecoder(reader)

		for reader.Len() > 0 {
			var result errorresult;
			err := decoder.Decode(&result)
			if err != nil {
				fmt.Fprint(os.Stderr, err)
				continue
			}
			fmt.Fprint(os.Stderr, result.Type, " ", result.Error," ",reader.Len(), "\n")
		}
	}
}

func writeMain(out *bufio.Writer) {
	// This can be used to quickly test commands. Start with "--test" flag.
	encoder := cbor.NewEncoder(out)

	//encoder.Encode(command{
	//	Id:   1,
	//	Type: "find",
	//	Find: find{Directory: "/tmp/"},
	//})

	encoder.Encode(command{
		Id:   1,
		Type: "stat",
		Stat: stat{Path: "/Users/Users"},
	})

	// /Users/mtillmanns/tmp/untitled/CMakeLists.txt.user.df20d15
	encoder.Encode(command{
		Id:   1,
		Type: "readfile",
		ReadFile: readfile{Path: "/Users/mtillmanns/tmp/untitled/CMakeLists.txt.user.df20d15", Offset: 0, Limit: -1},
	})
	// encoder.Encode(command{
	// 	Id: 1,
	// 	Type: "watch",
	// 	Path: "/private/tmp",
	// })

	out.Flush()
}

func watchDogLoop(channel chan bool, watchDogTimeOut time.Duration) {
	if watchDogTimeOut <= 0 {
		// Watchdog disabled. Drain the channel so senders do not block.
		for range channel {
		}
		return
	}

	timer := time.NewTimer(watchDogTimeOut)

	for {
		select {
		case turnOn := <-channel:
		    if turnOn {
				timer.Reset(watchDogTimeOut)
			} else {
				timer.Stop()
			}
		case <-timer.C:
			// If we don't get a signal for one minute, we assume that the connection is dead.
			fmt.Fprintln(os.Stderr, "Watchdog timeout, exiting.")
			os.Exit(100)
		}
	}
}

func readMain(test bool, watchDogTimeOut time.Duration) {
	commandChannel := make(chan command)
	outputChannel := make(chan []byte)

	watchDogChannel := make(chan bool)
	go watchDogLoop(watchDogChannel, watchDogTimeOut)

	watcher := NewWatcherHandler()
	socketHandler := NewSocketForwardHandler()

	var outputWG sync.WaitGroup
	outputWG.Add(1)
	go func() {
		defer outputWG.Done()
		if test {
			prettyOutputter(outputChannel)
		} else {
			outputter(outputChannel)
		}
	}()

	globalWaitGroup.Add(1)
	go func() {
		defer globalWaitGroup.Done()
		executor(watcher, socketHandler, commandChannel, outputChannel)
	}()

	globalWaitGroup.Add(1)
	go func() {
		defer globalWaitGroup.Done()
		watcher.start(outputChannel)
	}()

	var in *bufio.Reader
	if test {
		var buf bytes.Buffer
		{
			out := bufio.NewWriter(&buf)
			writeMain(out)
		}
		in = bufio.NewReader(&buf)
	} else {
		in = bufio.NewReader(os.Stdin)
	}
	decoder := cbor.NewDecoder(in)
	for {
		_, err := in.Peek(1)
		if err == io.EOF {
		    time.Sleep(50*time.Millisecond)
			continue
		}
		// disable watchdog while we are busy processing data
		watchDogChannel <- false
		// One read can carry several commands, and the decoder keeps what it did
		// not decode yet. Those bytes are gone from the reader Peek() looks at, so
		// keep decoding as long as the decoder still has some.
		eof := false
		for more := true; more; more = hasBufferedData(decoder) {
			cmd, err := readPacket(decoder)
			if err == io.EOF {
				close(commandChannel)
				eof = true
				break
			} else if err != nil {
				commandChannel <- command{Type: "error", Id: cmd.Id, Error: err.Error()}
			} else {
				commandChannel <- *cmd
			}
		}
		if eof {
			break
		}
		// reset watchdog timer
		watchDogChannel <- true
	}

	globalWaitGroup.Wait()

	// Tells the outputter to finish sending.
	outputChannel <- []byte{}

	outputWG.Wait()
}

// writePidMarker reports pid substituted into marker's %1, the format the
// launcher parses off stdout. An empty marker reports nothing; one without a
// hole for the pid reports nothing either, since a line that does not frame a
// pid is output of the started command as far as the launcher can tell.
func writePidMarker(w io.Writer, marker string, pid int) error {
	if marker == "" {
		return nil
	}
	if !strings.Contains(marker, "%1") {
		return fmt.Errorf("pidMarker %q has no %%1 to report the pid in", marker)
	}
	_, err := fmt.Fprintln(w, strings.Replace(marker, "%1", strconv.Itoa(pid), 1))
	return err
}

func main() {
	test := flag.Bool("test", false, "test instead of read from stdin")
	write := flag.Bool("write", false, "write instead of read data")
	deleteOnStart := flag.Bool("deleteOnStart", false, "delete cmdbridge directly on startup")
	watchDogTimeout := flag.Int("watchdogTimeout", 60,
		"watchdog timeout in seconds; 0 disables the watchdog")
	pidMarker := flag.String("pidMarker", "",
		"report our pid on stdout, substituted into this template's %1")

	flag.Parse()

	// Before anything below, all of which can fail.
	if err := writePidMarker(os.Stdout, *pidMarker, os.Getpid()); err != nil {
		fmt.Fprintln(os.Stderr, err)
	}

	if *deleteOnStart {
		executable, err := os.Executable()
		if err == nil {
			err := os.Remove(executable)
			if (err != nil) {
				fmt.Fprintln(os.Stderr, "deleteOnStart: Error deleting executable:", err)
			}
		} else {
			fmt.Fprintln(os.Stderr, "deleteOnStart: Error getting executable path:", err)
		}
	}

    if *write {
		writeMain(bufio.NewWriter(os.Stdout))
	} else {
		readMain(*test, time.Duration(*watchDogTimeout)*time.Second)
	}
}
