// Copyright IBM Corp. 2020, 2026
// SPDX-License-Identifier: MPL-2.0

package tfexec

import (
	"context"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"os"
	"runtime"
	"sync"
	"time"

	"github.com/hashicorp/go-version"
)

type printfer interface {
	Printf(format string, v ...interface{})
}

// Terraform represents the Terraform CLI executable and working directory.
//
// Typically this is constructed against the root module of a Terraform configuration
// but you can override paths used in some commands depending on the available
// options.
//
// All functions that execute CLI commands take a context.Context. It should be noted that
// exec.Cmd.Run will not return context.DeadlineExceeded or context.Canceled by default, we
// have augmented our wrapped errors to respond true to errors.Is for context.DeadlineExceeded
// and context.Canceled if those are present on the context when the error is parsed. See
// https://github.com/golang/go/issues/21880 for more about the Go limitations.
//
// By default, the instance inherits the environment from the calling code (using os.Environ)
// but it ignores certain environment variables that are managed within the code and prohibits
// setting them through SetEnv:
//
//   - TF_APPEND_USER_AGENT
//   - TF_IN_AUTOMATION
//   - TF_INPUT
//   - TF_LOG
//   - TF_LOG_PATH
//   - TF_REATTACH_PROVIDERS
//   - TF_DISABLE_PLUGIN_TLS
//   - TF_SKIP_PROVIDER_VERIFY
type Terraform struct {
	execPath           string
	workingDir         string
	appendUserAgent    string
	disablePluginTLS   bool
	skipProviderVerify bool
	env                map[string]string

	stdout io.Writer
	stderr io.Writer
	logger printfer

	// TF_LOG environment variable, defaults to TRACE if logPath is set.
	log string

	// TF_LOG_CORE environment variable
	logCore string

	// TF_LOG_PATH environment variable
	logPath string

	// TF_LOG_PROVIDER environment variable
	logProvider string

	// waitDelay represents the WaitDelay field of the [exec.Cmd] of Terraform
	waitDelay time.Duration

	// enableLegacyPipeClosing closes the stdout/stderr pipes before calling [exec.Cmd.Wait]
	enableLegacyPipeClosing bool

	versionLock  sync.Mutex
	execVersion  *version.Version
	provVersions map[string]*version.Version
}

// NewTerraform returns a Terraform struct with default values for all fields.
// If a blank execPath is supplied, NewTerraform will error.
// Use hc-install or output from os.LookPath to get a desirable execPath.
func NewTerraform(workingDir string, execPath string) (*Terraform, error) {
	if workingDir == "" {
		return nil, fmt.Errorf("Terraform cannot be initialised with empty workdir")
	}

	if _, err := os.Stat(workingDir); err != nil {
		return nil, fmt.Errorf("error initialising Terraform with workdir %s: %s", workingDir, err)
	}

	if execPath == "" {
		err := fmt.Errorf("NewTerraform: please supply the path to a Terraform executable using execPath, e.g. using the github.com/hashicorp/hc-install module.")
		return nil, &ErrNoSuitableBinary{
			err: err,
		}
	}
	tf := Terraform{
		execPath:   execPath,
		workingDir: workingDir,
		env:        nil, // explicit nil means copy os.Environ
		logger:     log.New(ioutil.Discard, "", 0),
		waitDelay:  60 * time.Second,
	}

	return &tf, nil
}

// SetEnv allows you to override environment variables, this should not be used for any well known
// Terraform environment variables that are already covered in options. Pass nil to copy the values
// from os.Environ. Attempting to set environment variables that should be managed manually will
// result in ErrManualEnvVar being returned.
func (tf *Terraform) SetEnv(env map[string]string) error {
	prohibited := ProhibitedEnv(env)
	if len(prohibited) > 0 {
		// just error on the first instance
		return &ErrManualEnvVar{prohibited[0]}
	}

	tf.env = env
	return nil
}

// SetLogger specifies a logger for tfexec to use.
func (tf *Terraform) SetLogger(logger printfer) {
	tf.logger = logger
}

// SetStdout specifies a writer to stream stdout to for every command.
//
// This should be used for information or logging purposes only, not control
// flow. Any parsing necessary should be added as functionality to this package.
func (tf *Terraform) SetStdout(w io.Writer) {
	tf.stdout = w
}

