package interpret

import (
	"fmt"
	"strings"

	"github.com/spf13/cobra"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/util/errors"
	"k8s.io/cli-runtime/pkg/genericclioptions"
	"k8s.io/cli-runtime/pkg/resource"
	cmdutil "k8s.io/kubectl/pkg/cmd/util"
	"k8s.io/kubectl/pkg/cmd/util/editor"
	"k8s.io/kubectl/pkg/util/templates"

	configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1"
	workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2"
	"github.com/karmada-io/karmada/pkg/karmadactl/options"
	"github.com/karmada-io/karmada/pkg/karmadactl/util"
	"github.com/karmada-io/karmada/pkg/karmadactl/util/genericresource"
	"github.com/karmada-io/karmada/pkg/util/gclient"
	"github.com/karmada-io/karmada/pkg/util/helper"
	"github.com/karmada-io/karmada/pkg/util/interpreter"
)

var (
	interpretLong = templates.LongDesc(`
        Validate, test and edit interpreter customization before applying it to the control plane.

        1. Validate the ResourceInterpreterCustomization configuration as per API schema
           and try to load the scripts for syntax check.

        2. Run the rules locally and test if the result is expected. Similar to the dry run.

		3. Edit customization. Similar to the kubectl edit.
`)

	interpretExample = templates.Examples(`
        # Check the customizations in file
        %[1]s interpret -f customization.json --check

		# Execute the retention rule
        %[1]s interpret -f customization.yml --operation retain --desired-file desired.yml --observed-file observed.yml

		# Execute the replicaResource rule
        %[1]s interpret -f customization.yml --operation interpretReplica --observed-file observed.yml

		# Execute the replicaRevision rule
        %[1]s interpret -f customization.yml --operation reviseReplica --observed-file observed.yml --desired-replica 2

		# Execute the statusReflection rule
        %[1]s interpret -f customization.yml --operation interpretStatus --observed-file observed.yml

		# Execute the healthInterpretation rule
        %[1]s interpret -f customization.yml --operation interpretHealth --observed-file observed.yml

		# Execute the dependencyInterpretation rule 
        %[1]s interpret -f customization.yml --operation interpretDependency --observed-file observed.yml

		# Execute the statusAggregation rule
        %[1]s interpret -f customization.yml --operation aggregateStatus --observed-file observed.yml --status-file status.yml

		# Fetch observed object from url, and status items from stdin (specified with -)
        %[1]s interpret -f customization.yml --operation aggregateStatus --observed-file https://example.com/observed.yml --status-file -

		# Edit customization
		%[1]s interpret -f customization.yml --edit
`)
)

const (
	customizationResourceName = "resourceinterpretercustomizations"
)

// NewCmdInterpret new interpret command.
func NewCmdInterpret(f util.Factory, parentCommand string, streams genericclioptions.IOStreams) *cobra.Command {
	editorFlags := editor.NewEditOptions(editor.NormalEditMode, streams)
	editorFlags.PrintFlags = editorFlags.PrintFlags.WithTypeSetter(gclient.NewSchema())

	o := &Options{
		EditOptions: editorFlags,
		IOStreams:   streams,
		Rules:       interpreter.AllResourceInterpreterCustomizationRules,
	}
	cmd := &cobra.Command{
		Use:                   "interpret (-f FILENAME) (--operation OPERATION) [--ARGS VALUE]... ",
		Short:                 "Validate, test and edit interpreter customization before applying it to the control plane",
		Long:                  interpretLong,
		SilenceUsage:          true,
		DisableFlagsInUseLine: true,
		Example:               fmt.Sprintf(interpretExample, parentCommand),
		Run: func(cmd *cobra.Command, args []string) {
			cmdutil.CheckErr(o.Complete(f, cmd, args))
			cmdutil.CheckErr(o.Validate())
			cmdutil.CheckErr(o.Run())
		},
		Annotations: map[string]string{
			util.TagCommandGroup: util.GroupClusterTroubleshootingAndDebugging,
		},
	}

	flags := cmd.Flags()
	options.AddKubeConfigFlags(flags)
	o.EditOptions.RecordFlags.AddFlags(cmd)
	o.EditOptions.PrintFlags.AddFlags(cmd)
	flags.StringVar(&o.Operation, "operation", o.Operation, "The interpret operation to use. One of: ("+strings.Join(o.Rules.Names(), ",")+")")
	flags.BoolVar(&o.Check, "check", false, "Validates the given ResourceInterpreterCustomization configuration(s)")
	flags.BoolVar(&o.Edit, "edit", false, "Edit customizations")
	flags.BoolVar(&o.ShowDoc, "show-doc", false, "Show document of rules when editing")
	flags.StringVar(&o.DesiredFile, "desired-file", o.DesiredFile, "Filename, directory, or URL to files identifying the resource to use as desiredObj argument in rule script.")
	flags.StringVar(&o.ObservedFile, "observed-file", o.ObservedFile, "Filename, directory, or URL to files identifying the resource to use as observedObj argument in rule script.")
	flags.StringVar(&o.StatusFile, "status-file", o.StatusFile, "Filename, directory, or URL to files identifying the resource to use as statusItems argument in rule script.")
	flags.Int32Var(&o.DesiredReplica, "desired-replica", o.DesiredReplica, "The desiredReplica argument in rule script.")
	cmdutil.AddJsonFilenameFlag(flags, &o.FilenameOptions.Filenames, "Filename, directory, or URL to files containing the customizations")
	flags.BoolVarP(&o.FilenameOptions.Recursive, "recursive", "R", false, "Process the directory used in -f, --filename recursively. Useful when you want to manage related manifests organized within the same directory.")

	return cmd
}

