package handlers

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

	"github.com/gin-gonic/gin"

	"switchboard-core/database"
	"switchboard-core/models"
	"switchboard-core/sandbox"
	"switchboard-core/store"
	"switchboard-core/triggers"
)

// InstallBundledPackages scans bundledDir for .pkg archives and installs
// any that don't already exist in the database. Called once at startup
// (unless SKIP_BUNDLED_PACKAGES=true).
//
// allowlist is an optional comma-separated list of package IDs to install.
// Empty string means install all. This allows Helm/K8s deployments to
// select a subset: e.g. "tasks,schedules,hello-dashboard".
//
// Design: install-once, skip-if-present. If an admin uninstalls a bundled
// package, it won't be re-installed on the next restart.
func InstallBundledPackages(bundledDir, packagesDir, allowlist string, stores store.Stores, runner *sandbox.Runner) {
	entries, err := os.ReadDir(bundledDir)
	if err != nil {
		if os.IsNotExist(err) {
			log.Printf("[bundled] No bundled packages directory at %s — skipping", bundledDir)
			return
		}
		log.Printf("[bundled] ⚠️ Cannot read bundled packages: %v", err)
		return
	}

	// Parse allowlist into a set (empty = allow all)
	allowed := parseBundleAllowlist(allowlist)
	if len(allowed) > 0 {
		log.Printf("[bundled] Allowlist: %v", allowlist)
	}

	var installed, skipped, filtered int
	for _, entry := range entries {
		if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".pkg") {
			continue
		}

		// Check allowlist by filename (strip .pkg extension = package ID)
		if len(allowed) > 0 {
			pkgName := strings.TrimSuffix(entry.Name(), ".pkg")
			if !allowed[pkgName] {
				filtered++
				continue
			}
		}

		pkgPath := filepath.Join(bundledDir, entry.Name())
		result, err := installBundledPackage(pkgPath, packagesDir, stores, runner)
		if err != nil {
			log.Printf("[bundled] ⚠️ Failed to install %s: %v", entry.Name(), err)
			continue
		}
		if result == "skipped" {
			skipped++
		} else {
			installed++
		}
	}

	if installed > 0 || skipped > 0 || filtered > 0 {
		msg := fmt.Sprintf("[bundled] Installed %d, skipped %d existing", installed, skipped)
		if filtered > 0 {
			msg += fmt.Sprintf(", filtered %d by allowlist", filtered)
		}
		log.Println(msg)
	}
}

// parseBundleAllowlist parses a comma-separated string into a set of
// package IDs. Returns nil for empty input (meaning "allow all").
func parseBundleAllowlist(s string) map[string]bool {
	s = strings.TrimSpace(s)
	if s == "" {
		return nil
	}
	parts := strings.Split(s, ",")
	m := make(map[string]bool, len(parts))
	for _, p := range parts {
		p = strings.TrimSpace(p)
		if p != "" {
			m[p] = true
		}
	}
	return m
}

