package triggers

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"io"
	"log"
	"net/http"
	"time"

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

	"switchboard-core/models"
)

// HandleWebhook is the Gin handler for inbound webhook triggers.
// Route: POST/GET /api/v1/hooks/:package_id/:slug
func (e *Engine) HandleWebhook(c *gin.Context) {
	packageID := c.Param("package_id")
	slug := c.Param("slug")

	if e.stores.Triggers == nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "triggers not available"})
		return
	}

	// Resolve trigger
	trigger, err := e.stores.Triggers.GetWebhook(c.Request.Context(), packageID, slug)
	if err != nil {
		log.Printf("  ⚠️  webhook: lookup error %s/%s: %v", packageID, slug, err)
		c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
		return
	}
	if trigger == nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "webhook not found"})
		return
	}
	if !trigger.Enabled {
		c.JSON(http.StatusNotFound, gin.H{"error": "webhook disabled"})
		return
	}

	// Read body
	body, err := io.ReadAll(io.LimitReader(c.Request.Body, 1<<20)) // 1MB limit
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "failed to read body"})
		return
	}

	// Verify HMAC signature if secret is set
	if trigger.Secret != "" {
		sig := c.GetHeader("X-Switchboard-Signature")
		if !verifyHMAC(body, trigger.Secret, sig) {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid signature"})
			return
		}
	}

	// Load package
	pkg, err := e.stores.Packages.Get(c.Request.Context(), packageID)
	if err != nil || pkg == nil || pkg.Status != models.PackageStatusActive {
		c.JSON(http.StatusNotFound, gin.H{"error": "package not available"})
		return
	}

	// Check permission
	if !e.hasPermission(c.Request.Context(), packageID, models.ExtPermTriggersRegister) {
		c.JSON(http.StatusForbidden, gin.H{"error": "triggers.register permission not granted"})
		return
	}

	// Build Starlark context dict
	ctxDict := starlark.NewDict(8)
	_ = ctxDict.SetKey(starlark.String("trigger_type"), starlark.String("webhook"))
	_ = ctxDict.SetKey(starlark.String("trigger_id"), starlark.String(trigger.ID))
	_ = ctxDict.SetKey(starlark.String("method"), starlark.String(c.Request.Method))
	_ = ctxDict.SetKey(starlark.String("path"), starlark.String(c.Request.URL.Path))
	_ = ctxDict.SetKey(starlark.String("body"), starlark.String(string(body)))

	// Headers as dict
	headers := starlark.NewDict(len(c.Request.Header))
	for k, v := range c.Request.Header {
		if len(v) > 0 {
			_ = headers.SetKey(starlark.String(k), starlark.String(v[0]))
		}
	}
	_ = ctxDict.SetKey(starlark.String("headers"), headers)

	// Query params as dict
	query := starlark.NewDict(len(c.Request.URL.Query()))
	for k, v := range c.Request.URL.Query() {
		if len(v) > 0 {
			_ = query.SetKey(starlark.String(k), starlark.String(v[0]))
		}
	}
	_ = ctxDict.SetKey(starlark.String("query"), query)

	// Fire synchronously — return result as HTTP response
	start := time.Now()
	val, output, callErr := e.runner.CallEntryPoint(c.Request.Context(), pkg, trigger.EntryPoint,
		starlark.Tuple{ctxDict}, nil, nil)

	duration := int(time.Since(start).Milliseconds())
	errStr := ""
	if callErr != nil {
		errStr = callErr.Error()
		log.Printf("  ⚠️  trigger[webhook] %s/%s error: %v", packageID, slug, callErr)
	}

	// Update fire state
	_ = e.stores.Triggers.UpdateFireState(c.Request.Context(), trigger.ID, start, errStr)

	// Log execution
	e.logExecution(trigger.ID, "", start, duration, callErr == nil, errStr, output)
	e.publishEvent("trigger.fired", trigger.ID)

	if callErr != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "handler error", "detail": errStr})
		return
	}

	// Return Starlark result
	response := parseWebhookResponse(val)
	c.JSON(response.status, response.body)
}

// webhookResponse is the parsed response from a Starlark webhook handler.
type webhookResponse struct {
	status int
	body   any
}

// parseWebhookResponse extracts HTTP status and body from the Starlark return value.
// Returns {status: X, body: Y} or default 200 OK.
func parseWebhookResponse(val starlark.Value) webhookResponse {
	if val == nil || val == starlark.None {
		return webhookResponse{status: 200, body: gin.H{"ok": true}}
	}

	// If it's a dict with "status" and/or "body" keys, use them
	if d, ok := val.(*starlark.Dict); ok {
		resp := webhookResponse{status: 200}

		if statusVal, found, _ := d.Get(starlark.String("status")); found {
			if s, ok := statusVal.(starlark.Int); ok {
				if i, ok := s.Int64(); ok {
					resp.status = int(i)
				}
			}
		}

		if bodyVal, found, _ := d.Get(starlark.String("body")); found {
			resp.body = starlarkToGo(bodyVal)
		} else {
			resp.body = gin.H{"ok": true}
		}

		return resp
	}

	// String return → plain text body
	if s, ok := val.(starlark.String); ok {
		return webhookResponse{status: 200, body: gin.H{"result": string(s)}}
	}

	return webhookResponse{status: 200, body: gin.H{"ok": true}}
}

// starlarkToGo converts a Starlark value to a Go value (for JSON serialization).
func starlarkToGo(v starlark.Value) any {
	switch val := v.(type) {
	case starlark.NoneType:
		return nil
	case starlark.Bool:
		return bool(val)
	case starlark.Int:
		if i, ok := val.Int64(); ok {
			return i
		}
		return val.String()
	case starlark.Float:
		return float64(val)
	case starlark.String:
		return string(val)
	case *starlark.List:
		result := make([]any, val.Len())
		for i := 0; i < val.Len(); i++ {
			result[i] = starlarkToGo(val.Index(i))
		}
		return result
	case *starlark.Dict:
		result := make(map[string]any, val.Len())
		for _, item := range val.Items() {
			if k, ok := item[0].(starlark.String); ok {
				result[string(k)] = starlarkToGo(item[1])
			}
		}
		return result
	default:
		return v.String()
	}
}

// verifyHMAC checks the HMAC-SHA256 signature of a webhook payload.
func verifyHMAC(body []byte, secret, signature string) bool {
	if signature == "" {
		return false
	}
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write(body)
	expected := hex.EncodeToString(mac.Sum(nil))
	return hmac.Equal([]byte(expected), []byte(signature))
}
