package main

import (
	"bytes"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"runtime"
	"strings"

	"github.com/spf13/cobra"

	semver "github.com/zzet/gortex/internal/version"
)

var versionShort bool

var versionCmd = &cobra.Command{
	Use:   "version",
	Short: "Print version information (SemVer + commit + build metadata)",
	Long: `Prints the running build's SemVer identity in canonical form:

  vMAJOR.MINOR.PATCH[-PRERELEASE][+COMMITSHA]

With --short, only the canonical string is emitted — useful for shell
substitution and tool-version checks. Without --short, the command
emits a multi-line summary including commit SHA, build timestamp, Go
toolchain, and host os/arch for bug reports.`,
	Run: runVersion,
}

func init() {
	versionCmd.Flags().BoolVar(&versionShort, "short", false,
		"print just the canonical SemVer string (useful for scripts)")
	rootCmd.AddCommand(versionCmd)
}

func runVersion(cmd *cobra.Command, _ []string) {
	v, err := semver.Compose(versionFromMain(), commitFromMain())
	if err != nil {
		// Treat injection failures the same as a dev build rather than
		// erroring out — users running `gortex version` on a broken
		// build still need the rest of the summary to report the issue.
		v = semver.Version{Build: commitFromMain()}
	}

	if versionShort {
		_, _ = fmt.Fprintln(cmd.OutOrStdout(), canonical(v))
		return
	}

	w := cmd.OutOrStdout()
	_, _ = fmt.Fprintf(w, "gortex %s\n", canonical(v))
	if v.IsZero() && v.Build == "" {
		_, _ = fmt.Fprintln(w, "  (dev build — no ldflags injected; build via `make build` or `goreleaser` for a versioned binary)")
	}
	_, _ = fmt.Fprintf(w, "  commit:  %s\n", printable(commitFromMain(), "(none)"))
	_, _ = fmt.Fprintf(w, "  built:   %s\n", printable(dateFromMain(), "(unknown)"))
	_, _ = fmt.Fprintf(w, "  go:      %s\n", runtime.Version())
	_, _ = fmt.Fprintf(w, "  os/arch: %s/%s\n", runtime.GOOS, runtime.GOARCH)
}

// canonical renders a Version with a graceful fallback for dev builds.
// A zero Version with no build slot renders "v0.0.0-dev" so scripts
// using `gortex version --short` always get a parseable-ish string
// instead of an unexpected "v0.0.0" that looks like a real release.
func canonical(v semver.Version) string {
	if v.IsZero() && v.Build == "" {
		return "v0.0.0-dev"
	}
	return v.String()
}

// versionFromMain / commitFromMain / dateFromMain are tiny wrappers so
// the version logic stays testable — tests that want to rewrite these
// for a fake build can shadow them without having to reach into main.
func versionFromMain() string { return version }
func commitFromMain() string  { return commit }
func dateFromMain() string    { return date }

// printable picks a non-empty string or returns the provided fallback —
// used so empty injection slots render as "(none)" instead of a blank.
func printable(s, fallback string) string {
	if s == "" {
		return fallback
	}
	return s
}

// canonicalVersion returns the running binary's SemVer string in
// canonical form (e.g. "v0.1.0+abc1234"). Used by the daemon handshake
// and any other place where Gortex identifies itself to a peer — keeps
// the wire format consistent regardless of whether the caller holds
// `main.version` or reconstructs it.
func canonicalVersion() string {
	v, err := semver.Compose(versionFromMain(), commitFromMain())
	if err != nil {
		// Fall back to raw version string; better to surface the weird
		// value than to lie about it.
		return versionFromMain()
	}
	return canonical(v)
}

// --- gortex version bump <kind> -------------------------------------------

var versionBumpPrerelease string

var versionBumpCmd = &cobra.Command{
	Use:   "bump <major|minor|patch>",
	Short: "Bump the version in cmd/gortex/main.go and print the tag-release command",
	Long: `Rewrites main.version in cmd/gortex/main.go — the single source of truth
— by incrementing the requested part. After the file is updated, the
output suggests the git commit + tag commands to finish the release.

Must be run from the repo root (where cmd/gortex/main.go lives); the
bump is a source-file edit, not a git operation, so the command
refuses to run without that file in place.

Bump rules:
  major    MAJOR++, MINOR and PATCH → 0, prerelease cleared
  minor    MINOR++, PATCH → 0, prerelease cleared
  patch    PATCH++, prerelease cleared

--pre adds or replaces a prerelease identifier (e.g. --pre rc.1). Use
it to mark a release candidate: "gortex version bump minor --pre rc.1"
goes from v0.1.0 to v0.2.0-rc.1.`,
	Args:      cobra.ExactArgs(1),
	ValidArgs: []string{"major", "minor", "patch"},
	RunE:      runVersionBump,
}

