package yamlutil

import (
	"bufio"
	"bytes"
	"io"

	"github.com/go-logr/logr"
	"github.com/pkg/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	apiyaml "k8s.io/apimachinery/pkg/util/yaml"
	"sigs.k8s.io/yaml"

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

type (
	// APIObjectGenerator returns an implementor of the APIObject interface.
	APIObjectGenerator func() APIObject
	// ParsedProcessor fills the struct of type T with the parsed API objects in ObjectLookup.
	ParsedProcessor[T any] func(*T, ObjectLookup)

	// Parser allows to parse from yaml with kubernetes style objects and
	// store them in a type implementing Builder
	// It allows to dynamically register configuration for mappings between kind and concrete types.
	Parser struct {
		apiObjectMapping   map[string]APIObjectGenerator
		generateObjAnyKind APIObjectGenerator
		logger             logr.Logger
	}
)

func NewParser(logger logr.Logger) *Parser {
	return &Parser{
		apiObjectMapping: make(map[string]APIObjectGenerator),
		logger:           logger,
	}
}

// RegisterMapping records the mapping between a kubernetes Kind and an API concrete type.
func (c *Parser) RegisterMapping(kind string, generator APIObjectGenerator) error {
	if _, ok := c.apiObjectMapping[kind]; ok {
		return errors.Errorf("mapping for api object %s already registered", kind)
	}

	c.apiObjectMapping[kind] = generator
	return nil
}

// Mapping mapping between a kubernetes Kind and an API concrete type of type T.
type Mapping[T APIObject] struct {
	New  func() T
	Kind string
}

func NewMapping[T APIObject](kind string, new func() T) Mapping[T] {
	return Mapping[T]{
		Kind: kind,
		New:  new,
	}
}

// ToAPIObjectMapping is helper to convert from other concrete types of Mapping
// to a APIObject Mapping
// This is mostly to help pass Mappings to RegisterMappings.
func (m Mapping[T]) ToAPIObjectMapping() Mapping[APIObject] {
	return Mapping[APIObject]{
		Kind: m.Kind,
		New: func() APIObject {
			return m.New()
		},
	}
}

// RegisterMappings records a collection of mappings.
func (c *Parser) RegisterMappings(mappings ...Mapping[APIObject]) error {
	for _, m := range mappings {
		if err := c.RegisterMapping(m.Kind, m.New); err != nil {
			return err
		}
	}

	return nil
}

// RegisterMappingForAnyKind records an object generator that will be used
// as fallback when there is not a specific APIObjectGenerator registered for that particular kind.
func (c *Parser) RegisterMappingForAnyKind(generator APIObjectGenerator) {
	c.generateObjAnyKind = generator
}

// Builder processes the parsed API objects contained in a lookup.
type Builder interface {
	BuildFromParsed(ObjectLookup) error
}

// Parse reads yaml manifest content with the registered mappings and passes
// the result to the Builder for further processing.
func (p *Parser) Parse(yamlManifest []byte, b Builder) error {
	normalizedYamlManifest := v1alpha1.NormalizeKubernetesVersion(string(yamlManifest))
	return p.Read(bytes.NewReader([]byte(normalizedYamlManifest)), b)
}

// Read reads yaml manifest content with the registered mappings and passes
// the result to the Builder for further processing.
func (p *Parser) Read(reader io.Reader, b Builder) error {
	parsed, err := p.unmarshal(reader)
	if err != nil {
		return err
	}

	return p.buildConfigFromParsed(parsed, b)
}

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
}

func (p *Parser) unmarshal(reader io.Reader) (*parsed, error) {
	parsed := &parsed{
		objects: ObjectLookup{},
	}

	yamlReader := apiyaml.NewYAMLReader(bufio.NewReader(reader))
	for {
		// Read one YAML document at a time, until io.EOF is returned
		b, err := yamlReader.Read()
		if err != nil {
			if err == io.EOF {
				break
			}
			return nil, errors.Wrap(err, "failed to read yaml")
		}
		if len(b) == 0 {
			break
		}

		k := &basicAPIObject{}
		if err = yaml.Unmarshal(b, k); err != nil {
			return nil, errors.Wrap(err, "invalid yaml kubernetes object")
		}

		// 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
		}

		var obj APIObject
		if generateApiObj, ok := p.apiObjectMapping[k.Kind]; ok {
			obj = generateApiObj()
		} else if p.generateObjAnyKind != nil {
			obj = p.generateObjAnyKind()
		} else {
			p.logger.V(2).Info("Ignoring object in yaml of unknown type during parsing", "kind", k.Kind)
			continue
		}

		if err := yaml.Unmarshal(b, obj); err != nil {
			return nil, errors.Wrapf(err, "invalid yaml for %s", k.Kind)
		}
		parsed.objects.add(obj)
	}

	return parsed, nil
}

func (p *Parser) buildConfigFromParsed(parsed *parsed, b Builder) error {
	return b.BuildFromParsed(parsed.objects)
}
