// Package dialecttransport is the platform's gin transport binder for gin-free
// dialects (issue #1743). It binds a dialect's declarative route Manifest
// (github.com/pingcap-inc/nexuscrm-api-host/dialect) onto gin: it applies the host's base
// data-API middleware and the route's declarative permission Gate, builds a neutral
// dialect.Request from the live *gin.Context, calls the Handler, and writes or
// projects the dialect.Response.
//
// gin lives HERE, in the api-host transport module — the dialect contract stays
// gin-free, so an external app pack never imports gin. This is the Stage-2 split
// decided in docs/refactor/l0-l1-boundary (seam-1 D-2): gin-free codec in
// shared, transport binder in api-host.
//
// It returns neutral dialect.Mount records so the host has one read path for
// mounted version discovery without learning a product dialect's route grammar.
package dialecttransport

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/textproto"
	"path"
	"sort"
	"strconv"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/pingcap-inc/nexuscrm-api-host/dialect"
	"github.com/pingcap-inc/nexuscrm-api-host/hostruntime"
	apihostmiddleware "github.com/pingcap-inc/nexuscrm-api-host/middleware"
)

// UserExtractor pulls the authenticated user from the live gin context as the
// neutral shared type. The host supplies its existing extraction (one read path);
// the binder never re-implements it. gin-typed extraction is correct HERE (backend
// transport), as opposed to the gin-free dialect contract.
type UserExtractor func(*gin.Context) *hostruntime.UserSession

// PermissionResolver reports whether the host's active permission catalog provides
// perm. It is a host-provided capability the binder BINDS each gate permission
// reference against — the same dependency-injection shape as UserExtractor and the
// pack catalog: a dialect references a host permission by neutral key, and the host,
// not the binder, owns the catalog those keys resolve against.
//
// The binder calls it at mount for every gate permission a dialect declares and
// fails closed (panics) on any the host does not provide — so a typo'd or
// missing-capability gate (e.g. dialect.Permission("ViewSetp")) can never mount.
// Without this resolution such a route would mount silently: because the runtime
// superuser short-circuit in the host permission gate precedes the permission-map
// lookup, an unknown permission key is held by NObody, so the route would be
// reachable only by superusers — a malformed surface, not the intended gate.
//
// The host wires it from its runtime permission catalog
// (PermissionService.ListSystemPermissions → the active rows' APIName). The snapshot
// MUST be taken after the active packs' permission rows are loaded, or a
// legitimately-provided permission would be misjudged unknown and fail the mount
// closed.
type PermissionResolver func(dialect.Permission) bool

// PermissionGateProvider turns a validated declarative gate into host middleware.
// The backend supplies its permission middleware as an adapter; standalone
// api-host tests can use the neutral default provider below.
type PermissionGateProvider interface {
	RequireAll([]dialect.Permission) []gin.HandlerFunc
	RequireAny([]dialect.Permission) gin.HandlerFunc
}

// RateLimitSubjectExtractor reads the canonical per-app rate-limit subject from
// the live gin context after the host scope middleware has accepted a scoped
// Connected App session.
type RateLimitSubjectExtractor func(*gin.Context) string

type MountOption func(*mountOptions)

type mountOptions struct {
	permissionGates         PermissionGateProvider
	rateLimitSubjectFromCtx RateLimitSubjectExtractor
	credentialStack         CredentialStackProvider
	credentialPolicy        RouteCredentialPolicyResolver
}

func defaultMountOptions() mountOptions {
	return mountOptions{}
}

func WithPermissionGateProvider(provider PermissionGateProvider) MountOption {
	return func(opts *mountOptions) {
		if provider != nil {
			opts.permissionGates = provider
		}
	}
}

func WithPermissionSessionContext(sessionContext apihostmiddleware.PermissionSessionContext) MountOption {
	return func(opts *mountOptions) {
		if sessionContext != nil {
			opts.permissionGates = systemPermissionGateProvider{sessionContext: sessionContext}
		}
	}
}

func WithRateLimitSubjectExtractor(extractor RateLimitSubjectExtractor) MountOption {
	return func(opts *mountOptions) {
		opts.rateLimitSubjectFromCtx = extractor
	}
}

