package opsservice

import (
	"context"
	"fmt"
	"os"
	"os/exec"
	"strings"

	"sigs.k8s.io/yaml"

	"github.com/tidbcloud/observability/pkg/common/message"
)

// commandExecutor abstracts shell command execution for testability.
type commandExecutor interface {
	Run(ctx context.Context, name string, args ...string) ([]byte, error)
}

// osCommandExecutor runs real OS commands using exec.CommandContext.
type osCommandExecutor struct{}

func (osCommandExecutor) Run(ctx context.Context, name string, args ...string) ([]byte, error) {
	out, err := exec.CommandContext(ctx, name, args...).CombinedOutput()
	if err != nil {
		return nil, fmt.Errorf("%w: %s", err, strings.TrimSpace(string(out)))
	}
	return out, nil
}

// commandHelmReleaseDiffRunner implements helmReleaseDiffRunner by shelling out
// to helm diff and helm show chart.
type commandHelmReleaseDiffRunner struct {
	executor   commandExecutor
	helmBinary string
}

// newCommandHelmReleaseDiffRunner returns a runner using the HELM_BINARY env
// var or defaulting to "helm".
func newCommandHelmReleaseDiffRunner() commandHelmReleaseDiffRunner {
	bin := os.Getenv("HELM_BINARY")
	if bin == "" {
		bin = "helm"
	}
	return commandHelmReleaseDiffRunner{executor: osCommandExecutor{}, helmBinary: bin}
}

// chartReference builds the full Helm chart reference from the request.
// For OCI-backed source-controller registries, the chart reference must be
// a full OCI URL such as oci://.../observability-charts/<component>.
func chartReference(req message.DiffHelmReleaseReq) (string, error) {
	if req.Config.SourceRef != nil && req.Config.SourceRef.URL != "" {
		return strings.TrimRight(req.Config.SourceRef.URL, "/") + "/" + req.Component, nil
	}
	return "", fmt.Errorf("config.sourceRef.url is required for helm diff")
}

// Diff runs helm diff upgrade and helm show chart, returning a HelmReleaseRenderedDiff.
func (r commandHelmReleaseDiffRunner) Diff(ctx context.Context, req message.DiffHelmReleaseReq) (message.HelmReleaseRenderedDiff, error) {
	chartRef, err := chartReference(req)
	if err != nil {
		return message.HelmReleaseRenderedDiff{}, err
	}

	valuesFile, err := r.writeValuesFile(req.Config.Values)
	if err != nil {
		return message.HelmReleaseRenderedDiff{}, fmt.Errorf("failed to write values file: %w", err)
	}
	if valuesFile != "" {
		defer os.Remove(valuesFile)
	}

	diffOutput, err := r.runHelmDiff(ctx, req, chartRef, valuesFile)
	if err != nil {
		return message.HelmReleaseRenderedDiff{}, err
	}

	resolvedDigest, err := r.resolveDigest(ctx, chartRef, req.Config.Version)
	if err != nil {
		return message.HelmReleaseRenderedDiff{}, err
	}

	trimmed := strings.TrimRight(string(diffOutput), "\n")
	return message.HelmReleaseRenderedDiff{
		Changed:        strings.TrimSpace(trimmed) != "",
		Output:         trimmed,
		ResolvedDigest: resolvedDigest,
	}, nil
}

// writeValuesFile marshals values to a temp YAML file and returns its path.
// Returns "" when values is empty.
func (r commandHelmReleaseDiffRunner) writeValuesFile(values map[string]interface{}) (string, error) {
	if len(values) == 0 {
		return "", nil
	}
	data, err := yaml.Marshal(values)
	if err != nil {
		return "", err
	}
	f, err := os.CreateTemp("", "helm-values-*.yaml")
	if err != nil {
		return "", err
	}
	defer f.Close()
	if _, err := f.Write(data); err != nil {
		os.Remove(f.Name())
		return "", err
	}
	return f.Name(), nil
}

// runHelmDiff executes: helm diff upgrade <name> <chartRef> --namespace <ns>
// --allow-unreleased --version <ver> [--values <file>]
func (r commandHelmReleaseDiffRunner) runHelmDiff(ctx context.Context, req message.DiffHelmReleaseReq, chartRef, valuesFile string) ([]byte, error) {
	args := []string{
		"diff", "upgrade",
		req.Name, chartRef,
		"--namespace", req.Namespace,
		"--allow-unreleased",
		"--version", req.Config.Version,
	}
	if valuesFile != "" {
		args = append(args, "--values", valuesFile)
	}
	out, err := r.executor.Run(ctx, r.helmBinary, args...)
	if err != nil {
		return nil, fmt.Errorf("helm diff upgrade failed: %w", err)
	}
	return out, nil
}

// resolveDigest runs: helm show chart <chartRef> --version <ver>
// and returns the value of the "digest:" line.
func (r commandHelmReleaseDiffRunner) resolveDigest(ctx context.Context, chartRef, version string) (string, error) {
	out, err := r.executor.Run(ctx, r.helmBinary, "show", "chart", chartRef, "--version", version)
	if err != nil {
		return "", fmt.Errorf("helm show chart failed: %w", err)
	}
	for _, line := range strings.Split(string(out), "\n") {
		if strings.HasPrefix(line, "digest:") {
			return strings.TrimSpace(strings.TrimPrefix(line, "digest:")), nil
		}
	}
	return "", nil
}
