package hcl2template

import (
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"os/exec"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/hclparse"
	"github.com/hashicorp/hcl/v2/hclwrite"
)

type HCL2Formatter struct {
	ShowDiff, Write bool
	Output          io.Writer
	parser          *hclparse.Parser
}

// NewHCL2Formatter creates a new formatter, ready to format configuration files.
func NewHCL2Formatter() *HCL2Formatter {
	return &HCL2Formatter{
		parser: hclparse.NewParser(),
	}
}

// Format all HCL2 files in path and return the total bytes formatted.
// If any error is encountered, zero bytes will be returned.
//
// Path can be a directory or a file.
func (f *HCL2Formatter) Format(path string) (int, hcl.Diagnostics) {
	hclFiles, _, diags := GetHCL2Files(path, hcl2FileExt, hcl2JsonFileExt)
	if diags.HasErrors() {
		return 0, diags
	}

	hclVarFiles, _, diags := GetHCL2Files(path, hcl2VarFileExt, hcl2VarJsonFileExt)
	if diags.HasErrors() {
		return 0, diags
	}

	allHclFiles := append(hclFiles, hclVarFiles...)

	if len(allHclFiles) == 0 {
		diags = append(diags, &hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  fmt.Sprintf("Cannot tell whether %s contains HCL2 configuration data", path),
		})

		return 0, diags
	}

	if f.parser == nil {
		f.parser = hclparse.NewParser()
	}

	var bytesModified int
	for _, fn := range allHclFiles {
		data, err := f.processFile(fn)
		if err != nil {
			diags = append(diags, &hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  fmt.Sprintf("encountered an error while formatting %s", fn),
				Detail:   err.Error(),
			})
		}
		bytesModified += len(data)
	}

	return bytesModified, diags
}

// processFile formats the source contents of filename and return the formatted data.
// overwriting the contents of the original when the f.Write is true; a diff of the changes
// will be outputted if f.ShowDiff is true.
func (f *HCL2Formatter) processFile(filename string) ([]byte, error) {
	if f.Output == nil {
		f.Output = os.Stdout
	}

	in, err := os.Open(filename)
	if err != nil {
		return nil, fmt.Errorf("failed to open %s: %s", filename, err)
	}

	inSrc, err := ioutil.ReadAll(in)
	if err != nil {
		return nil, fmt.Errorf("failed to read %s: %s", filename, err)
	}

	_, diags := f.parser.ParseHCL(inSrc, filename)
	if diags.HasErrors() {
		return nil, fmt.Errorf("failed to parse HCL %s", filename)
	}

	outSrc := hclwrite.Format(inSrc)

	if bytes.Equal(inSrc, outSrc) {
		return nil, nil
	}

	s := []byte(fmt.Sprintf("%s\n", filename))
	_, _ = f.Output.Write(s)

	if f.Write {
		if err := ioutil.WriteFile(filename, outSrc, 0644); err != nil {
			return nil, err
		}
	}

	if f.ShowDiff {
		diff, err := bytesDiff(inSrc, outSrc, filename)
		if err != nil {
			return outSrc, fmt.Errorf("failed to generate diff for %s: %s", filename, err)
		}
		_, _ = f.Output.Write(diff)
	}

	return outSrc, nil
}

// bytesDiff returns the unified diff of b1 and b2
// Shamelessly copied from Terraform's fmt command.
func bytesDiff(b1, b2 []byte, path string) (data []byte, err error) {
	f1, err := ioutil.TempFile("", "")
	if err != nil {
		return
	}
	defer os.Remove(f1.Name())
	defer f1.Close()

	f2, err := ioutil.TempFile("", "")
	if err != nil {
		return
	}
	defer os.Remove(f2.Name())
	defer f2.Close()

	_, _ = f1.Write(b1)
	_, _ = f2.Write(b2)

	data, err = exec.Command("diff", "--label=old/"+path, "--label=new/"+path, "-u", f1.Name(), f2.Name()).CombinedOutput()
	if len(data) > 0 {
		// diff exits with a non-zero status when the files don't match.
		// Ignore that failure as long as we get output.
		err = nil
	}
	return
}
