package inspect

import (
	"encoding/json"
	"fmt"
	stdlog "log"
	"os"
	"strings"
	"text/tabwriter"

	"github.com/tidbcloud/infra-provider/infra-inspector/api"
	"github.com/tidbcloud/infra-provider/infra-inspector/pkg/config"
	"github.com/tidbcloud/infra-provider/infra-inspector/pkg/deps"
	"github.com/tidbcloud/infra-provider/infra-inspector/pkg/inspect"

	"github.com/go-logr/logr"
	"github.com/go-logr/stdr"
	"github.com/spf13/cobra"
	k8sutil "github.com/tidbcloud/infra-provider/pkg/k8s"
	"golang.org/x/exp/slices"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/kubernetes"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/yaml"
)

const (
	examples = `
# Inspect resources in a kubernetes cluster.
	KUBECONFIG=${kubeconfig}infra-inspector inspect --rego=/path/to/rego-rule cloud.pingcap.com/v1:Cluster

# Inspect resources in each dataplanes.
	KUBECONFIG=${infra_kubeconfig} infra-inspector inspect --gardener-env=dev --gardener-config=${gardener_kubeconfig} --dataplane --rego=/path/to/rego-rule v1:Pod
# OR
# Inspect in-tree resources by the field selector in each dataplanes.
	KUBECONFIG=${infra_kubeconfig} infra-inspector inspect --gardener-env=dev --gardener-config=${gardener_kubeconfig} --dataplane --always-deny --field-selector status.phase==Pending v1:Pod
`
)

var Cmd = &cobra.Command{
	Use:     "inspect",
	Short:   "inspect resources in infra/gardener/dataplane",
	Example: examples,
	Run:     run,
	Args:    cobra.MinimumNArgs(1),
}

var (
	flagEngineRego       string
	flagEngineAlwaysDeny bool

	flagLabelSelector string
	flagFieldSelector string

	flagDataplane      bool
	flagGardenerConfig string
	flagGardenerEnv    string
	flagOutput         string
	flagLogLevel       int
)

func init() {
	Cmd.Flags().StringVar(&flagEngineRego, "rego", "", "file or dir where rego rules are stored")
	Cmd.Flags().BoolVar(&flagEngineAlwaysDeny, "always-deny", false, "deny all resources")

	Cmd.Flags().StringVarP(&flagLabelSelector, "selector", "l", "", "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints.")
	Cmd.Flags().StringVar(&flagFieldSelector, "field-selector", "", " Selector (field query) to filter on, supports '=', '==', and '!='.(e.g. --field-selectorkey1=value1,key2=value2). The server only supports a limited number of field queries per type.")

	Cmd.Flags().BoolVar(&flagDataplane, "dataplane", false, "inspect dataplane resources")
	Cmd.Flags().StringVar(&flagGardenerConfig, "gardener-config", "", "path to gardener kubeconfig, it is required when --dataplane is not set")
	Cmd.Flags().StringVar(&flagGardenerEnv, "gardener-env", "", "dev/staging/prod, it is required when --dataplane is not set")

	Cmd.Flags().StringVarP(&flagOutput, "output", "o", "", "output format, can be json/yaml")
	Cmd.Flags().IntVarP(&flagLogLevel, "log-level", "v", 0, "log level")

}

