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

package tfexec

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"regexp"
	"strconv"
	"strings"

	"github.com/hashicorp/go-version"
	tfjson "github.com/hashicorp/terraform-json"
)

var (
	tf0_4_1  = version.Must(version.NewVersion("0.4.1"))
	tf0_5_0  = version.Must(version.NewVersion("0.5.0"))
	tf0_6_13 = version.Must(version.NewVersion("0.6.13"))
	tf0_7_7  = version.Must(version.NewVersion("0.7.7"))
	tf0_8_0  = version.Must(version.NewVersion("0.8.0"))
	tf0_9_2  = version.Must(version.NewVersion("0.9.2"))
	tf0_10_0 = version.Must(version.NewVersion("0.10.0"))
	tf0_12_0 = version.Must(version.NewVersion("0.12.0"))
	tf0_13_0 = version.Must(version.NewVersion("0.13.0"))
	tf0_14_0 = version.Must(version.NewVersion("0.14.0"))
	tf0_15_0 = version.Must(version.NewVersion("0.15.0"))
	tf0_15_2 = version.Must(version.NewVersion("0.15.2"))
	tf0_15_3 = version.Must(version.NewVersion("0.15.3"))
	tf0_15_4 = version.Must(version.NewVersion("0.15.4"))
	tf1_1_0  = version.Must(version.NewVersion("1.1.0"))
	tf1_4_0  = version.Must(version.NewVersion("1.4.0"))
	tf1_5_0  = version.Must(version.NewVersion("1.5.0"))
	tf1_6_0  = version.Must(version.NewVersion("1.6.0"))
	tf1_9_0  = version.Must(version.NewVersion("1.9.0"))
	tf1_10_0 = version.Must(version.NewVersion("1.10.0"))
	tf1_13_0 = version.Must(version.NewVersion("1.13.0"))
	tf1_14_0 = version.Must(version.NewVersion("1.14.0"))
)

// Version returns structured output from the terraform version command including both the Terraform CLI version
// and any initialized provider versions. This will read cached values when present unless the skipCache parameter
// is set to true.
func (tf *Terraform) Version(ctx context.Context, skipCache bool) (tfVersion *version.Version, providerVersions map[string]*version.Version, err error) {
	tf.versionLock.Lock()
	defer tf.versionLock.Unlock()

	if tf.execVersion == nil || skipCache {
		tf.execVersion, tf.provVersions, err = tf.version(ctx)
		if err != nil {
			return nil, nil, err
		}
	}

	return tf.execVersion, tf.provVersions, nil
}

// version does not use the locking on the Terraform instance and should probably not be used directly, prefer Version.
func (tf *Terraform) version(ctx context.Context) (*version.Version, map[string]*version.Version, error) {
	versionCmd := tf.buildTerraformCmd(ctx, nil, "version", "-json")

	var outBuf bytes.Buffer
	versionCmd.Stdout = &outBuf

	err := tf.runTerraformCmd(ctx, versionCmd)
	if err != nil {
		return nil, nil, err
	}

	tfVersion, providerVersions, err := parseJsonVersionOutput(outBuf.Bytes())
	if err != nil {
		if _, ok := err.(*json.SyntaxError); ok {
			return tf.versionFromPlaintext(ctx)
		}
	}

	return tfVersion, providerVersions, err
}

func parseJsonVersionOutput(stdout []byte) (*version.Version, map[string]*version.Version, error) {
	var out tfjson.VersionOutput
	err := json.Unmarshal(stdout, &out)
	if err != nil {
		return nil, nil, err
	}

	tfVersion, err := version.NewVersion(out.Version)
	if err != nil {
		return nil, nil, fmt.Errorf("unable to parse version %q: %w", out.Version, err)
	}

	providerVersions := make(map[string]*version.Version, 0)
	for provider, versionStr := range out.ProviderSelections {
		v, err := version.NewVersion(versionStr)
		if err != nil {
			return nil, nil, fmt.Errorf("unable to parse %q version %q: %w",
				provider, versionStr, err)
		}
		providerVersions[provider] = v
	}

	return tfVersion, providerVersions, nil
}

func (tf *Terraform) versionFromPlaintext(ctx context.Context) (*version.Version, map[string]*version.Version, error) {
	versionCmd := tf.buildTerraformCmd(ctx, nil, "version")

	var outBuf strings.Builder
	versionCmd.Stdout = &outBuf

	err := tf.runTerraformCmd(ctx, versionCmd)
	if err != nil {
		return nil, nil, err
	}

	tfVersion, providerVersions, err := parsePlaintextVersionOutput(outBuf.String())
	if err != nil {
		return nil, nil, fmt.Errorf("unable to parse version: %w", err)
	}

	return tfVersion, providerVersions, nil
}