// Mount binds every active dialect's Manifest onto router and returns one
// dialect.Mount per mounted version. It fails closed (panics) at mount on a nil
// router/extractor/permission-resolver, an empty or duplicate dialect Name (the
// unique registry key), a nil error projector, a malformed BasePath/version, a
// malformed/unknown Gate, a permission gate without a host gate provider, a gate
// permission the host does not provide, a malformed or duplicate route, or a path
// collision within this binder — boot-time, never a silent partial surface. A
// dialect whose OwningPack is known-but-inactive OR absent from the host pack catalog
// is SKIPPED (not panicked): an absent OwningPack is a not-yet-installed source-backed
// pack, the documented fail-OPEN delta.
//
// A manifest references host-provided capabilities by neutral key (gate permissions,
// the OwningPack). The binder resolves gate permissions against the host's permission
// resolver and fails CLOSED on any it does not provide (a malformed surface — a
// superuser-only route for an unknown permission). It resolves OwningPack against the
// host's pack catalog: packCatalog is the host's FULL 3-state known-pack set keyed to
// its active flag — a name present with TRUE mounts (known & active); a name present
// with FALSE is a known-but-inactive pack (installing/removing/failed) and is SKIPPED;
// a name ABSENT is treated as a not-yet-installed source-backed app pack and is also
// SKIPPED (the binary has no authoritative source-backed name list to validate an
// app-pack OwningPack against — a typo therefore fails OPEN to skip, the documented
// delta). The binder still fails CLOSED on malformed DECLARATIONS (nil projector,
// invalid base/version, duplicate Name, path collision, unknown gate permission).
//
// The binder enforces collisions among all registered gin-free dialects.
func Mount(
	router *gin.Engine,
	dialects []dialect.Dialect,
	deps dialect.Deps,
	base []gin.HandlerFunc,
	userFromCtx UserExtractor,
	permKnown PermissionResolver,
	packCatalog map[string]bool,
	opts ...MountOption,
) []dialect.Mount {
	if router == nil {
		panic("dialecttransport: nil router")
	}
	if userFromCtx == nil {
		panic("dialecttransport: nil user extractor")
	}
	if permKnown == nil {
		// Fail closed: with no catalog to resolve gate permissions against, the binder
		// cannot tell a real capability from a typo, so it refuses to mount at all
		// rather than wave every permission key through unvalidated.
		panic("dialecttransport: nil permission resolver")
	}
	if deps.Dispatch == nil {
		deps.Dispatch = routerDispatcher(router)
	}
	options := defaultMountOptions()
	for _, opt := range opts {
		if opt != nil {
			opt(&options)
		}
	}
	if options.credentialPolicy != nil && options.credentialStack == nil {
		// Fail closed: declared policies that nothing resolves to a stack would be
		// silently ignored, which is the downgrade this contract prevents.
		panic("dialecttransport: a route credential policy resolver requires a credential stack provider")
	}

	sorted := append([]dialect.Dialect(nil), dialects...)
	sort.Slice(sorted, func(i, j int) bool { return sorted[i].Name() < sorted[j].Name() })

	seen := map[string]string{}
	seenNames := map[string]bool{}
	var mounted []dialect.Mount
	for _, d := range sorted {
		// Name is the unique registry key. Enforce it across the
		// whole list — including inactive dialects — exactly as the A1 registry's
		// Register does, so a duplicate/empty key fails closed at boot rather than
		// producing ambiguous Mount identities a host/discovery would key on.
		name := strings.TrimSpace(d.Name())
		if name == "" {
			panic("dialecttransport: a dialect declares an empty Name (Name is the unique registry key)")
		}
		if seenNames[name] {
			panic(fmt.Sprintf("dialecttransport: duplicate dialect name %q (Name is the unique registry key)", name))
		}
		seenNames[name] = true

		isActive, known := resolvePackActive(d, packCatalog)
		if !known {
			// Absent OwningPack = a not-yet-installed source-backed app pack: skip.
			// The catalog reads only external authorities (DB rows + the platform
			// list), so a pack the binary has not yet installed is legitimately absent
			// — it is NOT a malformed declaration. A known-but-inactive pack (installing/
			// removing/failed) appears present with a false flag and also skips below;
			// the (known) distinction is kept truthful so a future caller can still tell
			// not-yet-installed from in-flight. DOCUMENTED DELTA: an app-pack OwningPack
			// typo also fails OPEN to skip — the binary has no authoritative source-backed
			// name list to validate against (enumerating one would be an L1-in-L0 leak and
			// a second SSoT vs the DB row), so the dialect's route surface silently vanishes
			// until that pack installs. Malformed DECLARATIONS still fail closed (panics
			// below); only legitimately-uninstalled packs skip.
			continue
		}
		if !isActive {
			continue
		}
		basePath := strings.TrimSpace(d.BasePath())
		if basePath != "" && (!strings.HasPrefix(basePath, "/") || basePath != path.Clean(basePath)) {
			panic(fmt.Sprintf("dialecttransport: dialect %q declares invalid BasePath %q (must be empty or a clean absolute path)", d.Name(), d.BasePath()))
		}
		projector := d.ErrorProjector()
		if projector == nil {
			panic(fmt.Sprintf("dialecttransport: dialect %q declares nil error projector", d.Name()))
		}
		internalProjector := d.InternalErrorProjector()
		if internalProjector == nil {
			panic(fmt.Sprintf("dialecttransport: dialect %q declares nil internal error projector", d.Name()))
		}
		manifest := d.Manifest()
		routeKeys := make(map[string]bool, len(manifest))
		for i := range manifest {
			validateRoute(d.Name(), manifest[i], permKnown, options.permissionGates != nil)
			key := manifest[i].Method + " " + manifest[i].Path
			if routeKeys[key] {
				panic(fmt.Sprintf("dialecttransport: dialect %q declares duplicate route %s", d.Name(), key))
			}
			routeKeys[key] = true
		}
		for _, rawVersion := range d.Versions() {
			version := strings.TrimSpace(rawVersion)
			if version == "" || strings.ContainsRune(version, '/') || version == "." || version == ".." {
				panic(fmt.Sprintf("dialecttransport: dialect %q declares invalid version %q (must be a single clean segment)", d.Name(), rawVersion))
			}
			full := path.Join("/", basePath, version)
			if owner, exists := seen[full]; exists {
				panic(fmt.Sprintf("dialecttransport: dialect %q path %q conflicts with dialect %q", d.Name(), full, owner))
			}
			seen[full] = d.Name()

			// Group middleware order: version param, then the host's base data-API
			// stack (auth/scope/rate-limit/permLoader); per-route the declarative
			// gate, then the handler adapter.
			group := router.Group(full, versionParam(version))
			if options.credentialStack == nil {
				// Unchanged path: one base stack for the whole version group.
				group.Use(base...)
			}
			for _, r := range manifest {
				var handlers []gin.HandlerFunc
				if options.credentialStack != nil {
					// Per-route admission selected from the route's declared policy,
					// so a browser-capable route no longer has to share one
					// unlabelled stack with every other route in the version.
					subject := fmt.Sprintf("dialect %q route %s %s", d.Name(), r.Method, r.Path)
					policy := dialect.CredentialProgrammaticOnly
					if options.credentialPolicy != nil {
						policy = options.credentialPolicy(d.Name(), r)
						if !policy.Valid() {
							// An unrecognized policy must not fall back to the zero
							// value: that would silently downgrade an intended browser
							// policy to programmatic-only.
							panic(fmt.Sprintf("dialecttransport: %s resolves to unknown credential policy %s", subject, policy))
						}
					}
					handlers = append(handlers, resolveCredentialStack(options.credentialStack, policy, subject)...)
				}
				handlers = append(handlers, gateHandlers(r.Gate, options.permissionGates)...)
				handlers = append(handlers, adapt(r, deps, projector, internalProjector, version, userFromCtx, options.rateLimitSubjectFromCtx))
				group.Handle(r.Method, r.Path, handlers...)
			}
			mounted = append(mounted, dialect.Mount{Dialect: d.Name(), BasePath: basePath, Version: version, Path: full})
		}
	}
	return mounted
}

