package terraform

import (
	"context"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"syscall"

	cp9errors "github.com/tidbcloud/controlplane9/internal/errors"
)

const ParallelismEnv = "CP9_TERRAFORM_PARALLELISM"

func (r Runner) Init(ctx context.Context, paths *Paths) error {
	if paths == nil {
		return cp9errors.New(cp9errors.CategoryConfig, "terraform workspace paths are required")
	}
	if err := r.verifyBinary(); err != nil {
		return err
	}
	if err := os.MkdirAll(paths.LogsDir, 0o755); err != nil {
		return cp9errors.Newf(cp9errors.CategoryResource, "create terraform logs dir: %v", err)
	}
	return nil
}

func (r Runner) Plan(ctx context.Context, paths *Paths) ([]byte, error) {
	if paths == nil {
		return nil, cp9errors.New(cp9errors.CategoryConfig, "terraform workspace paths are required")
	}
	if err := r.verifyBinary(); err != nil {
		return nil, err
	}
	statePath, err := terraformCommandPath(paths.StatePath, "terraform state path")
	if err != nil {
		return nil, err
	}
	tfVarsPath, err := terraformCommandPath(paths.TFVarsPath, "terraform var-file path")
	if err != nil {
		return nil, err
	}
	planPath, err := terraformCommandPath(paths.PlanPath, "terraform plan path")
	if err != nil {
		return nil, err
	}
	parallelismArgs, err := r.parallelismArgs()
	if err != nil {
		return nil, err
	}

	initStdout, initStderr, err := r.initializeProviderPlugins(ctx, paths)
	if writeErr := writeLog(paths.InitLogPath, initStdout, initStderr); writeErr != nil {
		return nil, writeErr
	}
	if err != nil {
		return nil, terraformCommandError("terraform init", initStderr, err)
	}

	run := r.commandRunner()
	planArgs := []string{
		"plan",
		"-input=false",
		"-no-color",
		"-state=" + statePath,
		"-var-file=" + tfVarsPath,
		"-out=" + planPath,
	}
	planArgs = append(planArgs, parallelismArgs...)
	planStdout, planStderr, err := run(ctx, paths.SourceDir, "terraform", planArgs...)
	if writeErr := writeLog(paths.PlanLogPath, planStdout, planStderr); writeErr != nil {
		return nil, writeErr
	}
	if err != nil {
		return nil, terraformCommandError("terraform plan", planStderr, err)
	}

	showStdout, showStderr, err := run(ctx, paths.SourceDir, "terraform", "show", "-json", planPath)
	if err != nil {
		return nil, terraformCommandError("terraform show -json", showStderr, err)
	}
	if err := os.WriteFile(paths.PlanJSONPath, showStdout, 0o600); err != nil {
		return nil, cp9errors.Newf(cp9errors.CategoryResource, "write terraform plan JSON: %v", err)
	}
	if err := GuardPlanEKS(showStdout, r.AllowEKSPlan); err != nil {
		return nil, err
	}
	return showStdout, nil
}

func (r Runner) initializeProviderPlugins(
	ctx context.Context,
	paths *Paths,
) (stdout, stderr []byte, err error) {
	pluginCacheDir := filepath.Join(filepath.Dir(filepath.Dir(paths.RootDir)), ".internal", "terraform-plugin-cache")
	pluginCacheDir, err = terraformCommandPath(pluginCacheDir, "terraform plugin cache dir")
	if err != nil {
		return nil, nil, err
	}
	if err := os.MkdirAll(pluginCacheDir, 0o755); err != nil {
		return nil, nil, cp9errors.Newf(cp9errors.CategoryResource, "create terraform plugin cache: %v", err)
	}
	lock, err := os.OpenFile(pluginCacheDir+".lock", os.O_CREATE|os.O_RDWR, 0o600)
	if err != nil {
		return nil, nil, cp9errors.Newf(cp9errors.CategoryResource, "open terraform plugin cache lock: %v", err)
	}
	defer lock.Close()
	if err := syscall.Flock(int(lock.Fd()), syscall.LOCK_EX); err != nil {
		return nil, nil, cp9errors.Newf(cp9errors.CategoryResource, "lock terraform plugin cache: %v", err)
	}
	defer func() {
		_ = syscall.Flock(int(lock.Fd()), syscall.LOCK_UN)
	}()
	run := r.commandRunner()
	return run(
		ctx,
		paths.SourceDir,
		"env",
		"TF_PLUGIN_CACHE_DIR="+pluginCacheDir,
		"terraform",
		"init",
		"-input=false",
		"-no-color",
	)
}

