package exec

import (
	"bytes"
	"encoding/json"
	"strings"
)

const (
	mutationReceiptMetadataKey      = "mutation_receipt"
	mutationAppliedMetadataKey      = "mutation_applied"
	maxMutationReceiptValueRunes    = 256
	maxMutationReceiptBatchRunes    = 16 << 10
	maxMutationReceiptPathRunes     = 1024
	maxMutationReceiptIdentifiers   = 64
	maxMutationReceiptMetadataBytes = 32 << 10

	mutationAppliedHint = "Mutation outcome is recorded; inspect the receipt and retrieval path before issuing another command."
)

// mutationSuccessReceipt is a compact, secret-free handle for a mutation whose
// full success projection did not fit the Exec result boundary. The referenced
// resource and its idempotency/revision facts are durable in the owning API; the
// receipt only carries the bounded facts needed to retrieve that durable state.
// Identifiers are assembled by fixed per-action allowlists below. They are not
// scraped from arbitrary JSON keys, so a distractor field can never displace a
// required selector.
type mutationSuccessReceipt struct {
	Applied        bool              `json:"applied"`
	Action         string            `json:"action"`
	Identifiers    map[string]string `json:"identifiers,omitempty"`
	Revision       string            `json:"revision,omitempty"`
	IdempotencyKey string            `json:"idempotencyKey,omitempty"`
	RetrievalPath  string            `json:"retrievalPath,omitempty"`
}

func mutationReceiptForAction(domain, action string, rawInput json.RawMessage, data any) *mutationSuccessReceipt {
	if !builtinActionIsMutationForInput(domain, action, rawInput) {
		return nil
	}

	// The caller has already successfully marshaled the complete typed result
	// before asking for a receipt. Re-marshal only to inspect the closed result
	// selectors; a failure here means the result is not trustworthy enough to
	// claim Applied.
	encodedData, err := json.Marshal(data)
	if err != nil {
		return nil
	}
	input := decodeReceiptObject(rawInput)
	output := decodeReceiptObject(encodedData)
	identifiers, ok := mutationReceiptIdentifiersForAction(domain, action, input, output)
	if !ok {
		return nil
	}
	receipt := &mutationSuccessReceipt{
		Applied:        true,
		Action:         domain + " " + action,
		Identifiers:    identifiers,
		Revision:       capMutationReceiptValue(firstReceiptOutputRevision(output)),
		IdempotencyKey: capMutationReceiptValue(firstReceiptString(input, "idempotencyKey", "requestId")),
	}
	if len(receipt.Identifiers) == 0 {
		receipt.Identifiers = nil
	}
	receipt.RetrievalPath = capMutationReceiptPath(mutationRetrievalPath(domain, action, receipt.Identifiers))
	return receipt
}

func firstReceiptOutputRevision(output map[string]json.RawMessage) string {
	for _, key := range []string{"revision", "version", "updatedAt"} {
		if value := receiptStringField(output, key); value != "" {
			return value
		}
	}
	for _, container := range []string{
		"plan", "draft", "channel", "skill", "context", "record", "trigger", "run", "step", "task", "version",
	} {
		child := receiptObjectField(output, container)
		for _, key := range []string{"revision", "version", "updatedAt"} {
			if value := receiptStringField(child, key); value != "" {
				return value
			}
		}
	}
	return ""
}

func firstReceiptString(object map[string]json.RawMessage, keys ...string) string {
	for _, key := range keys {
		if value := receiptStringField(object, key); value != "" {
			return value
		}
	}
	return ""
}

func decodeReceiptObject(raw []byte) map[string]json.RawMessage {
	if len(bytes.TrimSpace(raw)) == 0 {
		return nil
	}
	var object map[string]json.RawMessage
	decoder := json.NewDecoder(bytes.NewReader(raw))
	if decoder.Decode(&object) != nil {
		return nil
	}
	if object == nil {
		return nil
	}
	return object
}

func receiptObjectField(object map[string]json.RawMessage, key string) map[string]json.RawMessage {
	if object == nil {
		return nil
	}
	raw, ok := object[key]
	if !ok {
		return nil
	}
	return decodeReceiptObject(raw)
}

func receiptStringField(object map[string]json.RawMessage, key string) string {
	if object == nil {
		return ""
	}
	raw, ok := object[key]
	if !ok {
		return ""
	}
	var value string
	if json.Unmarshal(raw, &value) == nil {
		return strings.TrimSpace(value)
	}
	var number json.Number
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.UseNumber()
	if decoder.Decode(&number) == nil && number.String() != "" {
		return number.String()
	}
	return ""
}

func mutationReceiptIdentifierAllowed(key string) bool {
	switch key {
	case "id", "agentId", "agentIds", "sourceAgentId", "teamId", "channelId", "channel",
		"skillId", "roleId", "requestId", "approvalId", "planId", "versionId", "triggerId",
		"runId", "stepId", "contextId", "thread", "threadId", "taskScopeId", "taskNumber",
		"taskNumbers", "number", "numbers", "scopeId", "deletedTaskNumbers", "promotedTaskNumbers":
		return true
	default:
		return false
	}
}

func capMutationReceiptValue(value string) string {
	value = strings.TrimSpace(strings.ToValidUTF8(value, "�"))
	if len([]rune(value)) > maxMutationReceiptValueRunes {
		return ""
	}
	return value
}

func capMutationReceiptPath(value string) string {
	value = strings.TrimSpace(strings.ToValidUTF8(value, "�"))
	if len([]rune(value)) > maxMutationReceiptPathRunes {
		return ""
	}
	return value
}

func mutationReceiptValueLimit(key string) int {
	switch key {
	case "agentIds", "taskNumbers", "numbers", "deletedTaskNumbers", "promotedTaskNumbers":
		return maxMutationReceiptBatchRunes
	default:
		return maxMutationReceiptValueRunes
	}
}

// mutationRetrievalPath only returns a path when every selector required by
// the target get/show/request-get contract is present. An empty path is a
// deliberate fail-closed result, not a generic `{}` replay instruction.
func attachMutationReceiptMetadata(envelope *ErrorEnvelope, receipt *mutationSuccessReceipt) {
	if envelope == nil || receipt == nil || !receipt.Applied {
		return
	}
	if envelope.Metadata == nil {
		envelope.Metadata = make(map[string]any)
	}
	envelope.Metadata[mutationAppliedMetadataKey] = true
	envelope.Metadata[mutationReceiptMetadataKey] = receipt
}
