package localruntime

import (
	"encoding/json"
	"io"
	"log/slog"
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"
)

const (
	clientObservationMaxBodyBytes = 16 << 10
	clientObservationMaxString    = 512
	clientObservationMaxDetail    = 12
)

type clientObservationRequest struct {
	AgeMs         *int64            `json:"ageMs,omitempty"`
	CurrentPath   string            `json:"currentPath,omitempty"`
	Detail        map[string]string `json:"detail,omitempty"`
	Event         string            `json:"event"`
	FrontendBuild string            `json:"frontendBuild,omitempty"`
	NavType       string            `json:"navType,omitempty"`
	PreviousPath  string            `json:"previousPath,omitempty"`
	Reason        string            `json:"reason,omitempty"`
	ReloadID      string            `json:"reloadId,omitempty"`
	ReloadReason  string            `json:"reload_reason,omitempty"`
}

func newClientObservationHandler(logger *slog.Logger) gin.HandlerFunc {
	if logger == nil {
		logger = slog.Default()
	}
	return func(c *gin.Context) {
		if _, _, ok := requireSession(c); !ok {
			return
		}
		var request clientObservationRequest
		decoder := json.NewDecoder(io.LimitReader(c.Request.Body, clientObservationMaxBodyBytes))
		decoder.DisallowUnknownFields()
		if err := decoder.Decode(&request); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{
				"code":  "INVALID_REQUEST",
				"error": "client observation payload is invalid.",
			})
			return
		}
		reason := boundedClientObservationString(request.ReloadReason)
		if reason == "" {
			reason = boundedClientObservationString(request.Reason)
		}
		if boundedClientObservationString(request.Event) != "frontend_reload" || reason == "" {
			c.JSON(http.StatusBadRequest, gin.H{
				"code":  "INVALID_REQUEST",
				"error": "frontend_reload observation reason is required.",
			})
			return
		}

		attrs := []slog.Attr{
			slog.String("event", "frontend_reload"),
			slog.String("reload_reason", reason),
			slog.String("reload_id", boundedClientObservationString(request.ReloadID)),
			slog.String("previous_path", boundedClientObservationString(request.PreviousPath)),
			slog.String("current_path", boundedClientObservationString(request.CurrentPath)),
			slog.String("frontend_build", boundedClientObservationString(request.FrontendBuild)),
			slog.String("nav_type", boundedClientObservationString(request.NavType)),
		}
		if request.AgeMs != nil && *request.AgeMs >= 0 {
			attrs = append(attrs, slog.Int64("age_ms", *request.AgeMs))
		}
		if len(request.Detail) > 0 {
			attrs = append(attrs, slog.Any("detail", boundedClientObservationDetail(request.Detail)))
		}

		logger.LogAttrs(c.Request.Context(), slog.LevelInfo, "desktop frontend reload", attrs...)
		c.Status(http.StatusNoContent)
	}
}

func boundedClientObservationDetail(detail map[string]string) map[string]string {
	bounded := make(map[string]string, min(len(detail), clientObservationMaxDetail))
	count := 0
	for key, value := range detail {
		if count >= clientObservationMaxDetail {
			break
		}
		normalizedKey := boundedClientObservationString(key)
		if normalizedKey == "" {
			continue
		}
		bounded[normalizedKey] = boundedClientObservationString(value)
		count++
	}
	return bounded
}

func boundedClientObservationString(value string) string {
	trimmed := strings.TrimSpace(value)
	if len(trimmed) <= clientObservationMaxString {
		return trimmed
	}
	return trimmed[:clientObservationMaxString]
}
