package local

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"os"
	"os/exec"
	"syscall"

	apiresource "github.com/pingcap/test-infra/common/model/resource"
	"github.com/pingcap/test-infra/sdk/core"
	"github.com/pingcap/test-infra/sdk/resource"
)

var _ resource.WorkloadNode = &WorkloadNode{}

type WorkloadNode struct {
	// api object
	apiresource.WorkloadNode
}

type RemoteCommander struct {
	cmd           *exec.Cmd
	interruptible bool
}

func (j *RemoteCommander) Interrupt() error {
	if j.interruptible {
		// Kill it:
		if err := j.cmd.Process.Signal(syscall.SIGTERM); err != nil {
			return err
		}
		return nil
	}
	return fmt.Errorf("uninterruptible")
}

func (j *RemoteCommander) Wait() resource.RemoteCommandErr {
	var exitCode int
	err := j.cmd.Wait()
	if exiterr, ok := err.(*exec.ExitError); ok {
		// The program has exited with an exit code != 0
		exitCode = exiterr.ExitCode()
	}
	return resource.RemoteCommandErr{
		Err:      err,
		ExitCode: exitCode,
	}
}

func (t *WorkloadNode) Handle() apiresource.ResourceHandle {
	return t.WorkloadNode.ResourceHandle
}

func (t *WorkloadNode) Exec(opts resource.WorkloadNodeExecOptions) (outStr string, errStr string, exitCode int, err error) {
	cmd := exec.Command("/bin/sh", "-c", opts.Command)
	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr

	err = cmd.Run()
	if exiterr, ok := err.(*exec.ExitError); ok {
		// The program has exited with an exit code != 0
		exitCode = exiterr.ExitCode()
	}
	outStr, errStr = stdout.String(), stderr.String()
	return
}

func (t *WorkloadNode) ExecAsync(stdout, stderr io.WriteCloser, opts resource.WorkloadNodeExecOptions) (remote resource.RemoteCommander, err error) {
	cmd := exec.Command("/bin/sh", "-c", opts.Command)
	cmd.Stdout = stdout
	cmd.Stderr = stderr
	if err = cmd.Start(); err != nil {
		return
	}
	remote = &RemoteCommander{
		cmd:           cmd,
		interruptible: false,
	}
	return
}

func (t *WorkloadNode) ExecAsyncInterruptible(stdout io.WriteCloser, opts resource.WorkloadNodeExecOptions) (remote resource.RemoteCommanderInterruptible, err error) {
	cmd := exec.Command("/bin/sh", "-c", opts.Command)
	cmd.Stdout = stdout
	cmd.Stderr = stdout
	if err = cmd.Start(); err != nil {
		return
	}
	remote = &RemoteCommander{
		cmd:           cmd,
		interruptible: true,
	}
	return
}

func (t *WorkloadNode) ChaosSelect(selector resource.WorkloadNodeSelectorOpts) ([]resource.ChaosSelector, error) {
	panic("unavailable api")
}

func (t *WorkloadNode) ChaosClient() resource.ChaosClient {
	panic("unavailable api")
}

func (t *WorkloadNode) WriteFile(opts resource.WorkloadNodeWriteFileOptions) (err error) {
	fo, err := os.OpenFile(opts.FilePath, os.O_CREATE|os.O_RDWR, 0666)
	if err != nil {
		return
	}

	defer fo.Close()
	w := bufio.NewWriter(fo)

	// make a buffer to keep chunks that are read
	buf := make([]byte, 1024)
	for {
		// read a chunk
		n, err := opts.Content.Read(buf)
		if err != nil && err != io.EOF {
			return err
		}
		if n == 0 {
			break
		}

		// write a chunk
		if _, err := w.Write(buf[:n]); err != nil {
			return err
		}
	}

	if err = w.Flush(); err != nil {
		return
	}
	return
}

type workloadNodeGetter struct{ handle apiresource.ResourceHandle }

func (t *workloadNodeGetter) Get() (core.Resource, error) {
	return &WorkloadNode{
		WorkloadNode: apiresource.WorkloadNode{
			ResourceHandle: t.handle,
		},
	}, nil
}

func init() {
	core.RegisterProvider(resource.FQDN(WorkloadNode{}),
		func(handle apiresource.ResourceHandle) core.ResourceGetter {
			return &workloadNodeGetter{handle: handle}
		})
}