// routerDispatcher is the host implementation of dialect.Dispatcher.
// It keeps the route-composition primitive in L0 while hiding gin from L1 modules:
// a dialect passes a method/path/headers/body, and the binder routes it through the
// same mounted gin engine and middleware stack as an external request.
func routerDispatcher(router *gin.Engine) dialect.Dispatcher {
	return func(_ context.Context, dispatchReq dialect.DispatchRequest) (*dialect.DispatchResponse, error) {
		method := strings.TrimSpace(dispatchReq.Method)
		if method == "" {
			method = http.MethodGet
		}

		var body io.Reader
		if len(dispatchReq.Body) > 0 {
			body = bytes.NewReader(dispatchReq.Body)
		}
		// Match the deleted composite handlers' isolation semantics: each internal
		// subrequest starts with a fresh context, then re-enters the router so the
		// middleware stack installs request-scoped state from scratch. Do not thread
		// the parent request context here; values such as permission/RLS caches must
		// not leak from one sequential composite subrequest into the next.
		req, err := http.NewRequest(method, dispatchReq.Path, body)
		if err != nil {
			return nil, err
		}
		headerKeys := make([]string, 0, len(dispatchReq.Header))
		for k := range dispatchReq.Header {
			if strings.TrimSpace(k) == "" {
				continue
			}
			headerKeys = append(headerKeys, k)
		}
		sort.Strings(headerKeys)
		for _, k := range headerKeys {
			canonical := textproto.CanonicalMIMEHeaderKey(strings.TrimSpace(k))
			if canonical == "" {
				continue
			}
			req.Header.Set(canonical, dispatchReq.Header[k])
		}

		recorder := newDispatchResponseRecorder()
		router.ServeHTTP(recorder, req)
		return &dialect.DispatchResponse{
			Status: recorder.status,
			Header: firstHeaderValues(recorder.Header()),
			Body:   recorder.body.Bytes(),
		}, nil
	}
}

