// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package jobspec

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"regexp"
	"strconv"

	multierror "github.com/hashicorp/go-multierror"
	"github.com/hashicorp/hcl"
	"github.com/hashicorp/hcl/hcl/ast"
	"github.com/hashicorp/nomad/api"
	"github.com/mitchellh/mapstructure"
)

var reDynamicPorts = regexp.MustCompile("^[a-zA-Z0-9_]+$")
var errPortLabel = fmt.Errorf("Port label does not conform to naming requirements %s", reDynamicPorts.String())

// Parse parses the job spec from the given io.Reader.
//
// Due to current internal limitations, the entire contents of the
// io.Reader will be copied into memory first before parsing.
func Parse(r io.Reader) (*api.Job, error) {
	// Copy the reader into an in-memory buffer first since HCL requires it.
	var buf bytes.Buffer
	if _, err := io.Copy(&buf, r); err != nil {
		return nil, err
	}

	// Parse the buffer
	root, err := hcl.Parse(buf.String())
	if err != nil {
		return nil, fmt.Errorf("error parsing: %s", err)
	}
	buf.Reset()

	// Top-level item should be a list
	list, ok := root.Node.(*ast.ObjectList)
	if !ok {
		return nil, fmt.Errorf("error parsing: root should be an object")
	}

	// Check for invalid keys
	valid := []string{
		"job",
	}
	if err := checkHCLKeys(list, valid); err != nil {
		return nil, err
	}

	var job api.Job

	// Parse the job out
	matches := list.Filter("job")
	if len(matches.Items) == 0 {
		return nil, fmt.Errorf("'job' block not found")
	}
	if err := parseJob(&job, matches); err != nil {
		return nil, fmt.Errorf("error parsing 'job': %s", err)
	}

	return &job, nil
}

// ParseFile parses the given path as a job spec.
func ParseFile(path string) (*api.Job, error) {
	path, err := filepath.Abs(path)
	if err != nil {
		return nil, err
	}

	f, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	return Parse(f)
}

func parseReschedulePolicy(final **api.ReschedulePolicy, list *ast.ObjectList) error {
	list = list.Elem()
	if len(list.Items) > 1 {
		return fmt.Errorf("only one 'reschedule' block allowed")
	}

	// Get our job object
	obj := list.Items[0]

	// Check for invalid keys
	valid := []string{
		"attempts",
		"interval",
		"unlimited",
		"delay",
		"max_delay",
		"delay_function",
	}
	if err := checkHCLKeys(obj.Val, valid); err != nil {
		return err
	}

	var m map[string]interface{}
	if err := hcl.DecodeObject(&m, obj.Val); err != nil {
		return err
	}

	var result api.ReschedulePolicy
	dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
		DecodeHook:       mapstructure.StringToTimeDurationHookFunc(),
		WeaklyTypedInput: true,
		Result:           &result,
	})
	if err != nil {
		return err
	}
	if err := dec.Decode(m); err != nil {
		return err
	}

	*final = &result
	return nil
}

func parseConstraints(result *[]*api.Constraint, list *ast.ObjectList) error {
	for _, o := range list.Elem().Items {
		// Check for invalid keys
		valid := []string{
			"attribute",
			"distinct_hosts",
			"distinct_property",
			"operator",
			"regexp",
			"set_contains",
			"value",
			"version",
			"semver",
		}
		if err := checkHCLKeys(o.Val, valid); err != nil {
			return err
		}

		var m map[string]interface{}
		if err := hcl.DecodeObject(&m, o.Val); err != nil {
			return err
		}

		m["LTarget"] = m["attribute"]
		m["RTarget"] = m["value"]
		m["Operand"] = m["operator"]

		// If "version" is provided, set the operand
		// to "version" and the value to the "RTarget"
		if constraint, ok := m[api.ConstraintVersion]; ok {
			m["Operand"] = api.ConstraintVersion
			m["RTarget"] = constraint
		}

		// If "semver" is provided, set the operand
		// to "semver" and the value to the "RTarget"
		if constraint, ok := m[api.ConstraintSemver]; ok {
			m["Operand"] = api.ConstraintSemver
			m["RTarget"] = constraint
		}

		// If "regexp" is provided, set the operand
		// to "regexp" and the value to the "RTarget"
		if constraint, ok := m[api.ConstraintRegex]; ok {
			m["Operand"] = api.ConstraintRegex
			m["RTarget"] = constraint
		}

		// If "set_contains" is provided, set the operand
		// to "set_contains" and the value to the "RTarget"
		if constraint, ok := m[api.ConstraintSetContains]; ok {
			m["Operand"] = api.ConstraintSetContains
			m["RTarget"] = constraint
		}

		if value, ok := m[api.ConstraintDistinctHosts]; ok {
			enabled, err := parseBool(value)
			if err != nil {
				return fmt.Errorf("distinct_hosts should be set to true or false; %v", err)
			}

			// If it is not enabled, skip the constraint.
			if !enabled {
				continue
			}

			m["Operand"] = api.ConstraintDistinctHosts
		}

		if property, ok := m[api.ConstraintDistinctProperty]; ok {
			m["Operand"] = api.ConstraintDistinctProperty
			m["LTarget"] = property
		}

		// Build the constraint
		var c api.Constraint
		if err := mapstructure.WeakDecode(m, &c); err != nil {
			return err
		}
		if c.Operand == "" {
			c.Operand = "="
		}

		*result = append(*result, &c)
	}

	return nil
}

