package handlers

// package_export.go — v0.30.0 CS3
//
// Exports an installed package as a downloadable .pkg archive for
// cross-instance sharing. Includes manifest, static assets, and
// Starlark scripts. Does NOT include extension table data or secrets.

import (
	"archive/zip"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"

	"github.com/gin-gonic/gin"

	"chat-switchboard/store"
)

// PackageExportHandler handles package export operations.
type PackageExportHandler struct {
	stores      store.Stores
	packagesDir string
}

// NewPackageExportHandler creates a new export handler.
func NewPackageExportHandler(s store.Stores, packagesDir string) *PackageExportHandler {
	return &PackageExportHandler{stores: s, packagesDir: packagesDir}
}

// ExportPackage downloads an installed package as a .pkg archive.
// GET /api/v1/admin/packages/:id/export
func (h *PackageExportHandler) ExportPackage(c *gin.Context) {
	id := c.Param("id")
	pkg, err := h.stores.Packages.Get(c.Request.Context(), id)
	if err != nil || pkg == nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "package not found"})
		return
	}

	if pkg.Source == "core" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "cannot export core packages"})
		return
	}

	// Build manifest for export
	manifest := make(map[string]any, len(pkg.Manifest))
	for k, v := range pkg.Manifest {
		// Skip internal fields that shouldn't be exported
		if k == "_script" {
			continue
		}
		manifest[k] = v
	}

	// Include package_settings as default_settings so importing instance
	// gets admin defaults (but not secrets)
	if len(pkg.PackageSettings) > 0 && string(pkg.PackageSettings) != "{}" {
		var ps map[string]any
		if json.Unmarshal(pkg.PackageSettings, &ps) == nil && len(ps) > 0 {
			manifest["default_settings"] = ps
		}
	}

	// Set response headers
	filename := fmt.Sprintf("%s-%s.pkg", id, pkg.Version)
	c.Header("Content-Type", "application/zip")
	c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))

	// Create zip writer directly to response
	zw := zip.NewWriter(c.Writer)
	defer zw.Close()

	// Write manifest.json
	manifestJSON, err := json.MarshalIndent(manifest, "", "  ")
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to serialize manifest"})
		return
	}
	mf, err := zw.Create("manifest.json")
	if err != nil {
		return
	}
	mf.Write(manifestJSON)

	// Include Starlark script as script.star if present
	if script, ok := pkg.Manifest["_starlark_script"].(string); ok && script != "" {
		sf, err := zw.Create("script.star")
		if err == nil {
			sf.Write([]byte(script))
		}
	}

	// Walk packagesDir/{id}/ for static assets
	if h.packagesDir != "" {
		assetsDir := filepath.Join(h.packagesDir, id)
		if info, err := os.Stat(assetsDir); err == nil && info.IsDir() {
			filepath.Walk(assetsDir, func(path string, info os.FileInfo, err error) error {
				if err != nil || info.IsDir() {
					return nil
				}
				relPath, err := filepath.Rel(assetsDir, path)
				if err != nil {
					return nil
				}
				// Normalize path separators for zip
				relPath = strings.ReplaceAll(relPath, string(filepath.Separator), "/")

				f, err := zw.Create(relPath)
				if err != nil {
					return nil
				}
				src, err := os.Open(path)
				if err != nil {
					return nil
				}
				defer src.Close()
				io.Copy(f, src)
				return nil
			})
		}
	}
}
