package executor

import (
	"context"
	"encoding/json"
	"slices"

	"github.com/cockroachdb/errors"
	kkcorev1alpha1 "github.com/kubesphere/kubekey/api/core/v1alpha1"
	kkprojectv1 "github.com/kubesphere/kubekey/api/project/v1"
	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/kubesphere/kubekey/v4/pkg/converter"
	"github.com/kubesphere/kubekey/v4/pkg/modules"
	"github.com/kubesphere/kubekey/v4/pkg/variable"
)

type blockExecutor struct {
	*option

	// playbook level config
	hosts        []string // which hosts will run playbook
	ignoreErrors *bool    // IgnoreErrors for playbook
	// blocks level config
	blocks []kkprojectv1.Block
	role   string   // role name of blocks
	when   []string // when condition for blocks
	tags   kkprojectv1.Taggable
}

// Exec block. convert block to task and executor it.
func (e blockExecutor) Exec(ctx context.Context) error {
	for _, block := range e.blocks {
		hosts := e.dealRunOnce(block.RunOnce)
		tags := e.dealTags(block.Taggable)
		ignoreErrors := e.dealIgnoreErrors(block.IgnoreErrors)
		when := e.dealWhen(block.When)
		// merge variable which defined in block
		if err := e.variable.Merge(variable.MergeRuntimeVariable(block.Vars.Nodes, hosts...)); err != nil {
			return err
		}

		switch {
		case len(block.Block) != 0:
			if err := e.dealBlock(ctx, hosts, ignoreErrors, when, tags, block); err != nil {
				return err
			}
		case block.IncludeTasks != "":
			// do nothing. include tasks has converted to blocks.
		default:
			// check tags
			if tags.IsEnabled(e.playbook.Spec.Tags, e.playbook.Spec.SkipTags) {
				// if not match the tags. skip
				if err := e.dealTask(ctx, hosts, when, block); err != nil {
					return err
				}
			}
		}
	}

	return nil
}

// dealRunOnce "run_once" argument in block.
// If RunOnce is true, it's always only run in the first host.
// Otherwise, return hosts which defined in parent block.
func (e blockExecutor) dealRunOnce(runOnce bool) []string {
	hosts := e.hosts
	if runOnce {
		// runOnce only run in first node
		hosts = hosts[:1]
	}

	return hosts
}

// dealIgnoreErrors "ignore_errors" argument in block.
// if ignore_errors not defined in block, set it which defined in parent block.
func (e blockExecutor) dealIgnoreErrors(ie *bool) *bool {
	if ie == nil {
		ie = e.ignoreErrors
	}

	return ie
}

// dealTags "tags" argument in block. block tags inherits parent block
func (e blockExecutor) dealTags(taggable kkprojectv1.Taggable) kkprojectv1.Taggable {
	return kkprojectv1.JoinTag(taggable, e.tags)
}

// dealWhen argument in block. block when inherits parent block.
func (e blockExecutor) dealWhen(when kkprojectv1.When) []string {
	w := e.when
	for _, d := range when.Data {
		if !slices.Contains(w, d) {
			w = append(w, d)
		}
	}

	return w
}

// dealBlock handles the execution of a block, including its "block", "rescue", and "always" sections.
// The execution order is: block -> rescue (if block fails) -> always (always runs after block/rescue).
// - If the main block fails and a rescue block is defined, the rescue block is executed.
// - If the main block fails and no rescue block is defined, the error is collected and returned.
// - The always block is executed after the main block (and rescue, if run), regardless of errors.
// All errors encountered are joined and returned.
func (e blockExecutor) dealBlock(ctx context.Context, hosts []string, ignoreErrors *bool, when []string, tags kkprojectv1.Taggable, block kkprojectv1.Block) error {
	var errs error

	// Execute the main block section
	if err := (blockExecutor{
		option:       e.option,
		hosts:        hosts,
		ignoreErrors: ignoreErrors,
		role:         e.role,
		blocks:       block.Block,
		when:         when,
		tags:         tags,
	}.Exec(ctx)); err != nil {
		// If the main block fails and a rescue block is defined, execute the rescue block
		if len(block.Rescue) != 0 {
			if err := (blockExecutor{
				option:       e.option,
				hosts:        hosts,
				ignoreErrors: ignoreErrors,
				blocks:       block.Rescue,
				role:         e.role,
				when:         when,
				tags:         tags,
			}.Exec(ctx)); err != nil {
				// Collect errors from rescue block
				errs = errors.Join(errs, err)
			}
		} else {
			// If no rescue block, collect the error from the main block
			errs = errors.Join(errs, err)
		}
	}

	// Execute the always block after the main/rescue block(s)
	if len(block.Always) != 0 {
		if err := (blockExecutor{
			option:       e.option,
			hosts:        hosts,
			ignoreErrors: ignoreErrors,
			blocks:       block.Always,
			role:         e.role,
			when:         when,
			tags:         tags,
		}.Exec(ctx)); err != nil {
			// Collect errors from always block
			errs = errors.Join(errs, err)
		}
	}

	// Return any collected errors (nil if none)
	return errs
}

// dealTask "block" argument is not defined in block.
func (e blockExecutor) dealTask(ctx context.Context, hosts []string, when []string, block kkprojectv1.Block) error {
	task := converter.MarshalBlock(hosts, when, block)
	// complete module by unknown field
	for n, a := range block.UnknownField {
		data, err := json.Marshal(a)
		if err != nil {
			return errors.Wrapf(err, "failed to marshal block %q unknown filed %q in playbook", block.Name, n, ctrlclient.ObjectKeyFromObject(e.playbook))
		}
		if m := modules.FindModule(n); m != nil {
			task.Spec.Module.Name = n
			task.Spec.Module.Args = runtime.RawExtension{Raw: data}

			break
		}
	}
	if task.Spec.Module.Name == "" { // action is necessary for a task
		return errors.Errorf("no module/action detected in task: [%s]%s", task.Annotations[kkcorev1alpha1.TaskAnnotationRelativePath], task.Spec.Name)
	}
	// complete by playbook
	task.GenerateName = e.playbook.Name + "-"
	task.Namespace = e.playbook.Namespace
	if err := ctrl.SetControllerReference(e.playbook, task, e.client.Scheme()); err != nil {
		return errors.Wrapf(err, "failed to set playbook %q ownerReferences to %q", ctrlclient.ObjectKeyFromObject(e.playbook), block.Name)
	}

	return (&taskExecutor{option: e.option, task: task}).Exec(ctx)
}
