package firecracker

import (
	"context"
	"fmt"
	"regexp"
	"strconv"
	"strings"
)

// MinSupportedVersion is the lowest firecracker version banger has
// been validated against. Below this, banger refuses to launch — the
// jailer flags banger relies on (notably the `--exec-file` /
// `--chroot-base-dir` pair plus the structured chroot layout) might
// behave differently or be missing entirely.
//
// Bumping this is a deliberate decision; it should change in lockstep
// with whatever feature in the helper started requiring the newer
// firecracker.
const MinSupportedVersion = "1.5.0"

// KnownTestedVersion is the firecracker version banger's smoke suite
// is currently exercised against. Newer versions usually work
// (firecracker keeps its API stable within a major) but they sit
// outside the tested window — `banger doctor` warns rather than fails
// when it finds a higher version.
const KnownTestedVersion = "1.14.1"

// versionPattern matches the canonical `Firecracker v1.14.1` line
// emitted by `firecracker --version`. The pre-release suffix
// (e.g. `-beta`) is captured for fidelity in the reported string but
// ignored for ordering.
var versionPattern = regexp.MustCompile(`Firecracker v(\d+)\.(\d+)\.(\d+)(?:-([\w.]+))?`)

// SemVer is a structural representation of a `MAJOR.MINOR.PATCH`
// triple plus an optional pre-release label. Comparisons use only
// the triple; pre-releases are kept for display.
type SemVer struct {
	Major, Minor, Patch int
	PreRelease          string
}

// String renders the SemVer back to its canonical form, with a
// leading "v" so it matches firecracker's own output.
func (s SemVer) String() string {
	if s.PreRelease == "" {
		return fmt.Sprintf("v%d.%d.%d", s.Major, s.Minor, s.Patch)
	}
	return fmt.Sprintf("v%d.%d.%d-%s", s.Major, s.Minor, s.Patch, s.PreRelease)
}

// Compare returns -1, 0, or +1 based on the (Major, Minor, Patch)
// triple. Pre-release labels are ignored — banger doesn't
// distinguish `v1.10.0` from `v1.10.0-rc1` for compatibility purposes.
func (s SemVer) Compare(other SemVer) int {
	if s.Major != other.Major {
		return cmpInt(s.Major, other.Major)
	}
	if s.Minor != other.Minor {
		return cmpInt(s.Minor, other.Minor)
	}
	return cmpInt(s.Patch, other.Patch)
}

// ParseVersionOutput pulls the SemVer out of `firecracker --version`
// stdout. firecracker historically prints the version line followed
// by a freeform "exiting successfully" line; we match the first
// occurrence of the pattern anywhere in the output to be tolerant of
// future cosmetic changes.
func ParseVersionOutput(out string) (SemVer, error) {
	m := versionPattern.FindStringSubmatch(out)
	if m == nil {
		return SemVer{}, fmt.Errorf("unrecognised firecracker version output: %q", strings.TrimSpace(out))
	}
	major, _ := strconv.Atoi(m[1])
	minor, _ := strconv.Atoi(m[2])
	patch, _ := strconv.Atoi(m[3])
	return SemVer{Major: major, Minor: minor, Patch: patch, PreRelease: m[4]}, nil
}

// MustParseSemVer parses a `MAJOR.MINOR.PATCH` (optionally `v`-prefixed)
// constant. Panics on malformed input — only used for the package-level
// constants above and in tests, so a malformed string is a developer
// error rather than a runtime concern.
func MustParseSemVer(s string) SemVer {
	parts := strings.SplitN(strings.TrimPrefix(s, "v"), ".", 3)
	if len(parts) != 3 {
		panic("MustParseSemVer: " + s)
	}
	major, err := strconv.Atoi(parts[0])
	if err != nil {
		panic("MustParseSemVer: " + s)
	}
	minor, err := strconv.Atoi(parts[1])
	if err != nil {
		panic("MustParseSemVer: " + s)
	}
	patch, err := strconv.Atoi(parts[2])
	if err != nil {
		panic("MustParseSemVer: " + s)
	}
	return SemVer{Major: major, Minor: minor, Patch: patch}
}

// VersionRunner is the slim contract QueryVersion needs from a
// command-runner. system.Runner satisfies it; defining the interface
// inline keeps internal/firecracker free of cross-cutting imports.
type VersionRunner interface {
	Run(ctx context.Context, name string, args ...string) ([]byte, error)
}

// QueryVersion runs `<bin> --version` and parses the result. Returns
// only the parsed SemVer — firecracker's stdout includes a trailing
// "exiting successfully" log line that we have no use for; callers
// render the result via SemVer.String() ("v1.14.1") for display.
func QueryVersion(ctx context.Context, runner VersionRunner, bin string) (SemVer, error) {
	out, err := runner.Run(ctx, bin, "--version")
	if err != nil {
		return SemVer{}, err
	}
	return ParseVersionOutput(string(out))
}

func cmpInt(a, b int) int {
	switch {
	case a < b:
		return -1
	case a > b:
		return 1
	default:
		return 0
	}
}