type dispatchResponseRecorder struct {
	header http.Header
	body   bytes.Buffer
	status int
}

func newDispatchResponseRecorder() *dispatchResponseRecorder {
	return &dispatchResponseRecorder{
		header: make(http.Header),
		status: http.StatusOK,
	}
}

func (w *dispatchResponseRecorder) Header() http.Header { return w.header }

func (w *dispatchResponseRecorder) Write(b []byte) (int, error) {
	if w.status == 0 {
		w.status = http.StatusOK
	}
	return w.body.Write(b)
}

func (w *dispatchResponseRecorder) WriteHeader(status int) {
	w.status = status
}

func firstHeaderValues(h http.Header) map[string]string {
	res := make(map[string]string, len(h))
	for k, values := range h {
		if len(values) > 0 {
			res[k] = values[0]
		}
	}
	return res
}

// methodHasBody reports whether an HTTP method conventionally carries a request
// body. The binder skips draining the body for the bodyless read methods (GET/HEAD/
// …), so a read-heavy data API (describe/query/retrieve) never pays a body
// read+alloc; body-bearing handlers still receive the exact bytes.
func methodHasBody(method string) bool {
	switch method {
	case http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete:
		return true
	default:
		return false
	}
}

// resolvePackActive resolves a dialect's OwningPack reference against the host's pack
// catalog, distinguishing the three cases the caller must treat differently:
//   - owner == "": no pack dependency, always mounts (active=true, known=true).
//   - owner names a pack present in the catalog: active reflects its flag (known=true).
//     A present-but-false pack (installing/removing/failed) is known-but-inactive; the
//     caller skips it.
//   - owner is ABSENT from the catalog: a not-yet-installed source-backed app pack (or
//     a typo) — returns (active=false, known=false). The caller SKIPS it rather than
//     panicking: the binary has no authoritative source-backed name list to validate an
//     app-pack OwningPack against, so an absent reference cannot be told apart from an
//     uninstalled pack and must fail OPEN to skip (the documented delta). The (known)
//     flag stays false so the caller still KNOWS the pack was absent vs present-inactive.
func resolvePackActive(d dialect.Dialect, packCatalog map[string]bool) (active, known bool) {
	owner := strings.TrimSpace(d.OwningPack())
	if owner == "" {
		return true, true
	}
	for name, on := range packCatalog {
		if strings.EqualFold(strings.TrimSpace(name), owner) {
			return on, true
		}
	}
	return false, false
}