// SetStderr specifies a writer to stream stderr to for every command.
//
// This should be used for information or logging purposes only, not control
// flow. Any parsing necessary should be added as functionality to this package.
func (tf *Terraform) SetStderr(w io.Writer) {
	tf.stderr = w
}

// SetLog sets the TF_LOG environment variable for Terraform CLI execution.
// This must be combined with a call to SetLogPath to take effect.
//
// This is only compatible with Terraform CLI 0.15.0 or later as setting the
// log level was unreliable in earlier versions. It will default to TRACE when
// SetLogPath is called on versions 0.14.11 and earlier, or if SetLogCore and
// SetLogProvider have not been called before SetLogPath on versions 0.15.0 and
// later.
func (tf *Terraform) SetLog(log string) error {
	err := tf.compatible(context.Background(), tf0_15_0, nil)
	if err != nil {
		return err
	}
	tf.log = log
	return nil
}

// SetLogCore sets the TF_LOG_CORE environment variable for Terraform CLI
// execution. This must be combined with a call to SetLogPath to take effect.
//
// This is only compatible with Terraform CLI 0.15.0 or later.
func (tf *Terraform) SetLogCore(logCore string) error {
	err := tf.compatible(context.Background(), tf0_15_0, nil)
	if err != nil {
		return err
	}
	tf.logCore = logCore
	return nil
}

// SetLogPath sets the TF_LOG_PATH environment variable for Terraform CLI
// execution.
func (tf *Terraform) SetLogPath(path string) error {
	tf.logPath = path
	// Prevent setting the log path without enabling logging
	if tf.log == "" && tf.logCore == "" && tf.logProvider == "" {
		tf.log = "TRACE"
	}
	return nil
}

// SetLogProvider sets the TF_LOG_PROVIDER environment variable for Terraform
// CLI execution. This must be combined with a call to SetLogPath to take
// effect.
//
// This is only compatible with Terraform CLI 0.15.0 or later.
func (tf *Terraform) SetLogProvider(logProvider string) error {
	err := tf.compatible(context.Background(), tf0_15_0, nil)
	if err != nil {
		return err
	}
	tf.logProvider = logProvider
	return nil
}

// SetAppendUserAgent sets the TF_APPEND_USER_AGENT environment variable for
// Terraform CLI execution.
func (tf *Terraform) SetAppendUserAgent(ua string) error {
	tf.appendUserAgent = ua
	return nil
}

// SetDisablePluginTLS sets the TF_DISABLE_PLUGIN_TLS environment variable for
// Terraform CLI execution.
func (tf *Terraform) SetDisablePluginTLS(disabled bool) error {
	tf.disablePluginTLS = disabled
	return nil
}

// SetSkipProviderVerify sets the TF_SKIP_PROVIDER_VERIFY environment variable
// for Terraform CLI execution. This is no longer used in 0.13.0 and greater.
func (tf *Terraform) SetSkipProviderVerify(skip bool) error {
	err := tf.compatible(context.Background(), nil, tf0_13_0)
	if err != nil {
		return err
	}
	tf.skipProviderVerify = skip
	return nil
}

// SetWaitDelay sets the WaitDelay of running Terraform process as [exec.Cmd]
func (tf *Terraform) SetWaitDelay(delay time.Duration) error {
	if runtime.GOOS == "windows" {
		return errors.New("cannot set WaitDelay, graceful cancellation not supported on windows")
	}
	tf.waitDelay = delay
	return nil
}

// SetEnableLegacyPipeClosing causes the library to "force-close" stdio pipes.
// This works around a bug in Terraform < v1.1 that would otherwise leave
// the process (and caller) hanging after graceful shutdown.
//
// This option can be safely ignored (set to false) with Terraform 1.1+.
func (tf *Terraform) SetEnableLegacyPipeClosing(enabled bool) error {
	tf.enableLegacyPipeClosing = enabled
	return nil
}

// WorkingDir returns the working directory for Terraform.
func (tf *Terraform) WorkingDir() string {
	return tf.workingDir
}

// ExecPath returns the path to the Terraform executable.
func (tf *Terraform) ExecPath() string {
	return tf.execPath
}
