package recordmutationv1

import (
	"bytes"
	"crypto/sha256"
	"encoding/binary"
	"encoding/hex"
	"errors"
	"sort"
)

// NormalizeRequest deep-copies request and sorts field assignments into
// canonical bytewise UTF-8 order. Operation order is semantic and is preserved.
func NormalizeRequest(request Request) Request {
	result := request
	result.Operations = make([]Operation, len(request.Operations))
	for i, operation := range request.Operations {
		result.Operations[i] = operation
		result.Operations[i].Fields = append([]FieldValue(nil), operation.Fields...)
		sort.Slice(result.Operations[i].Fields, func(a, b int) bool {
			return result.Operations[i].Fields[a].Name < result.Operations[i].Fields[b].Name
		})
		for fieldIndex := range result.Operations[i].Fields {
			field := &result.Operations[i].Fields[fieldIndex]
			if field.Kind == ValueKindJSON && field.JSON != nil {
				canonical, err := CanonicalSourceJSON(*field.JSON)
				if err == nil {
					field.JSON = &canonical
				}
			}
		}
	}
	return result
}

// CanonicalCommandBytes is the v1 cross-language semantic digest grammar:
//
//   - ASCII domain prefix "nexuscrm-record-mutation-v1";
//   - batch mode is one byte: INDEPENDENT=1, ATOMIC=2;
//   - operation kind is one byte: CREATE=1, UPDATE=2, DELETE=3, UPSERT=4;
//   - value kind is one byte: STRING=1 through NULL=8, NUMBER=9, JSON=10;
//   - every string/byte slice is unsigned-varint length followed by raw bytes;
//   - every count and integer is unsigned varint (signed integers use zig-zag);
//   - optionals are one presence byte followed by their value when present;
//   - operations retain request order and fields use bytewise UTF-8 name order;
//   - after an UPSERT's fields, append the length-prefixed ASCII marker
//     "external-identity", then the length-prefixed field ID and value; other
//     operation kinds append no identity suffix;
//   - CommandID is excluded because it names the content, not the content itself.
//
// The grammar is deliberately independent of protobuf serialization. Protobuf
// deterministic mode is not a cross-language canonicalization guarantee.
func CanonicalCommandBytes(request Request) ([]byte, error) {
	normalized := NormalizeRequest(request)
	var out bytes.Buffer
	writeBytes(&out, []byte("nexuscrm-record-mutation-v1"))
	out.WriteByte(batchModeTag(normalized.BatchMode))
	if normalized.MinCatalogRevision == nil {
		out.WriteByte(0)
	} else {
		out.WriteByte(1)
		writeUint(&out, *normalized.MinCatalogRevision)
	}
	writeUint(&out, uint64(len(normalized.Operations)))
	for _, operation := range normalized.Operations {
		out.WriteByte(operationKindTag(operation.Kind))
		writeString(&out, operation.ObjectID)
		writeString(&out, operation.RecordID)
		writeUint(&out, uint64(len(operation.Fields)))
		for _, field := range operation.Fields {
			writeString(&out, field.Name)
			out.WriteByte(valueKindTag(field.Kind))
			if field.Null {
				out.WriteByte(0)
				continue
			}
			out.WriteByte(1)
			switch field.Kind {
			case ValueKindString:
				if field.String == nil {
					return nil, errors.New("record mutation: missing string arm")
				}
				writeString(&out, *field.String)
			case ValueKindBoolean:
				if field.Boolean == nil {
					return nil, errors.New("record mutation: missing boolean arm")
				}
				if *field.Boolean {
					out.WriteByte(1)
				} else {
					out.WriteByte(0)
				}
			case ValueKindInteger:
				if field.Integer == nil {
					return nil, errors.New("record mutation: missing integer arm")
				}
				writeUint(&out, uint64((*field.Integer<<1)^(*field.Integer>>63)))
			case ValueKindDecimal:
				if field.Decimal == nil {
					return nil, errors.New("record mutation: missing decimal arm")
				}
				writeString(&out, field.Decimal.Coefficient)
				writeUint(&out, uint64(field.Decimal.Scale))
			case ValueKindDate:
				if field.Date == nil {
					return nil, errors.New("record mutation: missing date arm")
				}
				writeString(&out, *field.Date)
			case ValueKindDateTime:
				if field.DateTime == nil {
					return nil, errors.New("record mutation: missing datetime arm")
				}
				writeString(&out, *field.DateTime)
			case ValueKindReference:
				if field.Reference == nil {
					return nil, errors.New("record mutation: missing reference arm")
				}
				writeString(&out, *field.Reference)
			case ValueKindNumber:
				if field.Number == nil {
					return nil, errors.New("record mutation: missing number arm")
				}
				writeString(&out, *field.Number)
			case ValueKindJSON:
				if field.JSON == nil {
					return nil, errors.New("record mutation: missing JSON arm")
				}
				writeString(&out, *field.JSON)
			default:
				return nil, errors.New("record mutation: invalid value kind")
			}
		}
		if operation.Kind == OperationUpsert {
			if operation.ExternalIdentity == nil {
				return nil, errors.New("record mutation: missing external identity")
			}
			writeBytes(&out, []byte("external-identity"))
			writeString(&out, operation.ExternalIdentity.FieldID)
			writeString(&out, operation.ExternalIdentity.Value)
		}
	}
	// Keep the originally released v1 digest byte-for-byte stable for N-1
	// callers. Explicit use-case intent is an additive extension after the old
	// command grammar and therefore cannot collide with an old command.
	if normalized.UseCase != MutationUseCaseUnspecified {
		writeBytes(&out, []byte("mutation-use-case"))
		out.WriteByte(mutationUseCaseTag(normalized.UseCase))
	}
	return out.Bytes(), nil
}

