package controllercmd

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

	"github.com/spf13/cobra"

	"github.com/openshift/library-go/pkg/config/client"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/runtime/serializer"
	kyaml "k8s.io/apimachinery/pkg/util/yaml"
	"k8s.io/client-go/rest"
)

// ControllerFlags provides the "normal" controller flags
type ControllerFlags struct {
	// ConfigFile hold the configfile to load
	ConfigFile string
	// KubeConfigFile points to a kubeconfig file if you don't want to use the in cluster config
	KubeConfigFile string
	// Namespace points to a base namespace for the controller and related events
	Namespace string
	// BindAddress is the ip:port to serve on
	BindAddress string
	// TerminateOnFiles is a list of files. If any of these changes, the process terminates.
	TerminateOnFiles []string
}

// NewControllerFlags returns flags with default values set
func NewControllerFlags() *ControllerFlags {
	return &ControllerFlags{}
}

// Validate makes sure the required flags are specified and no illegal combinations are found
func (o *ControllerFlags) Validate() error {
	// everything is optional currently
	return nil
}

// AddFlags register and binds the default flags
func (f *ControllerFlags) AddFlags(cmd *cobra.Command) {
	flags := cmd.Flags()
	// This command only supports reading from config
	flags.StringVar(&f.ConfigFile, "config", f.ConfigFile, "Location of the master configuration file to run from.")
	cmd.MarkFlagFilename("config", "yaml", "yml")
	flags.StringVar(&f.KubeConfigFile, "kubeconfig", f.KubeConfigFile, "Location of the master configuration file to run from.")
	cmd.MarkFlagFilename("kubeconfig", "kubeconfig")
	flags.StringVar(&f.Namespace, "namespace", f.Namespace, "Namespace where the controller is running. Auto-detected if run in cluster.")
	flags.StringVar(&f.BindAddress, "listen", f.BindAddress, "The ip:port to serve on.")
	flags.StringArrayVar(&f.TerminateOnFiles, "terminate-on-files", f.TerminateOnFiles, "A list of files. If one of them changes, the process will terminate.")
}

// ToConfigObj given completed flags, returns a config object for the flag that was specified.
// TODO versions goes away in 1.11
func (f *ControllerFlags) ToConfigObj() ([]byte, *unstructured.Unstructured, error) {
	// no file means empty, not err
	if len(f.ConfigFile) == 0 {
		return nil, nil, nil
	}

	content, err := os.ReadFile(f.ConfigFile)
	if err != nil {
		return nil, nil, err
	}
	// empty file means empty, not err
	if len(content) == 0 {
		return nil, nil, err
	}

	data, err := kyaml.ToJSON(content)
	if err != nil {
		return nil, nil, err
	}
	uncastObj, err := runtime.Decode(unstructured.UnstructuredJSONScheme, data)
	if err != nil {
		return nil, nil, err
	}

	return content, uncastObj.(*unstructured.Unstructured), nil
}

// ToClientConfig given completed flags, returns a rest.Config.  overrides are optional
func (f *ControllerFlags) ToClientConfig(overrides *client.ClientConnectionOverrides) (*rest.Config, error) {
	return client.GetKubeConfigOrInClusterConfig(f.KubeConfigFile, overrides)
}

// ReadYAML decodes a runtime.Object from the provided scheme
// TODO versions goes away with more complete scheme in 1.11
func ReadYAML(data []byte, configScheme *runtime.Scheme, versions ...schema.GroupVersion) (runtime.Object, error) {
	data, err := kyaml.ToJSON(data)
	if err != nil {
		return nil, err
	}
	configCodecFactory := serializer.NewCodecFactory(configScheme)
	obj, err := runtime.Decode(configCodecFactory.UniversalDecoder(versions...), data)
	if err != nil {
		return nil, captureSurroundingJSONForError("error reading config: ", data, err)
	}
	return obj, err
}

// ReadYAMLFile read a file and decodes a runtime.Object from the provided scheme
func ReadYAMLFile(filename string, configScheme *runtime.Scheme, versions ...schema.GroupVersion) (runtime.Object, error) {
	data, err := os.ReadFile(filename)
	if err != nil {
		return nil, err
	}
	obj, err := ReadYAML(data, configScheme, versions...)
	if err != nil {
		return nil, fmt.Errorf("could not load config file %q due to an error: %v", filename, err)
	}
	return obj, err
}

// TODO: we ultimately want a better decoder for JSON that allows us exact line numbers and better
// surrounding text description. This should be removed / replaced when that happens.
func captureSurroundingJSONForError(prefix string, data []byte, err error) error {
	if syntaxErr, ok := err.(*json.SyntaxError); err != nil && ok {
		offset := syntaxErr.Offset
		begin := offset - 20
		if begin < 0 {
			begin = 0
		}
		end := offset + 20
		if end > int64(len(data)) {
			end = int64(len(data))
		}
		return fmt.Errorf("%s%v (found near '%s')", prefix, err, string(data[begin:end]))
	}
	if err != nil {
		return fmt.Errorf("%s%v", prefix, err)
	}
	return err
}
