package cli

import (
	"context"

	"banger/internal/api"
	"banger/internal/paths"
	"banger/internal/rpc"

	"github.com/spf13/cobra"
)

// Completion helpers. Design notes:
//
//   - Never auto-start the daemon. If it isn't running, return no
//     suggestions + NoFileComp so the shell doesn't fall back to file
//     completion (there are no local files that would plausibly match a
//     VM or image name).
//   - Filter out names already in args — avoids suggesting the same VM
//     twice on variadic commands like `vm stop a b <tab>`.
//   - Fail silently. Completion is advisory; any error path returns an
//     empty suggestion list rather than propagating to the user.

// defaultCompletionLister backs the *deps.completionLister field;
// tests inject their own fake via the struct instead of mutating
// package-level vars.
func defaultCompletionLister(ctx context.Context, socketPath, method string) ([]string, error) {
	switch method {
	case "vm.list":
		result, err := rpc.Call[api.VMListResult](ctx, socketPath, method, api.Empty{})
		if err != nil {
			return nil, err
		}
		names := make([]string, 0, len(result.VMs))
		for _, vm := range result.VMs {
			if vm.Name != "" {
				names = append(names, vm.Name)
			}
		}
		return names, nil
	case "image.list":
		result, err := rpc.Call[api.ImageListResult](ctx, socketPath, method, api.Empty{})
		if err != nil {
			return nil, err
		}
		names := make([]string, 0, len(result.Images))
		for _, image := range result.Images {
			if image.Name != "" {
				names = append(names, image.Name)
			}
		}
		return names, nil
	case "kernel.list":
		result, err := rpc.Call[api.KernelListResult](ctx, socketPath, method, api.Empty{})
		if err != nil {
			return nil, err
		}
		names := make([]string, 0, len(result.Entries))
		for _, entry := range result.Entries {
			if entry.Name != "" {
				names = append(names, entry.Name)
			}
		}
		return names, nil
	}
	return nil, nil
}

// daemonSocketForCompletion returns the socket path IFF the daemon is
// already running. Returns "", false when no daemon is up — completion
// callers use this as the bail signal.
func (d *deps) daemonSocketForCompletion(ctx context.Context) (string, bool) {
	layout := paths.ResolveSystem()
	if _, err := d.daemonPing(ctx, layout.SocketPath); err != nil {
		return "", false
	}
	return layout.SocketPath, true
}

// filterPrefix returns the subset of candidates starting with toComplete
// that aren't in exclude. Comparison is case-sensitive because VM/image
// names preserve case.
func filterPrefix(candidates, exclude []string, toComplete string) []string {
	excludeSet := make(map[string]struct{}, len(exclude))
	for _, e := range exclude {
		excludeSet[e] = struct{}{}
	}
	out := make([]string, 0, len(candidates))
	for _, c := range candidates {
		if _, skip := excludeSet[c]; skip {
			continue
		}
		if toComplete == "" || hasPrefix(c, toComplete) {
			out = append(out, c)
		}
	}
	return out
}

func hasPrefix(s, prefix string) bool {
	return len(s) >= len(prefix) && s[:len(prefix)] == prefix
}

func (d *deps) completeVMNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
	socket, ok := d.daemonSocketForCompletion(cmd.Context())
	if !ok {
		return nil, cobra.ShellCompDirectiveNoFileComp
	}
	names, err := d.completionLister(cmd.Context(), socket, "vm.list")
	if err != nil {
		return nil, cobra.ShellCompDirectiveNoFileComp
	}
	return filterPrefix(names, args, toComplete), cobra.ShellCompDirectiveNoFileComp
}

// completeVMNameOnlyAtPos0 restricts VM-name completion to the first
// positional argument. Used by commands like `vm ssh <vm> [ssh args...]`
// where args after pos 0 are free-form.
func (d *deps) completeVMNameOnlyAtPos0(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
	if len(args) > 0 {
		return nil, cobra.ShellCompDirectiveNoFileComp
	}
	return d.completeVMNames(cmd, args, toComplete)
}

func (d *deps) completeImageNameOnlyAtPos0(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
	if len(args) > 0 {
		return nil, cobra.ShellCompDirectiveNoFileComp
	}
	return d.completeImageNames(cmd, args, toComplete)
}

func (d *deps) completeKernelNameOnlyAtPos0(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
	if len(args) > 0 {
		return nil, cobra.ShellCompDirectiveNoFileComp
	}
	return d.completeKernelNames(cmd, args, toComplete)
}

func (d *deps) completeImageNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
	socket, ok := d.daemonSocketForCompletion(cmd.Context())
	if !ok {
		return nil, cobra.ShellCompDirectiveNoFileComp
	}
	names, err := d.completionLister(cmd.Context(), socket, "image.list")
	if err != nil {
		return nil, cobra.ShellCompDirectiveNoFileComp
	}
	return filterPrefix(names, args, toComplete), cobra.ShellCompDirectiveNoFileComp
}

func (d *deps) completeKernelNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
	socket, ok := d.daemonSocketForCompletion(cmd.Context())
	if !ok {
		return nil, cobra.ShellCompDirectiveNoFileComp
	}
	names, err := d.completionLister(cmd.Context(), socket, "kernel.list")
	if err != nil {
		return nil, cobra.ShellCompDirectiveNoFileComp
	}
	return filterPrefix(names, args, toComplete), cobra.ShellCompDirectiveNoFileComp
}

// completeKernelCatalogNameOnlyAtPos0 completes kernel names from the
// remote catalog (pulled + available) at position 0 only.
func (d *deps) completeKernelCatalogNameOnlyAtPos0(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
	if len(args) > 0 {
		return nil, cobra.ShellCompDirectiveNoFileComp
	}
	socket, ok := d.daemonSocketForCompletion(cmd.Context())
	if !ok {
		return nil, cobra.ShellCompDirectiveNoFileComp
	}
	result, err := rpc.Call[api.KernelCatalogResult](cmd.Context(), socket, "kernel.catalog", api.Empty{})
	if err != nil {
		return nil, cobra.ShellCompDirectiveNoFileComp
	}
	names := make([]string, 0, len(result.Entries))
	for _, entry := range result.Entries {
		if entry.Name != "" {
			names = append(names, entry.Name)
		}
	}
	return filterPrefix(names, args, toComplete), cobra.ShellCompDirectiveNoFileComp
}

// completeImageCatalogNameOnlyAtPos0 falls back to the locally-installed
// image list (there is no remote image catalog RPC today).
func (d *deps) completeImageCatalogNameOnlyAtPos0(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
	return d.completeImageNameOnlyAtPos0(cmd, args, toComplete)
}
