package svctaskrunhandler

import (
	"context"
	"fmt"
	"time"

	"github.com/jmoiron/sqlx"
	"github.com/pingcap/test-infra/services/tcms/utils"

	commM "github.com/pingcap/test-infra/common/model"
	"github.com/pingcap/test-infra/common/model/tcms/request"
	taskM "github.com/pingcap/test-infra/common/model/tcms/taskruns"
)

type PlanExecSvr interface {
	Get(ctx utils.Context, execID commM.ID) (*commM.TestPlanExecution, error)
	CreateOneShotPlanTx(ctx utils.Context, plan *request.PlanCreateRequest, db *sqlx.Tx) (*commM.TestPlan, commM.ID, error)
	CreateExecTx(ctx utils.Context, exec commM.TestPlanExecution, db *sqlx.Tx) (commM.ID, error)
}

type planHandler struct {
	taskClient SvcTaskRefService
	planExec   PlanExecClient
	baseURL    string
}

func (r planHandler) OnCreate(ctx context.Context, obj *taskM.SvcTaskRun) (*taskM.SvcTaskRun, error) {
	return r.submit(ctx, obj)
}

func (r planHandler) OnSync(ctx context.Context, obj *taskM.SvcTaskRun) (*taskM.SvcTaskRun, error) {
	if obj.Status.Implementation.Plan != nil {
		return r.sync(ctx, obj)
	} else {
		return nil, Unrecoverable(fmt.Errorf("must be submitted"))
	}
}

func (r *planHandler) submit(ctx context.Context, obj *taskM.SvcTaskRun) (*taskM.SvcTaskRun, error) {
	task, err := r.taskClient.GetByName(ctx, obj.Spec.SvcTaskName, taskM.GetSvcTaskOpts{})
	if err != nil {
		return nil, err
	}
	var submitted commM.ID
	if task.Implementation.Plan.Spec != nil {
		submitted, err = r.submitOneShotPlan(ctx, *obj, *task)
	} else if task.Implementation.Plan.PlanID != 0 {
		submitted, err = r.submitPlanExec(ctx, *obj, *task)
	} else {
		return nil, Unrecoverable(fmt.Errorf("not implemented"))
	}
	if err != nil {
		return nil, err
	}
	if obj.Status.Implementation.Plan == nil {
		obj.Status.Implementation.Plan = &taskM.PlanExecRef{}
	}
	obj.Status.Implementation.Plan.ExecID = submitted
	return obj, nil
}

func (r planHandler) submitOneShotPlan(ctx context.Context, obj taskM.SvcTaskRun, task taskM.SvcTask) (commM.ID, error) {
	plan, err := renderOneShot(obj, task)
	if err != nil {
		return 0, Unrecoverable(err)
	}
	return r.planExec.CreateOneShotPlan(ctx, plan)
}

func (r planHandler) submitPlanExec(ctx context.Context, obj taskM.SvcTaskRun, task taskM.SvcTask) (commM.ID, error) {
	variables, err := RenderWithInputs(task.Implementation.Plan.VariableBinds, obj.Spec.Inputs, task.Inputs)
	if err != nil {
		return 0, err
	}
	exec := commM.TestPlanExecution{
		PlanID:        task.Implementation.Plan.PlanID,
		PlanIDAndName: commM.PlanIDAndName{ID: task.Implementation.Plan.PlanID},
		VariableBinds: *variables,
		Priority:      commM.ExecPriorityUserDebug,
	}
	return r.planExec.CreateExecTx(ctx, exec)
}

func renderOneShot(obj taskM.SvcTaskRun, task taskM.SvcTask) (*request.PlanCreateRequest, error) {
	rt, err := RenderWithInputs(*task.Implementation.Plan.Spec, obj.Spec.Inputs, task.Inputs)
	if err != nil {
		return nil, err
	}
	rt.Type = commM.TestPlanKindOneShot
	return rt, nil
}

func (r *planHandler) sync(ctx context.Context, obj *taskM.SvcTaskRun) (*taskM.SvcTaskRun, error) {
	exec, err := r.planExec.Get(ctx, obj.Status.Implementation.Plan.ExecID)
	if err != nil {
		return nil, err
	}
	if exec.Status.IsCompleted() {
		obj.Status.Completed = true
		svcTask, err := r.taskClient.GetByName(ctx, obj.Spec.SvcTaskName, taskM.GetSvcTaskOpts{})
		if err != nil {
			return nil, err
		}
		r.collectOutputs(&obj.Status.Outputs, exec, *svcTask)
	}
	return obj, err
}

func (r planHandler) collectOutputs(outputs *taskM.TaskRunOutputs, planExec *commM.TestPlanExecution, task taskM.SvcTask) {
	// todo generate output according to output spec
	exitCode := "0"
	if planExec.Status != commM.ExecutionStatusSuccess {
		exitCode = "1"
	}
	for _, output := range task.Outputs {
		if output.Name == "@exitCode" {
			outputs.SetValue("@exitCode", exitCode)
		}
		if output.Name == "reportURL" {
			outputs.SetValue("reportURL", fmt.Sprintf("%s/api/v1/reports/plan/%d", r.baseURL, planExec.ID))
		}
	}
}

func NewPlanHandler(taskClient SvcTaskRefService, execRepo PlanExecSvr, baseURL string) SvcRunHandler {
	return planHandler{taskClient: taskClient, planExec: planExecClient{delegate: execRepo}, baseURL: baseURL}
}

type PlanExecClient interface {
	Get(ctx context.Context, execID commM.ID) (*commM.TestPlanExecution, error)
	CreateOneShotPlan(ctx context.Context, plan *request.PlanCreateRequest) (commM.ID, error)
	CreateExecTx(ctx context.Context, exec commM.TestPlanExecution) (commM.ID, error)
}

type planExecClient struct {
	delegate PlanExecSvr
}

func (p planExecClient) Get(ctx context.Context, execID commM.ID) (*commM.TestPlanExecution, error) {
	return p.delegate.Get(utils.GetUtilContext(ctx), execID)
}

func (p planExecClient) CreateOneShotPlan(ctx context.Context, plan *request.PlanCreateRequest) (commM.ID, error) {
	tx := GetSqlxTx(ctx)
	if tx == nil {
		panic("should be in transaction")
	}
	_, id, err := p.delegate.CreateOneShotPlanTx(utils.GetUtilContext(ctx), plan, tx)
	return id, err
}

func (p planExecClient) CreateExecTx(ctx context.Context, exec commM.TestPlanExecution) (commM.ID, error) {
	tx := GetSqlxTx(ctx)
	if tx == nil {
		panic("should be in transaction")
	}
	exec.CreatedBy.ID = utils.GetUtilContext(ctx).User().ID
	exec.CreatedAt = commM.Timestamp(time.Now().Unix())
	id, err := p.delegate.CreateExecTx(utils.GetUtilContext(ctx), exec, tx)
	return id, err
}
