//go:build !headless && !lite

package admin

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net"
	"net/http"

	"breadbox/internal/app"
	"breadbox/internal/appconfig"
	"breadbox/internal/db"
	"breadbox/internal/pgconv"
	"breadbox/internal/version"
)

// updateCallout reports whether the "update available" sidebar callout should
// render, combining the cached GitHub version check with the admin's last
// dismissal. The callout is suppressed once the newest release has been
// dismissed (stored in app_config under "update_dismissed_version" by
// DismissUpdateHandler) and resurfaces automatically when a release newer than
// the dismissed one ships. cfg may be nil, in which case dismissal is ignored.
//
// Intentionally consulted only for the passive sidebar nudge — the Settings →
// System version badge always reflects true version state regardless of
// dismissal, so dismissing the nudge never makes the app claim it's "up to
// date" when it isn't.
func updateCallout(ctx context.Context, vc *version.Checker, cfg appconfig.Reader) (show bool, latestVersion, latestURL string) {
	if vc == nil {
		return false, "", ""
	}
	available, latest, err := vc.CheckForUpdate(ctx)
	if err != nil || available == nil || !*available || latest == nil {
		return false, "", ""
	}
	if cfg != nil {
		if dismissed := appconfig.String(ctx, cfg, "update_dismissed_version", ""); dismissed == latest.Version {
			return false, "", ""
		}
	}
	return true, latest.Version, latest.URL
}

// DismissUpdateHandler handles POST /-/update/dismiss.
// It stores the dismissed version in app_config so the banner is hidden until
// a newer release is published (read back by updateCallout).
func DismissUpdateHandler(a *app.App) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var body struct {
			Version string `json:"version"`
		}
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Version == "" {
			writeError(w, http.StatusBadRequest, "INVALID_REQUEST", "version is required")
			return
		}

		_ = a.Queries.SetAppConfig(r.Context(), db.SetAppConfigParams{
			Key:   "update_dismissed_version",
			Value: pgconv.Text(body.Version),
		})

		writeOK(w)
	}
}

// TriggerUpdateHandler handles POST /-/update.
// It pulls the latest Docker image via the Docker Engine API (Unix socket).
// After pulling, the admin must run `docker compose up -d` to apply the update.
func TriggerUpdateHandler(a *app.App) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if !a.DockerSocketAvailable {
			writeError(w, http.StatusBadRequest, "DOCKER_UNAVAILABLE", "Docker socket not available")
			return
		}

		if err := pullImage(r.Context(), "ghcr.io/canalesb93/breadbox", "latest"); err != nil {
			a.Logger.Error("docker image pull failed", "error", err)
			writeError(w, http.StatusInternalServerError, "UPDATE_FAILED", fmt.Sprintf("Failed to pull image: %v", err))
			return
		}

		writeJSON(w, http.StatusOK, map[string]any{
			"ok":      true,
			"message": "Image pulled. Run 'docker compose up -d' to complete the update.",
			"pulled":  true,
		})
	}
}

// pullImage pulls a Docker image via the Docker Engine API over the Unix socket.
func pullImage(ctx context.Context, image, tag string) error {
	transport := &http.Transport{
		DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
			return net.Dial("unix", "/var/run/docker.sock")
		},
	}
	client := &http.Client{
		Transport: transport,
		Timeout:   0, // image pulls can be slow; context handles cancellation
	}

	url := fmt.Sprintf("http://localhost/v1.46/images/create?fromImage=%s&tag=%s", image, tag)
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil)
	if err != nil {
		return fmt.Errorf("create request: %w", err)
	}

	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("docker API request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
		return fmt.Errorf("docker API returned status %d: %s", resp.StatusCode, string(body))
	}

	// The Docker pull API streams JSON progress. We must consume the entire
	// body to ensure the pull completes before we return.
	if _, err := io.Copy(io.Discard, resp.Body); err != nil {
		return fmt.Errorf("read pull stream: %w", err)
	}

	return nil
}