// Options contains the input to the interpret command.
type Options struct {
	resource.FilenameOptions
	*editor.EditOptions

	Operation string
	Check     bool
	Edit      bool
	ShowDoc   bool

	// args
	DesiredFile    string
	ObservedFile   string
	StatusFile     string
	DesiredReplica int32

	CustomizationResult *resource.Result
	DesiredResult       *resource.Result
	ObservedResult      *resource.Result
	StatusResult        *genericresource.Result

	Rules interpreter.Rules

	genericclioptions.IOStreams
}

// Complete ensures that options are valid and marshals them if necessary
func (o *Options) Complete(f util.Factory, cmd *cobra.Command, args []string) error {
	if o.Check && o.Edit {
		return fmt.Errorf("you can't set both --check and --edit options")
	}

	scheme := gclient.NewSchema()
	o.CustomizationResult = f.NewBuilder().
		WithScheme(scheme, scheme.PrioritizedVersionsAllGroups()...).
		FilenameParam(false, &o.FilenameOptions).
		ResourceNames(customizationResourceName, args...).
		RequireObject(true).
		Local().
		Do()

	var errs []error
	errs = append(errs, o.CustomizationResult.Err())
	errs = append(errs, o.completeExecute(f)...)
	errs = append(errs, o.completeEdit())
	return errors.NewAggregate(errs)
}

// Validate validates Options.
func (o *Options) Validate() error {
	if o.Operation != "" {
		r := o.Rules.GetByOperation(o.Operation)
		if r == nil {
			return fmt.Errorf("operation %s is not supported. Use one of: %s", o.Operation, strings.Join(o.Rules.Names(), ", "))
		}
	}
	if o.Edit {
		err := o.EditOptions.Validate()
		if err != nil {
			return err
		}
	}
	return nil
}

// Run describe information of resources
func (o *Options) Run() error {
	switch {
	case o.Check:
		return o.runCheck()
	case o.Edit:
		return o.runEdit()
	default:
		return o.runExecute()
	}
}

func (o *Options) getCustomizationObject() ([]*configv1alpha1.ResourceInterpreterCustomization, error) {
	infos, err := o.CustomizationResult.Infos()
	if err != nil {
		return nil, err
	}

	customizations := make([]*configv1alpha1.ResourceInterpreterCustomization, len(infos))
	for i, info := range infos {
		c, err := asResourceInterpreterCustomization(info.Object)
		if err != nil {
			return nil, err
		}
		customizations[i] = c
	}
	return customizations, nil
}

func (o *Options) getAggregatedStatusItems() ([]workv1alpha2.AggregatedStatusItem, error) {
	if o.StatusResult == nil {
		return nil, nil
	}

	objs, err := o.StatusResult.Objects()
	if err != nil {
		return nil, err
	}
	items := make([]workv1alpha2.AggregatedStatusItem, len(objs))
	for i, obj := range objs {
		items[i] = *(obj.(*workv1alpha2.AggregatedStatusItem))
	}
	return items, nil
}

func getUnstructuredObjectFromResult(result *resource.Result) (*unstructured.Unstructured, error) {
	if result == nil {
		return nil, nil
	}

	infos, err := result.Infos()
	if err != nil {
		return nil, err
	}

	if len(infos) > 1 {
		return nil, fmt.Errorf("get %v objects, expect one at most", len(infos))
	}

	return helper.ToUnstructured(infos[0].Object)
}

func asResourceInterpreterCustomization(o runtime.Object) (*configv1alpha1.ResourceInterpreterCustomization, error) {
	c, ok := o.(*configv1alpha1.ResourceInterpreterCustomization)
	if !ok {
		return nil, fmt.Errorf("not a ResourceInterpreterCustomization, got %v", o.GetObjectKind().GroupVersionKind())
	}
	return c, nil
}
