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

package command

import (
	"bufio"
	"flag"
	"fmt"
	"io"
	"os"
	"strings"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/hclparse"
	packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
	"github.com/hashicorp/packer-plugin-sdk/template"
	kvflag "github.com/hashicorp/packer/command/flag-kv"
	"github.com/hashicorp/packer/hcl2template"
	"github.com/hashicorp/packer/helper/wrappedstreams"
	"github.com/hashicorp/packer/packer"
	"github.com/hashicorp/packer/version"
)

// FlagSetFlags is an enum to define what flags are present in the
// default FlagSet returned by Meta.FlagSet
type FlagSetFlags uint

const (
	FlagSetNone        FlagSetFlags = 0
	FlagSetBuildFilter FlagSetFlags = 1 << iota
	FlagSetVars
)

// Meta contains the meta-options and functionality that nearly every
// Packer command inherits.
type Meta struct {
	CoreConfig *packer.CoreConfig
	Ui         packersdk.Ui
	Version    string
}

// Core returns the core for the given template given the configured
// CoreConfig and user variables on this Meta.
func (m *Meta) Core(tpl *template.Template, cla *MetaArgs) (*packer.Core, error) {
	// Copy the config so we don't modify it
	config := *m.CoreConfig
	config.Template = tpl

	fj := &kvflag.FlagJSON{}
	// First populate fj with contents from var files
	for _, file := range cla.VarFiles {
		err := fj.Set(file)
		if err != nil {
			return nil, err
		}
	}
	// Now read fj values back into flagvars and set as config.Variables. Only
	// add to flagVars if the key doesn't already exist, because flagVars comes
	// from the command line and should not be overridden by variable files.
	if cla.Vars == nil {
		cla.Vars = map[string]string{}
	}
	for k, v := range *fj {
		if _, exists := cla.Vars[k]; !exists {
			cla.Vars[k] = v
		}
	}
	config.Variables = cla.Vars

	core := packer.NewCore(&config)
	return core, nil
}

// FlagSet returns a FlagSet with the common flags that every
// command implements. The exact behavior of FlagSet can be configured
// using the flags as the second parameter, for example to disable
// build settings on the commands that don't handle builds.
func (m *Meta) FlagSet(n string, _ FlagSetFlags) *flag.FlagSet {
	f := flag.NewFlagSet(n, flag.ContinueOnError)

	// Create an io.Writer that writes to our Ui properly for errors.
	// This is kind of a hack, but it does the job. Basically: create
	// a pipe, use a scanner to break it into lines, and output each line
	// to the UI. Do this forever.
	errR, errW := io.Pipe()
	errScanner := bufio.NewScanner(errR)
	go func() {
		for errScanner.Scan() {
			m.Ui.Error(errScanner.Text())
		}
	}()
	f.SetOutput(errW)

	return f
}

// ValidateFlags should be called after parsing flags to validate the
// given flags
func (m *Meta) ValidateFlags() error {
	// TODO
	return nil
}

// StdinPiped returns true if the input is piped.
func (m *Meta) StdinPiped() bool {
	fi, err := wrappedstreams.Stdin().Stat()
	if err != nil {
		// If there is an error, let's just say its not piped
		return false
	}

	return fi.Mode()&os.ModeNamedPipe != 0
}

func (m *Meta) GetConfig(cla *MetaArgs) (packer.Handler, int) {
	cfgType, err := cla.GetConfigType()
	if err != nil {
		m.Ui.Error(fmt.Sprintf("%q: %s", cla.Path, err))
		return nil, 1
	}

	switch cfgType {
	case ConfigTypeHCL2:
		packer.CheckpointReporter.SetTemplateType(packer.HCL2Template)
		// TODO(azr): allow to pass a slice of files here.
		return m.GetConfigFromHCL(cla)
	default:
		packer.CheckpointReporter.SetTemplateType(packer.JSONTemplate)
		// TODO: uncomment once we've polished HCL a bit more.
		// c.Ui.Say(`Legacy JSON Configuration Will Be Used.
		// The template will be parsed in the legacy configuration style. This style
		// will continue to work but users are encouraged to move to the new style.
		// See: https://packer.io/guides/hcl
		// `)
		return m.GetConfigFromJSON(cla)
	}
}

