// Package handlers — ext_api.go
//
// v0.29.1 CS1: Extension API routes. Starlark packages serve custom
// JSON endpoints via the existing Gin router. Mounted at
// /s/:slug/api/*path with JWT auth.
//
// Manifest format:
//
//	{
//	  "api_routes": [
//	    {"method": "GET",  "path": "/status"},
//	    {"method": "POST", "path": "/webhook"},
//	    {"method": "*",    "path": "/proxy/*"}
//	  ]
//	}
//
// Route matching:
//   - method: exact match (case-insensitive), or "*" for any method
//   - path: exact match, or trailing "*" for prefix match
//   - If api_routes is boolean true, all routes are forwarded
//
// Starlark contract:
//
//	def on_request(req):
//	    # req = {"method": "GET", "path": "/status", "headers": {...},
//	    #        "query": {...}, "body": "", "user_id": "..."}
//	    return {"status": 200, "body": '{"ok": true}'}
//
// Response dict fields:
//   - status: int (default 200)
//   - headers: dict (optional, string→string)
//   - body: string (default "")
package handlers

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"
	"go.starlark.net/starlark"

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

// ExtAPIHandler serves extension API routes via Starlark.
type ExtAPIHandler struct {
	stores store.Stores
	runner *sandbox.Runner
}

// NewExtAPIHandler creates the extension API handler.
func NewExtAPIHandler(stores store.Stores, runner *sandbox.Runner) *ExtAPIHandler {
	return &ExtAPIHandler{
		stores: stores,
		runner: runner,
	}
}

// Handle processes an incoming request to /s/:slug/api/*path.
// It loads the target package, validates it, matches the route against
// the manifest's api_routes, and dispatches to the Starlark on_request
// entry point.
func (h *ExtAPIHandler) Handle(c *gin.Context) {
	slug := c.Param("slug")
	rawPath := c.Param("path") // includes leading "/"

	if slug == "" {
		c.JSON(http.StatusNotFound, gin.H{"error": "extension not found"})
		return
	}

	// Gin *path captures include leading slash; normalize
	if rawPath == "" {
		rawPath = "/"
	}

	// ── 1. Load package ────────────────────────
	pkg, err := h.stores.Packages.Get(c.Request.Context(), slug)
	if err != nil || pkg == nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "extension not found"})
		return
	}

	// ── 2. Validate package state ──────────────
	if !pkg.Enabled {
		c.JSON(http.StatusNotFound, gin.H{"error": "extension is disabled"})
		return
	}
	if pkg.Status != models.PackageStatusActive {
		c.JSON(http.StatusForbidden, gin.H{"error": "extension is " + pkg.Status})
		return
	}
	if pkg.Tier != models.ExtTierStarlark {
		c.JSON(http.StatusBadRequest, gin.H{"error": "extension is not a starlark package"})
		return
	}

	// ── 3. Check permissions ──────────────────
	// The package needs at least one granted permission to be active.
	// api.http is NOT required here — a consumer package may delegate
	// HTTP to a library via lib.require(). The sandbox wires the http
	// module only when api.http is granted; without it the script simply
	// cannot call http.get() directly, which is correct for consumers.
	if h.stores.ExtPermissions == nil {
		c.JSON(http.StatusForbidden, gin.H{"error": "permission system unavailable"})
		return
	}
	granted, err := h.stores.ExtPermissions.GrantedForPackage(c.Request.Context(), pkg.ID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "permission check failed"})
		return
	}
	if len(granted) == 0 {
		c.JSON(http.StatusForbidden, gin.H{"error": "extension has no granted permissions"})
		return
	}

	// ── 4. Match route against manifest ────────
	if !matchAPIRoute(pkg.Manifest, c.Request.Method, rawPath) {
		c.JSON(http.StatusNotFound, gin.H{"error": "route not declared in extension manifest"})
		return
	}

	// ── 5. Build request dict ──────────────────
	reqDict, err := buildRequestDict(c, rawPath)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "failed to read request: " + err.Error()})
		return
	}

	// ── 6. Call on_request entry point ─────────
	rc := &sandbox.RunContext{UserID: getUserID(c)}
	val, output, err := h.runner.CallEntryPoint(
		c.Request.Context(), pkg, "on_request",
		starlark.Tuple{reqDict}, nil, rc,
	)
	if err != nil {
		log.Printf("⚠️ ext_api %s: on_request error: %v", pkg.ID, err)
		c.JSON(http.StatusInternalServerError, gin.H{"error": "extension error: " + err.Error()})
		return
	}

	if output != "" {
		log.Printf("  🔧 ext_api %s print: %s", pkg.ID, output)
	}

	// ── 7. Parse and write response ────────────
	writeStarlarkResponse(c, val)
}

// ─── Route Matching ─────────────────────────

