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

package jobspec

import (
	"fmt"

	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"
)

func parseJob(result *api.Job, list *ast.ObjectList) error {
	if len(list.Items) != 1 {
		return fmt.Errorf("only one 'job' block allowed")
	}
	list = list.Children()
	if len(list.Items) != 1 {
		return fmt.Errorf("'job' block missing name")
	}

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

	// Decode the full thing into a map[string]interface for ease
	var m map[string]interface{}
	if err := hcl.DecodeObject(&m, obj.Val); err != nil {
		return err
	}
	delete(m, "constraint")
	delete(m, "affinity")
	delete(m, "meta")
	delete(m, "migrate")
	delete(m, "parameterized")
	delete(m, "periodic")
	delete(m, "reschedule")
	delete(m, "update")
	delete(m, "vault")
	delete(m, "spread")
	delete(m, "multiregion")

	// Set the ID and name to the object key
	result.ID = stringToPtr(obj.Keys[0].Token.Value().(string))
	result.Name = stringToPtr(*result.ID)

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

	// Value should be an object
	var listVal *ast.ObjectList
	if ot, ok := obj.Val.(*ast.ObjectType); ok {
		listVal = ot.List
	} else {
		return fmt.Errorf("job '%s' value: should be an object", *result.ID)
	}

	// Check for invalid keys
	valid := []string{
		"all_at_once",
		"constraint",
		"affinity",
		"spread",
		"datacenters",
		"node_pool",
		"group",
		"id",
		"meta",
		"migrate",
		"name",
		"namespace",
		"parameterized",
		"periodic",
		"priority",
		"region",
		"reschedule",
		"task",
		"type",
		"update",
		"vault",
		"vault_token",
		"consul_token",
		"multiregion",
	}
	if err := checkHCLKeys(listVal, valid); err != nil {
		return multierror.Prefix(err, "job:")
	}

	// Parse constraints
	if o := listVal.Filter("constraint"); len(o.Items) > 0 {
		if err := parseConstraints(&result.Constraints, o); err != nil {
			return multierror.Prefix(err, "constraint ->")
		}
	}

	// Parse affinities
	if o := listVal.Filter("affinity"); len(o.Items) > 0 {
		if err := parseAffinities(&result.Affinities, o); err != nil {
			return multierror.Prefix(err, "affinity ->")
		}
	}

	// If we have an update strategy, then parse that
	if o := listVal.Filter("update"); len(o.Items) > 0 {
		if err := parseUpdate(&result.Update, o); err != nil {
			return multierror.Prefix(err, "update ->")
		}
	}

	// If we have a periodic definition, then parse that
	if o := listVal.Filter("periodic"); len(o.Items) > 0 {
		if err := parsePeriodic(&result.Periodic, o); err != nil {
			return multierror.Prefix(err, "periodic ->")
		}
	}

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

	// If we have a parameterized definition, then parse that
	if o := listVal.Filter("parameterized"); len(o.Items) > 0 {
		if err := parseParameterizedJob(&result.ParameterizedJob, o); err != nil {
			return multierror.Prefix(err, "parameterized ->")
		}
	}

	// If we have a reschedule block, then parse that
	if o := listVal.Filter("reschedule"); len(o.Items) > 0 {
		if err := parseReschedulePolicy(&result.Reschedule, o); err != nil {
			return multierror.Prefix(err, "reschedule ->")
		}
	}

	// If we have a migration strategy, then parse that
	if o := listVal.Filter("migrate"); len(o.Items) > 0 {
		if err := parseMigrate(&result.Migrate, o); err != nil {
			return multierror.Prefix(err, "migrate ->")
		}
	}

	// If we have a multiregion block, then parse that
	if o := listVal.Filter("multiregion"); len(o.Items) > 0 {
		var mr api.Multiregion
		if err := parseMultiregion(&mr, o); err != nil {
			return multierror.Prefix(err, "multiregion ->")
		}
		result.Multiregion = &mr
	}

	// Parse out meta fields. These are in HCL as a list so we need
	// to iterate over them and merge them.
	if metaO := listVal.Filter("meta"); len(metaO.Items) > 0 {
		for _, o := range metaO.Elem().Items {
			var m map[string]interface{}
			if err := hcl.DecodeObject(&m, o.Val); err != nil {
				return err
			}
			if err := mapstructure.WeakDecode(m, &result.Meta); err != nil {
				return err
			}
		}
	}

	// If we have tasks outside, create TaskGroups for them
	if o := listVal.Filter("task"); len(o.Items) > 0 {
		var tasks []*api.Task
		if err := parseTasks(&tasks, o); err != nil {
			return multierror.Prefix(err, "task:")
		}

		result.TaskGroups = make([]*api.TaskGroup, len(tasks), len(tasks)*2)
		for i, t := range tasks {
			result.TaskGroups[i] = &api.TaskGroup{
				Name:  stringToPtr(t.Name),
				Tasks: []*api.Task{t},
			}
		}
	}

	// Parse the task groups
	if o := listVal.Filter("group"); len(o.Items) > 0 {
		if err := parseGroups(result, o); err != nil {
			return multierror.Prefix(err, "group:")
		}
	}

	// If we have a vault block, then parse that
	if o := listVal.Filter("vault"); len(o.Items) > 0 {
		jobVault := &api.Vault{
			Env:         boolToPtr(true),
			DisableFile: boolToPtr(false),
			ChangeMode:  stringToPtr("restart"),
		}

		if err := parseVault(jobVault, o); err != nil {
			return multierror.Prefix(err, "vault ->")
		}

		// Go through the task groups/tasks and if they don't have a Vault block, set it
		for _, tg := range result.TaskGroups {
			for _, task := range tg.Tasks {
				if task.Vault == nil {
					task.Vault = jobVault
				}
			}
		}
	}

	return nil
}

func parsePeriodic(result **api.PeriodicConfig, list *ast.ObjectList) error {
	list = list.Elem()
	if len(list.Items) > 1 {
		return fmt.Errorf("only one 'periodic' block allowed per job")
	}

	// 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{
		"enabled",
		"cron",
		"crons",
		"prohibit_overlap",
		"time_zone",
	}
	if err := checkHCLKeys(o.Val, valid); err != nil {
		return err
	}

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

	// If "cron" is provided, set the type to "cron" and store the spec.
	if cron, ok := m["cron"]; ok {
		m["SpecType"] = api.PeriodicSpecCron
		m["Spec"] = cron
	}

	// If "crons" is provided, set the type to "cron" and store the spec.
	if cron, ok := m["crons"]; ok {
		m["SpecType"] = api.PeriodicSpecCron
		m["Specs"] = cron
	}

	// Build the constraint
	var p api.PeriodicConfig
	if err := mapstructure.WeakDecode(m, &p); err != nil {
		return err
	}
	*result = &p
	return nil
}

func parseParameterizedJob(result **api.ParameterizedJobConfig, list *ast.ObjectList) error {
	list = list.Elem()
	if len(list.Items) > 1 {
		return fmt.Errorf("only one 'parameterized' block allowed per job")
	}

	// 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{
		"payload",
		"meta_required",
		"meta_optional",
	}
	if err := checkHCLKeys(o.Val, valid); err != nil {
		return err
	}

	// Build the parameterized job block
	var d api.ParameterizedJobConfig
	if err := mapstructure.WeakDecode(m, &d); err != nil {
		return err
	}

	*result = &d
	return nil
}
