// Package dialect is the gin-free, backend-free dialect contract for NexusCRM
// (issue #1743). An external app pack implements Dialect depending ONLY on
// github.com/nexuscrm/shared — never on gin, net/http, or backend.
//
// The platform's gin transport binder
// (github.com/pingcap-inc/nexuscrm-api-host/dialecttransport) translates a dialect's
// declarative route Manifest into gin routes + permission middleware, builds a
// neutral Request from the live transport, calls the Handler, and writes or
// projects the Response. This is the Stage-2 "wire codec" shape decided in
// docs/refactor/l0-l1-boundary (seam-1 D-2, neutral-api-execution-contract
// D-2/D-3): the codec is gin-free and lives in shared; gin lives only in the
// api-host binder. The seal is a compile fact, enforced by seal_test.go.
//
// Coverage (Step-1 scope, #1743): this contract is sized to JSON
// request/response over the neutral planes, neutral in-process Dispatch for
// composing surfaces, and the first generic transport widenings needed by migrated
// API surfaces: bounded streamed request bodies and bounded raw responses.
// Capabilities not yet expressible here must be widened explicitly, not silently
// re-scoped away.
package dialect

import (
	"context"
	"errors"
	"io"

	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
	sharederrors "github.com/nexuscrm/shared/pkg/errors"
	"github.com/nexuscrm/shared/pkg/models"
)

// Dialect is a gin-free API dialect: a wire grammar (URLs, envelopes, error shape)
// mounted over the neutral contract. It declares its routes as data (Manifest) and
// owns its error projection; the binder owns all transport.
type Dialect interface {
	// Name is the unique registry key.
	Name() string
	// OwningPack is the app pack that owns this dialect, or "" to always mount.
	// When non-empty, the binder mounts the dialect only if its pack is active.
	OwningPack() string
	// BasePath is the dialect-owned mount root (e.g. "/api"), keeping the
	// wire-path convention in the dialect, not the binder.
	BasePath() string
	// Versions are the version path segments mounted under BasePath.
	Versions() []string
	// Manifest declares the dialect's routes; each Path is relative to
	// <BasePath>/<version>. This replaces the gin-coupled Routes(*gin.RouterGroup):
	// the dialect states what to mount as data, the binder performs the mount.
	Manifest() []Route
	// ErrorProjector renders a neutral engine error into this dialect's wire shape.
	// Mandatory and non-nil — the binder fails closed at mount if it is nil.
	ErrorProjector() Projector
	// InternalErrorProjector renders this dialect's canonical generic internal
	// error. Mandatory and non-nil: when the binder must fail closed for a
	// transport/handler invariant that has no safe business projection, L1 still
	// owns the outward wire envelope.
	InternalErrorProjector() InternalErrorProjector
}

// GateMode is how the binder evaluates a Route's permission requirement.
type GateMode int

const (
	// GateNone adds no permission requirement beyond the host's base data-API gate.
	// It must carry no Permissions — the binder fails closed at mount on GateNone
	// with permissions (the zero-value Mode + a populated Permissions slice is almost
	// always a forgotten Mode that would otherwise mount the route ungated).
	GateNone GateMode = iota
	// GateAll requires the caller to hold ALL of the listed permissions.
	GateAll
	// GateAny requires the caller to hold ANY of the listed permissions.
	GateAny
)

// Gate is a Route's declarative permission requirement. The binder translates it
// into the host's permission middleware, so only the neutral permission KEYS
// (shared/pkg/constants) cross the seam — never a gin middleware factory. The Mode
// is explicit (not an ambiguous slice) and extensible (e.g. a future GateAllOf with
// nested rules) without changing existing dialects.
type Gate struct {
	Mode        GateMode
	Permissions []constants.Permission
}

// Route is one endpoint a dialect declares. Path is relative to
// <BasePath>/<version> and may carry the binder's path-parameter syntax
// (e.g. "/sobjects/:object/:id").
type Route struct {
	Method string
	Path   string
	Gate   Gate
	// BodyMode controls how the binder exposes a body-bearing request to the
	// handler. The zero value preserves existing JSON routes: the binder buffers the
	// body into Request.Body under its default cap. For BodyBuffered, BodyLimitBytes
	// may only lower that cap; larger request bodies must use BodyStream. BodyStream
	// leaves parsing to the dialect handler (multipart/form-data, CSV, or other
	// streamed uploads) while the binder still applies the route's BodyLimitBytes
	// transport cap.
	BodyMode       RequestBodyMode
	BodyLimitBytes int64
	Handler        Handler
}

// RequestBodyMode is the neutral request-body transport shape for a route.
type RequestBodyMode int

const (
	// BodyBuffered buffers the request body into Request.Body. This is the default
	// for JSON routes and keeps existing manifests unchanged.
	BodyBuffered RequestBodyMode = iota
	// BodyStream passes a bounded stream in Request.BodyStream without pre-buffering
	// it. A route using BodyStream MUST declare BodyLimitBytes, so large upload
	// surfaces cannot silently become unbounded transport reads.
	BodyStream
)

// Handler serves one request over the neutral contract with transport-free I/O.
//
// ctx is the live request context — the binder passes the transport's request
// context (cancellation, deadlines, tracing, and request-scoped values middleware
// stores on it). Handlers MUST thread it into the contract planes, whose methods
// all take a context.Context; dropping it would lose cancellation/tracing and any
// request-scoped caches. (context is stdlib, so the gin-free seal holds.)
//
// It returns the Response to write, or an error. A *sharederrors.DomainWireError
// (directly or wrapped) is rendered by the dialect's Projector; any other error is
// rendered by the dialect's InternalErrorProjector.
type Handler func(ctx context.Context, req *Request, deps Deps) (*Response, error)

