package cli

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

	"banger/internal/api"
	"banger/internal/cli/style"
	"banger/internal/config"
	"banger/internal/model"
	"banger/internal/paths"
	"banger/internal/system"
)

// effectiveVMDefaults resolves the default VM sizing applied when
// --vcpu/--memory/--disk-size aren't given: config overrides win
// over host-derived heuristics, both fall back to baked-in
// constants. Called at command-build time so the cobra flag defaults
// reflect the resolved values.
func effectiveVMDefaults() model.VMDefaults {
	var override model.VMDefaultsOverride
	if layout, err := paths.Resolve(); err == nil {
		if cfg, err := config.Load(layout); err == nil {
			override = cfg.VMDefaults
		}
	}
	host, err := system.ReadHostResources()
	if err != nil {
		return model.ResolveVMDefaults(override, 0, 0)
	}
	return model.ResolveVMDefaults(override, host.CPUCount, host.TotalMemoryBytes)
}

// printVMSpecLine writes a one-line sizing summary to out. Always
// emitted (even non-TTY) so logs and CI output carry the numbers.
func printVMSpecLine(out io.Writer, params api.VMCreateParams) {
	vcpu := model.DefaultVCPUCount
	if params.VCPUCount != nil {
		vcpu = *params.VCPUCount
	}
	memory := model.DefaultMemoryMiB
	if params.MemoryMiB != nil {
		memory = *params.MemoryMiB
	}
	diskBytes := int64(model.DefaultWorkDiskSize)
	if strings.TrimSpace(params.WorkDiskSize) != "" {
		if parsed, err := model.ParseSize(params.WorkDiskSize); err == nil {
			diskBytes = parsed
		}
	}
	_, _ = fmt.Fprintf(out, "spec: %d vcpu | %d MiB | %s disk\n",
		vcpu, memory, model.FormatSizeBytes(diskBytes))
}

// runVMCreate drives the create RPC + polls for progress. stderr
// gets the spec line up front and the progress renderer thereafter.
// On context cancel we cooperate with the daemon to cancel the
// in-flight op so it doesn't leak partially-created VM state.
func (d *deps) runVMCreate(ctx context.Context, socketPath string, stderr io.Writer, params api.VMCreateParams, verbose bool) (model.VMRecord, error) {
	start := time.Now()
	printVMSpecLine(stderr, params)
	begin, err := d.vmCreateBegin(ctx, socketPath, params)
	if err != nil {
		return model.VMRecord{}, err
	}
	renderer := newVMCreateProgressRenderer(stderr, verbose)
	renderer.render(begin.Operation)

	op := begin.Operation
	for {
		if op.Done {
			renderer.render(op)
			if op.Success && op.VM != nil {
				renderer.clear()
				elapsed := formatVMCreateElapsed(time.Since(start))
				_, _ = fmt.Fprintf(stderr, "[vm create] ready in %s\n", style.Dim(stderr, elapsed))
				return *op.VM, nil
			}
			if strings.TrimSpace(op.Error) == "" {
				return model.VMRecord{}, errors.New("vm create failed")
			}
			return model.VMRecord{}, errors.New(op.Error)
		}

		select {
		case <-ctx.Done():
			cancelCtx, cancel := context.WithTimeout(context.Background(), time.Second)
			defer cancel()
			_ = d.vmCreateCancel(cancelCtx, socketPath, op.ID)
			return model.VMRecord{}, ctx.Err()
		case <-time.After(200 * time.Millisecond):
		}

		status, err := d.vmCreateStatus(ctx, socketPath, op.ID)
		if err != nil {
			if ctx.Err() != nil {
				cancelCtx, cancel := context.WithTimeout(context.Background(), time.Second)
				defer cancel()
				_ = d.vmCreateCancel(cancelCtx, socketPath, op.ID)
				return model.VMRecord{}, ctx.Err()
			}
			return model.VMRecord{}, err
		}
		op = status.Operation
		renderer.render(op)
	}
}

type vmCreateProgressRenderer struct {
	out      io.Writer
	enabled  bool
	inline   bool
	active   bool
	lastLine string
}

// newVMCreateProgressRenderer wires up progress for `vm create`. On
// non-TTY writers it stays disabled (CI/test logs already capture the
// spec + ready lines); on TTY it rewrites a single line via \r unless
// verbose is set or BANGER_NO_PROGRESS is exported, in which case it
// falls back to one line per stage.
func newVMCreateProgressRenderer(out io.Writer, verbose bool) *vmCreateProgressRenderer {
	tty := writerSupportsProgress(out)
	return &vmCreateProgressRenderer{
		out:     out,
		enabled: tty,
		inline:  tty && !verbose && !progressDisabledByEnv(),
	}
}

func (r *vmCreateProgressRenderer) render(op api.VMCreateOperation) {
	if r == nil || !r.enabled {
		return
	}
	line := formatVMCreateProgress(op)
	if line == "" || line == r.lastLine {
		return
	}
	r.lastLine = line
	if r.inline {
		_, _ = fmt.Fprint(r.out, "\r\x1b[K", line)
		r.active = true
		return
	}
	_, _ = fmt.Fprintln(r.out, line)
}

// clear resets the live inline line so the caller can write a clean
// terminating message. No-op outside inline mode.
func (r *vmCreateProgressRenderer) clear() {
	if r == nil || !r.enabled || !r.inline || !r.active {
		return
	}
	_, _ = fmt.Fprint(r.out, "\r\x1b[K")
	r.active = false
	r.lastLine = ""
}

