package dialectsfcrm

import (
	"context"
	"fmt"

	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/dialect"
	"github.com/nexuscrm/shared/pkg/models"
)

// tree.go ports the composite/tree-only helpers from
// backend/internal/interfaces/rest/composite_tree_handler.go. The only behavioral
// change is gin.H -> map[string]any in the result converters: gin.H IS
// map[string]any (gin/utils.go), so the marshalled JSON bytes are identical — this
// is what removes the gin dependency from the result-projection path while keeping
// byte parity (proven by the golden's success-strip case).

// treeRecordObjectName — copy of composite_tree_handler.go:48.
func treeRecordObjectName(record map[string]interface{}) string {
	if record == nil {
		return ""
	}
	if rawAttrs, ok := record["attributes"].(map[string]interface{}); ok {
		if objectName, ok := rawAttrs["type"].(string); ok {
			return objectName
		}
	}
	return ""
}

func treeRecordMutationDenial(ctx context.Context, req *dialect.Request, metadata contract.Metadata, record map[string]interface{}) (*models.RecordMutationDenial, *dialect.Response) {
	if record == nil {
		return nil, nil
	}
	denial, resp := recordMutationDenialForObject(ctx, req, metadata, treeRecordObjectName(record), models.RecordMutationSurfaceTreeCreate, models.SObject(record))
	if resp != nil || denial != nil {
		return denial, resp
	}
	for key, value := range record {
		if key == "attributes" {
			continue
		}
		nested, ok := value.(map[string]interface{})
		if !ok {
			continue
		}
		rawChildren, ok := nested["records"].([]interface{})
		if !ok {
			continue
		}
		for _, child := range rawChildren {
			childRecord, ok := child.(map[string]interface{})
			if !ok {
				continue
			}
			denial, resp := treeRecordMutationDenial(ctx, req, metadata, childRecord)
			if resp != nil || denial != nil {
				return denial, resp
			}
		}
	}
	return nil, nil
}

// treeNodesFromV63 decodes a Salesforce Composite-Tree v63 request's records array
// into the neutral nested tree IR []contract.TreeNode. This is the SF-grammar
// decode that L0-E PR-1 removed from the engine (backend insertTreeNode) and that
// now lives here in L1, where the SF wire grammar belongs: the engine's neutral
// CreateTreeNodes walks contract.TreeNode directly and never sees a SF-shaped map.
//
// It is a faithful port of the old engine decode (insertTreeNode /
// appendTreeObjectNames, persistence_bulk_ops.go). Each map in records becomes one
// root TreeNode, decoded with the URL :objectName as the root object-type default;
// children are decoded recursively with an empty default so their own
// attributes.type / _objectType wins, exactly as the old recursion passed
// childType "".
//
// Fail-closed decode: a malformed nested relationship child (a non-object entry
// in a relationship's "records" array, or a malformed grandchild any level deep)
// fails the WHOLE decode with an error rather than silently dropping the child.
// The legacy engine path (insertTreeNode) preserved the bad child and FAILED at
// insert, keeping the whole tree atomic; dropping it here would let a parent-only
// tree be created for input the engine would have rejected. The handler maps this
// error to the v63 malformed-request envelope BEFORE any mutation, so a malformed
// tree is rejected atomically, matching legacy.
func treeNodesFromV63(rootType string, records []map[string]interface{}) ([]contract.TreeNode, error) {
	if records == nil {
		return nil, nil
	}
	nodes := make([]contract.TreeNode, 0, len(records))
	for _, record := range records {
		node, err := treeNodeFromV63(rootType, record)
		if err != nil {
			return nil, err
		}
		nodes = append(nodes, node)
	}
	return nodes, nil
}

// treeNodeFromV63 decodes one SF Composite-Tree wire node into a contract.TreeNode.
// Mirrors backend insertTreeNode's per-node parse exactly:
//   - ObjectType: models.RecordObjectType (reads _objectType, then attributes.type),
//     falling back to defaultType (the URL :objectName for roots, "" for children).
//   - ReferenceID: attributes.referenceId.
//   - Fields: every key that is neither a record-metadata key (attributes /
//     _objectType / the exact-decimal marker) nor a relationship sub-object
//     ({"records":[...]}). The SF envelope/metadata keys are stripped here so the
//     neutral node carries only user field values.
//   - Children: each key whose value is a map holding a "records" []any is a
//     relationship; its child maps are decoded recursively (default "" so each
//     child self-describes its type).
func treeNodeFromV63(defaultType string, record map[string]interface{}) (contract.TreeNode, error) {
	node := contract.TreeNode{ObjectType: defaultType}
	if record == nil {
		return node, nil
	}

	if stampedType := models.RecordObjectType(record); stampedType != "" {
		node.ObjectType = stampedType
	}
	if attr, ok := record["attributes"].(map[string]interface{}); ok {
		node.ReferenceID, _ = attr["referenceId"].(string)
	}

	fields := make(contract.Record)
	for key, value := range record {
		if models.IsRecordMetadataKey(key) {
			continue
		}
		// A relationship node is a sub-object of the shape {"records":[...]}.
		if nested, ok := value.(map[string]interface{}); ok {
			if rawChildren, ok := nested["records"].([]interface{}); ok {
				childNodes := make([]contract.TreeNode, 0, len(rawChildren))
				for _, rawChild := range rawChildren {
					child, ok := rawChild.(map[string]interface{})
					if !ok {
						// Fail closed: a non-object entry in a relationship's
						// "records" array is malformed input. The legacy engine
						// preserved it and failed at insert (whole tree atomic);
						// silently dropping it would create a parent-only tree.
						return contract.TreeNode{}, fmt.Errorf("composite tree relationship %q has a non-object child record", key)
					}
					childNode, err := treeNodeFromV63("", child)
					if err != nil {
						// Propagate a malformed grandchild any level deep.
						return contract.TreeNode{}, err
					}
					childNodes = append(childNodes, childNode)
				}
				if node.Children == nil {
					node.Children = make(map[string][]contract.TreeNode)
				}
				node.Children[key] = childNodes
				continue
			}
		}
		fields[key] = value
	}
	node.Fields = fields
	return node, nil
}

// treeCreateResultsToV63 re-skins the engine's neutral []contract.TreeCreateResult
// back into the SF Composite-Tree v63 results array. It replaces the old
// []contract.Record projection: the neutral result type carries only the tree wire
// triple by construction, so there are no SF carriers to strip — the projection is
// now a pure typed->map mapping. The gin.H -> map[string]any change of the original
// relocation is preserved (map[string]any marshals identically).
func treeCreateResultsToV63(results []contract.TreeCreateResult) []map[string]any {
	if results == nil {
		return nil
	}
	out := make([]map[string]any, 0, len(results))
	for _, result := range results {
		out = append(out, treeCreateResultToV63(result))
	}
	return out
}

// treeCreateResultToV63 maps one neutral contract.TreeCreateResult to the v63
// {referenceId, id, errors} wire object. Byte parity note: encoding/json emits
// map keys alphabetically (errors, id, referenceId), matching the old
// contract.Record projection's bytes; Errors is normalized to a non-nil slice so
// it marshals as [] (never null), exactly as the old []interface{}{} did.
func treeCreateResultToV63(result contract.TreeCreateResult) map[string]any {
	errs := result.Errors
	if errs == nil {
		errs = []string{}
	}
	return map[string]any{
		"id":          result.ID,
		"referenceId": result.ReferenceID,
		"errors":      errs,
	}
}