func (m *Meta) GetConfigFromHCL(cla *MetaArgs) (*hcl2template.PackerConfig, int) {
	parser := &hcl2template.Parser{
		CorePackerVersion:       version.SemVer,
		CorePackerVersionString: version.FormattedVersion(),
		Parser:                  hclparse.NewParser(),
		PluginConfig:            m.CoreConfig.Components.PluginConfig,
		ValidationOptions: hcl2template.ValidationOptions{
			WarnOnUndeclaredVar: cla.WarnOnUndeclaredVar,
		},
	}
	cfg, diags := parser.Parse(cla.Path, cla.VarFiles, cla.Vars)
	return cfg, writeDiags(m.Ui, parser.Files(), diags)
}

func (m *Meta) GetConfigFromJSON(cla *MetaArgs) (packer.Handler, int) {
	// Parse the template
	var tpl *template.Template
	var err error
	if cla.Path == "" {
		// here cla validation passed so this means we want a default builder
		// and we probably are in the console command
		tpl, err = template.Parse(TiniestBuilder)
	} else {
		tpl, err = template.ParseFile(cla.Path)
	}

	if err != nil {
		m.Ui.Error(fmt.Sprintf("Failed to parse file as legacy JSON template: "+
			"if you are using an HCL template, check your file extensions; they "+
			"should be either *.pkr.hcl or *.pkr.json; see the docs for more "+
			"details: https://www.packer.io/docs/templates/hcl_templates. \n"+
			"Original error: %s", err))
		return nil, 1
	}

	// Get the core
	core, err := m.Core(tpl, cla)
	ret := 0
	if err != nil {
		m.Ui.Error(err.Error())
		ret = 1
	}
	return core, ret
}

func (m *Meta) DetectBundledPlugins(handler packer.Handler) hcl.Diagnostics {
	var plugins []string

	switch h := handler.(type) {
	case *packer.Core:
		plugins = m.detectBundledPluginsJSON(h)
	case *hcl2template.PackerConfig:
		plugins = m.detectBundledPluginsHCL2(handler.(*hcl2template.PackerConfig))
	}

	if len(plugins) == 0 {
		return nil
	}

	packer.CheckpointReporter.SetBundledUsage()

	buf := &strings.Builder{}
	buf.WriteString("This template relies on the use of plugins bundled into the Packer binary.\n")
	buf.WriteString("The practice of bundling external plugins into Packer will be removed in an upcoming version.\n\n")
	switch h := handler.(type) {
	case *packer.Core:
		buf.WriteString("To remove this warning and ensure builds keep working you can install these external plugins with the 'packer plugins install' command\n\n")

		for _, plugin := range plugins {
			fmt.Fprintf(buf, "* packer plugins install %s\n", plugin)
		}

		buf.WriteString("\nAlternatively, if you upgrade your templates to HCL2, you can use 'packer init' with a 'required_plugins' block to automatically install external plugins.\n\n")
		fmt.Fprintf(buf, "You can try HCL2 by running 'packer hcl2_upgrade %s'", h.Template.Path)
	case *hcl2template.PackerConfig:
		buf.WriteString("To remove this warning, add the following section to your template:\n")
		buf.WriteString(m.fixRequiredPlugins(h))
		buf.WriteString("\nThen run 'packer init' to manage installation of the plugins")
	}

	return hcl.Diagnostics{
		&hcl.Diagnostic{
			Severity: hcl.DiagWarning,
			Summary:  "Bundled plugins used",
			Detail:   buf.String(),
		},
	}
}

