package message

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
)

// ServiceNotificationMessageUpdate is the message-owned portion of a App
// notification update. The caller still owns the notification/card revision
// and idempotency facts, but those facts and this write must share one
// transaction.
type ServiceNotificationMessageUpdate struct {
	WorkspaceID          string
	ScopeID              string
	MessageID            string
	AuthorMemberID       string
	ExpectedEditRevision *int64
	Content              string
	Metadata             []byte
	UpdatedAt            time.Time
}

// ServiceNotificationMessageWithdrawal is the message-owned portion of a App
// notification withdrawal. It keeps the ordinary Human recall path separate:
// only the service member that authored the notification may use this path.
type ServiceNotificationMessageWithdrawal struct {
	WorkspaceID          string
	ScopeID              string
	MessageID            string
	AuthorMemberID       string
	ExpectedEditRevision *int64
	PlaceholderContent   string
	Reason               string
	WithdrawnAt          time.Time
}

// ServiceNotificationMessageMutationResult is the authoritative transcript
// result used by the notification service to publish message.update or
// message.delete after the surrounding transaction commits.
type ServiceNotificationMessageMutationResult struct {
	Message                 ScopeMessageRecord
	Changed                 bool
	IsLatestVisibleMessage  bool
	SuppressedActivityCount int64
	SuppressedOutboxCount   int64
	CreationOutboxRefreshed bool
	// DeletedAttachmentDerivatives are (workspaceID, attachmentID) pairs whose
	// thumbnail objects should be cleaned from OSS post-commit (best-effort).
	DeletedAttachmentDerivatives []DeletedAttachmentDerivative
}

