package cluster

import (
	"errors"
	"fmt"
	"regexp"
	"strings"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	utilerrors "k8s.io/apimachinery/pkg/util/errors"
	"sigs.k8s.io/yaml"

	anywherev1 "github.com/aws/eks-anywhere/pkg/api/v1alpha1"
	"github.com/aws/eks-anywhere/pkg/logger"
)

// ConfigManager allows to parse from yaml, set defaults and validate a Cluster struct
// It allows to dynamically register configuration for all those operations.
type ConfigManager struct { // TODO: find a better name
	entry *ConfigManagerEntry
}

// NewConfigManager builds a ConfigManager with empty configuration.
func NewConfigManager() *ConfigManager {
	return &ConfigManager{
		entry: NewConfigManagerEntry(),
	}
}

// Register records the configuration defined in a ConfigManagerEntry into the ConfigManager
// This is equivalent to the individual register methods.
func (c *ConfigManager) Register(entries ...*ConfigManagerEntry) error {
	return c.entry.Merge(entries...)
}

// RegisterMapping records the mapping between a kubernetes Kind and an API concrete type.
func (c *ConfigManager) RegisterMapping(kind string, generator APIObjectGenerator) error {
	return c.entry.RegisterMapping(kind, generator)
}

// RegisterProcessors records setters to fill the Config struct from the parsed API objects.
func (c *ConfigManager) RegisterProcessors(processors ...ParsedProcessor) {
	c.entry.RegisterProcessors(processors...)
}

// RegisterValidations records validations for a Config struct.
func (c *ConfigManager) RegisterValidations(validations ...Validation) {
	c.entry.RegisterValidations(validations...)
}

// RegisterDefaulters records defaults for a Config struct.
func (c *ConfigManager) RegisterDefaulters(defaulters ...Defaulter) {
	c.entry.RegisterDefaulters(defaulters...)
}

// Parse reads yaml manifest with at least one cluster object and generates the corresponding Config.
func (c *ConfigManager) Parse(yamlManifest []byte) (*Config, error) {
	parsed, err := c.unmarshal(yamlManifest)
	if err != nil {
		return nil, err
	}

	return c.buildConfigFromParsed(parsed)
}

// Parse set the registered defaults in a Config struct.
func (c *ConfigManager) SetDefaults(config *Config) error {
	var allErrs []error

	for _, d := range c.entry.Defaulters {
		if err := d(config); err != nil {
			allErrs = append(allErrs, err)
		}
	}

	if len(allErrs) > 0 {
		aggregate := utilerrors.NewAggregate(allErrs)
		return fmt.Errorf("setting defaults on cluster config: %v", aggregate)
	}

	return nil
}

// Validate performs the registered validations in a Config struct.
func (c *ConfigManager) Validate(config *Config) error {
	var allErrs []error

	for _, v := range c.entry.Validations {
		if err := v(config); err != nil {
			allErrs = append(allErrs, err)
		}
	}

	if len(allErrs) > 0 {
		aggregate := utilerrors.NewAggregate(allErrs)
		return fmt.Errorf("invalid cluster config: %v", aggregate)
	}

	return nil
}

type basicAPIObject struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`
}

func (k *basicAPIObject) empty() bool {
	return k.APIVersion == "" && k.Kind == ""
}

type parsed struct {
	objects ObjectLookup
	cluster *anywherev1.Cluster
}

var separatorRegex = regexp.MustCompile(`(?m)^---$`)

func (c *ConfigManager) unmarshal(yamlManifest []byte) (*parsed, error) {
	parsed := &parsed{
		objects: ObjectLookup{},
	}
	yamlObjs := separatorRegex.Split(string(yamlManifest), -1)

	for _, yamlObj := range yamlObjs {
		normalizedYamlObj := anywherev1.NormalizeKubernetesVersion(strings.TrimSuffix(yamlObj, "\n"))
		k := &basicAPIObject{}
		err := yaml.Unmarshal([]byte(normalizedYamlObj), k)
		if err != nil {
			return nil, err
		}

		// Ignore empty objects.
		// Empty objects are generated if there are weird things in manifest files like e.g. two --- in a row without a yaml doc in the middle
		if k.empty() {
			continue
		}

		if k.APIVersion == "" {
			return nil, fmt.Errorf("apiVersion not set for Kind: %s", k.Kind)
		}

		var obj APIObject

		if k.Kind == anywherev1.ClusterKind {
			if parsed.cluster != nil {
				return nil, errors.New("only one Cluster per yaml manifest is allowed")
			}
			parsed.cluster = &anywherev1.Cluster{}
			obj = parsed.cluster
		} else if generateApiObj, ok := c.entry.APIObjectMapping[k.Kind]; ok {
			obj = generateApiObj()
		} else {
			logger.V(2).Info("Ignoring object in yaml of unknown type when parsing cluster Config", "kind", k.Kind)
			continue
		}

		if err := yaml.Unmarshal([]byte(normalizedYamlObj), obj); err != nil {
			return nil, err
		}
		parsed.objects.add(obj)
	}

	return parsed, nil
}

func (c *ConfigManager) buildConfigFromParsed(p *parsed) (*Config, error) {
	if p.cluster == nil {
		return nil, errors.New("no Cluster found in manifest")
	}

	config := &Config{
		Cluster: p.cluster,
	}

	for _, processor := range c.entry.Processors {
		processor(config, p.objects)
	}

	return config, nil
}

// machineConfigsProcessor is a helper to generate a ParsedProcessor for all machine configs in a Cluster.
func machineConfigsProcessor(processMachineRef func(c *Config, o ObjectLookup, machineRef *anywherev1.Ref)) ParsedProcessor {
	return func(c *Config, o ObjectLookup) {
		for _, m := range c.Cluster.MachineConfigRefs() {
			processMachineRef(c, o, &m)
		}
	}
}
