// Package downloads serves release artifacts through the platform's object
// storage abstraction. It is deliberately app-blind: the mapping from a public
// download "kind" to its object-store key and friendly filename is supplied by
// deployment configuration (see LoadCatalogFromEnv), NOT hardcoded here — L0
// carries no product/artifact names. It serves the bytes using presigned URLs
// when the provider supports them (S3) and streaming otherwise (filesystem).
package downloads

import (
	"encoding/json"
	"errors"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/nexuscrm/storage"
)

// presignTTL bounds the lifetime of a presigned GET URL when the storage
// provider supports one. Short enough to limit link sharing, long enough for a
// slow client to start the download.
const presignTTL = 5 * time.Minute

// DownloadArtifactsEnv is the environment variable holding the JSON artifact
// catalog. The deployment (which owns product identity) sets it; L0 has no
// built-in knowledge of any product's artifacts. Shape:
//
//	{"cli":{"key":"release-artifact/…/cli.tgz","filename":"…-cli.tgz"}, …}
const DownloadArtifactsEnv = "NEXUSCRM_DOWNLOAD_ARTIFACTS"

// DownloadArtifactStorageRootEnv optionally places public release artifacts in
// a filesystem store independent from the mutable customer-file object store.
// When unset, ArtifactStoreFromEnv preserves the historical shared-store
// behavior for deployments that have not adopted the split yet.
const DownloadArtifactStorageRootEnv = "NEXUSCRM_DOWNLOAD_ARTIFACT_STORAGE_ROOT"

// Artifact describes one downloadable release artifact: the object-store key it
// lives at and the friendly filename presented to the client.
type Artifact struct {
	Key      string `json:"key"`
	Filename string `json:"filename"`
}

// Catalog maps a public download kind to its artifact. It is configuration, not
// code — see LoadCatalogFromEnv.
type Catalog map[string]Artifact

// LoadCatalogFromEnv parses the deployment-supplied artifact catalog. An unset
// or empty variable yields an empty catalog (every kind 404s), keeping L0
// app-blind by default. It is the single source of truth shared by the download
// handler and the publish-artifact tooling.
func LoadCatalogFromEnv() (Catalog, error) {
	raw := strings.TrimSpace(os.Getenv(DownloadArtifactsEnv))
	if raw == "" {
		return Catalog{}, nil
	}
	var cat Catalog
	if err := json.Unmarshal([]byte(raw), &cat); err != nil {
		return nil, err
	}
	return cat, nil
}

// ArtifactStoreFromEnv resolves the single object-store read path used by the
// public download handler. A configured artifact root is authoritative; the
// primary customer-file store is used only when no split has been configured.
func ArtifactStoreFromEnv(primary storage.ObjectStore) (storage.ObjectStore, error) {
	store, configured, err := configuredArtifactStoreFromEnv()
	if err != nil || configured {
		return store, err
	}
	return primary, nil
}

// NewArtifactStoreFromEnv resolves the single object-store write path used by
// publish-artifact. It shares the dedicated-root resolver with the download
// handler and falls back to the historical FILE_STORAGE_* store when the split
// has not been configured.
func NewArtifactStoreFromEnv() (storage.ObjectStore, error) {
	store, configured, err := configuredArtifactStoreFromEnv()
	if err != nil || configured {
		return store, err
	}
	return storage.NewStoreFromEnv()
}

func configuredArtifactStoreFromEnv() (storage.ObjectStore, bool, error) {
	root := strings.TrimSpace(os.Getenv(DownloadArtifactStorageRootEnv))
	if root == "" {
		return nil, false, nil
	}
	store, err := storage.NewRuntimeFilesystemStore(root)
	return store, true, err
}

// Handler serves release artifacts through the object store using an injected,
// deployment-supplied catalog.
type Handler struct {
	store   storage.ObjectStore
	catalog Catalog
}

// NewHandler constructs a downloads handler bound to the given object store and
// artifact catalog. A nil catalog serves no artifacts (all kinds 404).
func NewHandler(store storage.ObjectStore, catalog Catalog) *Handler {
	if catalog == nil {
		catalog = Catalog{}
	}
	return &Handler{store: store, catalog: catalog}
}

// Latest serves GET /downloads/:kind/latest. For a provider that supports
// presigned GET (S3), it responds 302 to the presigned URL; otherwise
// (filesystem) it streams the bytes as an attachment. Unknown kinds and missing
// objects return 404 JSON.
func (h *Handler) Latest(c *gin.Context) {
	kind := c.Param("kind")
	art, ok := h.catalog[kind]
	if !ok {
		c.JSON(http.StatusNotFound, gin.H{"error": "unknown artifact kind"})
		return
	}
	key, filename := art.Key, art.Filename

	ctx := c.Request.Context()

	// Confirm the object exists before handing back a presigned URL. S3 presign
	// signs a URL locally without checking existence, so a missing artifact would
	// otherwise 302 to a link that fails at the provider; HEAD lets us return a
	// clean 404 instead. (Also surfaces a real 404 to the streaming fallback.)
	if _, err := h.store.HeadObject(ctx, key); err != nil {
		if errors.Is(err, storage.ErrObjectNotFound) {
			c.JSON(http.StatusNotFound, gin.H{"error": "artifact not found"})
			return
		}
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to resolve artifact"})
		return
	}

	url, err := h.store.GeneratePresignedGetURL(ctx, key, presignTTL)
	switch {
	case err == nil:
		c.Redirect(http.StatusFound, url)
		return
	case errors.Is(err, storage.ErrPresignUnsupported):
		// Fall through to streaming for providers without presigned URLs.
	case errors.Is(err, storage.ErrObjectNotFound):
		c.JSON(http.StatusNotFound, gin.H{"error": "artifact not found"})
		return
	default:
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to resolve artifact"})
		return
	}

	body, meta, err := h.store.GetObject(ctx, key)
	if err != nil {
		if errors.Is(err, storage.ErrObjectNotFound) {
			c.JSON(http.StatusNotFound, gin.H{"error": "artifact not found"})
			return
		}
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read artifact"})
		return
	}
	defer func() { _ = body.Close() }()

	extraHeaders := map[string]string{
		"Content-Disposition": `attachment; filename="` + filename + `"`,
	}
	c.DataFromReader(http.StatusOK, meta.Size, "application/octet-stream", body, extraHeaders)
}
