package dialectsfcrm

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"

	"github.com/nexuscrm/dialect-sf-crm/wire"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/dialect"
	sharederrors "github.com/nexuscrm/shared/pkg/errors"
	"github.com/nexuscrm/shared/pkg/models"
)

// CompositeTreeNeutralHandler is the gin-free salesforce-v63 composite/tree write
// handler, relocated from backend/internal/interfaces/rest/composite_tree_dialect.go
// into this born-green dialect module (#2015 PR-0). It is a dialect.Handler: it reads
// only the neutral dialect.Request and the neutral engine tree-create seam — it never
// touches *ServiceManager, the Host slot, or *gin.Context.
//
// L0-E CreateTree PR-2: this handler now DECODES the SF Composite-Tree wire request
// into the neutral nested tree IR ([]contract.TreeNode) itself and calls the neutral
// contract.TreeNodeWriter.CreateTreeNodes, instead of handing the engine SF-shaped
// []contract.Record through the legacy Records.CreateTree. The SF-grammar decode
// (object type + intra-batch referenceId out of attributes, child relationship
// unwrap of {"records":[...]}, envelope/metadata-key stripping) lives here in L1
// where the SF wire grammar belongs; the engine walks the typed IR directly.
// TreeNodeWriter is a SEPARATE capability interface from Records, so the handler
// reaches it by type-asserting the Records value it already holds; if the neutral
// seam is absent the handler fails closed with the v63 internal-error envelope
// before any mutation.
//
// It depends on github.com/nexuscrm/shared + the module-local wire/errors/guards
// packages ONLY (proven by seal_test.go). The backend apierr envelope builders and
// the rest guard cluster it used in-tree are reproduced here as named transitional
// duplications (see doc.go); the success/error bytes are pinned by the module's
// golden and handler tests.
func CompositeTreeNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	objectName := req.Params["objectName"]
	records := deps.Contracts.Records
	metadata := deps.Contracts.Metadata
	if records == nil || metadata == nil {
		return internalErrorResponse("records and metadata contracts unavailable"), nil
	}
	treeWriter, ok := records.(contract.TreeNodeWriter)
	if !ok {
		return internalErrorResponse("tree-create contract unavailable"), nil
	}

	denial, lookupResp := recordMutationDenialForObject(ctx, req, metadata, objectName, models.RecordMutationSurfaceTreeCreate, nil)
	if lookupResp != nil {
		return lookupResp, nil
	}
	if resp := recordMutationDenialResponse(denial, wire.SFCodeInvalidFieldForInsertUpdate); resp != nil {
		return resp, nil
	}

	var body struct {
		Records []map[string]interface{} `json:"records"`
	}
	// Salesforce Tree API body format: { "records": [ ... ] }. Bind with a json.Decoder
	// to match the v63 surface's established body-parsing semantics: every other v63
	// write handler binds via gin c.ShouldBindJSON, which is json.Decoder.Decode — it
	// reads one JSON value and tolerates trailing bytes. Using strict json.Unmarshal
	// here would make this the lone outlier (rejecting `{...}<trailing>` with 400 where
	// every sibling returns 201) on an UNVERIFIED assumption about Salesforce.
	if err := json.NewDecoder(bytes.NewReader(req.Body)).Decode(&body); err != nil {
		return &dialect.Response{
			Status: http.StatusBadRequest,
			Body:   wire.SFErrorResponse(wire.SFCodeInvalidRequest, "Invalid request body"),
		}, nil
	}

	if len(body.Records) > 200 {
		return &dialect.Response{
			Status: http.StatusBadRequest,
			Body:   wire.SFErrorResponse(wire.SFCodeLimitExceeded, "Maximum 200 records allowed"),
		}, nil
	}
	for _, record := range body.Records {
		denial, lookupResp := treeRecordMutationDenial(ctx, req, metadata, record)
		if lookupResp != nil {
			return lookupResp, nil
		}
		if denial != nil {
			return recordMutationDenialResponse(denial, wire.SFCodeInvalidFieldForInsertUpdate), nil
		}
	}

	// Decode the SF Composite-Tree wire request into the neutral tree IR BEFORE any
	// mutation. A malformed nested relationship child (a non-object entry in a
	// relationship's "records" array) fails the whole decode here, so the tree is
	// rejected atomically with the v63 malformed-request envelope — matching the
	// legacy engine path, which preserved the bad child and failed at insert rather
	// than silently creating a parent-only tree.
	nodes, err := treeNodesFromV63(objectName, body.Records)
	if err != nil {
		return &dialect.Response{
			Status: http.StatusBadRequest,
			Body:   wire.SFErrorResponse(wire.SFCodeInvalidRequest, err.Error()),
		}, nil
	}
	results, err := treeWriter.CreateTreeNodes(ctx, nodes, req.User)
	if err != nil {
		return &dialect.Response{
			Status: http.StatusBadRequest,
			Body:   wire.SFErrorResponse(wire.SFCodeInvalidRequest, err.Error()),
		}, nil
	}

	return &dialect.Response{
		Status: http.StatusCreated,
		Body: map[string]interface{}{
			"hasErrors": false,
			"results":   treeCreateResultsToV63(results),
		},
	}, nil
}

// V63CompositeTreeErrorProjector is the partition's mandatory gin-free error
// projector. The handler returns its v63 envelopes as Responses directly, so this
// projector is exercised only if a neutral *DomainWireError escapes the handler OR
// the binder injects one (transport-level body-read failure / oversize — binder.go:
// 376-386). It mirrors RespondError's DomainWireError arm and declines anything it
// cannot faithfully render (fail closed).
//
// Return order is (status int, body any, projected bool) per shared/pkg/dialect.go:144
// — status FIRST, byte-identical to the live source composite_tree_dialect.go:122-127.
func V63CompositeTreeErrorProjector(req *dialect.Request, err *sharederrors.DomainWireError) (int, any, bool) {
	if sfErr, status, ok := wire.SFErrorFromDomainForVersion(err, req.Version); ok {
		return status, []wire.SFError{sfErr}, true
	}
	return 0, nil, false
}

// V63InternalErrorProjector returns the canonical v63 generic internal-error
// envelope for binder fail-closed paths. Keeping it in the CRM dialect preserves
// the L0/L1 boundary: shared exposes only the hook; L1 owns this wire grammar.
func V63InternalErrorProjector(_ *dialect.Request) (int, any) {
	return http.StatusInternalServerError, wire.SFGenericInternalErrorResponse()
}
