package cmd

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"nest/src/catalog"
	"nest/src/config"
	"nest/src/output"
	nestruntime "nest/src/runtime"
)

const refreshUsage = `Usage: nest refresh [--runtime podman|docker] [--catalog path] [--json]

Fetch the latest catalog from the pinned HTTPS URL and install it to the user
data directory if its version is newer (the system catalog is never touched).
Then update the catalog images already present locally; images that are not
local are never pulled (AGENTS.md §4.6).
`

func (a *App) cmdRefresh(cfg *config.Config, args []string) int {
	fs := a.newFlagSet("refresh", refreshUsage)
	runtimeFlag := fs.String("runtime", "", "container runtime: podman or docker")
	jsonOut := fs.Bool("json", false, "JSON output (stable schema, see AGENTS.md §4.8)")
	catalogFlag := fs.String("catalog", "", "use the catalog at path (development/testing)")
	if ok, code := a.parseFlags(fs, args); !ok {
		return code
	}

	current, _, code := a.loadCatalog(*catalogFlag)
	if code != exitOK {
		return code
	}

	result := output.Refresh{Images: []output.ImageUpdate{}}
	effective, code := a.refreshCatalog(cfg, current, &result)
	if code != exitOK {
		return code
	}
	a.refreshImages(cfg, *runtimeFlag, effective, &result)

	if *jsonOut {
		if err := output.JSON(a.Stdout, result); err != nil {
			fmt.Fprintf(a.Stderr, "nest: cannot render JSON: %v\n", err)
			return exitError
		}
		return exitOK
	}
	if result.Catalog.Updated {
		fmt.Fprintf(a.Stdout, "catalog updated v%d → v%d\n", current.Version, result.Catalog.Version)
	} else {
		fmt.Fprintln(a.Stdout, "catalog already up to date")
	}
	for _, img := range result.Images {
		fmt.Fprintf(a.Stdout, "image %s: %s\n", img.Image, img.Status)
	}
	return exitOK
}

// refreshCatalog downloads, validates, and — when newer — atomically installs
// the user catalog copy. It returns the catalog to use for image updates.
func (a *App) refreshCatalog(cfg *config.Config, current *catalog.Catalog, result *output.Refresh) (*catalog.Catalog, int) {
	url := cfg.ResolveCatalogURL(a.Getenv, catalog.DefaultURL)
	body, err := fetch(url)
	if err != nil {
		fmt.Fprintf(a.Stderr, "nest: cannot fetch catalog: %v\n", err)
		return nil, exitError
	}
	downloaded, err := catalog.Parse(body)
	if err != nil {
		fmt.Fprintf(a.Stderr, "nest: downloaded catalog is invalid, keeping the current one: %v\n", err)
		return nil, exitError
	}

	if downloaded.Version <= current.Version {
		result.Catalog = output.CatalogUpdate{Updated: false, Version: current.Version}
		return current, exitOK
	}
	if err := writeAtomic(a.CatalogLocs.User, body); err != nil {
		fmt.Fprintf(a.Stderr, "nest: cannot install catalog: %v\n", err)
		return nil, exitError
	}
	result.Catalog = output.CatalogUpdate{Updated: true, Version: downloaded.Version}
	return downloaded, exitOK
}

// refreshImages pulls each unique catalog image that is already local (§4.6).
// Without a usable runtime the section is skipped with a stderr note.
func (a *App) refreshImages(cfg *config.Config, flagValue string, cat *catalog.Catalog, result *output.Refresh) {
	rt, code := a.resolveRuntime(cfg, flagValue)
	if code == exitUsage {
		return // resolveRuntime already reported the usage error
	}
	if code != exitOK {
		fmt.Fprintf(a.Stderr, "nest: skipping image update\n")
		return
	}

	seen := map[string]bool{}
	for i := range cat.Tools {
		for _, image := range cat.Tools[i].Images {
			if seen[image] {
				continue
			}
			seen[image] = true
			result.Images = append(result.Images, a.refreshOneImage(rt, image))
		}
	}
}

func (a *App) refreshOneImage(rt *nestruntime.Runtime, image string) output.ImageUpdate {
	exists, err := rt.ImageExists(image)
	if err != nil {
		fmt.Fprintf(a.Stderr, "nest: %v\n", err)
		return output.ImageUpdate{Image: image, Status: "failed"}
	}
	if !exists {
		return output.ImageUpdate{Image: image, Status: "not-local"}
	}
	out, err := rt.Pull(image, a.Stderr)
	if err != nil {
		fmt.Fprintf(a.Stderr, "nest: %v\n", err)
		return output.ImageUpdate{Image: image, Status: "failed"}
	}
	if strings.Contains(strings.ToLower(out), "up to date") {
		return output.ImageUpdate{Image: image, Status: "up-to-date"}
	}
	return output.ImageUpdate{Image: image, Status: "updated"}
}

func fetch(url string) ([]byte, error) {
	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Get(url)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("%s returned %s", url, resp.Status)
	}
	return io.ReadAll(io.LimitReader(resp.Body, 10<<20))
}

// writeAtomic writes data to path via a temp file + rename (§4.6).
func writeAtomic(path string, data []byte) error {
	dir := filepath.Dir(path)
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return err
	}
	tmp, err := os.CreateTemp(dir, ".tools-*.yaml")
	if err != nil {
		return err
	}
	defer os.Remove(tmp.Name())
	if _, err := tmp.Write(data); err != nil {
		tmp.Close()
		return err
	}
	if err := tmp.Close(); err != nil {
		return err
	}
	return os.Rename(tmp.Name(), path)
}
