package main

import (
	"context"
	"flag"
	"fmt"
	"os"
	"sort"
	"strings"
	"text/tabwriter"
	"time"
)

func runApps(args []string) error {
	// Accept an optional "list" subcommand: `tinyforge apps` == `tinyforge apps list`.
	if len(args) > 0 && args[0] == "list" {
		args = args[1:]
	}
	fs := flag.NewFlagSet("apps", flag.ExitOnError)
	g := addGlobalFlags(fs)
	fs.Usage = func() {
		fmt.Fprint(os.Stderr, "Usage: tinyforge apps [list] [--base-url URL]\n\nList apps (workloads with a source) and their container state.\n")
		fs.PrintDefaults()
	}
	if err := fs.Parse(args); err != nil {
		return err
	}

	sess, err := newSession(g)
	if err != nil {
		return err
	}
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	var workloads []Workload
	if err := sess.client.doJSON(ctx, "GET", "/api/workloads", nil, &workloads); err != nil {
		return err
	}

	// One extra call fetches every container so state can be shown without an
	// N+1 per-app request.
	var containers []Container
	if err := sess.client.doJSON(ctx, "GET", "/api/containers", nil, &containers); err != nil {
		return err
	}
	byWorkload := map[string][]Container{}
	for _, c := range containers {
		byWorkload[c.WorkloadID] = append(byWorkload[c.WorkloadID], c)
	}

	apps := make([]Workload, 0, len(workloads))
	for _, w := range workloads {
		if w.isApp() {
			apps = append(apps, w)
		}
	}
	sort.Slice(apps, func(i, j int) bool { return apps[i].Name < apps[j].Name })

	if len(apps) == 0 {
		fmt.Println("No apps yet. Create one in the web UI, then deploy with 'tinyforge deploy <app>'.")
		return nil
	}

	tw := tabwriter.NewWriter(os.Stdout, 0, 2, 2, ' ', 0)
	fmt.Fprintln(tw, "NAME\tSOURCE\tSTATE\tID")
	for _, w := range apps {
		fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", w.Name, w.SourceKind, stateSummary(byWorkload[w.ID]), idShort(w.ID))
	}
	return tw.Flush()
}

// stateSummary condenses a workload's containers into one status word.
func stateSummary(cs []Container) string {
	if len(cs) == 0 {
		return "—"
	}
	running := 0
	for _, c := range cs {
		if c.State == "running" {
			running++
		}
	}
	switch {
	case running == len(cs):
		return "running"
	case running == 0:
		return cs[0].State // e.g. stopped / failed / missing
	default:
		return fmt.Sprintf("%d/%d running", running, len(cs))
	}
}

// resolveApp maps a user-supplied reference (name, full id, or id prefix) to a
// single app workload. Exact id wins, then exact name, then a unique id prefix.
func resolveApp(ctx context.Context, c *Client, ref string) (Workload, error) {
	var workloads []Workload
	if err := c.doJSON(ctx, "GET", "/api/workloads", nil, &workloads); err != nil {
		return Workload{}, err
	}

	var byID, byName, byPrefix []Workload
	for _, w := range workloads {
		if !w.isApp() {
			continue
		}
		switch {
		case w.ID == ref:
			byID = append(byID, w)
		case strings.EqualFold(w.Name, ref):
			byName = append(byName, w)
		case len(ref) >= 6 && strings.HasPrefix(w.ID, ref):
			byPrefix = append(byPrefix, w)
		}
	}

	if len(byID) == 1 {
		return byID[0], nil
	}
	if len(byName) == 1 {
		return byName[0], nil
	}
	if len(byName) > 1 {
		return Workload{}, ambiguousErr(ref, byName)
	}
	if len(byPrefix) == 1 {
		return byPrefix[0], nil
	}
	if len(byPrefix) > 1 {
		return Workload{}, ambiguousErr(ref, byPrefix)
	}
	return Workload{}, fmt.Errorf("no app matching %q (try 'tinyforge apps list')", ref)
}

func ambiguousErr(ref string, matches []Workload) error {
	var b strings.Builder
	fmt.Fprintf(&b, "%q matches multiple apps; use the id:\n", ref)
	for _, w := range matches {
		fmt.Fprintf(&b, "  %s  %s\n", idShort(w.ID), w.Name)
	}
	return fmt.Errorf("%s", strings.TrimRight(b.String(), "\n"))
}

func idShort(id string) string {
	if len(id) > 8 {
		return id[:8]
	}
	return id
}