// UpdateServiceNotificationMessageTx updates an ordinary or interactive-card
// App notification without widening the Human edit primitive. Search and sync
// projections advance in the same transaction; browser realtime is emitted by
// the notification service from the returned authoritative row after commit.
func UpdateServiceNotificationMessageTx(ctx context.Context, tx coredb.Tx, request ServiceNotificationMessageUpdate) (ServiceNotificationMessageMutationResult, error) {
	if tx == nil {
		return ServiceNotificationMessageMutationResult{}, invalidArgument("service notification message update transaction is required")
	}
	request.WorkspaceID = strings.TrimSpace(request.WorkspaceID)
	request.ScopeID = strings.TrimSpace(request.ScopeID)
	request.MessageID = strings.TrimSpace(request.MessageID)
	request.AuthorMemberID = strings.TrimSpace(request.AuthorMemberID)
	request.Content = strings.TrimSpace(request.Content)
	if request.WorkspaceID == "" || request.ScopeID == "" || request.MessageID == "" || request.AuthorMemberID == "" {
		return ServiceNotificationMessageMutationResult{}, invalidArgument("service notification message update workspace, scope, message, and service member are required")
	}
	if request.Content == "" {
		return ServiceNotificationMessageMutationResult{}, invalidArgument("service notification message fallback content is required")
	}
	if request.UpdatedAt.IsZero() {
		request.UpdatedAt = time.Now().UTC()
	} else {
		request.UpdatedAt = request.UpdatedAt.UTC()
	}
	if _, err := DecodeMessageMetadata(request.Metadata); err != nil {
		return ServiceNotificationMessageMutationResult{}, err
	}

	locked, err := lockServiceNotificationMessage(ctx, tx, request.WorkspaceID, request.ScopeID, request.MessageID, request.AuthorMemberID)
	if err != nil {
		return ServiceNotificationMessageMutationResult{}, err
	}
	if metadata, decodeErr := DecodeMessageMetadata(locked.Metadata); decodeErr != nil {
		return ServiceNotificationMessageMutationResult{}, decodeErr
	} else if MessageMetadataIsTombstone(metadata) {
		return ServiceNotificationMessageMutationResult{}, conflict("withdrawn service notification messages cannot be updated")
	}
	if err := requireExpectedServiceMessageRevision(locked.EditRevision, request.ExpectedEditRevision); err != nil {
		return ServiceNotificationMessageMutationResult{}, err
	}

	result, err := tx.Exec(ctx, `
UPDATE scope_messages
SET content = $5,
    metadata = $6,
    updated_at = $7,
    edited_at = $7,
    edited_by_member_id = $4,
    edit_revision = edit_revision + 1
WHERE workspace_id = $1
  AND scope_id = $2
  AND id = $3
  AND author_type = 'service'
  AND author_member_id = $4
  AND COALESCE(message_type, '') IN ('', $8)
  AND edit_revision = $9
`, request.WorkspaceID, request.ScopeID, request.MessageID, request.AuthorMemberID, request.Content, nullableJSON(request.Metadata), request.UpdatedAt, MessageTypeAppInteractiveCard, locked.EditRevision)
	if err != nil {
		return ServiceNotificationMessageMutationResult{}, fmt.Errorf("update service notification message: %w", err)
	}
	if result.RowsAffected() != 1 {
		return ServiceNotificationMessageMutationResult{}, conflict("service notification message changed concurrently")
	}

	repos := NewScopeRepositories(tx)
	updated, err := repos.Messages.LoadByID(ctx, request.WorkspaceID, request.ScopeID, request.MessageID)
	if err != nil {
		return ServiceNotificationMessageMutationResult{}, err
	}
	if err := repos.Search.InsertDoc(ctx, ScopeSearchDocRecord{
		MessageID:   updated.ID,
		WorkspaceID: updated.WorkspaceID,
		ScopeID:     updated.ScopeID,
		GlobalSeq:   updated.WorkspaceGlobalSeq,
		Content:     updated.Content,
		CreatedAt:   updated.CreatedAt,
	}); err != nil {
		return ServiceNotificationMessageMutationResult{}, fmt.Errorf("update service notification search projection: %w", err)
	}
	outboxRefreshed, err := repos.Outbox.RefreshUndeliveredMessagePayload(ctx, updated)
	if err != nil {
		return ServiceNotificationMessageMutationResult{}, err
	}
	if err := repos.SyncDirty.MarkMessageDirtyForScope(ctx, updated.WorkspaceID, updated.ScopeID, updated.Seq, "service_notification_update", request.UpdatedAt); err != nil {
		return ServiceNotificationMessageMutationResult{}, err
	}
	return ServiceNotificationMessageMutationResult{Message: updated, Changed: true, CreationOutboxRefreshed: outboxRefreshed}, nil
}

