package config

import (
	"fmt"
	"strconv"
	"strings"

	"github.com/hashicorp/terraform-plugin-sdk/internal/flatmap"
	"github.com/hashicorp/terraform-plugin-sdk/terraform"
)

// Validator is a helper that helps you validate the configuration
// of your resource, resource provider, etc.
//
// At the most basic level, set the Required and Optional lists to be
// specifiers of keys that are required or optional. If a key shows up
// that isn't in one of these two lists, then an error is generated.
//
// The "specifiers" allowed in this is a fairly rich syntax to help
// describe the format of your configuration:
//
//   * Basic keys are just strings. For example: "foo" will match the
//       "foo" key.
//
//   * Nested structure keys can be matched by doing
//       "listener.*.foo". This will verify that there is at least one
//       listener element that has the "foo" key set.
//
//   * The existence of a nested structure can be checked by simply
//       doing "listener.*" which will verify that there is at least
//       one element in the "listener" structure. This is NOT
//       validating that "listener" is an array. It is validating
//       that it is a nested structure in the configuration.
//
type Validator struct {
	Required []string
	Optional []string
}

func (v *Validator) Validate(
	c *terraform.ResourceConfig) (ws []string, es []error) {
	// Flatten the configuration so it is easier to reason about
	flat := flatmap.Flatten(c.Raw)

	keySet := make(map[string]validatorKey)
	for i, vs := range [][]string{v.Required, v.Optional} {
		req := i == 0
		for _, k := range vs {
			vk, err := newValidatorKey(k, req)
			if err != nil {
				es = append(es, err)
				continue
			}

			keySet[k] = vk
		}
	}

	purged := make([]string, 0)
	for _, kv := range keySet {
		p, w, e := kv.Validate(flat)
		if len(w) > 0 {
			ws = append(ws, w...)
		}
		if len(e) > 0 {
			es = append(es, e...)
		}

		purged = append(purged, p...)
	}

	// Delete all the keys we processed in order to find
	// the unknown keys.
	for _, p := range purged {
		delete(flat, p)
	}

	// The rest are unknown
	for k := range flat {
		es = append(es, fmt.Errorf("Unknown configuration: %s", k))
	}

	return
}

type validatorKey interface {
	// Validate validates the given configuration and returns viewed keys,
	// warnings, and errors.
	Validate(map[string]string) ([]string, []string, []error)
}

func newValidatorKey(k string, req bool) (validatorKey, error) {
	var result validatorKey

	parts := strings.Split(k, ".")
	if len(parts) > 1 && parts[1] == "*" {
		result = &nestedValidatorKey{
			Parts:    parts,
			Required: req,
		}
	} else {
		result = &basicValidatorKey{
			Key:      k,
			Required: req,
		}
	}

	return result, nil
}

// basicValidatorKey validates keys that are basic such as "foo"
type basicValidatorKey struct {
	Key      string
	Required bool
}

func (v *basicValidatorKey) Validate(
	m map[string]string) ([]string, []string, []error) {
	for k := range m {
		// If we have the exact key its a match
		if k == v.Key {
			return []string{k}, nil, nil
		}
	}

	if !v.Required {
		return nil, nil, nil
	}

	return nil, nil, []error{fmt.Errorf(
		"Key not found: %s", v.Key)}
}

type nestedValidatorKey struct {
	Parts    []string
	Required bool
}

func (v *nestedValidatorKey) validate(
	m map[string]string,
	prefix string,
	offset int) ([]string, []string, []error) {
	if offset >= len(v.Parts) {
		// We're at the end. Look for a specific key.
		v2 := &basicValidatorKey{Key: prefix, Required: v.Required}
		return v2.Validate(m)
	}

	current := v.Parts[offset]

	// If we're at offset 0, special case to start at the next one.
	if offset == 0 {
		return v.validate(m, current, offset+1)
	}

	// Determine if we're doing a "for all" or a specific key
	if current != "*" {
		// We're looking at a specific key, continue on.
		return v.validate(m, prefix+"."+current, offset+1)
	}

	// We're doing a "for all", so we loop over.
	countStr, ok := m[prefix+".#"]
	if !ok {
		if !v.Required {
			// It wasn't required, so its no problem.
			return nil, nil, nil
		}

		return nil, nil, []error{fmt.Errorf(
			"Key not found: %s", prefix)}
	}

	count, err := strconv.ParseInt(countStr, 0, 0)
	if err != nil {
		// This shouldn't happen if flatmap works properly
		panic("invalid flatmap array")
	}

	var e []error
	var w []string
	u := make([]string, 1, count+1)
	u[0] = prefix + ".#"
	for i := 0; i < int(count); i++ {
		prefix := fmt.Sprintf("%s.%d", prefix, i)

		// Mark that we saw this specific key
		u = append(u, prefix)

		// Mark all prefixes of this
		for k := range m {
			if !strings.HasPrefix(k, prefix+".") {
				continue
			}
			u = append(u, k)
		}

		// If we have more parts, then validate deeper
		if offset+1 < len(v.Parts) {
			u2, w2, e2 := v.validate(m, prefix, offset+1)

			u = append(u, u2...)
			w = append(w, w2...)
			e = append(e, e2...)
		}
	}

	return u, w, e
}

func (v *nestedValidatorKey) Validate(
	m map[string]string) ([]string, []string, []error) {
	return v.validate(m, "", 0)
}