func parseAffinities(result *[]*api.Affinity, list *ast.ObjectList) error {
	for _, o := range list.Elem().Items {
		// Check for invalid keys
		valid := []string{
			"attribute",
			"operator",
			"regexp",
			"set_contains",
			"set_contains_any",
			"set_contains_all",
			"value",
			"version",
			"semver",
			"weight",
		}
		if err := checkHCLKeys(o.Val, valid); err != nil {
			return err
		}

		var m map[string]interface{}
		if err := hcl.DecodeObject(&m, o.Val); err != nil {
			return err
		}

		m["LTarget"] = m["attribute"]
		m["RTarget"] = m["value"]
		m["Operand"] = m["operator"]

		// If "version" is provided, set the operand
		// to "version" and the value to the "RTarget"
		if affinity, ok := m[api.ConstraintVersion]; ok {
			m["Operand"] = api.ConstraintVersion
			m["RTarget"] = affinity
		}

		// If "semver" is provided, set the operand
		// to "semver" and the value to the "RTarget"
		if affinity, ok := m[api.ConstraintSemver]; ok {
			m["Operand"] = api.ConstraintSemver
			m["RTarget"] = affinity
		}

		// If "regexp" is provided, set the operand
		// to "regexp" and the value to the "RTarget"
		if affinity, ok := m[api.ConstraintRegex]; ok {
			m["Operand"] = api.ConstraintRegex
			m["RTarget"] = affinity
		}

		// If "set_contains_any" is provided, set the operand
		// to "set_contains_any" and the value to the "RTarget"
		if affinity, ok := m[api.ConstraintSetContainsAny]; ok {
			m["Operand"] = api.ConstraintSetContainsAny
			m["RTarget"] = affinity
		}

		// If "set_contains_all" is provided, set the operand
		// to "set_contains_all" and the value to the "RTarget"
		if affinity, ok := m[api.ConstraintSetContainsAll]; ok {
			m["Operand"] = api.ConstraintSetContainsAll
			m["RTarget"] = affinity
		}

		// set_contains is a synonym of set_contains_all
		if affinity, ok := m[api.ConstraintSetContains]; ok {
			m["Operand"] = api.ConstraintSetContains
			m["RTarget"] = affinity
		}

		// Build the affinity
		var a api.Affinity
		if err := mapstructure.WeakDecode(m, &a); err != nil {
			return err
		}
		if a.Operand == "" {
			a.Operand = "="
		}

		*result = append(*result, &a)
	}

	return nil
}

func parseSpread(result *[]*api.Spread, list *ast.ObjectList) error {
	for _, o := range list.Elem().Items {
		// Check for invalid keys
		valid := []string{
			"attribute",
			"weight",
			"target",
		}
		if err := checkHCLKeys(o.Val, valid); err != nil {
			return err
		}

		// We need this later
		var listVal *ast.ObjectList
		if ot, ok := o.Val.(*ast.ObjectType); ok {
			listVal = ot.List
		} else {
			return fmt.Errorf("spread should be an object")
		}

		var m map[string]interface{}
		if err := hcl.DecodeObject(&m, o.Val); err != nil {
			return err
		}
		delete(m, "target")
		// Build spread
		var s api.Spread
		if err := mapstructure.WeakDecode(m, &s); err != nil {
			return err
		}

		// Parse spread target
		if o := listVal.Filter("target"); len(o.Items) > 0 {
			if err := parseSpreadTarget(&s.SpreadTarget, o); err != nil {
				return multierror.Prefix(err, "target ->")
			}
		}

		*result = append(*result, &s)
	}

	return nil
}