func (r Runner) Apply(ctx context.Context, paths *Paths) ([]byte, error) {
	if paths == nil {
		return nil, cp9errors.New(cp9errors.CategoryConfig, "terraform workspace paths are required")
	}
	if err := r.verifyBinary(); err != nil {
		return nil, err
	}
	statePath, err := terraformCommandPath(paths.StatePath, "terraform state path")
	if err != nil {
		return nil, err
	}
	planPath, err := terraformCommandPath(paths.PlanPath, "terraform plan path")
	if err != nil {
		return nil, err
	}
	parallelismArgs, err := r.parallelismArgs()
	if err != nil {
		return nil, err
	}
	run := r.commandRunner()
	showStdout, showStderr, err := run(ctx, paths.SourceDir, "terraform", "show", "-json", planPath)
	if err != nil {
		return nil, terraformCommandError("terraform show -json", showStderr, err)
	}
	if err := os.WriteFile(paths.PlanJSONPath, showStdout, 0o600); err != nil {
		return nil, cp9errors.Newf(cp9errors.CategoryResource, "write terraform plan JSON: %v", err)
	}
	if err := GuardPlanEKS(showStdout, r.AllowEKSPlan); err != nil {
		return nil, err
	}
	applyArgs := []string{
		"apply",
		"-input=false",
		"-no-color",
		"-auto-approve",
		"-state=" + statePath,
	}
	applyArgs = append(applyArgs, parallelismArgs...)
	applyArgs = append(applyArgs, planPath)
	applyStdout, applyStderr, err := run(ctx, paths.SourceDir, "terraform", applyArgs...)
	if writeErr := writeLog(paths.ApplyLogPath, applyStdout, applyStderr); writeErr != nil {
		return nil, writeErr
	}
	if err != nil {
		return nil, terraformCommandError("terraform apply", applyStderr, err)
	}
	outputStdout, outputStderr, err := run(
		ctx,
		paths.SourceDir,
		"terraform",
		"output",
		"-json",
		"-state="+statePath,
	)
	if err != nil {
		return nil, terraformCommandError("terraform output -json", outputStderr, err)
	}
	return outputStdout, nil
}

// Destroy exposes the reusable substrate destroy hook for later destroy sequencing work.
func (r Runner) Destroy(ctx context.Context, paths *Paths) error {
	if paths == nil {
		return cp9errors.New(cp9errors.CategoryConfig, "terraform workspace paths are required")
	}
	if _, err := os.Stat(paths.StatePath); err != nil {
		if os.IsNotExist(err) {
			return nil
		}
		return cp9errors.Newf(cp9errors.CategoryResource, "read terraform state %q: %v", paths.StatePath, err)
	}
	if err := r.verifyBinary(); err != nil {
		return err
	}
	statePath, err := terraformCommandPath(paths.StatePath, "terraform state path")
	if err != nil {
		return err
	}
	tfVarsPath, err := terraformCommandPath(paths.TFVarsPath, "terraform var-file path")
	if err != nil {
		return err
	}
	parallelismArgs, err := r.parallelismArgs()
	if err != nil {
		return err
	}
	run := r.commandRunner()
	destroyArgs := []string{
		"destroy",
		"-input=false",
		"-no-color",
		"-auto-approve",
		"-state=" + statePath,
		"-var-file=" + tfVarsPath,
	}
	destroyArgs = append(destroyArgs, parallelismArgs...)
	stdout, stderr, err := run(ctx, paths.SourceDir, "terraform", destroyArgs...)
	if writeErr := writeLog(paths.DestroyLog, stdout, stderr); writeErr != nil {
		return writeErr
	}
	if err != nil {
		return terraformCommandError("terraform destroy", stderr, err)
	}
	return nil
}

func (r Runner) parallelismArgs() ([]string, error) {
	parallelism := r.Parallelism
	if parallelism == 0 {
		raw := strings.TrimSpace(os.Getenv(ParallelismEnv))
		if raw == "" {
			return nil, nil
		}
		parsed, err := strconv.Atoi(raw)
		if err != nil || parsed < 1 {
			return nil, cp9errors.Newf(cp9errors.CategoryConfig, "%s must be a positive integer: %q", ParallelismEnv, raw)
		}
		parallelism = parsed
	}
	if parallelism < 1 {
		return nil, cp9errors.Newf(cp9errors.CategoryConfig, "terraform parallelism must be a positive integer: %d", parallelism)
	}
	return []string{"-parallelism=" + strconv.Itoa(parallelism)}, nil
}

func terraformCommandPath(path, label string) (string, error) {
	value := strings.TrimSpace(path)
	if value == "" {
		return "", cp9errors.Newf(cp9errors.CategoryConfig, "%s is required", label)
	}
	if filepath.IsAbs(value) {
		return value, nil
	}
	abs, err := filepath.Abs(value)
	if err != nil {
		return "", cp9errors.Newf(cp9errors.CategoryConfig, "resolve %s %q: %v", label, path, err)
	}
	return abs, nil
}

func writeLog(path string, stdout, stderr []byte) error {
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return cp9errors.Newf(cp9errors.CategoryResource, "create terraform log dir: %v", err)
	}
	var builder strings.Builder
	if len(stdout) > 0 {
		_, _ = builder.WriteString("stdout:\n")
		_, _ = builder.Write(stdout)
		if !strings.HasSuffix(builder.String(), "\n") {
			_ = builder.WriteByte('\n')
		}
	}
	if len(stderr) > 0 {
		_, _ = builder.WriteString("stderr:\n")
		_, _ = builder.Write(stderr)
		if !strings.HasSuffix(builder.String(), "\n") {
			_ = builder.WriteByte('\n')
		}
	}
	if err := os.WriteFile(path, []byte(builder.String()), 0o600); err != nil {
		return cp9errors.Newf(cp9errors.CategoryResource, "write terraform log %q: %v", path, err)
	}
	return nil
}

func terraformCommandError(command string, stderr []byte, err error) error {
	msg := strings.TrimSpace(string(stderr))
	if msg == "" {
		msg = err.Error()
	}
	return cp9errors.Newf(cp9errors.CategoryResource, "%s failed: %s", command, msg)
}