// versionParam exposes the mounted version segment to handlers as the "version"
// request param, matching the legacy door's convention so version-aware handlers
// behave identically.
func versionParam(v string) gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Params = append(c.Params, gin.Param{Key: "version", Value: v})
		c.Next()
	}
}

// validHTTPMethod reports whether m is an HTTP method the binder will mount.
func validHTTPMethod(m string) bool {
	switch m {
	case http.MethodGet, http.MethodHead, http.MethodPost, http.MethodPut,
		http.MethodPatch, http.MethodDelete, http.MethodOptions:
		return true
	default:
		return false
	}
}

// validateRoute fails closed at mount on ANY malformed route declaration, so a
// public dialect contract never lets a bad declaration slip to request time. It
// rejects an invalid method, a non-absolute path, a nil handler, and a malformed
// permission gate — including the silent fail-open holes: permissions declared under
// GateNone (a forgotten Mode would otherwise mount the route UNGATED), an empty
// gate, and a gate permission the host does not provide (an unknown/typo'd capability
// would otherwise mount a route reachable only by superusers — see PermissionResolver).
func validateRoute(dialectName string, r dialect.Route, permKnown PermissionResolver, hasPermissionGateProvider bool) {
	if !validHTTPMethod(r.Method) {
		panic(fmt.Sprintf("dialecttransport: dialect %q route %q declares invalid HTTP method %q", dialectName, r.Path, r.Method))
	}
	if !strings.HasPrefix(r.Path, "/") {
		panic(fmt.Sprintf("dialecttransport: dialect %q route %s declares invalid path %q (must be absolute, e.g. \"/sobjects/:id\")", dialectName, r.Method, r.Path))
	}
	for _, seg := range strings.Split(r.Path, "/") {
		if seg == "." || seg == ".." {
			// Fail closed: a "." / ".." segment lets gin's path.Clean (run when joining
			// the route onto <BasePath>/<version>) escape the dialect's mount root, so a
			// route could mount OUTSIDE its declared namespace while Mount still reports
			// it under the version root — defeating collision/discovery.
			panic(fmt.Sprintf("dialecttransport: dialect %q route %s declares path %q with a %q segment (would escape the mount root)", dialectName, r.Method, r.Path, seg))
		}
	}
	if r.Handler == nil {
		panic(fmt.Sprintf("dialecttransport: dialect %q route %s %s declares a nil handler", dialectName, r.Method, r.Path))
	}
	switch r.Gate.Mode {
	case dialect.GateNone:
		if len(r.Gate.Permissions) > 0 {
			// Fail closed on the silent fail-open: a route that declared
			// permissions but (probably) forgot Mode must NOT mount ungated.
			panic(fmt.Sprintf("dialecttransport: dialect %q route %s %s declares permissions under GateNone (set Mode to GateAll/GateAny, or clear Permissions)", dialectName, r.Method, r.Path))
		}
	case dialect.GateAll, dialect.GateAny:
		if !hasPermissionGateProvider {
			panic(fmt.Sprintf("dialecttransport: dialect %q route %s %s declares a permission gate but no permission gate provider is configured", dialectName, r.Method, r.Path))
		}
		if len(r.Gate.Permissions) == 0 {
			panic(fmt.Sprintf("dialecttransport: dialect %q route %s %s declares a permission gate with no permissions", dialectName, r.Method, r.Path))
		}
		for _, p := range r.Gate.Permissions {
			if !permKnown(p) {
				// Resolve the gate's host-capability reference: a permission the host
				// does not provide (typo, removed/renamed capability, inactive pack's
				// permission) would mount a route only superusers can reach instead of
				// its intended holders. Fail closed before any route is registered.
				panic(fmt.Sprintf("dialecttransport: dialect %q route %s %s gates on permission %q the host does not provide (unknown/typo'd capability)", dialectName, r.Method, r.Path, p))
			}
		}
	default:
		panic(fmt.Sprintf("dialecttransport: dialect %q route %s %s declares unknown gate mode %d", dialectName, r.Method, r.Path, r.Gate.Mode))
	}
	switch r.BodyMode {
	case dialect.BodyBuffered:
		if r.BodyLimitBytes < 0 {
			panic(fmt.Sprintf("dialecttransport: dialect %q route %s %s declares negative BodyLimitBytes %d", dialectName, r.Method, r.Path, r.BodyLimitBytes))
		}
		if r.BodyLimitBytes > maxNeutralRequestBody {
			panic(fmt.Sprintf("dialecttransport: dialect %q route %s %s declares buffered BodyLimitBytes %d above the %d byte cap; use BodyStream for larger bodies", dialectName, r.Method, r.Path, r.BodyLimitBytes, maxNeutralRequestBody))
		}
	case dialect.BodyStream:
		if !methodHasBody(r.Method) {
			panic(fmt.Sprintf("dialecttransport: dialect %q route %s %s declares BodyStream on a bodyless method", dialectName, r.Method, r.Path))
		}
		if r.BodyLimitBytes <= 0 {
			panic(fmt.Sprintf("dialecttransport: dialect %q route %s %s declares BodyStream without a positive BodyLimitBytes", dialectName, r.Method, r.Path))
		}
	default:
		panic(fmt.Sprintf("dialecttransport: dialect %q route %s %s declares unknown body mode %d", dialectName, r.Method, r.Path, r.BodyMode))
	}
}