// WithdrawServiceNotificationMessageTx applies the shared message tombstone
// lifecycle for a App-authored notification. Derived message sidecars are
// scrubbed by the same contract as Human recall, while the App notification
// card row remains owned by its domain so it can retain a closed audit state.
func WithdrawServiceNotificationMessageTx(ctx context.Context, tx coredb.Tx, request ServiceNotificationMessageWithdrawal) (ServiceNotificationMessageMutationResult, error) {
	if tx == nil {
		return ServiceNotificationMessageMutationResult{}, invalidArgument("service notification message withdrawal transaction is required")
	}
	request.WorkspaceID = strings.TrimSpace(request.WorkspaceID)
	request.ScopeID = strings.TrimSpace(request.ScopeID)
	request.MessageID = strings.TrimSpace(request.MessageID)
	request.AuthorMemberID = strings.TrimSpace(request.AuthorMemberID)
	request.PlaceholderContent = strings.TrimSpace(request.PlaceholderContent)
	request.Reason = strings.TrimSpace(request.Reason)
	if request.WorkspaceID == "" || request.ScopeID == "" || request.MessageID == "" || request.AuthorMemberID == "" {
		return ServiceNotificationMessageMutationResult{}, invalidArgument("service notification message withdrawal workspace, scope, message, and service member are required")
	}
	if request.PlaceholderContent == "" {
		return ServiceNotificationMessageMutationResult{}, invalidArgument("service notification message withdrawal placeholder is required")
	}
	if request.WithdrawnAt.IsZero() {
		request.WithdrawnAt = time.Now().UTC()
	} else {
		request.WithdrawnAt = request.WithdrawnAt.UTC()
	}

	locked, err := lockServiceNotificationMessage(ctx, tx, request.WorkspaceID, request.ScopeID, request.MessageID, request.AuthorMemberID)
	if err != nil {
		return ServiceNotificationMessageMutationResult{}, err
	}
	lockedMetadata, err := DecodeMessageMetadata(locked.Metadata)
	if err != nil {
		return ServiceNotificationMessageMutationResult{}, err
	}
	if MessageMetadataIsTombstone(lockedMetadata) {
		return ServiceNotificationMessageMutationResult{Message: locked, Changed: false}, nil
	}
	if err := requireExpectedServiceMessageRevision(locked.EditRevision, request.ExpectedEditRevision); err != nil {
		return ServiceNotificationMessageMutationResult{}, err
	}
	encodedMetadata, _, err := EncodeMessageMetadata(BuildServiceNotificationWithdrawalMetadata(request.AuthorMemberID, request.WithdrawnAt, request.PlaceholderContent, request.Reason))
	if err != nil {
		return ServiceNotificationMessageMutationResult{}, err
	}

	result, err := tx.Exec(ctx, `
UPDATE scope_messages
SET content = $5,
    metadata = $6,
    updated_at = $7,
    edited_at = $7,
    edited_by_member_id = $4,
    edit_revision = edit_revision + 1
WHERE workspace_id = $1
  AND scope_id = $2
  AND id = $3
  AND author_type = 'service'
  AND author_member_id = $4
  AND COALESCE(message_type, '') IN ('', $8)
  AND edit_revision = $9
`, request.WorkspaceID, request.ScopeID, request.MessageID, request.AuthorMemberID, request.PlaceholderContent, nullableJSON(encodedMetadata), request.WithdrawnAt, MessageTypeAppInteractiveCard, locked.EditRevision)
	if err != nil {
		return ServiceNotificationMessageMutationResult{}, fmt.Errorf("withdraw service notification message: %w", err)
	}
	if result.RowsAffected() != 1 {
		return ServiceNotificationMessageMutationResult{}, conflict("service notification message changed concurrently")
	}

	messageIDs := []string{request.MessageID}
	mutation := ServiceNotificationMessageMutationResult{Changed: true}
	mutation.SuppressedActivityCount, err = suppressScopeActivityEventsForMessages(ctx, tx, request.ScopeID, messageIDs, request.WithdrawnAt)
	if err != nil {
		return ServiceNotificationMessageMutationResult{}, err
	}
	mutation.SuppressedOutboxCount, err = suppressScopeOutboxForMessages(ctx, tx, request.ScopeID, messageIDs, request.WithdrawnAt, "service_notification_withdrawn")
	if err != nil {
		return ServiceNotificationMessageMutationResult{}, err
	}
	withdrawDerivatives, err := scrubScopeRecallMessageProjections(ctx, tx, request.ScopeID, messageIDs)
	if err != nil {
		return ServiceNotificationMessageMutationResult{}, err
	}
	mutation.DeletedAttachmentDerivatives = withdrawDerivatives
	repos := NewScopeRepositories(tx)
	mutation.Message, err = repos.Messages.LoadByID(ctx, request.WorkspaceID, request.ScopeID, request.MessageID)
	if err != nil {
		return ServiceNotificationMessageMutationResult{}, err
	}
	mutation.IsLatestVisibleMessage, err = isLatestVisibleScopeMessage(ctx, tx, request.WorkspaceID, request.ScopeID, mutation.Message.Seq)
	if err != nil {
		return ServiceNotificationMessageMutationResult{}, err
	}
	if err := repos.SyncDirty.MarkMessageDirtyForScope(ctx, request.WorkspaceID, request.ScopeID, mutation.Message.Seq, "service_notification_withdrawn", request.WithdrawnAt); err != nil {
		return ServiceNotificationMessageMutationResult{}, err
	}
	return mutation, nil
}

