// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package command

import (
	"encoding/json"
	"fmt"
	"os"
	"regexp"
	"strings"

	"github.com/spf13/cobra"
	"go.yaml.in/yaml/v3"
	"k8s.io/client-go/util/jsonpath"
)

var (
	outputOpt string
	re        = regexp.MustCompile(`^jsonpath\=(.*)`)
)

// OutputOption returns true if an output option was specified.
func OutputOption() bool {
	return len(outputOpt) > 0
}

// OutputOptionString returns the output option as a string
func OutputOptionString() string {
	if outputOpt == "yaml" {
		return "YAML"
	}

	if outputOpt == "json" || re.MatchString(outputOpt) {
		return "JSON"
	}

	return "unknown"
}

// AddOutputOption adds the -o|--output option to any cmd to export to json or yaml.
func AddOutputOption(cmd *cobra.Command) {
	cmd.Flags().StringVarP(&outputOpt, "output", "o", "", "json| yaml| jsonpath='{}'")
}

// ForceJSON sets output mode to JSON (for unit tests)
func ForceJSON() {
	outputOpt = "json"
}

// PrintOutput receives an interface and dump the data using the --output flag.
// ATM only json or jsonpath. In the future yaml
func PrintOutput(data any) error {
	return PrintOutputWithType(data, outputOpt)
}

// PrintOutputWithPatch merges data with patch and dump the data using the --output flag.
func PrintOutputWithPatch(data any, patch any) error {
	mergedInterface, err := mergeInterfaces(data, patch)
	if err != nil {
		return fmt.Errorf("Unable to merge Interfaces: %w", err)
	}
	return PrintOutputWithType(mergedInterface, outputOpt)
}

func mergeInterfaces(data, patch any) (any, error) {
	var i1, i2 any

	data1, err := json.Marshal(data)
	if err != nil {
		return nil, err
	}
	data2, err := json.Marshal(patch)
	if err != nil {
		return nil, err
	}
	err = json.Unmarshal(data1, &i1)
	if err != nil {
		return nil, err
	}

	err = json.Unmarshal(data2, &i2)
	if err != nil {
		return nil, err
	}
	return recursiveMerge(i1, i2), nil
}

func recursiveMerge(i1, i2 any) any {
	switch i1 := i1.(type) {
	case map[string]any:
		i2, ok := i2.(map[string]any)
		if !ok {
			return i1
		}
		for k, v2 := range i2 {
			if v1, ok := i1[k]; ok {
				i1[k] = recursiveMerge(v1, v2)
			} else {
				i1[k] = v2
			}
		}
	case nil:
		i2, ok := i2.(map[string]any)
		if ok {
			return i2
		}
	}
	return i1
}

// PrintOutputWithType receives an interface and dump the data using the --output flag.
// ATM only json, yaml, or jsonpath.
func PrintOutputWithType(data any, outputType string) error {
	if outputType == "json" {
		return dumpJSON(data, "")
	}

	if outputType == "yaml" {
		return dumpYAML(data)
	}

	if re.MatchString(outputType) {
		return dumpJSON(data, re.ReplaceAllString(outputType, "$1"))
	}

	return fmt.Errorf("couldn't find output printer")
}

// DumpJSONToString dumps the contents of data into a string. If jsonpath is
// non-empty, will attempt to do jsonpath filtering using said string. Returns a
// string containing the JSON in data, or an error if any JSON marshaling,
// parsing operations fail.
func DumpJSONToString(data any, jsonPath string) (string, error) {
	if len(jsonPath) == 0 {
		result, err := json.MarshalIndent(data, "", "  ")
		if err != nil {
			fmt.Fprintf(os.Stderr, "Couldn't marshal to json: '%s'\n", err)
			return "", err
		}
		fmt.Println(string(result))
		return "", nil
	}

	parser := jsonpath.New("").AllowMissingKeys(true)
	if err := parser.Parse(jsonPath); err != nil {
		fmt.Fprintf(os.Stderr, "Couldn't parse jsonpath expression: '%s'\n", err)
		return "", err
	}

	var sb strings.Builder
	if err := parser.Execute(&sb, data); err != nil {
		fmt.Fprintf(os.Stderr, "Couldn't parse jsonpath expression: '%s'\n", err)
		return "", err

	}
	return sb.String(), nil
}

// dumpJSON dumps the data variable to the stdout as json.
// If something fails, it returns an error
// If jsonPath is passed, it runs the json query over data var.
func dumpJSON(data any, jsonPath string) error {
	jsonStr, err := DumpJSONToString(data, jsonPath)
	if err != nil {
		return err
	}
	fmt.Println(jsonStr)
	return nil
}

// dumpYAML dumps the data variable to the stdout as yaml.
// If something fails, it returns an error
func dumpYAML(data any) error {
	result, err := yaml.Marshal(data)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Couldn't marshal to yaml: '%s'\n", err)
		return err
	}
	fmt.Println(string(result))
	return nil
}
