package resolve

import (
	"fmt"

	"github.com/Aptomi/aptomi/pkg/lang"
	"github.com/Aptomi/aptomi/pkg/runtime"
	"github.com/Aptomi/aptomi/pkg/util"
)

// PolicyResolution contains resolution data for the policy. It essentially represents the desired state calculated
// by policy resolver. It contains a calculated map of component instances with their data, with information about
// resolved claims.
type PolicyResolution struct {
	// Resolved component instances: componentKey -> componentInstance
	ComponentInstanceMap map[string]*ComponentInstance
}

// NewPolicyResolution creates new empty PolicyResolution, given a flag indicating whether it's a
// desired state (generated by a resolver), or actual state (loaded from the store)
func NewPolicyResolution() *PolicyResolution {
	return &PolicyResolution{
		ComponentInstanceMap: make(map[string]*ComponentInstance),
	}
}

// GetComponentInstanceEntry retrieves a component instance entry by key, or creates an new entry if it doesn't exist
func (resolution *PolicyResolution) GetComponentInstanceEntry(cik *ComponentInstanceKey) *ComponentInstance {
	key := cik.GetKey()
	if _, ok := resolution.ComponentInstanceMap[key]; !ok {
		resolution.ComponentInstanceMap[key] = newComponentInstance(cik)
	}
	return resolution.ComponentInstanceMap[key]
}

// RecordResolved takes a component instance and adds a new claim record into it
func (resolution *PolicyResolution) RecordResolved(cik *ComponentInstanceKey, claim *lang.Claim, depth int, ruleResult *lang.RuleActionResult) {
	instance := resolution.GetComponentInstanceEntry(cik)
	instance.addClaim(runtime.KeyForStorable(claim), depth)
	instance.addRuleInformation(ruleResult)
}

// RecordCodeParams stores calculated code params for component instance
func (resolution *PolicyResolution) RecordCodeParams(cik *ComponentInstanceKey, codeParams util.NestedParameterMap) error {
	instance := resolution.GetComponentInstanceEntry(cik)
	instance.IsCode = true
	return instance.addCodeParams(codeParams)
}

// RecordDiscoveryParams stores calculated discovery params for component instance
func (resolution *PolicyResolution) RecordDiscoveryParams(cik *ComponentInstanceKey, discoveryParams util.NestedParameterMap) error {
	return resolution.GetComponentInstanceEntry(cik).addDiscoveryParams(discoveryParams)
}

// RecordLabels stores calculated labels for component instance
func (resolution *PolicyResolution) RecordLabels(cik *ComponentInstanceKey, labels *lang.LabelSet) {
	resolution.GetComponentInstanceEntry(cik).addLabels(labels)
}

// StoreEdge stores incoming/outgoing graph edges for component instance for observability and reporting
func (resolution *PolicyResolution) StoreEdge(src *ComponentInstanceKey, dst *ComponentInstanceKey) {
	// Arrival key can be empty at the very top of the recursive function in engine, so let's check for that
	if src != nil && dst != nil {
		resolution.GetComponentInstanceEntry(src).addEdgeOut(dst.GetKey())
	}
}

// AppendData appends data to the current PolicyResolution record by aggregating data over component instances.
// If there is a conflict (e.g. components have different code parameters), then the corresponding component instances
// will be marked with errors.
func (resolution *PolicyResolution) AppendData(ops *PolicyResolution) {
	for key, instance := range ops.ComponentInstanceMap {
		// if component doesn't exist, copy it over
		if _, ok := resolution.ComponentInstanceMap[key]; !ok {
			resolution.ComponentInstanceMap[key] = instance
		} else { // otherwise, update data
			resolution.ComponentInstanceMap[key].appendData(instance)
		}
	}
}