// progressDisabledByEnv is the BANGER_NO_PROGRESS escape hatch — a
// non-empty value forces line-per-stage output even on a TTY, so users
// can pipe `script(1)` / tmux capture without \r artifacts.
func progressDisabledByEnv() bool {
	return strings.TrimSpace(os.Getenv("BANGER_NO_PROGRESS")) != ""
}

// writerSupportsProgress returns true only when out is a terminal.
// Keeps stage lines + heartbeat dots out of piped / logged output
// where they'd just be noise.
func writerSupportsProgress(out io.Writer) bool {
	file, ok := out.(*os.File)
	if !ok {
		return false
	}
	info, err := file.Stat()
	if err != nil {
		return false
	}
	return info.Mode()&os.ModeCharDevice != 0
}

// withHeartbeat runs fn while emitting a dot to stderr every 2
// seconds so the user sees long-running RPCs (bundle downloads, etc.)
// aren't wedged. No-op when stderr isn't a terminal, so piped or
// logged output stays clean.
func withHeartbeat(stderr io.Writer, label string, fn func() error) error {
	if !writerSupportsProgress(stderr) {
		return fn()
	}
	fmt.Fprintf(stderr, "[%s] ", label)
	stop := make(chan struct{})
	done := make(chan struct{})
	go func() {
		defer close(done)
		ticker := time.NewTicker(2 * time.Second)
		defer ticker.Stop()
		for {
			select {
			case <-stop:
				return
			case <-ticker.C:
				fmt.Fprint(stderr, ".")
			}
		}
	}()
	err := fn()
	close(stop)
	<-done
	fmt.Fprintln(stderr)
	return err
}

func formatVMCreateProgress(op api.VMCreateOperation) string {
	stage := strings.TrimSpace(op.Stage)
	detail := strings.TrimSpace(op.Detail)
	label := vmCreateStageLabel(stage)
	if label == "" && detail == "" {
		return ""
	}
	if label == "" {
		return "[vm create] " + detail
	}
	if detail == "" {
		return "[vm create] " + label
	}
	return "[vm create] " + label + ": " + detail
}

// vmCreateStageLabel humanises the daemon-side stage IDs. Anything
// unknown falls through to `strings.ReplaceAll(_, "_", " ")` so new
// stages still render meaningfully without a code change.
func vmCreateStageLabel(stage string) string {
	switch strings.TrimSpace(stage) {
	case "queued":
		return "queued"
	case "resolve_image":
		return "resolving image"
	case "reserve_vm":
		return "allocating vm"
	case "preflight":
		return "checking host prerequisites"
	case "prepare_rootfs":
		return "preparing root filesystem"
	case "prepare_host_features":
		return "preparing host features"
	case "prepare_work_disk":
		return "preparing work disk"
	case "boot_firecracker":
		return "starting firecracker"
	case "wait_vsock_agent":
		return "waiting for vsock agent"
	case "wait_guest_ready":
		return "waiting for guest services"
	case "apply_dns":
		return "publishing dns"
	case "apply_nat":
		return "configuring nat"
	case "finalize":
		return "finalizing"
	case "ready":
		return "ready"
	default:
		return strings.ReplaceAll(stage, "_", " ")
	}
}

// formatVMCreateElapsed renders a wall-clock duration as a friendly
// "ready in 4.7s" / "ready in 1m02s" string. Sub-second durations
// keep one decimal so quick smoke runs don't print "0s".
func formatVMCreateElapsed(d time.Duration) string {
	if d < time.Second {
		return fmt.Sprintf("%dms", d.Milliseconds())
	}
	if d < time.Minute {
		return fmt.Sprintf("%.1fs", d.Seconds())
	}
	d = d.Round(time.Second)
	minutes := int(d / time.Minute)
	seconds := int((d % time.Minute) / time.Second)
	return fmt.Sprintf("%dm%02ds", minutes, seconds)
}

func validatePositiveSetting(label string, value int) error {
	if value <= 0 {
		return fmt.Errorf("%s must be a positive integer", label)
	}
	return nil
}

// shortID and relativeTime are small display helpers used across
// every printer; kept here alongside the other render-time helpers.
func shortID(id string) string {
	if len(id) <= 12 {
		return id
	}
	return id[:12]
}

func relativeTime(t time.Time) string {
	if t.IsZero() {
		return "-"
	}
	delta := time.Since(t)
	switch {
	case delta < 30*time.Second:
		return "moments ago"
	case delta < time.Minute:
		return fmt.Sprintf("%d seconds ago", int(delta.Seconds()))
	case delta < 2*time.Minute:
		return "1 minute ago"
	case delta < time.Hour:
		return fmt.Sprintf("%d minutes ago", int(delta.Minutes()))
	case delta < 2*time.Hour:
		return "1 hour ago"
	case delta < 24*time.Hour:
		return fmt.Sprintf("%d hours ago", int(delta.Hours()))
	case delta < 48*time.Hour:
		return "1 day ago"
	case delta < 7*24*time.Hour:
		return fmt.Sprintf("%d days ago", int(delta.Hours()/24))
	case delta < 14*24*time.Hour:
		return "1 week ago"
	default:
		return fmt.Sprintf("%d weeks ago", int(delta.Hours()/(24*7)))
	}
}
