package deploy

import (
	"context"
	"fmt"
	"time"

	"go.uber.org/multierr"
	ctrl "sigs.k8s.io/controller-runtime"
)

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: %v", r.stepType, r.err)
	}
	return nil
}

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) WithRequeueAfter(d time.Duration) Result {
	r.err = 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., Network or Cluster
type TaskCategory string

const (
	O11yTask TaskCategory = "O11yTask"
)

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 := ShouldRequeue(err)
			res = minResult(tmpRes, res)

			if 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

	createResult Result
}

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

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

var _ Task = &TaskWraper{}

func (t *TaskWraper) Create(ctx context.Context) Result {
	res := t.Task.Create(ctx)
	t.createResult = res
	return res
}

func (t *TaskWraper) PostCreate(ctx context.Context) Result {
	res := t.Task.PostCreate(ctx)

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

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

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