package cli

import (
	"errors"
	"fmt"
	"os"
	"strings"

	"banger/internal/buildinfo"
	"banger/internal/daemon"
	"banger/internal/paths"
	"banger/internal/roothelper"
	"banger/internal/store"

	"github.com/spf13/cobra"
)

// bangerdExit is var-injected so tests can capture the exit code
// without terminating the test process. Production points at os.Exit.
var bangerdExit = os.Exit

func NewBangerdCommand() *cobra.Command {
	var systemMode bool
	var rootHelperMode bool
	var checkMigrations bool
	cmd := &cobra.Command{
		Use:           "bangerd",
		Version:       strings.Replace(formatVersionLine(buildinfo.Current()), "banger ", "bangerd ", 1),
		Short:         "Run the banger daemon",
		SilenceUsage:  true,
		SilenceErrors: true,
		Args:          noArgsUsage("usage: bangerd"),
		RunE: func(cmd *cobra.Command, args []string) error {
			if systemMode && rootHelperMode {
				return errors.New("choose only one of --system or --root-helper")
			}
			if checkMigrations {
				return runCheckMigrations(cmd, systemMode)
			}
			if rootHelperMode {
				server, err := roothelper.Open()
				if err != nil {
					return err
				}
				defer server.Close()
				return server.Serve(cmd.Context())
			}
			open := daemon.Open
			if systemMode {
				open = daemon.OpenSystem
			}
			d, err := open(cmd.Context())
			if err != nil {
				return err
			}
			defer d.Close()
			return d.Serve(cmd.Context())
		},
	}
	cmd.Flags().BoolVar(&systemMode, "system", false, "run as the owner-user system service")
	cmd.Flags().BoolVar(&rootHelperMode, "root-helper", false, "run as the privileged root helper service")
	cmd.Flags().BoolVar(&checkMigrations, "check-migrations", false, "inspect the state DB and report whether this binary's schema matches; exit 0=compatible, 1=migrations needed, 2=incompatible")
	cmd.SetVersionTemplate("{{.Version}}\n")
	cmd.CompletionOptions.DisableDefaultCmd = true
	return cmd
}

// runCheckMigrations is the entry point for `bangerd --check-migrations`.
// Used by `banger update` to gate a binary swap on a staged binary
// before service restart: if the staged binary doesn't recognise the
// running install's schema, the swap is aborted before any host state
// changes.
//
// Exit codes are part of the contract:
//
//	0 — compatible (no migrations to apply on Open)
//	1 — migrations needed (binary newer than DB; safe to swap)
//	2 — incompatible (DB has migrations this binary doesn't know;
//	    swapping would leave the daemon unable to open the store)
func runCheckMigrations(cmd *cobra.Command, systemMode bool) error {
	layout := paths.ResolveSystem()
	if !systemMode {
		userLayout, err := paths.Resolve()
		if err != nil {
			return err
		}
		layout = userLayout
	}
	state, err := store.InspectSchemaState(layout.DBPath)
	if err != nil {
		return fmt.Errorf("inspect %s: %w", layout.DBPath, err)
	}
	out := cmd.OutOrStdout()
	switch state.Compatibility {
	case store.SchemaCompatible:
		fmt.Fprintf(out, "compatible: db at v%d, binary knows up to v%d\n", lastID(state.AppliedIDs), state.KnownMaxID)
		return nil
	case store.SchemaMigrationsNeeded:
		fmt.Fprintf(out, "migrations needed: pending %v (binary will apply on first Open)\n", state.Pending)
		// Distinct exit code so callers can tell "safe to swap, will
		// auto-migrate" apart from "compatible, no work pending".
		// Returning a cobra error would also exit non-zero, but we
		// want a specific code (1) — and we don't want SilenceErrors
		// to print our message twice.
		bangerdExit(1)
		return nil
	case store.SchemaIncompatible:
		fmt.Fprintf(out, "incompatible: db has unknown migrations %v (binary knows up to v%d)\n", state.Unknown, state.KnownMaxID)
		bangerdExit(2)
		return nil
	default:
		return fmt.Errorf("unexpected schema-state classification %d", state.Compatibility)
	}
}

// lastID returns the largest int in xs, or 0 when empty. The schema-
// migrations table doesn't guarantee insert order, so we scan rather
// than trusting xs[len-1].
func lastID(xs []int) int {
	max := 0
	for _, x := range xs {
		if x > max {
			max = x
		}
	}
	return max
}