// GetClaimResolution returns resolution status for a particular claim
func (resolution *PolicyResolution) GetClaimResolution(claim *lang.Claim) *ClaimResolution {
	claimKey := runtime.KeyForStorable(claim)

	var dError error
	var dComponentKey string
	for _, instance := range resolution.ComponentInstanceMap {
		if depth, found := instance.ClaimKeys[claimKey]; found {
			// see if claim components have errors
			if dError == nil {
				dError = instance.Error
			}

			// if it's a bundle at depth 0, we have found the bundle instance to which our claim resolved
			if depth == 0 && instance.Metadata.Key.IsBundle() {
				dComponentKey = instance.Metadata.Key.GetKey()
			}
		}
	}

	return newClaimResolution(dError == nil && len(dComponentKey) > 0, dComponentKey)
}

// Validate checks that the state is valid, meaning that all objects references are valid and all components are valid
//  - it takes all the instances and verifies that all bundles exist, all clusters exist, etc
//  - it also verifies that there are no components with global errors (e.g. conflicting code/discovery params)
func (resolution *PolicyResolution) Validate(policy *lang.Policy) error {
	// component instances must not have global errors (e.g. conflicting code/discovery params)
	for _, instance := range resolution.ComponentInstanceMap {
		if instance.Error != nil {
			return instance.Error
		}
	}

	// component instances must point to valid objects
	for _, instance := range resolution.ComponentInstanceMap {
		componentKey := instance.Metadata.Key

		// verify that service exists
		serviceObj, err := policy.GetObject(lang.TypeService.Kind, componentKey.ServiceName, componentKey.Namespace)
		if serviceObj == nil || err != nil {
			// component instance points to non-existing service, meaning this component instance is now orphan
			return fmt.Errorf("service '%s/%s' can only be deleted after it's no longer in use. still used by: %s", componentKey.Namespace, componentKey.ServiceName, componentKey.GetKey())
		}

		// verify that context within a service exists
		service := serviceObj.(*lang.Service) // nolint: errcheck
		contextExists := false
		for _, context := range service.Contexts {
			if context.Name == componentKey.ContextName {
				contextExists = true
				break
			}
		}
		if !contextExists {
			// component instance points to non-existing context within a service, meaning this component instance is now orphan
			return fmt.Errorf("context '%s/%s/%s' can only be deleted after it's no longer in use. still used by: %s", componentKey.Namespace, componentKey.ServiceName, componentKey.ContextName, componentKey.GetKey())
		}

		// verify that bundle exists
		bundleObj, err := policy.GetObject(lang.TypeBundle.Kind, componentKey.BundleName, componentKey.Namespace)
		if bundleObj == nil || err != nil {
			// component instance points to non-existing bundle, meaning this component instance is now orphan
			return fmt.Errorf("bundle '%s/%s' can only be deleted after it's no longer in use. still used by: %s", componentKey.Namespace, componentKey.BundleName, componentKey.GetKey())
		}

		if componentKey.ComponentName != componentRootName {
			// verify that component within a bundle exists
			bundle := bundleObj.(*lang.Bundle) // nolint: errcheck
			component, found := bundle.GetComponentsMap()[componentKey.ComponentName]
			if component == nil || !found {
				// component instance points to non-existing component within a bundle, meaning this component instance is now orphan
				return fmt.Errorf("component '%s/%s/%s' can only be deleted after it's no longer in use. still used by: %s", componentKey.Namespace, componentKey.BundleName, componentKey.ComponentName, componentKey.GetKey())
			}
		}

		// verify that cluster exists
		clusterObj, err := policy.GetObject(lang.TypeCluster.Kind, componentKey.ClusterName, componentKey.ClusterNameSpace)
		if clusterObj == nil || err != nil {
			// component instance points to non-existing cluster, meaning this component instance is now orphan
			return fmt.Errorf("cluster '%s/%s' can only be deleted after it's no longer in use. still used by: %s", componentKey.Namespace, componentKey.ClusterName, componentKey.GetKey())
		}
	}

	return nil
}