var (
	simpleVersionRe = `v?(?P<version>[0-9]+(?:\.[0-9]+)*(?:-[A-Za-z0-9\.]+)?)`

	versionOutputRe         = regexp.MustCompile(`Terraform ` + simpleVersionRe)
	providerVersionOutputRe = regexp.MustCompile(`(\n\+ provider[\. ](?P<name>\S+) ` + simpleVersionRe + `)`)
)

func parsePlaintextVersionOutput(stdout string) (*version.Version, map[string]*version.Version, error) {
	stdout = strings.TrimSpace(stdout)

	submatches := versionOutputRe.FindStringSubmatch(stdout)
	if len(submatches) != 2 {
		return nil, nil, fmt.Errorf("unexpected number of version matches %d for %s", len(submatches), stdout)
	}
	v, err := version.NewVersion(submatches[1])
	if err != nil {
		return nil, nil, fmt.Errorf("unable to parse version %q: %w", submatches[1], err)
	}

	allSubmatches := providerVersionOutputRe.FindAllStringSubmatch(stdout, -1)
	provV := map[string]*version.Version{}

	for _, submatches := range allSubmatches {
		if len(submatches) != 4 {
			return nil, nil, fmt.Errorf("unexpected number of provider version matches %d for %s", len(submatches), stdout)
		}

		v, err := version.NewVersion(submatches[3])
		if err != nil {
			return nil, nil, fmt.Errorf("unable to parse provider version %q: %w", submatches[3], err)
		}

		provV[submatches[2]] = v
	}

	return v, provV, err
}

func errorVersionString(v *version.Version) string {
	if v == nil {
		return "-"
	}
	return v.String()
}

// compatible asserts compatibility of the cached Terraform version with a set of constraints, and returns a well known error if not.
//
// Each command implementation with compatibility constraints will use this method to check that the available version of Terraform
// is compatible with a given command, e.g. detect if a project consuming this library is attempting to use a flag that isn't present
// in the Terraform binary it supplied. A common example is checking whether the version is sufficient to use the `-json` flag.
//
// Remember, terraform-exec is invoked with a path to an executable and has no way to know which version of Terraform it represents
// until the executable is used.
func (tf *Terraform) compatible(ctx context.Context, minInclusive *version.Version, maxExclusive *version.Version) error {
	tfv, _, err := tf.Version(ctx, false)
	if err != nil {
		return err
	}
	if ok := versionInRange(tfv, minInclusive, maxExclusive); !ok {
		return &ErrVersionMismatch{
			MinInclusive: errorVersionString(minInclusive),
			MaxExclusive: errorVersionString(maxExclusive),
			Actual:       errorVersionString(tfv),
		}
	}

	return nil
}

// experimentsEnabled asserts the cached terraform version has experiments enabled in the executable,
// and returns a well known error if not. Experiments are enabled in alpha and (potentially) dev builds of Terraform.
func (tf *Terraform) experimentsEnabled(ctx context.Context) error {
	tfv, _, err := tf.Version(ctx, false)
	if err != nil {
		return err
	}

	preRelease := tfv.Prerelease()
	if preRelease == "dev" || strings.Contains(preRelease, "alpha") {
		return nil
	}

	return fmt.Errorf("experiments are not enabled in version %s, as it's not an alpha or dev build", errorVersionString(tfv))
}

func stripPrereleaseAndMeta(v *version.Version) *version.Version {
	if v == nil {
		return nil
	}
	segs := []string{}
	for _, s := range v.Segments() {
		segs = append(segs, strconv.Itoa(s))
	}
	vs := strings.Join(segs, ".")
	clean, _ := version.NewVersion(vs)
	return clean
}

// versionInRange checks compatibility of the Terraform version. The minimum is inclusive and the max
// is exclusive, equivalent to min <= expected version < max.
//
// Pre-release information is ignored for comparison.
func versionInRange(tfv *version.Version, minInclusive *version.Version, maxExclusive *version.Version) bool {
	if minInclusive == nil && maxExclusive == nil {
		return true
	}
	tfv = stripPrereleaseAndMeta(tfv)
	minInclusive = stripPrereleaseAndMeta(minInclusive)
	maxExclusive = stripPrereleaseAndMeta(maxExclusive)
	if minInclusive != nil && !tfv.GreaterThanOrEqual(minInclusive) {
		return false
	}
	if maxExclusive != nil && !tfv.LessThan(maxExclusive) {
		return false
	}

	return true
}