// Request is the neutral, transport-free request a Handler receives. The binder
// builds it from the live transport: the mounted version, the authenticated user
// already projected to the neutral shared type, path params, query, request
// headers, and either a buffered raw body or a bounded body stream.
type Request struct {
	Version       string
	User          *models.UserSession
	Params        map[string]string
	Query         map[string][]string
	Header        map[string][]string
	Body          []byte
	BodyStream    io.Reader
	ContentLength int64
}

// BodyStreamErrorKind classifies transport read failures from Request.BodyStream
// without exposing backend/gin/http implementation details across the shared seam.
type BodyStreamErrorKind string

const (
	BodyStreamTooLarge   BodyStreamErrorKind = "BodyStreamTooLarge"
	BodyStreamReadFailed BodyStreamErrorKind = "BodyStreamReadFailed"
)

// BodyStreamError is returned by Request.BodyStream when the binder can classify a
// transport read failure. Handlers can use IsBodyStreamTooLarge or
// IsBodyStreamReadFailed to project the failure into their dialect wire shape.
type BodyStreamError struct {
	Kind  BodyStreamErrorKind
	Cause error
}

func (e *BodyStreamError) Error() string {
	switch e.Kind {
	case BodyStreamTooLarge:
		return "request body exceeds configured stream limit"
	case BodyStreamReadFailed:
		return "request body stream read failed"
	default:
		return "request body stream error"
	}
}

func (e *BodyStreamError) Unwrap() error { return e.Cause }

func IsBodyStreamTooLarge(err error) bool {
	var streamErr *BodyStreamError
	return errors.As(err, &streamErr) && streamErr.Kind == BodyStreamTooLarge
}

func IsBodyStreamReadFailed(err error) bool {
	var streamErr *BodyStreamError
	return errors.As(err, &streamErr) && streamErr.Kind == BodyStreamReadFailed
}

// Response is the neutral, transport-free response a Handler returns. Body is the
// value the binder marshals to JSON; Stream is copied as raw bytes. A response must
// use one shape or the other, so a binary download route never gets JSON-quoted.
type Response struct {
	Status  int
	Body    any
	Headers map[string]string
	Stream  *ResponseStream
}

// Mount is the host's authoritative record of one mounted dialect version. It is
// intentionally only location metadata: which dialect owns the version and where
// the host mounted it. Product wire shape, route grammar, and transport details
// stay in the dialect/binder, not in this shared record.
type Mount struct {
	// Dialect is the registry name of the dialect that mounted this version.
	Dialect string
	// BasePath is the dialect's mount root ("" means mounted at "/").
	BasePath string
	// Version is the mounted version path segment (e.g. "v1").
	Version string
	// Path is the full mounted path: <BasePath>/<Version>.
	Path string
}

// ResponseStream is a bounded raw response body. Reader is owned by the binder
// after the handler returns and will be closed after the binder validates the
// exact byte count. Headers still live on Response so the dialect owns
// wire-specific names such as Content-Disposition without the binder knowing the
// dialect's conventions. Content-Length and Content-Type are owned by
// ResponseStream, not Headers, for a single transport fact path.
type ResponseStream struct {
	Reader      io.ReadCloser
	ContentType string
	// ContentLength is mandatory for now: the binder reads and verifies exactly this
	// many bytes before writing headers, because late stream copy failures cannot be
	// converted into a fail-closed dialect response after a 2xx has started.
	ContentLength *int64
}

// DispatchRequest is a transport-free internal request. It lets a dialect surface
// such as Composite ask the host to route a subrequest through the same mounted API
// stack without learning gin or holding the host router.
type DispatchRequest struct {
	Method string
	Path   string
	Header map[string]string
	Body   []byte
}

// DispatchResponse is the captured response from an internal Dispatch request.
// Body is raw bytes because the composing dialect owns how to embed or parse the
// subresponse in its own wire envelope.
type DispatchResponse struct {
	Status int
	Header map[string]string
	Body   []byte
}

// Dispatcher is the neutral in-process re-dispatch capability. The backend binder
// supplies the router-backed implementation; an external app pack sees only this
// function signature.
type Dispatcher func(ctx context.Context, req DispatchRequest) (*DispatchResponse, error)

// Deps are the neutral capabilities a Handler may use — the contract planes plus
// host-provided generic substrate.
// It is a struct (not a bare contract.Deps) so a capability a re-dispatching or
// composing surface needs later (e.g. a Dispatch hook for Composite) can be added
// without changing the Handler signature.
type Deps struct {
	Contracts contract.Deps
	Dispatch  Dispatcher
}

// Projector renders a neutral engine error into this dialect's wire shape. It is
// gin-free — it returns the status + body value (the binder marshals) instead of
// writing to a transport context — but it keeps the two request-aware capabilities
// the gin-coupled projector had:
//
//   - it receives the Request, so a multi-version dialect can vary the error wire
//     shape by req.Version, and
//   - it returns projected=false to DECLINE — an incomplete or unmapped condition
//     fails closed through the dialect's InternalErrorProjector instead of silently
//     shipping a wrong wire contract. A projector must never return projected=true
//     with a body it could not faithfully render.
type Projector func(req *Request, err *sharederrors.DomainWireError) (status int, body any, projected bool)

// InternalErrorProjector renders the dialect's canonical generic internal-error
// envelope. It is the fail-closed sink for binder-originated invariant failures,
// declined business-error projections, and handler errors that must not be exposed
// as a dialect business error. The hook is intentionally request-aware so a
// multi-version dialect can vary the envelope by mounted version without putting
// product wire shape in shared or the backend binder.
type InternalErrorProjector func(req *Request) (status int, body any)
