package infra

import (
	"bytes"
	"context"
	"fmt"
	"os/exec"
	"strings"
	"time"
)

const (
	defaultTimeoutS  = 60
	defaultSeverity  = "critical"
	maxStdoutBytes   = 4096
	healthIntervalMs = 500
)

// E2ERunChecks executes a list of E2ECheck declarations in order and returns
// one CheckResult per check. The slice is always len(checks) long; individual
// check failures do not abort the run.
//
// For each check:
//   - If Cmd is non-empty, it is executed via "bash -c". Commands ending with
//     "&" are launched in background; the function does not wait for exit and
//     proceeds to Health (if any) or records an immediate pass.
//   - If Health is non-empty (after any Cmd), HealthCheckHTTP polls the URL
//     until status 200 or timeout.
//   - ExpectExit (default 0) and ExpectStdoutContains are evaluated after Cmd.
//   - Ref is not yet implemented: the check is recorded as skip with a
//     descriptive error.
//   - Checks with no Cmd, Health, or Ref are skipped.
//
// workDir sets the working directory for subprocesses. Pass "" to inherit the
// current process directory.
//
// Returns an error only for setup failures (e.g. bad workDir), not for
// individual check failures.
func E2ERunChecks(checks []E2ECheck, workDir string) ([]CheckResult, error) {
	results := make([]CheckResult, 0, len(checks))

	for _, ch := range checks {
		result := runSingleCheck(ch, workDir)
		results = append(results, result)
	}
	return results, nil
}

func runSingleCheck(ch E2ECheck, workDir string) CheckResult {
	sev := ch.Severity
	if sev == "" {
		sev = defaultSeverity
	}
	timeoutS := ch.TimeoutS
	if timeoutS <= 0 {
		timeoutS = defaultTimeoutS
	}

	base := CheckResult{
		ID:       ch.ID,
		Severity: sev,
	}

	// Skip: nothing to do.
	if ch.Cmd == "" && ch.Ref == "" && ch.Health == "" {
		base.Status = "skip"
		return base
	}

	// Ref: not implemented yet.
	if ch.Ref != "" && ch.Cmd == "" && ch.Health == "" {
		base.Status = "skip"
		base.Error = "ref handler not implemented"
		return base
	}

	start := time.Now()

	// Run Cmd if present.
	if ch.Cmd != "" {
		background := strings.HasSuffix(strings.TrimSpace(ch.Cmd), "&")

		if background {
			// Launch background process, do not wait.
			bgCmd := exec.Command("bash", "-c", ch.Cmd)
			if workDir != "" {
				bgCmd.Dir = workDir
			}
			if err := bgCmd.Start(); err != nil {
				base.DurationMs = time.Since(start).Milliseconds()
				base.Status = "fail"
				base.Error = fmt.Sprintf("background start failed: %v", err)
				return base
			}
			// Do not wait; proceed to Health or pass.
		} else {
			ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutS)*time.Second)
			defer cancel()

			fgCmd := exec.CommandContext(ctx, "bash", "-c", ch.Cmd)
			if workDir != "" {
				fgCmd.Dir = workDir
			}
			var stdout, stderr bytes.Buffer
			fgCmd.Stdout = &stdout
			fgCmd.Stderr = &stderr

			runErr := fgCmd.Run()
			base.DurationMs = time.Since(start).Milliseconds()

			stdoutStr := truncate(stdout.String(), maxStdoutBytes)
			stderrStr := truncate(stderr.String(), maxStdoutBytes)
			base.Stdout = stdoutStr
			base.Stderr = stderrStr

			// Exit code.
			exitCode := 0
			if fgCmd.ProcessState != nil {
				exitCode = fgCmd.ProcessState.ExitCode()
			}
			base.ExitCode = exitCode

			if ctx.Err() == context.DeadlineExceeded {
				base.Status = "fail"
				base.Error = fmt.Sprintf("command timed out after %ds", timeoutS)
				return base
			}

			expectedExit := 0
			if ch.ExpectExit != nil {
				expectedExit = *ch.ExpectExit
			}
			if exitCode != expectedExit {
				base.Status = "fail"
				if runErr != nil {
					base.Error = runErr.Error()
				} else {
					base.Error = fmt.Sprintf("exit code %d, expected %d", exitCode, expectedExit)
				}
				return base
			}

			if ch.ExpectStdoutContains != "" && !strings.Contains(stdoutStr, ch.ExpectStdoutContains) {
				base.Status = "fail"
				base.Error = fmt.Sprintf("stdout does not contain %q", ch.ExpectStdoutContains)
				return base
			}
		}
	}

	// Health check (after Cmd or standalone).
	if ch.Health != "" {
		if err := HealthCheckHTTP(ch.Health, timeoutS, healthIntervalMs); err != nil {
			base.DurationMs = time.Since(start).Milliseconds()
			base.Status = "fail"
			base.Error = fmt.Sprintf("health check failed: %v", err)
			return base
		}
	}

	base.DurationMs = time.Since(start).Milliseconds()
	base.Status = "pass"
	return base
}

func truncate(s string, max int) string {
	if len(s) <= max {
		return s
	}
	return s[:max]
}
