package handlers

// user_packages.go — v0.30.0 CS5
//
// Non-admin package management. Users install personal-scoped packages;
// team admins install team-scoped packages.
//
// Restrictions for non-global scope:
//   - Tier: browser only (no starlark/sidecar)
//   - Permissions: only notifications.send allowed
//   - Prevents privilege escalation through user-installed packages

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

	"github.com/gin-gonic/gin"

	"switchboard-core/store"
)

// allowedNonGlobalPermissions is the set of permissions that non-admin
// (team/personal scope) packages may declare.
var allowedNonGlobalPermissions = map[string]bool{
	"notifications.send": true,
}

// UserPackageHandler manages user and team scoped package operations.
type UserPackageHandler struct {
	stores      store.Stores
	packagesDir string
}

// NewUserPackageHandler creates a new user package handler.
func NewUserPackageHandler(s store.Stores, packagesDir string) *UserPackageHandler {
	return &UserPackageHandler{stores: s, packagesDir: packagesDir}
}

// ListVisiblePackages returns packages visible to the current user
// (global + user's teams + personal).
// GET /api/v1/packages
func (h *UserPackageHandler) ListVisiblePackages(c *gin.Context) {
	userID := c.GetString("user_id")
	if userID == "" {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
		return
	}

	pkgs, err := h.stores.Packages.ListVisiblePackages(c.Request.Context(), userID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list packages"})
		return
	}
	if pkgs == nil {
		pkgs = []store.PackageRegistration{}
	}

	c.JSON(http.StatusOK, gin.H{"data": pkgs})
}

// InstallPersonalPackage installs a package with personal scope.
// POST /api/v1/packages/install
func (h *UserPackageHandler) InstallPersonalPackage(c *gin.Context) {
	userID := c.GetString("user_id")
	if userID == "" {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
		return
	}

	h.installScoped(c, "personal", "", userID)
}

// InstallTeamPackage installs a package with team scope.
// POST /api/v1/teams/:teamId/packages/install
func (h *UserPackageHandler) InstallTeamPackage(c *gin.Context) {
	userID := c.GetString("user_id")
	teamID := c.Param("teamId")
	if userID == "" || teamID == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "missing user or team context"})
		return
	}

	h.installScoped(c, "team", teamID, userID)
}

// DeletePersonalPackage deletes a personal-scoped package owned by the user.
// DELETE /api/v1/packages/:id
func (h *UserPackageHandler) DeletePersonalPackage(c *gin.Context) {
	userID := c.GetString("user_id")
	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.Scope != "personal" || pkg.InstalledBy == nil || *pkg.InstalledBy != userID {
		c.JSON(http.StatusForbidden, gin.H{"error": "can only delete your own personal packages"})
		return
	}

	if err := h.stores.Packages.Delete(c.Request.Context(), id); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete package"})
		return
	}

	// Clean up assets
	if h.packagesDir != "" {
		os.RemoveAll(filepath.Join(h.packagesDir, id))
	}

	c.JSON(http.StatusOK, gin.H{"deleted": id})
}

// DeleteTeamPackage deletes a team-scoped package.
// DELETE /api/v1/teams/:teamId/packages/:id
func (h *UserPackageHandler) DeleteTeamPackage(c *gin.Context) {
	teamID := c.Param("teamId")
	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.Scope != "team" || pkg.TeamID == nil || *pkg.TeamID != teamID {
		c.JSON(http.StatusForbidden, gin.H{"error": "package does not belong to this team"})
		return
	}

	if err := h.stores.Packages.Delete(c.Request.Context(), id); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete package"})
		return
	}

	if h.packagesDir != "" {
		os.RemoveAll(filepath.Join(h.packagesDir, id))
	}

	c.JSON(http.StatusOK, gin.H{"deleted": id})
}

