package deploy

import (
	"context"
	"fmt"
	"time"

	"go.opentelemetry.io/otel/codes"
	"go.opentelemetry.io/otel/trace"
	"go.uber.org/multierr"
	ctrl "sigs.k8s.io/controller-runtime"

	"github.com/tidbcloud/ticdc-service-ng/pkg/k8s"
	"github.com/tidbcloud/ticdc-service-ng/pkg/util"
)

type state string

const (
	// Task finished with no errors.
	stateDone state = "Done"
	// Task finished with errors.
	stateError state = "Error"
	// Task finished with errors. The post action should NOT be run.
	stateFatal state = "Fatal"
	// Task finished with errors. The entire deployment should stop.
	// TODO(tangent): Consider how reconcile requeue should work during panic
	statePanic state = "Panic"
)

type stepType string

const (
	stepCreate     stepType = "Create"
	stepPostCreate stepType = "PostCreate"
	stepDelete     stepType = "Delete"
	stepPostDelete stepType = "PostDelete"
)

type Result struct {
	task     string
	stepType stepType

	state state
	err   error
}

func (r *Result) Error() error {
	switch r.state {
	case stateError, stateFatal, statePanic:
		return fmt.Errorf("%s: %w", r.stepType, r.err)
	}
	return nil
}

func (r *Result) RawError() error {
	return r.err
}

func (r Result) IsDoneState() bool {
	return r.state == stateDone
}

func Done() Result {
	return Result{
		state: stateDone,
	}
}

func PanicOrDone(err error) Result {
	if err == nil {
		return Done()
	}
	return Panic(err)
}

func ErrorOrDone(err error) Result {
	if err == nil {
		return Done()
	}
	return Error(err)
}

func Error(err error) Result {
	return Result{
		state: stateError,
		err:   err,
	}
}

func Fatal(err error) Result {
	return Result{
		state: stateFatal,
		err:   err,
	}
}

func Panic(err error) Result {
	return Result{
		state: statePanic,
		err:   err,
	}
}

func (r Result) WithRequeuAfter(d time.Duration) Result {
	if r.state == stateDone {
		r.err = k8s.NewRetriableError(k8s.RequeueOnlyError, ctrl.Result{Requeue: true, RequeueAfter: d})
	} else {
		r.err = k8s.NewRetriableError(r.err, ctrl.Result{Requeue: true, RequeueAfter: d})
	}
	return r
}

func (r Result) WithName(name string) Result {
	r.task = name
	return r
}

// TaskCategory defines the category of the task,
// i.e., CDC or SinkEndpoint
type TaskCategory string

const (
	CDCTask          TaskCategory = "CDCTask"
	SinkEndpointTask TaskCategory = "SinkEndpointTask"
)

type Task interface {
	Name() string
	Category() TaskCategory

	Create(context.Context) Result
	PostCreate(context.Context) Result

	Delete(context.Context) Result
	PostDelete(context.Context) Result

	MetricsStartTime() *time.Time
}

func minResult(lhs ctrl.Result, rhs ctrl.Result) ctrl.Result {
	if lhs.RequeueAfter > 0 {
		lhs.Requeue = true
	}
	if rhs.RequeueAfter > 0 {
		rhs.Requeue = true
	}

	if lhs.Requeue != rhs.Requeue {
		if lhs.Requeue {
			return lhs
		}
		return rhs
	}

	if lhs.RequeueAfter < rhs.RequeueAfter {
		return lhs
	}
	return rhs
}

// MinRequeue iteratures tasks' results, returns the minimum requeue
// duration and the errs from all tasks.
func MinRequeue(results []Result) (res ctrl.Result, errs error, retriableErrs error) {
	for _, r := range results {
		if err := r.err; err != nil {
			tmpRes := k8s.ShouldRequeue(err)
			res = minResult(tmpRes, res)

			if k8s.RequeueOnly(err) {
				continue
			}

			err := fmt.Errorf("%s: %w", r.task, err)
			if !tmpRes.IsZero() {
				retriableErrs = multierr.Append(retriableErrs, err)
			} else {
				errs = multierr.Append(errs, err)
			}
		}
	}
	return
}

type TaskWraper struct {
	Task
	hook         CreateSuccessHook
	tracer       trace.Tracer
	createResult Result
}

func NewTaskWraper(task Task, hook CreateSuccessHook, tracer trace.Tracer) *TaskWraper {
	return &TaskWraper{
		Task:   task,
		hook:   hook,
		tracer: tracer,
	}
}

type CreateSuccessHook func(ctx context.Context, task Task)

var _ Task = &TaskWraper{}

func (t *TaskWraper) Create(ctx context.Context) (res Result) {
	spanCtx, span := t.tracer.Start(ctx, fmt.Sprintf("%s-Create", t.Name()))
	defer func() { EndSpan(span, res.Error()) }()

	res = t.Task.Create(spanCtx)
	t.createResult = res
	return res
}

func (t *TaskWraper) PostCreate(ctx context.Context) (res Result) {
	spanCtx, span := t.tracer.Start(ctx, fmt.Sprintf("%s-PostCreate", t.Name()))
	defer func() { EndSpan(span, res.Error()) }()

	res = t.Task.PostCreate(spanCtx)

	// ref `MinRequeue` about "how is truly error"
	if t.createResult.err != nil && !k8s.RequeueOnly(t.createResult.err) {
		return res
	}

	if res.err != nil && !k8s.RequeueOnly(res.err) {
		return res
	}

	t.hook(ctx, t.Task)
	return res
}

type emptyTask struct {
	name     string
	category TaskCategory
	beginAt  time.Time
}

func (t *emptyTask) Name() string {
	return t.name
}

func (t *emptyTask) Category() TaskCategory {
	return t.category
}

func (t *emptyTask) Create(ctx context.Context) Result {
	t.beginAt = time.Now()
	return Done()
}

func (t *emptyTask) PostCreate(ctx context.Context) Result {
	return Done()
}

func (t *emptyTask) Delete(ctx context.Context) Result {
	t.beginAt = time.Now()
	return Done()
}

func (t *emptyTask) PostDelete(ctx context.Context) Result {
	return Done()
}

func (t emptyTask) MetricsStartTime() *time.Time {
	return &t.beginAt
}

var _ Task = &emptyTask{}

func NewEmptyTask(name string, category TaskCategory) Task {
	return &emptyTask{name: name, category: category}
}

func EndSpan(span trace.Span, err error) {
	if util.IsRealError(err) {
		span.RecordError(err)
		span.SetStatus(codes.Error, err.Error())
	}
	span.End()
}
