package preflight

import (
	"context"
	"os"
	"strings"

	"github.com/pkg/errors"
	"github.com/replicatedhq/troubleshoot/internal/specs"
	troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
	"github.com/replicatedhq/troubleshoot/pkg/constants"
	"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
	"github.com/replicatedhq/troubleshoot/pkg/loader"
	"github.com/spf13/viper"
	"helm.sh/helm/v3/pkg/strvals"
	"k8s.io/client-go/kubernetes"
	yaml "sigs.k8s.io/yaml"
)

func readSpecs(args []string) (*loader.TroubleshootKinds, error) {
	config, err := k8sutil.GetRESTConfig()
	if err != nil {
		return nil, errors.Wrap(err, "failed to convert kube flags to rest config")
	}

	client, err := kubernetes.NewForConfig(config)
	if err != nil {
		return nil, errors.Wrap(err, "failed to convert create k8s client")
	}

	// Pre-process v1beta3 specs with templates if values are provided
	processedArgs, tempFiles, err := preprocessV1Beta3Specs(args)
	if err != nil {
		return nil, errors.Wrap(err, "failed to preprocess v1beta3 specs")
	}
	// Ensure any temp files created during preprocessing are cleaned up
	defer func() {
		for _, f := range tempFiles {
			_ = os.Remove(f)
		}
	}()

	ctx := context.Background()
	kinds, err := specs.LoadFromCLIArgs(ctx, client, processedArgs, viper.GetViper())
	if err != nil {
		return nil, err
	}

	// Load additional specs from URIs
	// only when no-uri flag is not set
	if !viper.GetBool("no-uri") {
		specs.LoadAdditionalSpecFromURIs(ctx, kinds)
	}

	ret := loader.NewTroubleshootKinds()

	// Concatenate all preflight inclusterSpecs that don't have an upload destination
	inclusterSpecs := []troubleshootv1beta2.Preflight{}
	var concatenatedSpec *troubleshootv1beta2.Preflight
	for _, v := range kinds.PreflightsV1Beta2 {
		v := v // https://golang.org/doc/faq#closures_and_goroutines
		if v.Spec.UploadResultsTo == "" {
			concatenatedSpec = ConcatPreflightSpec(concatenatedSpec, &v)
		} else {
			inclusterSpecs = append(inclusterSpecs, v)
		}
	}

	if concatenatedSpec != nil {
		inclusterSpecs = append(inclusterSpecs, *concatenatedSpec)
	}
	ret.PreflightsV1Beta2 = inclusterSpecs

	var hostSpec *troubleshootv1beta2.HostPreflight
	for _, v := range kinds.HostPreflightsV1Beta2 {
		v := v // https://golang.org/doc/faq#closures_and_goroutines
		hostSpec = ConcatHostPreflightSpec(hostSpec, &v)
	}
	if hostSpec != nil {
		ret.HostPreflightsV1Beta2 = []troubleshootv1beta2.HostPreflight{*hostSpec}
	}

	return ret, nil
}