// installBundledPackage installs a single .pkg archive if its package ID
// doesn't already exist in the database. Returns "installed" or "skipped".
func installBundledPackage(pkgPath, packagesDir string, stores store.Stores, runner *sandbox.Runner) (string, error) {
	ctx := context.Background()

	// Open as zip
	zr, err := zip.OpenReader(pkgPath)
	if err != nil {
		return "", fmt.Errorf("invalid zip archive: %w", err)
	}
	defer zr.Close()

	// Find and parse manifest.json
	manifest, err := extractManifest(zr)
	if err != nil {
		return "", err
	}

	pkgID, _ := manifest["id"].(string)
	if pkgID == "" {
		return "", fmt.Errorf("manifest missing 'id'")
	}

	// Skip if already exists (admin uninstalled → stays uninstalled)
	existing, _ := stores.Packages.Get(ctx, pkgID)
	if existing != nil {
		return "skipped", nil
	}

	title, _ := manifest["title"].(string)
	if title == "" {
		return "", fmt.Errorf("manifest missing 'title'")
	}

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

	// Extract static assets to packagesDir/{id}/
	if packagesDir != "" {
		if err := extractPackageAssets(zr, packagesDir, pkgID); err != nil {
			return "", fmt.Errorf("asset extraction: %w", err)
		}
	}

	// Extract manifest fields for DB columns
	version, _ := manifest["version"].(string)
	if version == "" {
		version = "0.0.0"
	}
	description, _ := manifest["description"].(string)
	author, _ := manifest["author"].(string)
	tier, _ := manifest["tier"].(string)
	if tier == "" {
		tier = "browser"
	}

	// Register in database via Seed (upsert)
	if err := stores.Packages.Seed(ctx, pkgID, title, "bundled", manifest); err != nil {
		return "", fmt.Errorf("seed failed: %w", err)
	}

	// Update fields that Seed doesn't set
	pkg := &store.PackageRegistration{
		Title:       title,
		Type:        pkgType,
		Version:     version,
		Description: description,
		Author:      author,
		Tier:        tier,
		IsSystem:    false,
		Enabled:     true,
		Manifest:    manifest,
	}
	if err := stores.Packages.Update(ctx, pkgID, pkg); err != nil {
		log.Printf("[bundled] Failed to update package metadata for %s: %v", pkgID, err)
	}

	// Create namespaced DB tables declared in the manifest
	if tables, ok := ParseDBTables(manifest); ok {
		if err := CreateExtTables(ctx, database.DB, database.IsPostgres(), stores, pkgID, tables); err != nil {
			log.Printf("[bundled] [ext-db] schema create failed for %s: %v", pkgID, err)
		}
	}

	// Sync permissions — use a minimal gin.Context for the functions that need it
	fakeCtx := newBackgroundGinContext()
	SyncManifestPermissions(fakeCtx, stores, pkgID, manifest)

	// Sync triggers from manifest
	SyncManifestTriggers(ctx, stores, triggers.GlobalEngine(), pkgID, manifest)

	// Install workflow definition if applicable
	if pkgType == "workflow" {
		if err := InstallWorkflowFromManifest(fakeCtx, stores, pkgID, manifest); err != nil {
			log.Printf("[bundled] workflow install failed for %s: %v", pkgID, err)
		}
	}

	// Handle dormant status for packages with unmet requires
	knownCaps := map[string]bool{}
	if reqs, ok := manifest["requires"].([]any); ok && len(reqs) > 0 {
		var unmetReqs []string
		for _, r := range reqs {
			req, _ := r.(string)
			if req != "" && !knownCaps[req] {
				unmetReqs = append(unmetReqs, req)
			}
		}
		if len(unmetReqs) > 0 {
			stores.Packages.SetStatus(ctx, pkgID, "dormant")
			stores.Packages.SetEnabled(ctx, pkgID, false)
			log.Printf("[bundled] %s: dormant (unmet requires: %v)", pkgID, unmetReqs)
		}
	}

	// Auto-set default_surface when the first surface is installed
	if (pkgType == "surface" || pkgType == "full") {
		if raw, err := stores.GlobalConfig.Get(ctx, "default_surface"); err != nil || raw == nil {
			dflt := models.JSONMap{"id": pkgID}
			if setErr := stores.GlobalConfig.Set(ctx, "default_surface", dflt, ""); setErr != nil {
				log.Printf("[bundled] failed to auto-set default_surface to %s: %v", pkgID, setErr)
			}
		}
	}

	log.Printf("[bundled] Installed %s (%s) v%s", pkgID, pkgType, version)
	return "installed", nil
}

// extractManifest reads and parses manifest.json from a zip archive.
func extractManifest(zr *zip.ReadCloser) (map[string]any, error) {
	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, err := io.ReadAll(rc)
			rc.Close()
			if err != nil {
				continue
			}
			var manifest map[string]any
			if err := json.Unmarshal(data, &manifest); err != nil {
				return nil, fmt.Errorf("invalid manifest.json: %w", err)
			}
			return manifest, nil
		}
	}
	return nil, fmt.Errorf("archive missing manifest.json")
}

// extractPackageAssets extracts static assets from a zip archive to packagesDir/{pkgID}/.
func extractPackageAssets(zr *zip.ReadCloser, packagesDir, pkgID string) error {
	destDir := filepath.Join(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)

		// Security: prevent path traversal
		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()
	}
	return nil
}

// newBackgroundGinContext creates a minimal gin.Context backed by a
// background context. Used by startup code that calls functions
// originally designed for HTTP handlers.
func newBackgroundGinContext() *gin.Context {
	req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil)
	c := &gin.Context{}
	c.Request = req
	return c
}