// BuildServiceNotificationWithdrawalMetadata uses the shared recall tombstone
// status so history, search, permalink, attachment, reaction, and sidecar
// predicates converge without adding a parallel message lifecycle.
func BuildServiceNotificationWithdrawalMetadata(authorMemberID string, withdrawnAt time.Time, placeholder string, reason string) map[string]any {
	metadata := BuildMessageRecallMetadata(strings.TrimSpace(authorMemberID), withdrawnAt.UTC().Format(time.RFC3339Nano), strings.TrimSpace(placeholder))
	recall, _ := metadata[MessageRecallMetadataKey].(map[string]any)
	if recall == nil {
		recall = map[string]any{"status": MessageLifecycleStatusRecalled}
		metadata[MessageRecallMetadataKey] = recall
	}
	recall["kind"] = "service_notification_withdrawal"
	if trimmedReason := strings.TrimSpace(reason); trimmedReason != "" {
		recall["reason"] = trimmedReason
	}
	return metadata
}

func lockServiceNotificationMessage(ctx context.Context, tx coredb.Tx, workspaceID string, scopeID string, messageID string, authorMemberID string) (ScopeMessageRecord, error) {
	var record ScopeMessageRecord
	var rawMetadata []byte
	var clientMessageID sql.NullString
	var idempotencyPayloadDigest sql.NullString
	var editedAt sql.NullTime
	err := tx.QueryRow(ctx, `
SELECT id, workspace_id, scope_id, seq, workspace_global_seq, author_member_id,
       author_type,
       COALESCE(NULLIF(notify_principal_member_id, ''), author_member_id),
       COALESCE(NULLIF(idempotency_subject_member_id, ''), author_member_id),
       client_message_id, idempotency_payload_digest, message_type, content, metadata, created_at,
       updated_at, edited_at, COALESCE(edited_by_member_id, ''), edit_revision
FROM scope_messages
WHERE workspace_id = $1
  AND scope_id = $2
  AND id = $3
  AND author_type = 'service'
  AND author_member_id = $4
  AND COALESCE(message_type, '') IN ('', $5)
LIMIT 1
FOR UPDATE
`, workspaceID, scopeID, messageID, authorMemberID, MessageTypeAppInteractiveCard).Scan(
		&record.ID,
		&record.WorkspaceID,
		&record.ScopeID,
		&record.Seq,
		&record.WorkspaceGlobalSeq,
		&record.AuthorMemberID,
		&record.AuthorType,
		&record.NotifyPrincipalMemberID,
		&record.IdempotencySubjectMemberID,
		&clientMessageID,
		&idempotencyPayloadDigest,
		&record.MessageType,
		&record.Content,
		&rawMetadata,
		&record.CreatedAt,
		&record.UpdatedAt,
		&editedAt,
		&record.EditedByMemberID,
		&record.EditRevision,
	)
	if errors.Is(err, sql.ErrNoRows) {
		return ScopeMessageRecord{}, ErrNotFound
	}
	if err != nil {
		return ScopeMessageRecord{}, fmt.Errorf("lock service notification message: %w", err)
	}
	record.ClientMessageID = clientMessageID.String
	record.IdempotencyPayloadDigest = idempotencyPayloadDigest.String
	record.Metadata = append([]byte(nil), rawMetadata...)
	record.EditedAt = nullableTimePtr(editedAt)
	return record, nil
}

func requireExpectedServiceMessageRevision(actual int64, expected *int64) error {
	if expected == nil {
		return nil
	}
	if *expected < 0 {
		return invalidArgument("expected service notification message edit revision cannot be negative")
	}
	if actual != *expected {
		return conflict("service notification message edit revision does not match")
	}
	return nil
}
