package middleware

import (
	"net/http"
	"runtime/debug"
	"strings"

	"github.com/TaruLab/keluargamart-backend/pkg/httpresponse"
	"github.com/TaruLab/keluargamart-backend/pkg/logger"
)

// maxPanicStackBytes bounds what one panic can write to the log.
//
// A recovered panic in this codebase is nearly always a nil dereference, and a
// nil dereference with no stack is unactionable: the message is always the same
// string, so a log search tells you that *something* dereferenced nil and
// nothing about where. Logging the full runtime stack — which can run to tens of
// kilobytes for a deep handler chain — would let a single bad request flood a
// log file, so the tail is kept: the top of the stack is the panic site and the
// frames immediately under it are the callers that led there.
const maxPanicStackBytes = 8 << 10

// Recovery catches panics, logs the stack, and returns a generic 500 envelope.
func Recovery(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if rec := recover(); rec != nil {
				l := logger.FromContext(r.Context())
				l.Error("panic_recovered",
					"panic", rec,
					"method", r.Method,
					// The path is included because the panic value alone rarely
					// identifies the request: two different routes can both
					// dereference the same nil dependency, and knowing which
					// route is needed to decide whether the report is
					// reproducible. Query string is deliberately excluded — it
					// can carry values that do not belong in a log.
					"path", r.URL.Path,
					"stack", panicStack(),
				)
				httpresponse.Error(w, http.StatusInternalServerError,
					RequestIDFrom(r.Context()),
					httpresponse.CodeInternal, httpresponse.MsgInternal, nil)
			}
		}()
		next.ServeHTTP(w, r)
	})
}

// panicStack renders the current goroutine's stack, trimmed to
// maxPanicStackBytes.
//
// runtime/debug.Stack is used rather than runtime.Stack directly because it is
// the same call the runtime documents for exactly this situation, and it
// re-allocates to fit instead of silently truncating into a fixed buffer: a
// hand-rolled runtime.Stack(buf, false) returned an EMPTY string for a nil
// dereference in a deep handler chain, which is the one case the log exists to
// explain.
func panicStack() string {
	stack := string(debug.Stack())
	if len(stack) > maxPanicStackBytes {
		// Keep the tail as well as the head. The head is runtime/debug.Stack
		// itself and the recover frame; the frames naming the panicking
		// package sit further down, so a pure prefix cut can keep only the
		// frames that are always the same and throw away the useful ones.
		half := maxPanicStackBytes / 2
		stack = stack[:half] + "\n... (truncated) ...\n" + stack[len(stack)-half:]
	}
	return strings.TrimSpace(stack)
}
