/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package cluster

import (
	"cmp"
	"fmt"
	"github.com/apache/dubbo-kubernetes/dubboctl/pkg/cli"
	"github.com/apache/dubbo-kubernetes/operator/pkg/manifest"
	"github.com/apache/dubbo-kubernetes/operator/pkg/render"
	"github.com/apache/dubbo-kubernetes/operator/pkg/util/clog"
	"github.com/apache/dubbo-kubernetes/pkg/kube"
	"github.com/apache/dubbo-kubernetes/pkg/slices"
	"github.com/spf13/cobra"
	"sigs.k8s.io/yaml"
	"strings"
)

type manifestGenerateArgs struct {
	// filenames is an array of paths to input DubboOperator CR files.
	// filenames []string
	// sets is a string with the format "path=value".
	sets []string
}

func (a *manifestGenerateArgs) String() string {
	var b strings.Builder
	// b.WriteString("filenames:   " + fmt.Sprint(a.filenames) + "\n")
	b.WriteString("sets:           " + fmt.Sprint(a.sets) + "\n")
	return b.String()
}

func addManifestGenerateFlags(cmd *cobra.Command, args *manifestGenerateArgs) {
	// cmd.PersistentFlags().StringSliceVarP(&args.filenames, "filenames", "f", nil, ``)
	cmd.PersistentFlags().StringArrayVarP(&args.sets, "set", "s", nil, `Override dubboOperator values, such as selecting profiles, etc.`)
}

func ManifestCmd(ctx cli.Context) *cobra.Command {
	rootArgs := &RootArgs{}
	mgcArgs := &manifestGenerateArgs{}
	mgc := manifestGenerateCmd(ctx, rootArgs, mgcArgs)
	mc := &cobra.Command{
		Use:   "manifest",
		Short: "dubbo manifest related commands",
		Long:  "The manifest command will generates dubbo manifests.",
	}
	AddFlags(mc, rootArgs)
	AddFlags(mgc, rootArgs)
	addManifestGenerateFlags(mgc, mgcArgs)
	mc.AddCommand(mgc)
	return mc
}

var kubeClientFunc func() (kube.CLIClient, error)

func manifestGenerateCmd(ctx cli.Context, _ *RootArgs, mgArgs *manifestGenerateArgs) *cobra.Command {
	return &cobra.Command{
		Use:   "generate",
		Short: "Generates an Dubbo install manifest",
		Long:  "The generate subcommand generates an Dubbo install manifest and outputs to the console by default.",
		Example: `  # Generate a default Dubbo installation
  dubboctl manifest generate
  
  # Generate the demo profile
  dubboctl manifest generate --set profile=demo
`,
		RunE: func(cmd *cobra.Command, args []string) error {
			if kubeClientFunc == nil {
				kubeClientFunc = ctx.CLIClient
			}
			var kubeClient kube.CLIClient
			kc, err := kubeClientFunc()
			if err != nil {
				return err
			}
			kubeClient = kc

			cl := clog.NewConsoleLogger(cmd.OutOrStdout(), cmd.ErrOrStderr())
			return manifestGenerate(kubeClient, mgArgs, cl)
		},
	}
}

const (
	YAMLSeparator = "\n---\n"
)

func manifestGenerate(kc kube.CLIClient, mgArgs *manifestGenerateArgs, cl clog.Logger) error {
	setFlags := applyFlagAliases(mgArgs.sets)
	manifests, _, err := render.GenerateManifest(nil, setFlags, cl, kc)
	if err != nil {
		return err
	}
	for _, smf := range sortManifests(manifests) {
		cl.Print(smf + YAMLSeparator)
	}
	return nil
}

func sortManifests(raw []manifest.ManifestSet) []string {
	all := []manifest.Manifest{}
	for _, m := range raw {
		all = append(all, m.Manifests...)
	}
	slices.SortStableFunc(all, func(a, b manifest.Manifest) int {
		if r := cmp.Compare(objectKindOrder(a), objectKindOrder(b)); r != 0 {
			return r
		}
		if r := cmp.Compare(a.GroupVersionKind().Group, b.GroupVersionKind().Group); r != 0 {
			return r
		}
		if r := cmp.Compare(a.GroupVersionKind().Kind, b.GroupVersionKind().Kind); r != 0 {
			return r
		}
		return cmp.Compare(a.GetName(), b.GetName())
	})
	return slices.Map(all, func(e manifest.Manifest) string {
		res, _ := yaml.Marshal(e.Object)
		return string(res)
	})
}

func objectKindOrder(m manifest.Manifest) int {
	o := m.Unstructured
	gk := o.GroupVersionKind().Group + "/" + o.GroupVersionKind().Kind
	switch {
	// Create CRDs asap - both because they are slow and because we will likely create instances of them soon
	case gk == "apiextensions.k8s.io/CustomResourceDefinition":
		return -1000

		// We need to create ServiceAccounts, Roles before we bind them with a RoleBinding
	case gk == "/ServiceAccount" || gk == "rbac.authorization.k8s.io/ClusterRole":
		return 1
	case gk == "rbac.authorization.k8s.io/ClusterRoleBinding":
		return 2

		// Pods might need configmap or secrets - avoid backoff by creating them first
	case gk == "/ConfigMap" || gk == "/Secrets":
		return 100

		// Create the pods after we've created other things they might be waiting for
	case gk == "extensions/Deployment" || gk == "apps/Deployment":
		return 1000

		// Autoscalers typically act on a deployment
	case gk == "autoscaling/HorizontalPodAutoscaler":
		return 1001

		// Create services late - after pods have been started
	case gk == "/Service":
		return 10000

	default:
		return 1000
	}
}
