package cli

import (
	"context"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	"banger/internal/api"
	"banger/internal/daemon"
	"banger/internal/daemon/workspace"
	"banger/internal/guest"
	"banger/internal/paths"
	"banger/internal/rpc"
	"banger/internal/system"
	"banger/internal/toolingplan"
)

// deps holds the function seams production code dispatches through and
// tests replace with fakes. Keeping these on a per-invocation struct
// (instead of package-level mutable vars) makes the CLI's external
// surface explicit and lets tests run in parallel without leaking fakes
// across test cases.
//
// Every command builder, orchestrator, and helper that touches the RPC
// socket, spawns a subprocess, or reads host state hangs off a *deps
// receiver. Pure helpers (formatters, path resolvers, arg-count
// validators) stay package-level because they hold no references to
// external systems.
type deps struct {
	bangerdPath           func() (string, error)
	daemonExePath         func(pid int) string
	doctor                func(ctx context.Context) (system.Report, error)
	sshExec               func(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer, args []string) error
	hostCommandOutput     func(ctx context.Context, name string, args ...string) ([]byte, error)
	vmHealth              func(ctx context.Context, socketPath, idOrName string) (api.VMHealthResult, error)
	vmSSH                 func(ctx context.Context, socketPath, idOrName string) (api.VMSSHResult, error)
	vmDelete              func(ctx context.Context, socketPath, idOrName string) error
	vmList                func(ctx context.Context, socketPath string) (api.VMListResult, error)
	daemonPing            func(ctx context.Context, socketPath string) (api.PingResult, error)
	vmCreateBegin         func(ctx context.Context, socketPath string, params api.VMCreateParams) (api.VMCreateBeginResult, error)
	vmCreateStatus        func(ctx context.Context, socketPath, operationID string) (api.VMCreateStatusResult, error)
	vmCreateCancel        func(ctx context.Context, socketPath, operationID string) error
	vmPorts               func(ctx context.Context, socketPath, idOrName string) (api.VMPortsResult, error)
	vmWorkspacePrepare    func(ctx context.Context, socketPath string, params api.VMWorkspacePrepareParams) (api.VMWorkspacePrepareResult, error)
	vmWorkspaceExport     func(ctx context.Context, socketPath string, params api.WorkspaceExportParams) (api.WorkspaceExportResult, error)
	guestWaitForSSH       func(ctx context.Context, address, privateKeyPath string, interval time.Duration) error
	guestDial             func(ctx context.Context, address, privateKeyPath string) (vmRunGuestClient, error)
	buildVMRunToolingPlan func(ctx context.Context, repoRoot string) toolingplan.Plan
	cwd                   func() (string, error)
	completionLister      func(ctx context.Context, socketPath, method string) ([]string, error)
	// repoInspector is the CLI's single workspace-package Inspector.
	// Every code path that needs to shell out to git on the host
	// (preflight, dry-run, untracked-count note) goes through it, so
	// tests inject a stub Runner via this field instead of mutating a
	// package global.
	repoInspector *workspace.Inspector
}

func defaultDeps() *deps {
	return &deps{
		bangerdPath: paths.BangerdPath,
		daemonExePath: func(pid int) string {
			return filepath.Join("/proc", fmt.Sprintf("%d", pid), "exe")
		},
		doctor: daemon.Doctor,
		sshExec: func(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer, args []string) error {
			sshCmd := exec.CommandContext(ctx, "ssh", args...)
			sshCmd.Stdout = stdout
			sshCmd.Stderr = stderr
			sshCmd.Stdin = stdin
			return sshCmd.Run()
		},
		hostCommandOutput: func(ctx context.Context, name string, args ...string) ([]byte, error) {
			cmd := exec.CommandContext(ctx, name, args...)
			output, err := cmd.CombinedOutput()
			if err == nil {
				return output, nil
			}
			command := strings.TrimSpace(strings.Join(append([]string{name}, args...), " "))
			detail := strings.TrimSpace(string(output))
			if detail == "" {
				return output, fmt.Errorf("%s: %w", command, err)
			}
			return output, fmt.Errorf("%s: %w: %s", command, err, detail)
		},
		vmHealth: func(ctx context.Context, socketPath, idOrName string) (api.VMHealthResult, error) {
			return rpc.Call[api.VMHealthResult](ctx, socketPath, "vm.health", api.VMRefParams{IDOrName: idOrName})
		},
		vmSSH: func(ctx context.Context, socketPath, idOrName string) (api.VMSSHResult, error) {
			return rpc.Call[api.VMSSHResult](ctx, socketPath, "vm.ssh", api.VMRefParams{IDOrName: idOrName})
		},
		vmDelete: func(ctx context.Context, socketPath, idOrName string) error {
			_, err := rpc.Call[api.VMShowResult](ctx, socketPath, "vm.delete", api.VMRefParams{IDOrName: idOrName})
			return err
		},
		vmList: func(ctx context.Context, socketPath string) (api.VMListResult, error) {
			return rpc.Call[api.VMListResult](ctx, socketPath, "vm.list", api.Empty{})
		},
		daemonPing: func(ctx context.Context, socketPath string) (api.PingResult, error) {
			return rpc.Call[api.PingResult](ctx, socketPath, "ping", api.Empty{})
		},
		vmCreateBegin: func(ctx context.Context, socketPath string, params api.VMCreateParams) (api.VMCreateBeginResult, error) {
			return rpc.Call[api.VMCreateBeginResult](ctx, socketPath, "vm.create.begin", params)
		},
		vmCreateStatus: func(ctx context.Context, socketPath, operationID string) (api.VMCreateStatusResult, error) {
			return rpc.Call[api.VMCreateStatusResult](ctx, socketPath, "vm.create.status", api.VMCreateStatusParams{ID: operationID})
		},
		vmCreateCancel: func(ctx context.Context, socketPath, operationID string) error {
			_, err := rpc.Call[api.Empty](ctx, socketPath, "vm.create.cancel", api.VMCreateStatusParams{ID: operationID})
			return err
		},
		vmPorts: func(ctx context.Context, socketPath, idOrName string) (api.VMPortsResult, error) {
			return rpc.Call[api.VMPortsResult](ctx, socketPath, "vm.ports", api.VMRefParams{IDOrName: idOrName})
		},
		vmWorkspacePrepare: func(ctx context.Context, socketPath string, params api.VMWorkspacePrepareParams) (api.VMWorkspacePrepareResult, error) {
			return rpc.Call[api.VMWorkspacePrepareResult](ctx, socketPath, "vm.workspace.prepare", params)
		},
		vmWorkspaceExport: func(ctx context.Context, socketPath string, params api.WorkspaceExportParams) (api.WorkspaceExportResult, error) {
			return rpc.Call[api.WorkspaceExportResult](ctx, socketPath, "vm.workspace.export", params)
		},
		guestWaitForSSH: func(ctx context.Context, address, privateKeyPath string, interval time.Duration) error {
			knownHosts, _ := bangerKnownHostsPath()
			return guest.WaitForSSH(ctx, address, privateKeyPath, knownHosts, interval)
		},
		guestDial: func(ctx context.Context, address, privateKeyPath string) (vmRunGuestClient, error) {
			knownHosts, _ := bangerKnownHostsPath()
			return guest.Dial(ctx, address, privateKeyPath, knownHosts)
		},
		buildVMRunToolingPlan: toolingplan.Build,
		cwd:                   os.Getwd,
		completionLister:      defaultCompletionLister,
		repoInspector:         workspace.NewInspector(),
	}
}
