// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package hcl2template

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/gobwas/glob"
	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/hclsyntax"
	"github.com/hashicorp/packer/hcl2template/repl"
	hcl2shim "github.com/hashicorp/packer/hcl2template/shim"
	"github.com/zclconf/go-cty/cty"
)

func warningErrorsToDiags(block *hcl.Block, warnings []string, err error) hcl.Diagnostics {
	var diags hcl.Diagnostics

	for _, warning := range warnings {
		diags = append(diags, &hcl.Diagnostic{
			Summary:  warning,
			Subject:  &block.DefRange,
			Severity: hcl.DiagWarning,
		})
	}
	if err != nil {
		diags = append(diags, &hcl.Diagnostic{
			Summary:  err.Error(),
			Subject:  &block.DefRange,
			Severity: hcl.DiagError,
		})
	}
	return diags
}

func isDir(name string) (bool, error) {
	s, err := os.Stat(name)
	if err != nil {
		return false, err
	}
	return s.IsDir(), nil
}

// GetHCL2Files returns two slices of json formatted and hcl formatted files,
// hclSuffix and jsonSuffix tell which file is what. Filename can be a folder
// or a file.
//
// When filename is a folder all files of folder matching the suffixes will be
// returned. Otherwise if filename references a file and filename matches one
// of the suffixes it is returned in the according slice.
func GetHCL2Files(filename, hclSuffix, jsonSuffix string) (hclFiles, jsonFiles []string, diags hcl.Diagnostics) {
	if filename == "" {
		return
	}
	isDir, err := isDir(filename)
	if err != nil {
		diags = append(diags, &hcl.Diagnostic{
			Severity: hcl.DiagError,
			Detail:   err.Error(),
		})
		return nil, nil, diags
	}
	if !isDir {
		if strings.HasSuffix(filename, jsonSuffix) {
			return nil, []string{filename}, diags
		}
		if strings.HasSuffix(filename, hclSuffix) {
			return []string{filename}, nil, diags
		}
		return nil, nil, diags
	}

	fileInfos, err := os.ReadDir(filename)
	if err != nil {
		diag := &hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Cannot read hcl directory",
			Detail:   err.Error(),
		}
		diags = append(diags, diag)
		return nil, nil, diags
	}
	for _, fileInfo := range fileInfos {
		if fileInfo.IsDir() {
			continue
		}
		filename := filepath.Join(filename, fileInfo.Name())
		if strings.HasSuffix(filename, hclSuffix) {
			hclFiles = append(hclFiles, filename)
		} else if strings.HasSuffix(filename, jsonSuffix) {
			jsonFiles = append(jsonFiles, filename)
		}
	}

	return hclFiles, jsonFiles, diags
}

// Convert -only and -except globs to glob.Glob instances.
func convertFilterOption(patterns []string, optionName string) ([]glob.Glob, hcl.Diagnostics) {
	var globs []glob.Glob
	var diags hcl.Diagnostics

	for _, pattern := range patterns {
		g, err := glob.Compile(pattern)
		if err != nil {
			diags = append(diags, &hcl.Diagnostic{
				Summary:  fmt.Sprintf("Invalid -%s pattern %s: %s", optionName, pattern, err),
				Severity: hcl.DiagError,
			})
		}
		globs = append(globs, g)
	}

	return globs, diags
}

func PrintableCtyValue(v cty.Value) string {
	if !v.IsWhollyKnown() {
		return "<unknown>"
	}
	gval := hcl2shim.ConfigValueFromHCL2(v)
	str := repl.FormatResult(gval)
	return str
}

func ConvertPluginConfigValueToHCLValue(v interface{}) (cty.Value, error) {
	var buildValue cty.Value
	switch v := v.(type) {
	case bool:
		buildValue = cty.BoolVal(v)
	case string:
		buildValue = cty.StringVal(v)
	case uint8:
		buildValue = cty.NumberUIntVal(uint64(v))
	case float64:
		buildValue = cty.NumberFloatVal(v)
	case int64:
		buildValue = cty.NumberIntVal(v)
	case uint64:
		buildValue = cty.NumberUIntVal(v)
	case []string:
		vals := make([]cty.Value, len(v))
		for i, ev := range v {
			vals[i] = cty.StringVal(ev)
		}
		if len(vals) == 0 {
			buildValue = cty.ListValEmpty(cty.String)
		} else {
			buildValue = cty.ListVal(vals)
		}
	case []uint8:
		vals := make([]cty.Value, len(v))
		for i, ev := range v {
			vals[i] = cty.NumberUIntVal(uint64(ev))
		}
		if len(vals) == 0 {
			buildValue = cty.ListValEmpty(cty.Number)
		} else {
			buildValue = cty.ListVal(vals)
		}
	case []int64:
		vals := make([]cty.Value, len(v))
		for i, ev := range v {
			vals[i] = cty.NumberIntVal(ev)
		}
		if len(vals) == 0 {
			buildValue = cty.ListValEmpty(cty.Number)
		} else {
			buildValue = cty.ListVal(vals)
		}
	case []uint64:
		vals := make([]cty.Value, len(v))
		for i, ev := range v {
			vals[i] = cty.NumberUIntVal(ev)
		}
		if len(vals) == 0 {
			buildValue = cty.ListValEmpty(cty.Number)
		} else {
			buildValue = cty.ListVal(vals)
		}
	default:
		return cty.Value{}, fmt.Errorf("unhandled buildvar type: %T", v)
	}
	return buildValue, nil
}

// GetVarsByType walks through a hcl body, and gathers all the Traversals that
// have a root type matching one of the specified top-level labels.
//
// This will only work on finite, expanded, HCL bodies.
func GetVarsByType(block *hcl.Block, topLevelLabels ...string) []hcl.Traversal {
	var travs []hcl.Traversal

	switch body := block.Body.(type) {
	case *hclsyntax.Body:
		travs = getVarsByTypeForHCLSyntaxBody(body)
	default:
		attrs, _ := body.JustAttributes()
		for _, attr := range attrs {
			travs = append(travs, attr.Expr.Variables()...)
		}
	}

	return FilterTraversalsByType(travs, topLevelLabels...)
}

// FilterTraversalsByType lets the caller filter the traversals per top-level type.
//
// This can then be used to detect dependencies between block types.
func FilterTraversalsByType(travs []hcl.Traversal, topLevelLabels ...string) []hcl.Traversal {
	var rets []hcl.Traversal
	for _, t := range travs {
		varRootname := t.RootName()
		for _, lbl := range topLevelLabels {
			if varRootname == lbl {
				rets = append(rets, t)
				break
			}
		}
	}

	return rets
}

func getVarsByTypeForHCLSyntaxBody(body *hclsyntax.Body) []hcl.Traversal {
	var rets []hcl.Traversal

	for _, attr := range body.Attributes {
		rets = append(rets, attr.Expr.Variables()...)
	}

	for _, block := range body.Blocks {
		rets = append(rets, getVarsByTypeForHCLSyntaxBody(block.Body)...)
	}

	return rets
}