func run(cmd *cobra.Command, args []string) {
	var err error

	stdr.SetVerbosity(flagLogLevel)
	log.Log = stdr.New(stdlog.Default())

	validateFlags()

	// build list config
	listConfig := config.ListConfig{}
	matchOpt := config.ResourceMatchOption{}
	if flagLabelSelector != "" {
		matchOpt.LabelSelector, err = metav1.ParseToLabelSelector(flagLabelSelector)
		exitIfErr(fmt.Errorf("invalid label selector"), flagLabelSelector)
	}
	matchOpt.FieldSelector = flagFieldSelector
	for _, arg := range args[0:] {
		gvk := strings.Split(arg, ":")
		if len(gvk) != 2 {
			exitIfErr(fmt.Errorf("invalid resource gvk"), arg)
		}
		listConfig.Resources = append(listConfig.Resources, config.ResourceMatch{
			APIVersion:          gvk[0],
			Kind:                gvk[1],
			ResourceMatchOption: matchOpt,
		})
	}

	// build label selector
	ecfg := config.EngineConfig{}
	if flagEngineRego != "" {
		ecfg.Rego = &config.RegoEngineConfig{
			RegoRulePath: flagEngineRego,
		}
	}
	if flagEngineAlwaysDeny {
		ecfg.AlwaysDeny = true
	}

	// create inspector and run
	ctx := logr.NewContext(ctrl.SetupSignalHandler(), log.Log.WithName("Inspector"))
	var inspector inspect.Inspector

	if flagDataplane {
		deps := deps.New()
		err = deps.CreateClientsForInfra(os.Getenv("KUBECONFIG"))
		exitIfErr(err, "unable to create deps for infra")
		err = deps.CreateClientsForGardener(flagGardenerConfig)
		exitIfErr(err, "unable to create deps for gardener")

		inspector, err = inspect.NewDataplaneInspector(flagGardenerEnv, deps, ecfg)
		exitIfErr(err, "unable to create dataplane inspector")
	} else {
		kcfg, err := k8sutil.GetKubeConfig(os.Getenv("KUBECONFIG"))
		inspector, err = inspect.NewGenericInspector("", ecfg, kubernetes.NewForConfigOrDie(kcfg), dynamic.NewForConfigOrDie(kcfg))
		exitIfErr(err, "unable to create inspector")
	}

	results, err := inspector.Inspect(ctx, listConfig)
	exitIfErr(err, "unable to inspect resources")

	// print results
	switch flagOutput {
	case "json":
		output, err := json.MarshalIndent(results, "", "  ")
		exitIfErr(err, "unable to marshal result")
		fmt.Println(string(output))
	case "yaml":
		output, err := yaml.Marshal(results)
		exitIfErr(err, "unable to marshal result")
		fmt.Println(string(output))
	default:
		err := printResults(results)
		exitIfErr(err, "")
	}

	os.Exit(0)
}

func validateFlags() {
	if os.Getenv("KUBECONFIG") == "" {
		exitIfErr(fmt.Errorf("env 'KUBECONFIG' is required"), "")
	}

	if flagDataplane {
		if flagGardenerConfig == "" {
			exitIfErr(fmt.Errorf("--gardener-config is required when --dataplane is set"), "")
		}
		if !slices.Contains([]string{"dev", "staging", "prod"}, flagGardenerEnv) {
			exitIfErr(fmt.Errorf("--env must be one of dev/staging/prod"), "")
		}
	}
}

func printResults(results []*api.Result) error {

	for _, result := range results {
		count := map[api.Level]int{}

		lines := []string{}
		for _, violation := range result.Violations {
			for _, rule := range violation.ViolatedRules {
				s := fmt.Sprintf("%s\t%s\t%s\t%s\t%s\t%s\t",
					violation.Resource.Kind, violation.Resource.Namespace, violation.Resource.Name,
					rule.Level, rule.Name, rule.Message)
				lines = append(lines, s)

				count[rule.Level] += 1
			}
		}

		summary := fmt.Sprintf("\nTarget: %s, Total: %d, Critical: %d, Warning: %d, Info: %d",
			result.Target, len(lines), count[api.LevelCritical], count[api.LevelWarning], count[api.LevelInfo])
		_, err := fmt.Println(summary)
		if err != nil {
			return err
		}

		if len(lines) == 0 {
			continue
		}

		w := tabwriter.NewWriter(os.Stdout, 10, 4, 3, ' ', 0)
		_, err = fmt.Fprintln(w, "KIND\tNAMESPACE\tNAME\tLEVEL\tRULE\tMESSAGE")
		if err != nil {
			return err
		}
		for _, line := range lines {
			_, err := fmt.Fprintln(w, line)
			if err != nil {
				return err
			}
		}
		if err := w.Flush(); err != nil {
			return err
		}
	}

	return nil
}

func exitIfErr(err error, msg string) {
	if err != nil {
		log.Log.Error(err, msg)
		os.Exit(1)
	}
}