func CommandDigest(request Request) (string, error) {
	raw, err := CanonicalCommandBytes(request)
	if err != nil {
		return "", err
	}
	digest := sha256.Sum256(raw)
	return "sha256:" + hex.EncodeToString(digest[:]), nil
}

// SemanticRequestSize counts normalized caller-authored semantic payload,
// independent of JSON or protobuf wrapper spelling. CommandID is transport/
// idempotency identity and is excluded. The count includes identifiers,
// canonical source-JSON bytes, and exact scalar content plus fixed-width scalar
// values; it is never larger than the equivalent accepted JSON body merely
// because the neutral contract has a typed wrapper.
func SemanticRequestSize(request Request) int {
	request = NormalizeRequest(request)
	size := 1 // batch mode
	if request.UseCase != MutationUseCaseUnspecified {
		size++
	}
	if request.MinCatalogRevision != nil {
		size += 8
	}
	for _, operation := range request.Operations {
		size++
		size += len(operation.ObjectID) + len(operation.RecordID)
		if operation.ExternalIdentity != nil {
			size += len(operation.ExternalIdentity.FieldID) + len(operation.ExternalIdentity.Value)
		}
		for _, field := range operation.Fields {
			size++
			size += len(field.Name)
			switch field.Kind {
			case ValueKindString:
				if field.String != nil {
					size += len(*field.String)
				}
			case ValueKindBoolean:
				size++
			case ValueKindInteger:
				size += 8
			case ValueKindDecimal:
				if field.Decimal != nil {
					size += len(field.Decimal.Coefficient) + 4
				}
			case ValueKindDate:
				if field.Date != nil {
					size += len(*field.Date)
				}
			case ValueKindDateTime:
				if field.DateTime != nil {
					size += len(*field.DateTime)
				}
			case ValueKindReference:
				if field.Reference != nil {
					size += len(*field.Reference)
				}
			case ValueKindNumber:
				if field.Number != nil {
					size += len(*field.Number)
				}
			case ValueKindJSON:
				if field.JSON != nil {
					size += len(*field.JSON)
				}
			}
		}
	}
	return size
}

func batchModeTag(value BatchMode) byte {
	return map[BatchMode]byte{BatchIndependent: 1, BatchAtomic: 2}[value]
}

func operationKindTag(value OperationKind) byte {
	return map[OperationKind]byte{OperationCreate: 1, OperationUpdate: 2, OperationDelete: 3, OperationUpsert: 4}[value]
}

func valueKindTag(value ValueKind) byte {
	return map[ValueKind]byte{
		ValueKindString: 1, ValueKindBoolean: 2, ValueKindInteger: 3,
		ValueKindDecimal: 4, ValueKindDate: 5, ValueKindDateTime: 6,
		ValueKindReference: 7, ValueKindNull: 8, ValueKindNumber: 9,
		ValueKindJSON: 10,
	}[value]
}

func mutationUseCaseTag(value MutationUseCase) byte {
	return map[MutationUseCase]byte{MutationUseCaseSingle: 1, MutationUseCaseCollection: 2}[value]
}

func writeString(out *bytes.Buffer, value string) { writeBytes(out, []byte(value)) }

func writeBytes(out *bytes.Buffer, value []byte) {
	writeUint(out, uint64(len(value)))
	out.Write(value)
}

func writeUint(out *bytes.Buffer, value uint64) {
	var encoded [binary.MaxVarintLen64]byte
	n := binary.PutUvarint(encoded[:], value)
	out.Write(encoded[:n])
}
