// The file implements the conversion.Hub round-trip between the legacy
// v1alpha2 PatternFile and the v1beta3 design PatternFile.
// This is not autogenerated.
package design

import (
	"math/big"
	"reflect"
	"strings"

	"github.com/gofrs/uuid"
	"github.com/meshery/meshkit/utils"
	core "github.com/meshery/schemas/models/core"
	"github.com/meshery/schemas/models/conversion"
	"github.com/meshery/schemas/models/v1alpha2"
	"github.com/meshery/schemas/models/v1beta3"
	model "github.com/meshery/schemas/models/v1beta1/model"
	component "github.com/meshery/schemas/models/v1beta2/component"
	relationship "github.com/meshery/schemas/models/v1beta2/relationship"
	"github.com/pkg/errors"
)

type position struct {
	X float64 `json:"posX" yaml:"posX"`
	Y float64 `json:"posY" yaml:"posY"`
}

// ConvertTo converts the receiver PatternFile (v1beta3) into the legacy
// v1alpha2 PatternFile wrapped by `pattern`. The receiver is the source and
// `pattern` is the destination.
func (p *PatternFile) ConvertTo(pattern conversion.Hub) error {
	patternFile, err := utils.Cast[*v1alpha2.PatternFile](pattern)
	if err != nil {
		return err
	}

	patternFile.Name = p.Name
	patternFile.PatternID = p.ID.String()
	patternFile.Version = p.Version

	for _, cmp := range p.Components {
		if cmp == nil {
			continue
		}

		service := v1alpha2.Service{}

		service.ApiVersion = cmp.Component.Version
		service.Type = cmp.Component.Kind
		componentId := cmp.ID
		service.Id = &componentId
		service.IsAnnotation = cmp.Metadata.IsAnnotation
		if cmp.Model != nil {
			service.Model = cmp.Model.Name
		}
		service.Version = cmp.Version
		service.Name = cmp.DisplayName

		err := p.convertToSettings(&service, cmp)
		if err != nil {
			return err
		}

		err = p.convertToTraits(&service, cmp)
		if err != nil {
			return err
		}

		patternFile.Services[service.Id.String()] = &service
	}

	return nil
}

// ConvertFrom converts the v1alpha2 PatternFile wrapped by `pattern` into
// the receiver (v1beta3). `pattern` is the source and the receiver is the
// destination.
func (p *PatternFile) ConvertFrom(pattern conversion.Hub) error {
	patternFile, err := utils.Cast[*v1alpha2.PatternFile](pattern)
	if err != nil {
		return err
	}

	p.ID = uuid.FromStringOrNil(patternFile.PatternID)
	p.Name = patternFile.Name
	p.SchemaVersion = v1beta3.DesignSchemaVersion
	p.Version = patternFile.Version

	services := patternFile.Services

	for _, service := range services {
		// definition version
		compDefVersion := service.Version
		if compDefVersion == "" {
			compDefVersion = "v1.0.0"
		}
		cmp := component.ComponentDefinition{
			SchemaVersion: v1beta3.ComponentSchemaVersion,
			Version:       compDefVersion,
			DisplayName:   service.Name,
			Component: component.Component{
				Kind:    service.Type,
				Version: service.ApiVersion,
			},
			Model: &model.ModelDefinition{
				SchemaVersion: v1beta3.ModelSchemaVersion,
				Name:          service.Model,
			},
		}
		err := p.convertFromSettings(&cmp, service)
		if err != nil {
			return err
		}

		err = p.convertFromTraits(&cmp, service)
		if err != nil {
			return err
		}

		p.Components = append(p.Components, &cmp)
	}
	p.Relationships = make([]*relationship.RelationshipDefinition, 0)
	return nil
}