// matchAPIRoute checks whether the incoming method+path is declared in
// the package manifest's api_routes field.
//
// Formats accepted:
//   - boolean true: all routes match
//   - []any of route dicts: {"method": "GET", "path": "/status"}
//     method "*" matches any method; path trailing "*" matches prefix
func matchAPIRoute(manifest map[string]any, method, path string) bool {
	raw, ok := manifest["api_routes"]
	if !ok {
		return false
	}

	// Boolean shorthand: true = all routes
	if b, ok := raw.(bool); ok {
		return b
	}

	// Array of route declarations
	routes, ok := raw.([]any)
	if !ok {
		return false
	}

	method = strings.ToUpper(method)

	for _, entry := range routes {
		route, ok := entry.(map[string]any)
		if !ok {
			continue
		}

		// Match method
		rm, _ := route["method"].(string)
		rm = strings.ToUpper(rm)
		if rm != "*" && rm != method {
			continue
		}

		// Match path
		rp, _ := route["path"].(string)
		if rp == "" {
			continue
		}

		if strings.HasSuffix(rp, "*") {
			// Prefix match: "/proxy/*" matches "/proxy/anything"
			prefix := strings.TrimSuffix(rp, "*")
			if strings.HasPrefix(path, prefix) {
				return true
			}
		} else {
			// Exact match
			if path == rp {
				return true
			}
		}
	}

	return false
}

// ─── Request Building ───────────────────────

// buildRequestDict creates a Starlark dict from the Gin context:
//
//	{
//	  "method":  "GET",
//	  "path":    "/status",
//	  "headers": {"content-type": "application/json", ...},
//	  "query":   {"page": "1", ...},
//	  "body":    "...",
//	  "user_id": "uuid",
//	}
func buildRequestDict(c *gin.Context, path string) (*starlark.Dict, error) {
	// Read body (capped at 1 MB for safety)
	body, err := io.ReadAll(io.LimitReader(c.Request.Body, 1<<20))
	if err != nil {
		return nil, fmt.Errorf("reading body: %w", err)
	}

	// Headers dict (lowercase keys, first value)
	hdrs := starlark.NewDict(len(c.Request.Header))
	for k, vals := range c.Request.Header {
		if len(vals) > 0 {
			_ = hdrs.SetKey(starlark.String(strings.ToLower(k)), starlark.String(vals[0]))
		}
	}

	// Query dict (first value per key)
	query := starlark.NewDict(len(c.Request.URL.Query()))
	for k, vals := range c.Request.URL.Query() {
		if len(vals) > 0 {
			_ = query.SetKey(starlark.String(k), starlark.String(vals[0]))
		}
	}

	d := starlark.NewDict(6)
	_ = d.SetKey(starlark.String("method"), starlark.String(c.Request.Method))
	_ = d.SetKey(starlark.String("path"), starlark.String(path))
	_ = d.SetKey(starlark.String("headers"), hdrs)
	_ = d.SetKey(starlark.String("query"), query)
	_ = d.SetKey(starlark.String("body"), starlark.String(string(body)))
	_ = d.SetKey(starlark.String("user_id"), starlark.String(getUserID(c)))
	return d, nil
}

// ─── Response Writing ───────────────────────

// writeStarlarkResponse parses the Starlark return value and writes
// the HTTP response. Expected: dict with "status", "headers", "body".
// Missing fields use defaults (200, no extra headers, empty body).
func writeStarlarkResponse(c *gin.Context, val starlark.Value) {
	if val == nil || val == starlark.None {
		c.Status(http.StatusNoContent)
		c.Writer.WriteHeaderNow()
		return
	}

	dict, ok := val.(*starlark.Dict)
	if !ok {
		c.JSON(http.StatusInternalServerError, gin.H{
			"error": fmt.Sprintf("on_request must return a dict, got %s", val.Type()),
		})
		return
	}

	// Status code
	status := http.StatusOK
	if sv, found, _ := dict.Get(starlark.String("status")); found {
		if i, err := starlark.AsInt32(sv); err == nil {
			status = i
		}
	}

	// Response headers
	if hv, found, _ := dict.Get(starlark.String("headers")); found {
		if hd, ok := hv.(*starlark.Dict); ok {
			for _, item := range hd.Items() {
				k, ok1 := starlark.AsString(item[0])
				v, ok2 := starlark.AsString(item[1])
				if ok1 && ok2 {
					c.Header(k, v)
				}
			}
		}
	}

	// Body
	body := ""
	if bv, found, _ := dict.Get(starlark.String("body")); found {
		body, _ = starlark.AsString(bv)
	}

	// Auto-detect content type if not set explicitly
	if c.Writer.Header().Get("Content-Type") == "" {
		if len(body) > 0 && (body[0] == '{' || body[0] == '[') {
			c.Header("Content-Type", "application/json")
		} else {
			c.Header("Content-Type", "text/plain; charset=utf-8")
		}
	}

	c.String(status, body)
}

// ─── Helpers ────────────────────────────────

func hasPermission(granted []string, perm string) bool {
	for _, p := range granted {
		if p == perm {
			return true
		}
	}
	return false
}

// HasAPIRoutes checks whether a package manifest declares API routes.
// Used by startup discovery and admin UI to identify API-capable packages.
func HasAPIRoutes(manifest map[string]any) bool {
	raw, ok := manifest["api_routes"]
	if !ok {
		return false
	}
	if b, ok := raw.(bool); ok {
		return b
	}
	if routes, ok := raw.([]any); ok {
		return len(routes) > 0
	}
	return false
}
