package cli

import (
	"context"
	"errors"
	"fmt"
	"io"
	"os/exec"
	"strings"
	"time"

	"banger/internal/model"
	"banger/internal/paths"
	"banger/internal/system"
	"banger/internal/vsockagent"
)

// runSSHSession executes ssh with the given args. On exit it decides
// whether to print the "vm is still running" reminder: we skip it if
// the caller asked (e.g. --rm is about to delete the VM), if the
// ctx is already done, or if the ssh error isn't the one that
// typically means "user disconnected cleanly".
func (d *deps) runSSHSession(ctx context.Context, socketPath, vmRef string, stdin io.Reader, stdout, stderr io.Writer, sshArgs []string, skipReminder bool) error {
	sshErr := d.sshExec(ctx, stdin, stdout, stderr, sshArgs)
	if skipReminder || !shouldCheckSSHReminder(sshErr) || ctx.Err() != nil {
		return sshErr
	}
	pingCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()
	health, err := d.vmHealth(pingCtx, socketPath, vmRef)
	if err != nil {
		_, _ = fmt.Fprintln(stderr, vsockagent.WarningMessage(vmRef, err))
		return sshErr
	}
	if health.Healthy {
		name := health.Name
		if strings.TrimSpace(name) == "" {
			name = vmRef
		}
		_, _ = fmt.Fprintln(stderr, vsockagent.ReminderMessage(name))
	}
	return sshErr
}

func shouldCheckSSHReminder(err error) bool {
	if err == nil {
		return true
	}
	var exitErr *exec.ExitError
	if !errors.As(err, &exitErr) {
		return false
	}
	return exitErr.ExitCode() != 255
}

// sshCommandArgs builds the argv for `ssh` invocations against a VM.
// Host-key verification uses a banger-owned known_hosts file
// populated by the daemon's first successful Go-SSH dial to each VM
// (trust-on-first-use). `accept-new` means: accept-and-pin on first
// contact; strict-verify afterwards. The user's own
// ~/.ssh/known_hosts is never touched.
func sshCommandArgs(cfg model.DaemonConfig, guestIP string, extra []string) ([]string, error) {
	if guestIP == "" {
		return nil, errors.New("vm has no guest IP")
	}
	args := []string{}
	args = append(args, "-F", "/dev/null")
	if cfg.SSHKeyPath != "" {
		args = append(args, "-i", cfg.SSHKeyPath)
	}
	knownHosts, khErr := bangerKnownHostsPath()
	args = append(
		args,
		"-o", "IdentitiesOnly=yes",
		"-o", "BatchMode=yes",
		"-o", "PreferredAuthentications=publickey",
		"-o", "PasswordAuthentication=no",
		"-o", "KbdInteractiveAuthentication=no",
	)
	if khErr == nil {
		args = append(args,
			"-o", "UserKnownHostsFile="+knownHosts,
			"-o", "StrictHostKeyChecking=accept-new",
		)
	} else {
		// If we can't resolve the banger path (unusual — paths.Resolve
		// basically can't fail), fall through to a hard-fail posture
		// rather than silently disabling verification.
		args = append(args,
			"-o", "StrictHostKeyChecking=yes",
		)
	}
	args = append(args, "root@"+guestIP)
	// ssh(1) concatenates every argument after the host with spaces
	// before sending to the remote shell. That means passing extra
	// args raw — `ssh host sh -c 'exit 42'` — re-tokenises on the
	// remote side to `sh -c exit 42`, where `42` is $0 for the
	// already-completed `exit`, and the rc the user asked for is
	// lost. Shell-quote each element and join them ourselves so the
	// remote shell sees exactly the argv the user typed locally.
	if len(extra) > 0 {
		quoted := make([]string, len(extra))
		for i, a := range extra {
			quoted[i] = shellQuote(a)
		}
		args = append(args, strings.Join(quoted, " "))
	}
	return args, nil
}

// bangerKnownHostsPath resolves the TOFU file the daemon writes into
// and the CLI reads back. Both sides must agree on the path or the
// pin doesn't round-trip.
func bangerKnownHostsPath() (string, error) {
	layout, err := paths.Resolve()
	if err != nil {
		return "", err
	}
	return layout.KnownHostsPath, nil
}

func validateSSHPrereqs(cfg model.DaemonConfig) error {
	checks := system.NewPreflight()
	checks.RequireCommand("ssh", "install openssh-client")
	if strings.TrimSpace(cfg.SSHKeyPath) != "" {
		checks.RequireFile(cfg.SSHKeyPath, "ssh private key", `set "ssh_key_path" or let banger create its default key`)
	}
	return checks.Err("ssh preflight failed")
}

func validateVMRunPrereqs(cfg model.DaemonConfig) error {
	checks := system.NewPreflight()
	checks.RequireCommand("git", "install git")
	if strings.TrimSpace(cfg.SSHKeyPath) != "" {
		checks.RequireFile(cfg.SSHKeyPath, "ssh private key", `set "ssh_key_path" or let banger create its default key`)
	}
	return checks.Err("vm run preflight failed")
}