// gateHandlers translates a declarative Gate into the host's permission middleware.
func gateHandlers(g dialect.Gate, provider PermissionGateProvider) []gin.HandlerFunc {
	switch g.Mode {
	case dialect.GateAll:
		return provider.RequireAll(g.Permissions)
	case dialect.GateAny:
		return []gin.HandlerFunc{provider.RequireAny(g.Permissions)}
	default: // GateNone
		return nil
	}
}

type systemPermissionGateProvider struct {
	sessionContext apihostmiddleware.PermissionSessionContext
}

func (p systemPermissionGateProvider) RequireAll(perms []dialect.Permission) []gin.HandlerFunc {
	hs := make([]gin.HandlerFunc, 0, len(perms))
	for _, perm := range perms {
		hs = append(hs, apihostmiddleware.RequirePermissionWithSessionContext(p.sessionContext, string(perm)))
	}
	return hs
}

func (p systemPermissionGateProvider) RequireAny(perms []dialect.Permission) gin.HandlerFunc {
	keys := make([]string, 0, len(perms))
	for _, perm := range perms {
		keys = append(keys, string(perm))
	}
	return apihostmiddleware.RequireAnyPermissionWithSessionContext(p.sessionContext, keys...)
}

// maxNeutralRequestBody bounds how many body bytes the binder buffers for any
// gin-free route, so a caller cannot force unbounded transport-layer buffering
// (e.g. a small valid body trailed by arbitrarily large bytes). 25 MiB matches the
// platform's file-route cap (rest.maxFileUploadSize) and comfortably fits a
// 200-record composite/tree write. Buffered routes may only LOWER this in-memory
// cap with BodyLimitBytes; larger body surfaces must use BodyStream.
const maxNeutralRequestBody = 25 << 20 // 25 MiB

// maxNeutralResponseStreamBytes keeps the first raw-response widening bounded.
// It deliberately matches the current file upload/download cap; larger binary
// surfaces need an explicit transport widening that can fail closed on late copy
// errors without buffering before headers are written.
const maxNeutralResponseStreamBytes = maxNeutralRequestBody

func bufferedBodyLimit(r dialect.Route) int64 {
	if r.BodyLimitBytes > 0 {
		return r.BodyLimitBytes
	}
	return maxNeutralRequestBody
}

type neutralBodyStream struct {
	reader io.Reader
}