// preprocessV1Beta3Specs processes v1beta3 specs with template rendering if values are provided
func preprocessV1Beta3Specs(args []string) ([]string, []string, error) {
	valuesFiles := viper.GetStringSlice("values")
	setValues := viper.GetStringSlice("set")

	// Check if any args contain v1beta3 specs
	hasV1Beta3 := false
	for _, arg := range args {
		// Skip non-file arguments
		if arg == "-" || strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") ||
			strings.HasPrefix(arg, "secret/") || strings.HasPrefix(arg, "configmap/") {
			continue
		}

		// Check if file exists
		if _, err := os.Stat(arg); err != nil {
			continue
		}

		// Read the file
		content, err := os.ReadFile(arg)
		if err != nil {
			continue
		}

		// Check if it's a v1beta3 spec with templates
		contentStr := string(content)
		var parsed map[string]interface{}
		if err := yaml.Unmarshal(content, &parsed); err == nil {
			if apiVersion, ok := parsed["apiVersion"]; ok && apiVersion == constants.Troubleshootv1beta3Kind {
				// Only require values if the spec has Helm templates
				if strings.Contains(contentStr, "{{") && strings.Contains(contentStr, "}}") {
					hasV1Beta3 = true
					break
				}
			}
		} else {
			// If YAML parsing fails, check raw content for v1beta3 with templates
			if strings.Contains(contentStr, "apiVersion: troubleshoot.sh/v1beta3") &&
				strings.Contains(contentStr, "{{") && strings.Contains(contentStr, "}}") {
				hasV1Beta3 = true
				break
			}
		}
	}

	// If v1beta3 spec with templates found but no values provided, return error
	if hasV1Beta3 && len(valuesFiles) == 0 && len(setValues) == 0 {
		return nil, nil, errors.New("v1beta3 specs with Helm templates require a values file. Please provide values using --values or --set flags")
	}

	// If no values provided, return args unchanged
	if len(valuesFiles) == 0 && len(setValues) == 0 {
		return args, nil, nil
	}

	// Load values from files and --set flags
	values := make(map[string]interface{})
	for _, valuesFile := range valuesFiles {
		if valuesFile == "" {
			continue
		}
		data, err := os.ReadFile(valuesFile)
		if err != nil {
			return nil, nil, errors.Wrapf(err, "failed to read values file %s", valuesFile)
		}

		var fileValues map[string]interface{}
		if err := yaml.Unmarshal(data, &fileValues); err != nil {
			return nil, nil, errors.Wrapf(err, "failed to parse values file %s", valuesFile)
		}

		values = MergeMaps(values, fileValues)
	}

	// Apply --set values
	for _, setValue := range setValues {
		if err := strvals.ParseInto(setValue, values); err != nil {
			return nil, nil, errors.Wrapf(err, "failed to parse --set value: %s", setValue)
		}
	}

	// Process each arg
	processedArgs := make([]string, 0, len(args))
	tempFiles := make([]string, 0)
	for _, arg := range args {
		// Skip non-file arguments (like URLs, stdin, etc.)
		if arg == "-" || strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") ||
			strings.HasPrefix(arg, "secret/") || strings.HasPrefix(arg, "configmap/") {
			processedArgs = append(processedArgs, arg)
			continue
		}

		// Check if file exists
		if _, err := os.Stat(arg); err != nil {
			processedArgs = append(processedArgs, arg)
			continue
		}

		// Read the file
		content, err := os.ReadFile(arg)
		if err != nil {
			return nil, nil, errors.Wrapf(err, "failed to read file %s", arg)
		}

		// Check if it's a v1beta3 spec with templates
		var parsed map[string]interface{}
		if err := yaml.Unmarshal(content, &parsed); err != nil {
			// Not valid YAML, might be templated - try to detect v1beta3
			contentStr := string(content)
			if strings.Contains(contentStr, "apiVersion: troubleshoot.sh/v1beta3") &&
				strings.Contains(contentStr, "{{") && strings.Contains(contentStr, "}}") {
				// It's a v1beta3 template, render it
				// Seed default-false for referenced boolean flags and create parent maps for any
				// .Values.* paths so missing values behave as empty and blocks can be omitted.
				SeedDefaultBooleans(contentStr, values)
				SeedParentMapsForValueRefs(contentStr, values)
				rendered, err := RenderWithHelmTemplate(contentStr, values)
				if err != nil {
					return nil, nil, errors.Wrapf(err, "failed to render v1beta3 template %s", arg)
				}
				// Write to temp file
				tmpFile, err := os.CreateTemp("", "preflight-rendered-*.yaml")
				if err != nil {
					return nil, nil, errors.Wrap(err, "failed to create temp file")
				}
				if _, err := tmpFile.WriteString(rendered); err != nil {
					tmpFile.Close()
					os.Remove(tmpFile.Name())
					return nil, nil, errors.Wrap(err, "failed to write rendered template")
				}
				tmpFile.Close()
				processedArgs = append(processedArgs, tmpFile.Name())
				tempFiles = append(tempFiles, tmpFile.Name())
			} else {
				processedArgs = append(processedArgs, arg)
			}
		} else {
			// Valid YAML, check if it's v1beta3 with templates
			if apiVersion, ok := parsed["apiVersion"]; ok && apiVersion == constants.Troubleshootv1beta3Kind {
				contentStr := string(content)
				if strings.Contains(contentStr, "{{") && strings.Contains(contentStr, "}}") {
					// It's a v1beta3 template, render it
					// Seed default-false for referenced boolean flags and create parent maps for .Values.* paths
					SeedDefaultBooleans(contentStr, values)
					SeedParentMapsForValueRefs(contentStr, values)
					rendered, err := RenderWithHelmTemplate(contentStr, values)
					if err != nil {
						return nil, nil, errors.Wrapf(err, "failed to render v1beta3 template %s", arg)
					}
					// Write to temp file
					tmpFile, err := os.CreateTemp("", "preflight-rendered-*.yaml")
					if err != nil {
						return nil, nil, errors.Wrap(err, "failed to create temp file")
					}
					if _, err := tmpFile.WriteString(rendered); err != nil {
						tmpFile.Close()
						os.Remove(tmpFile.Name())
						return nil, nil, errors.Wrap(err, "failed to write rendered template")
					}
					tmpFile.Close()
					processedArgs = append(processedArgs, tmpFile.Name())
					tempFiles = append(tempFiles, tmpFile.Name())
				} else {
					// v1beta3 but no templates
					processedArgs = append(processedArgs, arg)
				}
			} else {
				// Not v1beta3
				processedArgs = append(processedArgs, arg)
			}
		}
	}

	return processedArgs, tempFiles, nil
}
