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

package command

import (
	"bytes"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"strings"

	"github.com/hashicorp/go-multierror"
	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/hclparse"
	"github.com/hashicorp/hcl/v2/hclsyntax"
	"github.com/hashicorp/hcl/v2/hclwrite"
	"github.com/posener/complete"
	"golang.org/x/crypto/ssh/terminal"
)

const (
	stdinArg  = "-"
	stdinPath = "<stdin>"
)

type FormatCommand struct {
	Meta

	diagWr hcl.DiagnosticWriter

	parser   *hclparse.Parser
	hclDiags hcl.Diagnostics

	errs *multierror.Error

	list         bool
	check        bool
	checkSuccess bool
	recursive    bool
	writeFile    bool
	writeStdout  bool
	paths        []string

	stdin io.Reader
}

func (*FormatCommand) Help() string {
	helpText := `
Usage: nomad fmt [flags] paths ...

  Formats Nomad agent configuration and job file to a canonical format.
  If a path is a directory, it will recursively format all files
  with .nomad and .hcl extensions in the directory.

  If you provide a single dash (-) as argument, fmt will read from standard
  input (STDIN) and output the processed output to standard output (STDOUT).

Format Options:

  -check
	Check if the files are valid HCL files. If not, exit status of the
	command will be 1 and the incorrect files will not be formatted. This
    flag overrides any -write flag value.

  -list
	List the files which contain formatting inconsistencies. Defaults
	to -list=true.

  -recursive
	Process files in subdirectories. By default only the given (or current)
	directory is processed.

  -write
	Overwrite the input files. Defaults to -write=true. Ignored if the input
    comes from STDIN.
`

	return strings.TrimSpace(helpText)
}

func (*FormatCommand) Synopsis() string {
	return "Rewrites Nomad config and job files to canonical format"
}

func (*FormatCommand) AutocompleteArgs() complete.Predictor {
	return complete.PredictFiles("*")
}

func (*FormatCommand) AutocompleteFlags() complete.Flags {
	return complete.Flags{
		"-check":     complete.PredictNothing,
		"-write":     complete.PredictNothing,
		"-list":      complete.PredictNothing,
		"-recursive": complete.PredictNothing,
	}
}

func (f *FormatCommand) Name() string { return "fmt" }

func (f *FormatCommand) Run(args []string) int {
	if f.stdin == nil {
		f.stdin = os.Stdin
	}

	flags := f.Meta.FlagSet(f.Name(), FlagSetClient)
	flags.Usage = func() { f.Ui.Output(f.Help()) }
	flags.BoolVar(&f.check, "check", false, "")
	flags.BoolVar(&f.writeFile, "write", true, "")
	flags.BoolVar(&f.list, "list", true, "")
	flags.BoolVar(&f.recursive, "recursive", false, "")

	if err := flags.Parse(args); err != nil {
		return 1
	}
	f.checkSuccess = true
	f.parser = hclparse.NewParser()

	color := terminal.IsTerminal(int(os.Stderr.Fd()))
	w, _, err := terminal.GetSize(int(os.Stdout.Fd()))
	if err != nil {
		w = 80
	}

	f.diagWr = hcl.NewDiagnosticTextWriter(os.Stderr, f.parser.Files(), uint(w), color)

	if len(flags.Args()) == 0 {
		f.paths = []string{"."}
	} else if flags.Args()[0] == stdinArg {
		f.writeStdout = true
		f.writeFile = false
		f.list = false
	} else {
		f.paths = flags.Args()
	}
	if f.check {
		f.writeFile = false
	}
	if !f.list && !f.writeFile {
		f.writeStdout = true
	}

	f.fmt()

	if f.hclDiags.HasErrors() {
		f.diagWr.WriteDiagnostics(f.hclDiags)
	}

	if f.errs != nil {
		f.Ui.Error(f.errs.Error())
		f.Ui.Error(commandErrorText(f))
	}

	if f.hclDiags.HasErrors() || f.errs != nil {
		return 1
	}

	if !f.checkSuccess {
		return 1
	}
	return 0
}

func (f *FormatCommand) fmt() {
	if len(f.paths) == 0 {
		f.processFile(stdinPath, f.stdin)
		return
	}

	for _, path := range f.paths {
		info, err := os.Stat(path)
		if err != nil {
			f.appendError(fmt.Errorf("No file or directory at %s", path))
			continue
		}

		if info.IsDir() {
			f.processDir(path)
		} else {
			if isNomadFile(info) {
				fp, err := os.Open(path)
				if err != nil {
					f.appendError(fmt.Errorf("Failed to open file %s: %w", path, err))
					continue
				}

				f.processFile(path, fp)

				fp.Close()
			} else {
				f.appendError(fmt.Errorf("Only .nomad and .hcl files can be processed using nomad fmt"))
				continue
			}
		}
	}
}

func (f *FormatCommand) processDir(path string) {
	entries, err := os.ReadDir(path)
	if err != nil {
		f.appendError(fmt.Errorf("Failed to list directory %s", path))
		return
	}

	for _, entry := range entries {
		name := entry.Name()
		subpath := filepath.Join(path, name)

		if entry.IsDir() {
			if f.recursive {
				f.processDir(subpath)
			}

			continue
		}

		info, err := entry.Info()
		if err != nil {
			f.appendError(err)
			continue
		}

		if isNomadFile(info) {
			fp, err := os.Open(subpath)
			if err != nil {
				f.appendError(fmt.Errorf("Failed to open file %s: %w", path, err))
				continue
			}

			f.processFile(subpath, fp)

			fp.Close()
		}
	}
}

func (f *FormatCommand) processFile(path string, r io.Reader) {
	src, err := io.ReadAll(r)
	if err != nil {
		f.appendError(fmt.Errorf("Failed to read file %s: %w", path, err))
		return
	}

	f.parser.AddFile(path, &hcl.File{
		Body:  hcl.EmptyBody(),
		Bytes: src,
	})

	_, syntaxDiags := hclsyntax.ParseConfig(src, path, hcl.InitialPos)
	if syntaxDiags.HasErrors() {
		f.hclDiags = append(f.hclDiags, syntaxDiags...)
		return
	}
	formattedFile, diags := hclwrite.ParseConfig(src, path, hcl.InitialPos)
	if diags.HasErrors() {
		f.hclDiags = append(f.hclDiags, diags...)
		return
	}

	out := formattedFile.Bytes()

	if !bytes.Equal(src, out) {
		if f.list {
			f.Ui.Output(path)
		}

		if f.check {
			f.checkSuccess = false
		}

		if f.writeFile {
			if err := os.WriteFile(path, out, 0644); err != nil {
				f.appendError(fmt.Errorf("Failed to write file %s: %w", path, err))
				return
			}
		}

	}

	if f.writeStdout {
		f.Ui.Output(string(out))
	}
}

func isNomadFile(file fs.FileInfo) bool {
	return !file.IsDir() && (filepath.Ext(file.Name()) == ".nomad" || filepath.Ext(file.Name()) == ".hcl")
}

func (f *FormatCommand) appendError(err error) {
	f.errs = multierror.Append(f.errs, err)
}