func (s neutralBodyStream) Read(p []byte) (int, error) {
	n, err := s.reader.Read(p)
	if err == nil || errors.Is(err, io.EOF) {
		return n, err
	}
	var maxErr *http.MaxBytesError
	if errors.As(err, &maxErr) {
		return n, &dialect.BodyStreamError{Kind: dialect.BodyStreamTooLarge, Cause: err}
	}
	return n, &dialect.BodyStreamError{Kind: dialect.BodyStreamReadFailed, Cause: err}
}

func bodyLimitLabel(limit int64) string {
	const mib = 1 << 20
	if limit > 0 && limit%mib == 0 {
		return fmt.Sprintf("%d MB", limit/mib)
	}
	return fmt.Sprintf("%d bytes", limit)
}

func renderInternalError(c *gin.Context, req *dialect.Request, projector dialect.InternalErrorProjector) {
	status, payload := projector(req)
	if status == 0 {
		status = http.StatusInternalServerError
	}
	c.JSON(status, payload)
}

// renderWireError projects a binder-originated neutral error (a transport-level
// body read failure or oversize) through the route's dialect projector, failing
// closed to the dialect's canonical generic 500 if the dialect declines. This is
// the single rule for every binder-side error: L1 owns the wire shape; L0 never
// emits an app-specific envelope, and an outward dialect route never emits an
// unknown wire shape.
func renderWireError(c *gin.Context, req *dialect.Request, projector dialect.Projector, internalProjector dialect.InternalErrorProjector, wireErr *dialect.DomainWireError) {
	if status, payload, projected := projector(req, wireErr); projected {
		c.JSON(status, payload)
		return
	}
	renderInternalError(c, req, internalProjector)
}

// adapt wraps a gin-free Handler as a gin.HandlerFunc: it builds the neutral
// Request from the live context, calls the Handler, and writes/projects the result.
func adapt(r dialect.Route, deps dialect.Deps, projector dialect.Projector, internalProjector dialect.InternalErrorProjector, version string, userFromCtx UserExtractor, rateLimitSubjectFromCtx RateLimitSubjectExtractor) gin.HandlerFunc {
	return func(c *gin.Context) {
		params := make(map[string]string, len(c.Params))
		for _, p := range c.Params {
			params[p.Key] = p.Value
		}
		req := &dialect.Request{
			Version:       version,
			User:          userFromCtx(c),
			Params:        params,
			Query:         map[string][]string(c.Request.URL.Query()),
			Header:        map[string][]string(c.Request.Header),
			ContentLength: c.Request.ContentLength,
		}
		if rateLimitSubjectFromCtx != nil {
			req.RateLimitSubjectID = strings.TrimSpace(rateLimitSubjectFromCtx(c))
		}

		if c.Request != nil && c.Request.Body != nil && methodHasBody(c.Request.Method) {
			if r.BodyMode == dialect.BodyStream {
				if c.Request.ContentLength > r.BodyLimitBytes {
					renderWireError(c, req, projector, internalProjector, dialect.NewRequestTooLargeError(
						fmt.Sprintf("Request body exceeds the maximum allowed size of %s", bodyLimitLabel(r.BodyLimitBytes))))
					return
				}
				c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, r.BodyLimitBytes)
				req.BodyStream = neutralBodyStream{reader: c.Request.Body}
			} else {
				// Bound the body read so a caller cannot force unbounded transport-layer
				// buffering — e.g. a small valid JSON value followed by arbitrarily large
				// trailing bytes that a lenient decoder ignores only AFTER the binder has
				// buffered them. Read at most one byte past the cap to detect oversize.
				// A read failure (client disconnect, malformed chunked encoding) and an
				// oversize body both route through the SAME L0->L1 projector seam as a
				// handler-returned error (renderWireError): the dialect (L1) owns the wire
				// shape, this generic binder (L0) stays app-neutral, and both fail closed
				// through the dialect-owned generic internal envelope if the dialect declines
				// rather than emitting an unknown wire shape on an outward dialect route. The
				// read-failure cause is logged, never serialized.
				limit := bufferedBodyLimit(r)
				b, readErr := io.ReadAll(io.LimitReader(c.Request.Body, limit+1))
				if readErr != nil {
					renderWireError(c, req, projector, internalProjector,
						dialect.NewMalformedRequestError("Invalid request body").WithCause(readErr))
					return
				}
				if int64(len(b)) > limit {
					renderWireError(c, req, projector, internalProjector, dialect.NewRequestTooLargeError(
						fmt.Sprintf("Request body exceeds the maximum allowed size of %s", bodyLimitLabel(limit))))
					return
				}
				req.Body = b
			}
		}

		// Thread the live request context so handlers pass it to the contract planes
		// (cancellation / tracing / request-scoped caches), not context.Background().
		resp, err := r.Handler(c.Request.Context(), req, deps)
		if err != nil {
			// Internal-first, mirroring GlobalErrorHandler (error_handler.go) and the
			// invariant locked by TestGlobalErrorHandler_InternalErrorWrappedDomainWireErrorStaysInternal:
			// an internal error — even one wrapping a DomainWireError as its cause
			// (e.g. a tx-rollback failure) — stays a 500 and is NEVER projected as a
			// dialect business error. Only a non-internal DomainWireError is the
			// dialect's to project; a declined projection, an internal error, or any
			// other error all fail closed to the dialect-owned generic 500.
			var wireErr *dialect.DomainWireError
			if !dialect.IsInternal(err) && errors.As(err, &wireErr) {
				if status, payload, projected := projector(req, wireErr); projected {
					c.JSON(status, payload)
					return
				}
			}
			renderInternalError(c, req, internalProjector)
			return
		}
		if resp == nil {
			c.Status(http.StatusNoContent)
			return
		}
		if resp.Stream != nil {
			writeStreamResponse(c, req, resp, internalProjector)
			return
		}
		writeDialectResponseHeaders(c, resp.Headers)
		c.JSON(resp.Status, resp.Body)
	}
}