func (m *Meta) detectBundledPluginsJSON(core *packer.Core) []string {
	bundledPlugins := map[string]struct{}{}

	tmpl := core.Template
	if tmpl == nil {
		panic("No template parsed. This is a Packer bug which should be reported, please open an issue on the project's issue tracker.")
	}

	for _, b := range tmpl.Builders {
		builderType := fmt.Sprintf("packer-builder-%s", b.Type)
		if bundledStatus[builderType] {
			bundledPlugins[builderType] = struct{}{}
		}
	}

	for _, p := range tmpl.Provisioners {
		provisionerType := fmt.Sprintf("packer-provisioner-%s", p.Type)
		if bundledStatus[provisionerType] {
			bundledPlugins[provisionerType] = struct{}{}
		}
	}

	for _, pps := range tmpl.PostProcessors {
		for _, pp := range pps {
			postProcessorType := fmt.Sprintf("packer-post-processor-%s", pp.Type)
			if bundledStatus[postProcessorType] {
				bundledPlugins[postProcessorType] = struct{}{}
			}
		}
	}

	return compileBundledPluginList(bundledPlugins)
}

var knownPluginPrefixes = map[string]string{
	"amazon":        "github.com/hashicorp/amazon",
	"ansible":       "github.com/hashicorp/ansible",
	"azure":         "github.com/hashicorp/azure",
	"docker":        "github.com/hashicorp/docker",
	"googlecompute": "github.com/hashicorp/googlecompute",
	"qemu":          "github.com/hashicorp/qemu",
	"vagrant":       "github.com/hashicorp/vagrant",
	"virtualbox":    "github.com/hashicorp/virtualbox",
	"vmware":        "github.com/hashicorp/vmware",
	"vsphere":       "github.com/hashicorp/vsphere",
}

func (m *Meta) fixRequiredPlugins(config *hcl2template.PackerConfig) string {
	plugins := map[string]struct{}{}

	for _, b := range config.Builds {
		for _, b := range b.Sources {
			for prefix, plugin := range knownPluginPrefixes {
				if strings.HasPrefix(b.Type, prefix) {
					plugins[plugin] = struct{}{}
				}
			}
		}

		for _, p := range b.ProvisionerBlocks {
			for prefix, plugin := range knownPluginPrefixes {
				if strings.HasPrefix(p.PType, prefix) {
					plugins[plugin] = struct{}{}
				}
			}
		}

		for _, pps := range b.PostProcessorsLists {
			for _, pp := range pps {
				for prefix, plugin := range knownPluginPrefixes {
					if strings.HasPrefix(pp.PType, prefix) {
						plugins[plugin] = struct{}{}
					}
				}
			}
		}
	}

	for _, ds := range config.Datasources {
		for prefix, plugin := range knownPluginPrefixes {
			if strings.HasPrefix(ds.Type, prefix) {
				plugins[plugin] = struct{}{}
			}
		}
	}

	retPlugins := make([]string, 0, len(plugins))
	for plugin := range plugins {
		retPlugins = append(retPlugins, plugin)
	}

	return generateRequiredPluginsBlock(retPlugins)
}

func (m *Meta) detectBundledPluginsHCL2(config *hcl2template.PackerConfig) []string {
	bundledPlugins := map[string]struct{}{}

	for _, b := range config.Builds {
		for _, src := range b.Sources {
			builderType := fmt.Sprintf("packer-builder-%s", src.Type)
			if bundledStatus[builderType] {
				bundledPlugins[builderType] = struct{}{}
			}
		}

		for _, p := range b.ProvisionerBlocks {
			provisionerType := fmt.Sprintf("packer-provisioner-%s", p.PType)
			if bundledStatus[provisionerType] {
				bundledPlugins[provisionerType] = struct{}{}
			}
		}

		for _, pps := range b.PostProcessorsLists {
			for _, pp := range pps {
				postProcessorType := fmt.Sprintf("packer-post-processor-%s", pp.PType)
				if bundledStatus[postProcessorType] {
					bundledPlugins[postProcessorType] = struct{}{}
				}
			}
		}
	}

	for _, ds := range config.Datasources {
		dsType := fmt.Sprintf("packer-datasource-%s", ds.Type)
		if bundledStatus[dsType] {
			bundledPlugins[dsType] = struct{}{}
		}
	}

	return compileBundledPluginList(bundledPlugins)
}
