// Package output renders plain and JSON command output. The JSON shapes are
// the stable contracts of AGENTS.md §4.8: keys are always present, empty
// collections serialize as [].
package output

import (
	"encoding/json"
	"io"
	"strings"
	"text/tabwriter"

	"nest/src/catalog"
	"nest/src/detect"
)

// ToolSummary is the list/search JSON element (§4.8). ContainerImage is nil —
// serialized as null — when the tool has no container available.
type ToolSummary struct {
	Name           string   `json:"name"`
	Description    string   `json:"description"`
	Package        string   `json:"package"`
	Installed      bool     `json:"installed"`
	ContainerImage *string  `json:"container_image"`
	Tags           []string `json:"tags"`
}

// Summarize builds a ToolSummary, detecting install status from the tool's
// executables.
func Summarize(t *catalog.Tool) ToolSummary {
	s := ToolSummary{
		Name:        t.Name,
		Description: t.Description,
		Package:     t.Package,
		Installed:   detect.AnyExecutable(t.Executables),
		Tags:        t.Tags,
	}
	if t.HasImage() {
		s.ContainerImage = &t.Images[0]
	}
	return s
}

// Executable is one entry of ToolInfo.Executables.
type Executable struct {
	Path    string `json:"path"`
	Present bool   `json:"present"`
}

// ToolInfo is the info JSON object (§4.8). Container is nil — serialized as
// null — when the tool declares no container options.
type ToolInfo struct {
	Name         string             `json:"name"`
	Description  string             `json:"description"`
	Package      string             `json:"package"`
	Installed    bool               `json:"installed"`
	Executables  []Executable       `json:"executables"`
	Images       []string           `json:"images"`
	Tags         []string           `json:"tags"`
	Container    *catalog.Container `json:"container"`
	SuggestedRun string             `json:"suggested_run"`
}

// Info builds the full ToolInfo for a catalog tool.
func Info(t *catalog.Tool) ToolInfo {
	installed := detect.AnyExecutable(t.Executables)
	ti := ToolInfo{
		Name:         t.Name,
		Description:  t.Description,
		Package:      t.Package,
		Installed:    installed,
		Executables:  make([]Executable, 0, len(t.Executables)),
		Images:       t.Images,
		Tags:         t.Tags,
		Container:    t.Container,
		SuggestedRun: "sudo apt install " + t.Package,
	}
	if t.HasImage() {
		ti.SuggestedRun = "nest run " + t.Name + " --container -- <args>"
	}
	if installed {
		ti.SuggestedRun = "nest run " + t.Name + " -- <args>"
	}
	for _, exe := range t.Executables {
		ti.Executables = append(ti.Executables, Executable{Path: exe, Present: detect.Executable(exe)})
	}
	return ti
}

// Version is the version JSON object (§4.8). Runtime is nil — serialized as
// null — when no runtime is detected.
type Version struct {
	Nest           string  `json:"nest"`
	CatalogVersion int     `json:"catalog_version"`
	CatalogSource  string  `json:"catalog_source"`
	Runtime        *string `json:"runtime"`
	ContainerMode  bool    `json:"container_mode"`
}

// CatalogUpdate reports the catalog half of a refresh run.
type CatalogUpdate struct {
	Updated bool `json:"updated"`
	Version int  `json:"version"`
}

// ImageUpdate reports one image's refresh status: up-to-date, updated,
// not-local, or failed (§4.6).
type ImageUpdate struct {
	Image  string `json:"image"`
	Status string `json:"status"`
}

// Refresh is the refresh JSON object (§4.8).
type Refresh struct {
	Catalog CatalogUpdate `json:"catalog"`
	Images  []ImageUpdate `json:"images"`
}

// Cleanup is the cleanup JSON object (§4.8). With DryRun set, the removal
// lists name what would be removed.
type Cleanup struct {
	DryRun            bool     `json:"dry_run"`
	ContainersRemoved []string `json:"containers_removed"`
	ImagesRemoved     []string `json:"images_removed"`
}

// JSON writes v as indented JSON followed by a newline. HTML escaping is off:
// these are CLI contracts, not web documents ("<args>" must stay readable).
func JSON(w io.Writer, v any) error {
	enc := json.NewEncoder(w)
	enc.SetIndent("", "  ")
	enc.SetEscapeHTML(false)
	return enc.Encode(v)
}

// Table writes aligned columns; with no rows only the header is printed (§4:
// empty result sets are headers-only, not errors).
func Table(w io.Writer, header []string, rows [][]string) error {
	tw := tabwriter.NewWriter(w, 0, 4, 2, ' ', 0)
	if _, err := tw.Write([]byte(strings.Join(header, "\t") + "\n")); err != nil {
		return err
	}
	for _, row := range rows {
		if _, err := tw.Write([]byte(strings.Join(row, "\t") + "\n")); err != nil {
			return err
		}
	}
	return tw.Flush()
}