// installScoped handles the common install flow for team/personal packages.
func (h *UserPackageHandler) installScoped(c *gin.Context, scope, teamID, userID string) {
	file, header, err := c.Request.FormFile("file")
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "no file uploaded"})
		return
	}
	defer file.Close()

	validExt := strings.HasSuffix(header.Filename, ".pkg") ||
		strings.HasSuffix(header.Filename, ".surface") ||
		strings.HasSuffix(header.Filename, ".zip")
	if !validExt {
		c.JSON(http.StatusBadRequest, gin.H{"error": "file must be a .pkg, .surface, or .zip archive"})
		return
	}

	if header.Size > 50*1024*1024 {
		c.JSON(http.StatusBadRequest, gin.H{"error": "archive too large (max 50MB)"})
		return
	}

	// Read into temp file
	tmpFile, err := os.CreateTemp("", "userpkg-*.zip")
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create temp file"})
		return
	}
	tmpPath := tmpFile.Name()
	defer os.Remove(tmpPath)

	if _, err := io.Copy(tmpFile, file); err != nil {
		tmpFile.Close()
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read upload"})
		return
	}
	tmpFile.Close()

	// Open as zip and extract manifest
	zr, err := zip.OpenReader(tmpPath)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid zip archive"})
		return
	}
	defer zr.Close()

	var manifest map[string]any
	for _, f := range zr.File {
		name := filepath.Base(f.Name)
		if name == "manifest.json" && !f.FileInfo().IsDir() {
			rc, err := f.Open()
			if err != nil {
				continue
			}
			data, _ := io.ReadAll(rc)
			rc.Close()
			json.Unmarshal(data, &manifest)
			break
		}
	}

	if manifest == nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "archive missing manifest.json"})
		return
	}

	pkgID, _ := manifest["id"].(string)
	title, _ := manifest["title"].(string)
	if pkgID == "" || title == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "manifest must have 'id' and 'title'"})
		return
	}

	if !validPackageID.MatchString(pkgID) {
		c.JSON(http.StatusBadRequest, gin.H{"error": "package id must be a lowercase slug"})
		return
	}

	// Enforce tier restriction: browser only for non-global
	tier, _ := manifest["tier"].(string)
	if tier == "" {
		tier = "browser"
	}
	if tier != "browser" {
		c.JSON(http.StatusForbidden, gin.H{
			"error": "only browser-tier packages can be installed at team/personal scope",
		})
		return
	}

	// Enforce permission restriction
	if permsRaw, ok := manifest["permissions"].([]any); ok {
		for _, p := range permsRaw {
			if perm, ok := p.(string); ok {
				if !allowedNonGlobalPermissions[perm] {
					c.JSON(http.StatusForbidden, gin.H{
						"error": "permission " + perm + " is not allowed for " + scope + "-scoped packages",
					})
					return
				}
			}
		}
	}

	// Check for conflicts
	existing, _ := h.stores.Packages.Get(c.Request.Context(), pkgID)
	if existing != nil && existing.Source == "core" {
		c.JSON(http.StatusConflict, gin.H{"error": "cannot overwrite core package"})
		return
	}

	// Extract static assets
	if h.packagesDir != "" {
		destDir := filepath.Join(h.packagesDir, pkgID)
		os.MkdirAll(destDir, 0755)
		for _, f := range zr.File {
			if f.FileInfo().IsDir() {
				continue
			}
			relPath := extractableRelPath(f.Name)
			if relPath == "" {
				continue
			}
			destPath := filepath.Join(destDir, relPath)
			if !strings.HasPrefix(filepath.Clean(destPath), filepath.Clean(destDir)) {
				continue
			}
			os.MkdirAll(filepath.Dir(destPath), 0755)
			rc, err := f.Open()
			if err != nil {
				continue
			}
			out, err := os.Create(destPath)
			if err != nil {
				rc.Close()
				continue
			}
			io.Copy(out, rc)
			out.Close()
			rc.Close()
		}
	}

	version, _ := manifest["version"].(string)
	if version == "" {
		version = "0.0.0"
	}

	pkgType, _ := manifest["type"].(string)
	if pkgType == "" {
		pkgType = "surface"
	}

	description, _ := manifest["description"].(string)
	author, _ := manifest["author"].(string)

	// Create the package with scope
	pkg := &store.PackageRegistration{
		ID:          pkgID,
		Title:       title,
		Type:        pkgType,
		Version:     version,
		Description: description,
		Author:      author,
		Tier:        tier,
		IsSystem:    false,
		Scope:       scope,
		Enabled:     true,
		Status:      "active",
		Source:      "extension",
		Manifest:    manifest,
	}

	if teamID != "" {
		pkg.TeamID = &teamID
	}
	pkg.InstalledBy = &userID

	if existing != nil {
		// Update existing
		if err := h.stores.Packages.Update(c.Request.Context(), pkgID, pkg); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update package"})
			return
		}
	} else {
		// Create new
		if err := h.stores.Packages.Create(c.Request.Context(), pkg); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create package"})
			return
		}
	}

	log.Printf("[packages] installed %s-scoped package %s by user %s", scope, pkgID, userID)

	c.JSON(http.StatusOK, gin.H{
		"id":      pkgID,
		"title":   title,
		"type":    pkgType,
		"version": version,
		"scope":   scope,
		"source":  "extension",
	})
}