func (p *PatternFile) convertFromTraits(cmp *component.ComponentDefinition, service *v1alpha2.Service) error {
	extensionsMetadata, err := utils.Cast[map[string]interface{}](service.Traits["meshmap"])
	if err != nil {
		return errors.Wrapf(err, "failed to extract meshmap traits for the design file")
	}

	// Handle node id: traits.meshmap.id
	compNodeUUID, _ := uuid.NewV4()

	compNodeID, ok := extensionsMetadata["id"].(string)
	if ok {
		nodeID, err := uuid.FromString(compNodeID)
		if err == nil {
			compNodeUUID = nodeID
		}
	}

	isNamespaced := false
	cmp.ID = compNodeUUID
	_metadata := extensionsMetadata["meshmodel-metadata"]
	metadata := map[string]interface{}{}
	if _metadata != nil {
		metadata, err = utils.Cast[map[string]interface{}](_metadata)
		if err != nil {
			return errors.Wrapf(err, "unable to extract metadata for the component \"%s\" of type \"%s\" from the design file", cmp.DisplayName, cmp.Component.Kind)
		}

		namespaced, ok := metadata["isNamespaced"]
		if ok {
			reflectType := reflect.TypeOf(namespaced)
			if reflectType.Kind() == reflect.String {
				val, _ := namespaced.(string)
				if strings.ToLower(val) == "true" {
					isNamespaced = true
				}
			} else if reflectType.Kind() == reflect.Bool {
				isNamespaced, _ = namespaced.(bool)
			}
		}
	}

	delete(metadata, "isNamespaced")
	// Handle component metadata: traits.meshmap.meshmodel-metadata
	_compMetadata, err := utils.MarshalAndUnmarshal[map[string]interface{}, component.ComponentDefinition_Metadata](metadata)
	if err != nil {
		return errors.Wrapf(err, "unable to extract metadata for the component \"%s\" of type \"%s\" from the design file", cmp.DisplayName, cmp.Component.Kind)
	}

	cmp.Metadata = _compMetadata
	cmp.Metadata.IsNamespaced = isNamespaced

	pos, err := utils.MarshalAndUnmarshal[interface{}, position](extensionsMetadata["position"])
	if err == nil {
		// Styles is *core.ComponentStyles on v1beta2 component; allocate it
		// if absent so the position write does not panic (the v1beta1
		// PatternFile.ConvertFrom this was ported from would nil-deref here
		// when Styles was unset). Position is an anonymous struct within
		// core.ComponentStyles; we construct it with an identical field
		// set (including struct tags) so the types match structurally.
		if cmp.Styles == nil {
			cmp.Styles = &core.ComponentStyles{}
		}
		if cmp.Styles.Position == nil {
			pp := struct {
				X float64 `json:"x" yaml:"x"`
				Y float64 `json:"y" yaml:"y"`
			}{}
			cmp.Styles.Position = &pp
		}
		cmp.Styles.Position.X, _ = big.NewFloat(pos.X).Float64()
		cmp.Styles.Position.Y, _ = big.NewFloat(pos.Y).Float64()
	}

	cmp.Metadata.AdditionalProperties = make(map[string]interface{}, 0)
	// Handle position properties: service.dependsOn/
	cmp.Metadata.AdditionalProperties["dependsOn"] = service.DependsOn

	// Handle whiteboardData: service.traits.whiteboardData
	cmp.Metadata.AdditionalProperties["whiteboardData"] = extensionsMetadata["whiteboardData"]

	// Handle fieldRef data
	cmp.Metadata.AdditionalProperties["fieldRefData"] = extensionsMetadata["fieldRefData"]

	return nil
}

func (p *PatternFile) convertToTraits(service *v1alpha2.Service, cmp *component.ComponentDefinition) error {
	extensionsMetadata := make(map[string]interface{}, 0)
	extensionsMetadata["meshmap"] = map[string]interface{}{
		"id":                 cmp.ID,
		"meshmodel-data":     cmp.Model,
		"meshmodel-metadata": cmp.Metadata,
		"position":           cmp.Metadata.AdditionalProperties["position"],
		"whiteboardData":     cmp.Metadata.AdditionalProperties["whiteboardData"],
		"fieldRefData":       cmp.Metadata.AdditionalProperties["fieldRefData"],
	}

	serviceDependencies, err := utils.Cast[[]string](cmp.Metadata.AdditionalProperties["dependsOn"])
	if err != nil {
		// this error can be ignored IMO
		return errors.Wrapf(err, "unable to extract dependOn (service.dependsOn)")
	}

	service.DependsOn = serviceDependencies
	service.Traits = extensionsMetadata
	// Handle relationships
	return nil
}

func (p *PatternFile) convertFromSettings(cmp *component.ComponentDefinition, service *v1alpha2.Service) error {
	metadata := make(map[string]interface{})

	if service.Labels != nil {
		metadata["labels"] = service.Labels
	}

	if service.Annotations != nil {
		metadata["annotations"] = service.Annotations
	}

	if service.Namespace != "" {
		metadata["namespace"] = service.Namespace
	}

	cmp.Configuration = service.Settings
	if cmp.Configuration == nil {
		cmp.Configuration = make(map[string]interface{}, 0)
	}

	if cmp.Configuration["metadata"] == nil {
		cmp.Configuration["metadata"] = make(map[string]interface{})
	}
	cmp.Configuration["metadata"] = metadata
	return nil
}

func (p *PatternFile) convertToSettings(service *v1alpha2.Service, cmp *component.ComponentDefinition) error {
	configurationMetadata := cmp.Configuration["metadata"]

	_configurationMetadata, err := utils.Cast[map[string]interface{}](configurationMetadata)
	if err != nil {
		// if errors occurs exit the conversion, do not perform partial conversion
		// Eventually ask the user whether partial conversion is ok or not and do accordingly
		return errors.Wrapf(err, "unable to extract configuration metdata (labels/annotations)")
	}

	labels := _configurationMetadata["labels"]
	_labels, err := utils.Cast[map[string]string](labels)
	if err != nil {
		return errors.Wrapf(err, "unable to extract metdata.labels")
	}

	annotations := _configurationMetadata["annotations"]
	_annotations, err := utils.Cast[map[string]string](annotations)
	if err != nil {
		return errors.Wrapf(err, "unable to extract metdata.annotations")
	}

	service.Labels = _labels
	service.Annotations = _annotations

	service.Namespace, err = utils.Cast[string](_configurationMetadata["namespace"])
	if err != nil {
		// assign "default" namespace if conversion fails, log it the err rand continue?
		return errors.Wrapf(err, "unable to extract metdata.namespace")
	}

	delete(_configurationMetadata, "labels")
	delete(_configurationMetadata, "annotations")
	delete(_configurationMetadata, "namespace")

	cmp.Configuration["metadata"] = _configurationMetadata // is this reassignment needed, map are references, check once.
	service.Settings = cmp.Configuration
	return nil
}
