// SPDX-License-Identifier: GPL-3.0-or-later

package api

import (
	"net/http"
	"runtime"

	"github.com/gin-gonic/gin"
	"github.com/privatebackup/client-backend/internal/updater"
)

// SetUpdater attaches the updater service to the handlers. Passing nil
// disables all update endpoints (they return 503).
func (h *Handlers) SetUpdater(u *updater.Updater) {
	h.mu.Lock()
	h.updater = u
	h.mu.Unlock()
}

func (h *Handlers) currentUpdater() *updater.Updater {
	h.mu.RLock()
	defer h.mu.RUnlock()
	return h.updater
}

// GetUpdateStatus handles GET /api/v1/updates/status.
func (h *Handlers) GetUpdateStatus(c *gin.Context) {
	u := h.currentUpdater()
	if u == nil {
		c.JSON(http.StatusServiceUnavailable, gin.H{"error": "updater not available"})
		return
	}
	c.JSON(http.StatusOK, u.State())
}

// CheckForUpdates handles POST /api/v1/updates/check. It performs a fresh
// manifest fetch (Tor-routed) and returns the latest state. If
// `?install=true` is passed and an update is available, the binary is
// downloaded and applied; the response then reflects the post-install
// state with PendingPath populated.
func (h *Handlers) CheckForUpdates(c *gin.Context) {
	u := h.currentUpdater()
	if u == nil {
		c.JSON(http.StatusServiceUnavailable, gin.H{"error": "updater not available"})
		return
	}
	install := c.Query("install") == "true"
	var err error
	if install {
		err = u.CheckAndApply(c.Request.Context())
	} else {
		_, err = u.CheckOnly(c.Request.Context())
	}
	if err != nil {
		c.JSON(http.StatusBadGateway, gin.H{
			"error": err.Error(),
			"state": u.State(),
		})
		return
	}
	c.JSON(http.StatusOK, u.State())
}

// UpdateConfigRequest represents a partial update to the updater config.
type UpdateConfigRequest struct {
	AutoUpdate *bool `json:"auto_update,omitempty"`
}

// GetUpdateConfig handles GET /api/v1/updates/config.
func (h *Handlers) GetUpdateConfig(c *gin.Context) {
	u := h.currentUpdater()
	if u == nil {
		// Even without a live updater, the persisted config still applies.
		c.JSON(http.StatusOK, gin.H{
			"auto_update": h.config.Updates.AutoUpdate,
			"channel":     h.config.Updates.Channel,
		})
		return
	}
	st := u.State()
	c.JSON(http.StatusOK, gin.H{
		"auto_update": st.AutoUpdate,
		"channel":     st.Channel,
	})
}

// UpdateUpdateConfig handles PUT /api/v1/updates/config.
func (h *Handlers) UpdateUpdateConfig(c *gin.Context) {
	var req UpdateConfigRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	if req.AutoUpdate != nil {
		h.config.Updates.AutoUpdate = *req.AutoUpdate
		if u := h.currentUpdater(); u != nil {
			u.SetAutoUpdate(*req.AutoUpdate)
		}
	}
	c.JSON(http.StatusOK, gin.H{
		"auto_update": h.config.Updates.AutoUpdate,
		"channel":     h.config.Updates.Channel,
	})
}

// FetchUpdateBinary handles GET /api/v1/updates/binary/:component.
//
// This is how the CLI and the Tauri frontend pull their own new binaries:
// the daemon downloads the artifact through Tor, verifies the signature,
// and streams the verified bytes back over localhost. The CLI then
// replaces its own executable with the response body. The endpoint never
// reveals untrusted data — every byte is signature-verified before being
// served.
func (h *Handlers) FetchUpdateBinary(c *gin.Context) {
	u := h.currentUpdater()
	if u == nil {
		c.JSON(http.StatusServiceUnavailable, gin.H{"error": "updater not available"})
		return
	}
	component := updater.Component(c.Param("component"))
	switch component {
	case updater.ComponentBackend, updater.ComponentCLI, updater.ComponentDesktop:
	default:
		c.JSON(http.StatusBadRequest, gin.H{"error": "unknown component"})
		return
	}
	body, artifact, err := u.FetchArtifact(c.Request.Context(), component)
	if err != nil {
		c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
		return
	}
	c.Header("X-Artifact-SHA256", artifact.SHA256)
	c.Header("X-Artifact-Size", itoa(artifact.Size))
	c.Header("X-Artifact-URL", artifact.URL)
	c.Data(http.StatusOK, "application/octet-stream", body)
}

// GetTauriManifest handles GET /api/v1/updates/tauri-manifest.
//
// tauri-plugin-updater expects a specific JSON shape. This endpoint
// translates the daemon's already-verified manifest into that shape so
// the desktop frontend can rely on the daemon's Tor-routed fetch instead
// of opening its own clearnet connection. The signature field uses the
// minisign payload published alongside the desktop bundle.
func (h *Handlers) GetTauriManifest(c *gin.Context) {
	u := h.currentUpdater()
	if u == nil {
		c.JSON(http.StatusServiceUnavailable, gin.H{"error": "updater not available"})
		return
	}
	m := u.CachedManifest()
	if m == nil {
		// Force a fresh check so the desktop has something to work with
		// on first launch.
		fresh, err := u.CheckOnly(c.Request.Context())
		if err != nil {
			c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
			return
		}
		m = fresh
	}
	platform, ok := m.Get(runtime.GOOS, runtime.GOARCH)
	if !ok || platform.Desktop == nil {
		c.JSON(http.StatusOK, gin.H{
			"version": m.Version,
			"notes":   "no desktop artifact for this platform",
		})
		return
	}
	// The desktop artifact URL still points at the public host. The Tauri
	// updater plugin will fetch it directly. To force that fetch through
	// the daemon (and therefore through Tor) we instead point the URL at
	// our /updates/binary/desktop proxy endpoint.
	proxyURL := proxyBinaryURL(c, "desktop")
	c.JSON(http.StatusOK, gin.H{
		"version":   m.Version,
		"pub_date":  m.Released,
		"notes":     m.ReleaseNotesURL,
		"platforms": tauriPlatformsFor(platform.Desktop, proxyURL),
	})
}

func tauriPlatformsFor(desktop *updater.Artifact, proxyURL string) map[string]map[string]string {
	// Tauri uses platform keys like "darwin-x86_64", "darwin-aarch64",
	// "linux-x86_64", "windows-x86_64". Map our runtime architecture into
	// the matching key. Tauri only consults the entry that matches its
	// own runtime, so we only need to populate the local one.
	key := tauriPlatformKey()
	return map[string]map[string]string{
		key: {
			"signature": desktop.MinisignSignature,
			"url":       proxyURL,
		},
	}
}

func tauriPlatformKey() string {
	arch := runtime.GOARCH
	switch arch {
	case "amd64":
		arch = "x86_64"
	case "arm64":
		arch = "aarch64"
	}
	return runtime.GOOS + "-" + arch
}

func proxyBinaryURL(c *gin.Context, component string) string {
	scheme := "http"
	if c.Request.TLS != nil {
		scheme = "https"
	}
	return scheme + "://" + c.Request.Host + "/api/v1/updates/binary/" + component
}

func itoa(n int64) string {
	if n == 0 {
		return "0"
	}
	neg := n < 0
	if neg {
		n = -n
	}
	buf := make([]byte, 0, 20)
	for n > 0 {
		buf = append([]byte{byte('0' + n%10)}, buf...)
		n /= 10
	}
	if neg {
		buf = append([]byte{'-'}, buf...)
	}
	return string(buf)
}