func writeStreamResponse(c *gin.Context, req *dialect.Request, resp *dialect.Response, internalProjector dialect.InternalErrorProjector) {
	stream := resp.Stream
	if stream.Reader == nil {
		renderInternalError(c, req, internalProjector)
		return
	}
	defer stream.Reader.Close()
	if resp.Body != nil {
		renderInternalError(c, req, internalProjector)
		return
	}
	if stream.ContentLength == nil || *stream.ContentLength < 0 {
		renderInternalError(c, req, internalProjector)
		return
	}
	if stream.ContentType == "" || hasHeader(resp.Headers, "Content-Length") || hasHeader(resp.Headers, "Content-Type") {
		renderInternalError(c, req, internalProjector)
		return
	}
	payload, ok := readExactStreamPayload(stream)
	if !ok {
		renderInternalError(c, req, internalProjector)
		return
	}
	writeDialectResponseHeaders(c, resp.Headers)
	c.Header("Content-Type", stream.ContentType)
	c.Header("Content-Length", strconv.FormatInt(*stream.ContentLength, 10))
	status := resp.Status
	if status == 0 {
		status = http.StatusOK
	}
	c.Status(status)
	_, _ = c.Writer.Write(payload)
}

func writeDialectResponseHeaders(c *gin.Context, headers map[string]string) {
	for key, value := range headers {
		if strings.EqualFold(key, apihostmiddleware.HeaderRefreshedToken) {
			continue
		}
		c.Header(key, value)
	}
}

func hasHeader(headers map[string]string, key string) bool {
	for k := range headers {
		if strings.EqualFold(k, key) {
			return true
		}
	}
	return false
}

func readExactStreamPayload(stream *dialect.ResponseStream) ([]byte, bool) {
	length := *stream.ContentLength
	if length > maxNeutralResponseStreamBytes {
		return nil, false
	}
	// Read one byte past the declared length before writing headers. This turns a
	// stale too-small ContentLength into a fail-closed 500 instead of a clean 2xx
	// response whose body is silently truncated by net/http.
	payload, err := io.ReadAll(io.LimitReader(stream.Reader, length+1))
	if err != nil {
		return nil, false
	}
	if int64(len(payload)) != length {
		return nil, false
	}
	return payload, true
}
