package cli

import (
	"encoding/json"
	"fmt"
	"io"
	"os"
	"sort"
	"strings"
	"text/tabwriter"

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

// anyWriter is the minimal writer surface every printer needs. Split
// out from io.Writer because some of our callers already hold a
// tabwriter/bytes.Buffer by value.
type anyWriter interface {
	Write(p []byte) (n int, err error)
}

// -- small helpers --------------------------------------------------

func humanSize(bytes int64) string {
	if bytes <= 0 {
		return "-"
	}
	const (
		kib = 1024
		mib = 1024 * kib
		gib = 1024 * mib
	)
	switch {
	case bytes >= gib:
		return fmt.Sprintf("%.1f GiB", float64(bytes)/float64(gib))
	case bytes >= mib:
		return fmt.Sprintf("%.1f MiB", float64(bytes)/float64(mib))
	case bytes >= kib:
		return fmt.Sprintf("%.1f KiB", float64(bytes)/float64(kib))
	default:
		return fmt.Sprintf("%d B", bytes)
	}
}

func dashIfEmpty(s string) string {
	if strings.TrimSpace(s) == "" {
		return "-"
	}
	return s
}

// -- generic printers -----------------------------------------------

func printJSON(out anyWriter, v any) error {
	data, err := json.MarshalIndent(v, "", "  ")
	if err != nil {
		return err
	}
	_, err = fmt.Fprintln(out, string(data))
	return err
}

// -- VM printers ----------------------------------------------------

func printVMSummary(out anyWriter, vm model.VMRecord) error {
	_, err := fmt.Fprintf(
		out,
		"%s\t%s\t%s\t%s\t%s\t%s\n",
		shortID(vm.ID),
		vm.Name,
		vm.State,
		vm.Runtime.GuestIP,
		model.FormatSizeBytes(vm.Spec.WorkDiskSizeBytes),
		vm.Runtime.DNSName,
	)
	return err
}

func printVMIDList(out anyWriter, vms []model.VMRecord) error {
	for _, vm := range vms {
		if _, err := fmt.Fprintln(out, vm.ID); err != nil {
			return err
		}
	}
	return nil
}

func printVMListTable(out anyWriter, vms []model.VMRecord, imageNames map[string]string) error {
	w := tabwriter.NewWriter(out, 0, 8, 2, ' ', 0)
	if _, err := fmt.Fprintln(w, "ID\tNAME\tSTATE\tIMAGE\tIP\tVCPU\tMEM\tDISK\tWORKSPACE\tCREATED"); err != nil {
		return err
	}
	for _, vm := range vms {
		if _, err := fmt.Fprintf(
			w,
			"%s\t%s\t%s\t%s\t%s\t%d\t%d MiB\t%s\t%s\t%s\n",
			shortID(vm.ID),
			vm.Name,
			vm.State,
			vmImageLabel(vm.ImageID, imageNames),
			vm.Runtime.GuestIP,
			vm.Spec.VCPUCount,
			vm.Spec.MemoryMiB,
			model.FormatSizeBytes(vm.Spec.WorkDiskSizeBytes),
			dashIfEmpty(vm.Workspace.GuestPath),
			relativeTime(vm.CreatedAt),
		); err != nil {
			return err
		}
	}
	return w.Flush()
}

func printVMPortsTable(out anyWriter, result api.VMPortsResult) error {
	type portRow struct {
		Proto    string
		Endpoint string
		Process  string
		Command  string
		Port     int
	}
	rows := make([]portRow, 0, len(result.Ports))
	for _, port := range result.Ports {
		rows = append(rows, portRow{
			Proto:    port.Proto,
			Endpoint: port.Endpoint,
			Process:  port.Process,
			Command:  port.Command,
			Port:     port.Port,
		})
	}
	sort.Slice(rows, func(i, j int) bool {
		if rows[i].Proto != rows[j].Proto {
			return rows[i].Proto < rows[j].Proto
		}
		if rows[i].Port != rows[j].Port {
			return rows[i].Port < rows[j].Port
		}
		if rows[i].Process != rows[j].Process {
			return rows[i].Process < rows[j].Process
		}
		return rows[i].Command < rows[j].Command
	})
	if len(rows) == 0 {
		return nil
	}

	w := tabwriter.NewWriter(out, 0, 8, 2, ' ', 0)
	if _, err := fmt.Fprintln(w, "PROTO\tENDPOINT\tPROCESS\tCOMMAND"); err != nil {
		return err
	}
	for _, row := range rows {
		if _, err := fmt.Fprintf(
			w,
			"%s\t%s\t%s\t%s\n",
			row.Proto,
			dashIfEmpty(row.Endpoint),
			dashIfEmpty(row.Process),
			dashIfEmpty(row.Command),
		); err != nil {
			return err
		}
	}
	return w.Flush()
}

// -- image printers -------------------------------------------------

func printImageSummary(out anyWriter, image model.Image) error {
	_, err := fmt.Fprintf(out, "%s\t%s\t%t\t%s\n", shortID(image.ID), image.Name, image.Managed, image.RootfsPath)
	return err
}

func imageNameIndex(images []model.Image) map[string]string {
	index := make(map[string]string, len(images))
	for _, image := range images {
		index[image.ID] = image.Name
	}
	return index
}

func vmImageLabel(imageID string, imageNames map[string]string) string {
	if name := strings.TrimSpace(imageNames[imageID]); name != "" {
		return name
	}
	return shortID(imageID)
}

func printImageListTable(out anyWriter, images []model.Image) error {
	w := tabwriter.NewWriter(out, 0, 8, 2, ' ', 0)
	if _, err := fmt.Fprintln(w, "ID\tNAME\tMANAGED\tROOTFS SIZE\tCREATED"); err != nil {
		return err
	}
	for _, image := range images {
		if _, err := fmt.Fprintf(
			w,
			"%s\t%s\t%t\t%s\t%s\n",
			shortID(image.ID),
			image.Name,
			image.Managed,
			rootfsSizeLabel(image.RootfsPath),
			relativeTime(image.CreatedAt),
		); err != nil {
			return err
		}
	}
	return w.Flush()
}

func rootfsSizeLabel(path string) string {
	info, err := os.Stat(path)
	if err != nil {
		return "-"
	}
	if info.Size() <= 0 {
		return "0"
	}
	return model.FormatSizeBytes(info.Size())
}

// -- kernel printers ------------------------------------------------

func printKernelListTable(out anyWriter, entries []api.KernelEntry) error {
	w := tabwriter.NewWriter(out, 0, 8, 2, ' ', 0)
	if _, err := fmt.Fprintln(w, "NAME\tDISTRO\tARCH\tKERNEL\tIMPORTED"); err != nil {
		return err
	}
	for _, entry := range entries {
		if _, err := fmt.Fprintf(
			w,
			"%s\t%s\t%s\t%s\t%s\n",
			entry.Name,
			dashIfEmpty(entry.Distro),
			dashIfEmpty(entry.Arch),
			dashIfEmpty(entry.KernelVersion),
			dashIfEmpty(entry.ImportedAt),
		); err != nil {
			return err
		}
	}
	return w.Flush()
}

func printKernelCatalogTable(out anyWriter, entries []api.KernelCatalogEntry) error {
	w := tabwriter.NewWriter(out, 0, 8, 2, ' ', 0)
	if _, err := fmt.Fprintln(w, "NAME\tDISTRO\tARCH\tKERNEL\tSIZE\tSTATE"); err != nil {
		return err
	}
	for _, entry := range entries {
		state := "available"
		if entry.Pulled {
			state = "pulled"
		}
		if _, err := fmt.Fprintf(
			w,
			"%s\t%s\t%s\t%s\t%s\t%s\n",
			entry.Name,
			dashIfEmpty(entry.Distro),
			dashIfEmpty(entry.Arch),
			dashIfEmpty(entry.KernelVersion),
			humanSize(entry.SizeBytes),
			state,
		); err != nil {
			return err
		}
	}
	return w.Flush()
}

// -- doctor printer -------------------------------------------------

func printDoctorReport(out anyWriter, report system.Report, verbose bool) error {
	colorWriter, _ := out.(io.Writer)

	var passes, warns, fails int
	for _, c := range report.Checks {
		switch c.Status {
		case system.CheckStatusPass:
			passes++
		case system.CheckStatusWarn:
			warns++
		case system.CheckStatusFail:
			fails++
		}
	}

	if !verbose && warns == 0 && fails == 0 {
		msg := fmt.Sprintf("all %d checks passed", passes)
		if colorWriter != nil {
			msg = style.Pass(colorWriter, msg)
		}
		_, err := fmt.Fprintln(out, msg)
		return err
	}

	for _, check := range report.Checks {
		if !verbose && check.Status == system.CheckStatusPass {
			continue
		}
		status := strings.ToUpper(string(check.Status))
		if colorWriter != nil {
			switch check.Status {
			case system.CheckStatusPass:
				status = style.Pass(colorWriter, status)
			case system.CheckStatusFail:
				status = style.Fail(colorWriter, status)
			case system.CheckStatusWarn:
				status = style.Warn(colorWriter, status)
			}
		}
		if _, err := fmt.Fprintf(out, "%s\t%s\n", status, check.Name); err != nil {
			return err
		}
		for _, detail := range check.Details {
			if _, err := fmt.Fprintf(out, "  - %s\n", detail); err != nil {
				return err
			}
		}
	}

	if !verbose {
		if _, err := fmt.Fprintf(out, "\n%d passed, %s, %s\n", passes, pluralCount(warns, "warning"), pluralCount(fails, "failure")); err != nil {
			return err
		}
	}

	return nil
}

func pluralCount(n int, word string) string {
	if n == 1 {
		return fmt.Sprintf("%d %s", n, word)
	}
	return fmt.Sprintf("%d %ss", n, word)
}
