package structs

import (
	"fmt"
	"strings"

	"github.com/mitchellh/hashstructure"
)

const (
	// NodeUniqueNamespace is a prefix that can be appended to node meta or
	// attribute keys to mark them for exclusion in computed node class.
	NodeUniqueNamespace = "unique."
)

// UniqueNamespace takes a key and returns the key marked under the unique
// namespace.
func UniqueNamespace(key string) string {
	return fmt.Sprintf("%s%s", NodeUniqueNamespace, key)
}

// IsUniqueNamespace returns whether the key is under the unique namespace.
func IsUniqueNamespace(key string) bool {
	return strings.HasPrefix(key, NodeUniqueNamespace)
}

// ComputeClass computes a derived class for the node based on its attributes.
// ComputedClass is a unique id that identifies nodes with a common set of
// attributes and capabilities. Thus, when calculating a node's computed class
// we avoid including any uniquely identifying fields.
func (n *Node) ComputeClass() error {
	hash, err := hashstructure.Hash(n, nil)
	if err != nil {
		return err
	}

	n.ComputedClass = fmt.Sprintf("v1:%d", hash)
	return nil
}

// HashInclude is used to denylist uniquely identifying node fields from being
// included in the computed node class.
func (n Node) HashInclude(field string, v interface{}) (bool, error) {
	switch field {
	case "Datacenter", "Attributes", "Meta", "NodeClass", "NodeResources":
		return true, nil
	default:
		return false, nil
	}
}

// HashIncludeMap is used to denylist uniquely identifying node map keys from being
// included in the computed node class.
func (n Node) HashIncludeMap(field string, k, v interface{}) (bool, error) {
	key, ok := k.(string)
	if !ok {
		return false, fmt.Errorf("map key %v not a string", k)
	}

	switch field {
	case "Meta", "Attributes":
		return !IsUniqueNamespace(key), nil
	default:
		return false, fmt.Errorf("unexpected map field: %v", field)
	}
}

// HashInclude is used to denylist uniquely identifying node fields from being
// included in the computed node class.
func (n NodeResources) HashInclude(field string, v interface{}) (bool, error) {
	switch field {
	case "Devices":
		return true, nil
	default:
		return false, nil
	}
}

// HashInclude is used to denylist uniquely identifying node fields from being
// included in the computed node class.
func (n NodeDeviceResource) HashInclude(field string, v interface{}) (bool, error) {
	switch field {
	case "Vendor", "Type", "Name", "Attributes":
		return true, nil
	default:
		return false, nil
	}
}

// HashIncludeMap is used to denylist uniquely identifying node map keys from being
// included in the computed node class.
func (n NodeDeviceResource) HashIncludeMap(field string, k, v interface{}) (bool, error) {
	key, ok := k.(string)
	if !ok {
		return false, fmt.Errorf("map key %v not a string", k)
	}

	switch field {
	case "Attributes":
		return !IsUniqueNamespace(key), nil
	default:
		return false, fmt.Errorf("unexpected map field: %v", field)
	}
}

// EscapedConstraints takes a set of constraints and returns the set that
// escapes computed node classes.
func EscapedConstraints(constraints []*Constraint) []*Constraint {
	var escaped []*Constraint
	for _, c := range constraints {
		if constraintTargetEscapes(c.LTarget) || constraintTargetEscapes(c.RTarget) {
			escaped = append(escaped, c)
		}
	}

	return escaped
}

// constraintTargetEscapes returns whether the target of a constraint escapes
// computed node class optimization.
func constraintTargetEscapes(target string) bool {
	switch {
	case strings.HasPrefix(target, "${node.unique."):
		return true
	case strings.HasPrefix(target, "${attr.unique."):
		return true
	case strings.HasPrefix(target, "${meta.unique."):
		return true
	default:
		return false
	}
}