func parseSpreadTarget(result *[]*api.SpreadTarget, list *ast.ObjectList) error {
	seen := make(map[string]struct{})
	for _, item := range list.Items {
		if len(item.Keys) != 1 {
			return fmt.Errorf("missing spread target")
		}
		n := item.Keys[0].Token.Value().(string)

		// Make sure we haven't already found this
		if _, ok := seen[n]; ok {
			return fmt.Errorf("target '%s' defined more than once", n)
		}
		seen[n] = struct{}{}

		// We need this later
		var listVal *ast.ObjectList
		if ot, ok := item.Val.(*ast.ObjectType); ok {
			listVal = ot.List
		} else {
			return fmt.Errorf("target should be an object")
		}

		// Check for invalid keys
		valid := []string{
			"percent",
			"value",
		}
		if err := checkHCLKeys(listVal, valid); err != nil {
			return multierror.Prefix(err, fmt.Sprintf("'%s' ->", n))
		}

		var m map[string]interface{}
		if err := hcl.DecodeObject(&m, item.Val); err != nil {
			return err
		}

		// Decode spread target
		var g api.SpreadTarget
		g.Value = n
		if err := mapstructure.WeakDecode(m, &g); err != nil {
			return err
		}
		*result = append(*result, &g)
	}
	return nil
}

// parseBool takes an interface value and tries to convert it to a boolean and
// returns an error if the type can't be converted.
func parseBool(value interface{}) (bool, error) {
	var enabled bool
	var err error
	switch data := value.(type) {
	case string:
		enabled, err = strconv.ParseBool(data)
	case bool:
		enabled = data
	default:
		err = fmt.Errorf("%v couldn't be converted to boolean value", value)
	}

	return enabled, err
}

func parseUpdate(result **api.UpdateStrategy, list *ast.ObjectList) error {
	list = list.Elem()
	if len(list.Items) > 1 {
		return fmt.Errorf("only one 'update' block allowed")
	}

	// Get our resource object
	o := list.Items[0]

	var m map[string]interface{}
	if err := hcl.DecodeObject(&m, o.Val); err != nil {
		return err
	}

	// Check for invalid keys
	valid := []string{
		"stagger",
		"max_parallel",
		"health_check",
		"min_healthy_time",
		"healthy_deadline",
		"progress_deadline",
		"auto_revert",
		"auto_promote",
		"canary",
	}
	if err := checkHCLKeys(o.Val, valid); err != nil {
		return err
	}

	dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
		DecodeHook:       mapstructure.StringToTimeDurationHookFunc(),
		WeaklyTypedInput: true,
		Result:           result,
	})
	if err != nil {
		return err
	}
	return dec.Decode(m)
}

func parseMigrate(result **api.MigrateStrategy, list *ast.ObjectList) error {
	list = list.Elem()
	if len(list.Items) > 1 {
		return fmt.Errorf("only one 'migrate' block allowed")
	}

	// Get our resource object
	o := list.Items[0]

	var m map[string]interface{}
	if err := hcl.DecodeObject(&m, o.Val); err != nil {
		return err
	}

	// Check for invalid keys
	valid := []string{
		"max_parallel",
		"health_check",
		"min_healthy_time",
		"healthy_deadline",
	}
	if err := checkHCLKeys(o.Val, valid); err != nil {
		return err
	}

	dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
		DecodeHook:       mapstructure.StringToTimeDurationHookFunc(),
		WeaklyTypedInput: true,
		Result:           result,
	})
	if err != nil {
		return err
	}
	return dec.Decode(m)
}

func parseVault(result *api.Vault, list *ast.ObjectList) error {
	list = list.Elem()
	if len(list.Items) == 0 {
		return nil
	}
	if len(list.Items) > 1 {
		return fmt.Errorf("only one 'vault' block allowed per task")
	}

	// Get our resource object
	o := list.Items[0]

	// We need this later
	var listVal *ast.ObjectList
	if ot, ok := o.Val.(*ast.ObjectType); ok {
		listVal = ot.List
	} else {
		return fmt.Errorf("vault: should be an object")
	}

	// Check for invalid keys
	valid := []string{
		"namespace",
		"policies",
		"env",
		"disable_file",
		"change_mode",
		"change_signal",
	}
	if err := checkHCLKeys(listVal, valid); err != nil {
		return multierror.Prefix(err, "vault ->")
	}

	var m map[string]interface{}
	if err := hcl.DecodeObject(&m, o.Val); err != nil {
		return err
	}

	if err := mapstructure.WeakDecode(m, result); err != nil {
		return err
	}

	return nil
}
