package svc_task

import (
	"context"
	"encoding/json"
	"io"
	"os"
	"strconv"
	"time"

	taskCli "github.com/pingcap/test-infra/common/client/tcms/taskruns"
	commM "github.com/pingcap/test-infra/common/model"
	"github.com/pingcap/test-infra/common/model/tcms/taskruns"
	"github.com/pingcap/test-infra/tools/tcctl/pkg/util"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/klog/v2"
)

const (
	ELocalError = -1
	ERemoteError = -2
	EBadResult = -3
)

type RunnerCfg struct {
	Client       taskCli.SvcTaskRunClient
	Timeout      time.Duration
	PollInterval time.Duration
}

type TaskSpec struct {
	TaskName   string
	Args       map[string]string
	OutputFile string
}

type RunResult struct {
	ExitCode int
	Output   map[string]interface{}
}

func Run(ctx context.Context, cfg RunnerCfg, spec TaskSpec) RunResult {
	return TaskRunner{cfg}.Run(ctx, spec)
}

type TaskRunner struct {
	RunnerCfg
}

func (r TaskRunner) Run(ctx context.Context, spec TaskSpec) RunResult {
	c, cancel := context.WithTimeout(ctx, r.Timeout)
	defer cancel()
	obj := taskruns.SvcTaskRun{Spec: taskruns.SvcTaskRunSpec{SvcTaskName: spec.TaskName, Inputs: argToInputs(spec.Args)}}
	obj.FillHeader()
	run, err := r.doRun(c, obj)
	if err != nil {
		klog.ErrorS(err, "run service error")
		return RunResult{ExitCode: ERemoteError}
	}
	out := taskToOutput(*run)
	var writer io.Writer
	if spec.OutputFile ==""{
		writer = io.Discard
	} else if spec.OutputFile == "-" {
		writer = os.Stdout
	} else {
		file, err := os.OpenFile(spec.OutputFile, os.O_WRONLY, os.ModePerm)
		if err != nil {
			klog.ErrorS(err, "open output file")
			out.ExitCode = ELocalError
			return out
		}
		defer file.Close()
		writer = file
	}
	if err != nil {
		return out
	}
	err = json.NewEncoder(writer).Encode(out.Output)
	if err != nil {
		return RunResult{ExitCode: ELocalError}
	}
	return out
}

func (r TaskRunner) doRun(context context.Context, obj taskruns.SvcTaskRun) (*taskruns.SvcTaskRun, error) {
	task, err := r.Client.Create(context, obj, taskruns.CreateSvcTaskRunOpts{})
	if err != nil {
		return nil, err
	}
	if isComplete(*task) {
		return task, nil
	}
	return r.pollUntilComplete(context, task.MetaData.ID)
}

func (r TaskRunner) pollUntilComplete(context context.Context, id commM.ID) (*taskruns.SvcTaskRun, error) {
	var rt *taskruns.SvcTaskRun
	err := wait.PollUntil(r.PollInterval, func() (done bool, err error) {
		task, err := r.Client.Get(context, id, taskruns.GetSvcTaskRunOpts{})
		if err != nil {
			return false, err
		}
		if isComplete(*task) {
			rt = task
			return true, nil
		} else {
			return false, err
		}
	}, context.Done())
	return rt, err
}

func isComplete(task taskruns.SvcTaskRun) bool {
	return task.Status.Completed
}

func taskToOutput(run taskruns.SvcTaskRun) (rt RunResult) {
	var err error
	var existExitCode bool = false
	for _, output := range run.Status.Outputs {
		if output.Name == "@exitCode" {
			existExitCode = true
			rt.ExitCode, err = strconv.Atoi(output.Value)
			if err != nil {
				util.LogErrorf("bad exit code output: %v", err)
				rt.ExitCode = EBadResult
			}
		}
	}
	if !existExitCode {
		rt.ExitCode = EBadResult
	}
	rt.Output = map[string]interface{}{}
	for _, output := range run.Status.Outputs {
		rt.Output[output.Name] = output
	}
	return
}

func argToInputs(arg map[string]string) (rt []taskruns.TaskInputBind) {
	for k, v := range arg {
		rt = append(rt, taskruns.TaskInputBind{Name: k, Value: v})
	}
	return
}
