package exec

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"strings"
	"unicode/utf8"
)

// decodeMutationReceiptMetadata is used only by the local daemon client. The
// decoder is closed and bounded: unknown metadata keys, wrong scalar types,
// oversized values, action mismatches, and request-selector mismatches all
// discard the complete receipt. The exact validated request input is required
// so a wire receipt can never redirect recovery to another resource.
func decodeMutationReceiptMetadata(raw json.RawMessage, expectedAction string, rawInput json.RawMessage) (bool, *mutationSuccessReceipt) {
	if len(raw) == 0 || len(raw) > maxMutationReceiptMetadataBytes || !mutationReceiptJSONHasUniqueKeys(raw) {
		return false, nil
	}
	var wire struct {
		Applied *bool           `json:"mutation_applied"`
		Receipt json.RawMessage `json:"mutation_receipt"`
	}
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.DisallowUnknownFields()
	if decoder.Decode(&wire) != nil || decoder.Decode(new(struct{})) != io.EOF {
		return false, nil
	}
	if wire.Applied == nil || !*wire.Applied || len(wire.Receipt) == 0 {
		return false, nil
	}
	receipt, ok := decodeMutationReceipt(wire.Receipt, expectedAction, rawInput)
	if !ok {
		return false, nil
	}
	return true, receipt
}

func decodeMutationReceipt(raw json.RawMessage, expectedAction string, rawInput json.RawMessage) (*mutationSuccessReceipt, bool) {
	if !mutationReceiptJSONHasUniqueKeys(raw) {
		return nil, false
	}
	var wire struct {
		Applied        *bool             `json:"applied"`
		Action         string            `json:"action"`
		Identifiers    map[string]string `json:"identifiers"`
		Revision       string            `json:"revision"`
		IdempotencyKey string            `json:"idempotencyKey"`
		RetrievalPath  string            `json:"retrievalPath"`
	}
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.DisallowUnknownFields()
	if decoder.Decode(&wire) != nil || decoder.Decode(new(struct{})) != io.EOF ||
		wire.Applied == nil || !*wire.Applied || wire.Action != expectedAction {
		return nil, false
	}
	if len(wire.Identifiers) > maxMutationReceiptIdentifiers || !validMutationReceiptText(wire.Action, maxMutationReceiptValueRunes) ||
		!validMutationReceiptText(wire.Revision, maxMutationReceiptValueRunes) ||
		!validMutationReceiptText(wire.IdempotencyKey, maxMutationReceiptValueRunes) ||
		!validMutationReceiptText(wire.RetrievalPath, maxMutationReceiptPathRunes) {
		return nil, false
	}
	for key, value := range wire.Identifiers {
		if !mutationReceiptIdentifierAllowed(key) || !validMutationReceiptText(value, mutationReceiptValueLimit(key)) {
			return nil, false
		}
	}
	canonicalPath := ""
	if domain, action, ok := strings.Cut(wire.Action, " "); ok && domain != "" && action != "" {
		canonicalPath = mutationRetrievalPath(domain, action, wire.Identifiers)
	}
	receipt := &mutationSuccessReceipt{
		Applied: true, Action: wire.Action, Identifiers: wire.Identifiers,
		Revision: wire.Revision, IdempotencyKey: wire.IdempotencyKey, RetrievalPath: canonicalPath,
	}
	parts := strings.SplitN(expectedAction, " ", 2)
	if len(parts) != 2 {
		return nil, false
	}
	input := decodeReceiptObject(rawInput)
	if !validateMutationReceiptAgainstInput(parts[0], parts[1], input, receipt) {
		return nil, false
	}
	canonicalPath = mutationRetrievalPath(parts[0], parts[1], wire.Identifiers)
	if wire.RetrievalPath != "" && wire.RetrievalPath != canonicalPath {
		return nil, false
	}
	receipt.RetrievalPath = canonicalPath
	return receipt, true
}

func validMutationReceiptText(value string, maxRunes int) bool {
	if value == "" {
		return true
	}
	return utf8.ValidString(value) && len([]rune(value)) <= maxRunes && strings.TrimSpace(value) == value && !strings.ContainsAny(value, "\r\n")
}

// mutationSerializationFailureError is intentionally not Applied. A custom
// Marshaler can fail before the desktop has a trustworthy bounded receipt, so
// the CLI must not suggest replay or pretend that a selector was durable.
func mutationSerializationFailureError(domain, action string, cause error) *Error {
	return &Error{
		Code:      CodeInternal,
		Problem:   fmt.Sprintf("Exec action %s %s result could not be serialized", domain, action),
		Hint:      "The mutation outcome is unknown; inspect the authoritative resource before issuing another command.",
		Retryable: false,
		Cause:     cause,
	}
}
