package message

import (
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"strings"
)

const scopeMessageIdempotencyKeyMaxBytes = 128

// MessageCreateDigestInput is the immutable, client-owned part of a message
// create command. Generated ids, sequence numbers, timestamps, author
// presentation, and message self-sources deliberately stay outside the digest.
type MessageCreateDigestInput struct {
	MessageType   string
	Content       string
	AttachmentIDs []string
	SourceRefs    []SourceRef
	Metadata      []byte
}

type messageCreateDigestDocument struct {
	Version       int                            `json:"version"`
	MessageType   string                         `json:"messageType"`
	Content       string                         `json:"content"`
	AttachmentIDs []string                       `json:"attachmentIds"`
	SourceRefs    []messageCreateDigestSourceRef `json:"sourceRefs"`
	Metadata      any                            `json:"metadata"`
}

type messageCreateDigestSourceRef struct {
	SourceScopeID   string `json:"sourceScopeId"`
	SourceSeq       int64  `json:"sourceSeq"`
	SourceMessageID string `json:"sourceMessageId"`
	SourceKind      string `json:"sourceKind"`
	SourceDedupeKey string `json:"sourceDedupeKey"`
}

// ComputeMessageCreatePayloadDigest returns the deterministic SHA-256 digest used
// by scope-message idempotency. Slice order is preserved because attachment
// and source-ref order are part of the command contract.
func ComputeMessageCreatePayloadDigest(input MessageCreateDigestInput) (string, error) {
	metadata, err := deterministicMessageDigestMetadata(input.Metadata)
	if err != nil {
		return "", err
	}
	attachmentIDs := make([]string, 0, len(input.AttachmentIDs))
	for _, attachmentID := range input.AttachmentIDs {
		if trimmed := strings.TrimSpace(attachmentID); trimmed != "" {
			attachmentIDs = append(attachmentIDs, trimmed)
		}
	}
	sourceRefs := make([]messageCreateDigestSourceRef, 0, len(input.SourceRefs))
	for _, source := range input.SourceRefs {
		sourceRefs = append(sourceRefs, messageCreateDigestSourceRef{
			SourceScopeID:   strings.TrimSpace(source.SourceScopeID),
			SourceSeq:       source.SourceSeq,
			SourceMessageID: strings.TrimSpace(source.SourceMessageID),
			SourceKind:      strings.TrimSpace(source.SourceKind),
			SourceDedupeKey: strings.TrimSpace(source.SourceDedupeKey),
		})
	}
	document := messageCreateDigestDocument{
		Version:       1,
		MessageType:   strings.TrimSpace(input.MessageType),
		Content:       input.Content,
		AttachmentIDs: attachmentIDs,
		SourceRefs:    sourceRefs,
		Metadata:      metadata,
	}
	payload, err := json.Marshal(document)
	if err != nil {
		return "", fmt.Errorf("marshal message create digest payload: %w", err)
	}
	digest := sha256.Sum256(payload)
	return hex.EncodeToString(digest[:]), nil
}

func deterministicMessageDigestMetadata(raw []byte) (any, error) {
	trimmed := bytes.TrimSpace(raw)
	if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) || bytes.Equal(trimmed, []byte("{}")) {
		return map[string]any{}, nil
	}
	decoder := json.NewDecoder(bytes.NewReader(trimmed))
	decoder.UseNumber()
	var metadata any
	if err := decoder.Decode(&metadata); err != nil {
		return nil, invalidArgument("message create metadata must be valid JSON")
	}
	var trailing any
	if err := decoder.Decode(&trailing); err != io.EOF {
		return nil, invalidArgument("message create metadata must be one JSON document")
	}
	if _, ok := metadata.(map[string]any); !ok {
		return nil, invalidArgument("message create metadata must be a JSON object")
	}
	// Persisted mention metadata is a server-derived routing/read projection.
	// Mention hints and current identity resolution may legitimately differ on
	// a retry, while the client-owned create command remains unchanged.
	delete(metadata.(map[string]any), MentionMetadataKey)
	return metadata, nil
}

func normalizeScopeMessageCreateContract(record ScopeMessageRecord) (ScopeMessageRecord, error) {
	record.AuthorMemberID = strings.TrimSpace(record.AuthorMemberID)
	if len(record.AuthorMemberID) > scopeMessageIdempotencyKeyMaxBytes {
		return ScopeMessageRecord{}, invalidArgument("message author member id must be at most 128 bytes")
	}
	record.NotifyPrincipalMemberID = strings.TrimSpace(record.NotifyPrincipalMemberID)
	if record.NotifyPrincipalMemberID == "" {
		record.NotifyPrincipalMemberID = record.AuthorMemberID
	}
	if len(record.NotifyPrincipalMemberID) > scopeMessageIdempotencyKeyMaxBytes {
		return ScopeMessageRecord{}, invalidArgument("notification principal member id must be at most 128 bytes")
	}
	record.IdempotencySubjectMemberID = strings.TrimSpace(record.IdempotencySubjectMemberID)
	if record.IdempotencySubjectMemberID == "" {
		record.IdempotencySubjectMemberID = record.AuthorMemberID
	}
	if len(record.IdempotencySubjectMemberID) > scopeMessageIdempotencyKeyMaxBytes {
		return ScopeMessageRecord{}, invalidArgument("idempotency subject member id must be at most 128 bytes")
	}
	record.ClientMessageID = strings.TrimSpace(record.ClientMessageID)
	if len(record.ClientMessageID) > scopeMessageIdempotencyKeyMaxBytes {
		return ScopeMessageRecord{}, invalidArgument("client message id must be at most 128 bytes")
	}
	record.IdempotencyPayloadDigest = strings.ToLower(strings.TrimSpace(record.IdempotencyPayloadDigest))
	if record.ClientMessageID == "" {
		if record.IdempotencyPayloadDigest != "" {
			return ScopeMessageRecord{}, invalidArgument("idempotency payload digest requires client message id")
		}
		return record, nil
	}
	if !isMessagePayloadDigest(record.IdempotencyPayloadDigest) {
		return ScopeMessageRecord{}, invalidArgument("valid idempotency payload digest is required with client message id")
	}
	return record, nil
}

func isMessagePayloadDigest(value string) bool {
	if len(value) != sha256.Size*2 {
		return false
	}
	_, err := hex.DecodeString(value)
	return err == nil
}

func requireScopeMessageReplayDigest(existing ScopeMessageRecord, requested ScopeMessageRecord) error {
	if strings.EqualFold(strings.TrimSpace(existing.IdempotencyPayloadDigest), strings.TrimSpace(requested.IdempotencyPayloadDigest)) {
		return nil
	}
	return conflict("client message id was already used with a different message payload")
}