func init() {
	versionBumpCmd.Flags().StringVar(&versionBumpPrerelease, "pre", "",
		"attach a prerelease identifier (e.g. rc.1, alpha.2); overrides any existing one")
	versionCmd.AddCommand(versionBumpCmd)
}

// mainGoVersionRe matches the `version = "..."` declaration in main.go,
// whether it lives inside a var (…) block (leading tab / spaces only) or
// on a top-level `var version = "..."` line (leading "var "). Capture 2
// is the version string itself without its surrounding quotes.
//
// Writing this as a single regex keeps the rewriter simple; the var-
// block form is what Gortex actually ships, but the plain form shows up
// in test fixtures and third-party forks.
var mainGoVersionRe = regexp.MustCompile(`(?m)^(\s*(?:var\s+)?version\s*=\s*)"([^"]*)"(.*)$`)

func runVersionBump(cmd *cobra.Command, args []string) error {
	kind := args[0]
	switch kind {
	case "major", "minor", "patch":
		// ok
	default:
		return fmt.Errorf("bump kind must be major, minor, or patch (got %q)", kind)
	}

	// Locate cmd/gortex/main.go relative to the current working
	// directory. Refuse to run outside the repo — this is a dev tool.
	path := filepath.Join("cmd", "gortex", "main.go")
	raw, err := os.ReadFile(path)
	if err != nil {
		return fmt.Errorf("read %s: %w (run this from the repo root)", path, err)
	}

	m := mainGoVersionRe.FindSubmatch(raw)
	if m == nil {
		return fmt.Errorf(`could not find a "version = \"...\"" line in %s`, path)
	}
	current := string(m[2])

	next, err := bumpSemver(current, kind, versionBumpPrerelease)
	if err != nil {
		return err
	}

	// Rewrite using FindSubmatchIndex so we only touch the quoted value.
	idx := mainGoVersionRe.FindSubmatchIndex(raw)
	// idx[4]/idx[5] are the start/end of the 2nd capture group (the
	// version string) — surgical replace avoids accidentally rewriting
	// the comment on the same line.
	var buf bytes.Buffer
	buf.Write(raw[:idx[4]])
	buf.WriteString(next.String()[1:]) // drop leading 'v' — main.go stores the raw semver
	buf.Write(raw[idx[5]:])

	if err := os.WriteFile(path, buf.Bytes(), 0o644); err != nil {
		return fmt.Errorf("write %s: %w", path, err)
	}

	w := cmd.OutOrStdout()
	_, _ = fmt.Fprintf(w, "Bumped %s: %s → %s\n", kind, current, next.String()[1:])
	_, _ = fmt.Fprintln(w)
	_, _ = fmt.Fprintln(w, "Next steps:")
	_, _ = fmt.Fprintf(w, "  git add %s\n", path)
	_, _ = fmt.Fprintf(w, "  git commit -m \"Bump version to %s\"\n", next.String())
	_, _ = fmt.Fprintf(w, "  git tag %s\n", next.String())
	_, _ = fmt.Fprintf(w, "  git push && git push origin %s\n", next.String())
	return nil
}

// bumpSemver applies the bump rule to a SemVer string and returns the
// resulting Version. Pre-release (if supplied) overrides whatever was
// in the source. Build slot is always cleared — it gets filled in at
// build time by the ldflag injection, not checked into main.go.
func bumpSemver(current, kind, pre string) (semver.Version, error) {
	v, err := semver.Parse("v" + strings.TrimPrefix(current, "v"))
	if err != nil {
		return semver.Version{}, fmt.Errorf("main.version %q is not valid semver: %w", current, err)
	}
	switch kind {
	case "major":
		v.Major++
		v.Minor = 0
		v.Patch = 0
	case "minor":
		v.Minor++
		v.Patch = 0
	case "patch":
		v.Patch++
	}
	v.Prerelease = pre
	v.Build = ""
	return v, nil
}
