package message

import (
	"context"
	"fmt"
	"strings"

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

type MessageApplyResult struct {
	Message     ScopeMessageInsertResult
	Attachments []attachment.AppAttachment
}

// MessageSidecarWriter is the narrow storage port used by ApplyMessagePlanTx
// to apply message-owned sidecar writes without importing the app/root store.
type MessageSidecarWriter interface {
	ClaimAttachmentsForMessage(ctx context.Context, tx coredb.Tx, claim AttachmentClaimWrite) ([]attachment.AppAttachment, error)
	CopyMessageAttachments(ctx context.Context, tx coredb.Tx, copyWrite AttachmentCopyWrite) ([]attachment.AppAttachment, error)
}

// ApplyMessagePlanTx is the current production transaction boundary for
// durable message facts. Callers may still build MessagePlan values near their
// domain-specific send paths, but the actual scope_messages write and its
// coupled projections must stay behind this function. MessagePlanner is a plan
// compiler surface, not a production write owner, until the send inputs are
// migrated behind one compiler contract.
func ApplyMessagePlanTx(ctx context.Context, tx coredb.Tx, plan MessagePlan) (MessageApplyResult, error) {
	if plan.SidecarWrites.HasWrites() {
		return MessageApplyResult{}, fmt.Errorf("message plan sidecar writes require ApplyMessagePlanTxWithSidecarWriter")
	}
	return applyMessagePlanTx(ctx, tx, plan, nil)
}

// ApplyMessagePlanTxWithSidecarWriter applies the authoritative message facts and
// append-time sidecar deltas behind one transaction boundary. This keeps the
// sidecar write timing in the message domain while delegating physical sidecar
// storage to a narrow writer.
func ApplyMessagePlanTxWithSidecarWriter(ctx context.Context, tx coredb.Tx, plan MessagePlan, writer MessageSidecarWriter) (MessageApplyResult, error) {
	if plan.SidecarWrites.HasWrites() && writer == nil {
		return MessageApplyResult{}, fmt.Errorf("message plan sidecar writer is required")
	}
	return applyMessagePlanTx(ctx, tx, plan, writer)
}

func applyMessagePlanTx(ctx context.Context, tx coredb.Tx, plan MessagePlan, writer MessageSidecarWriter) (MessageApplyResult, error) {
	if err := validateMessagePlanSideEffectProfile(plan); err != nil {
		return MessageApplyResult{}, err
	}
	normalizedMessage, err := normalizeScopeMessageCreateContract(plan.MessageWrite)
	if err != nil {
		return MessageApplyResult{}, err
	}
	plan.MessageWrite = normalizedMessage
	repos := NewScopeRepositories(tx)
	appliedMessage, err := applyScopeMessageWrite(ctx, repos.Messages, plan.MessageWrite)
	if err != nil {
		return MessageApplyResult{}, fmt.Errorf("apply scope message: %w", err)
	}
	result := MessageApplyResult{Message: appliedMessage}
	if !appliedMessage.Inserted {
		return result, nil
	}
	if err := validateInsertedMessagePlanScopeFanoutOutboxes(plan); err != nil {
		return MessageApplyResult{}, err
	}

	for _, record := range plan.SourceWrites {
		if err := repos.Sources.Insert(ctx, record); err != nil {
			return MessageApplyResult{}, fmt.Errorf("apply scope message source: %w", err)
		}
	}
	attachments, err := applyMessageSidecarWrites(ctx, tx, plan.SidecarWrites, writer)
	if err != nil {
		return MessageApplyResult{}, err
	}
	result.Attachments = attachments
	if len(attachments) > 0 {
		if err := ReconcileChannelAttachmentProjectionForMessage(ctx, tx, plan.MessageWrite.ID); err != nil {
			return MessageApplyResult{}, fmt.Errorf("reconcile channel attachment projection: %w", err)
		}
	}
	productScope := plan.SideEffectProfile == MessagePlanSideEffectProfileProductScopeFanout
	if !productScope {
		if err := repos.Search.InsertDoc(ctx, ScopeSearchDocRecord{
			MessageID:   plan.MessageWrite.ID,
			WorkspaceID: plan.MessageWrite.WorkspaceID,
			ScopeID:     plan.MessageWrite.ScopeID,
			GlobalSeq:   plan.MessageWrite.WorkspaceGlobalSeq,
			Content:     plan.MessageWrite.Content,
			CreatedAt:   plan.MessageWrite.CreatedAt,
		}); err != nil {
			return MessageApplyResult{}, fmt.Errorf("apply scope search doc: %w", err)
		}
	}
	for _, record := range plan.MembershipMutations {
		if err := repos.Memberships.Upsert(ctx, record); err != nil {
			return MessageApplyResult{}, fmt.Errorf("apply scope membership: %w", err)
		}
	}
	for _, record := range plan.NotificationMutations {
		if err := repos.Notifications.Upsert(ctx, record); err != nil {
			return MessageApplyResult{}, fmt.Errorf("apply scope notification: %w", err)
		}
	}
	if len(plan.TaskContextIngressRefs) > 0 {
		return MessageApplyResult{}, fmt.Errorf("apply task context ingress refs: task-native ingress writer is not connected")
	}
	for _, record := range plan.OutboxEvents {
		if err := repos.Outbox.Insert(ctx, record); err != nil {
			return MessageApplyResult{}, fmt.Errorf("apply scope fanout outbox: %w", err)
		}
	}
	humanVisible := IsHumanVisibleScopeMessage(plan.MessageWrite)
	recentAuthorMemberID := ""
	if IsReplyThreadPreviewAuthor(plan.MessageWrite) {
		recentAuthorMemberID = plan.MessageWrite.AuthorMemberID
	}
	if humanVisible && !productScope {
		if err := repos.SyncDirty.MarkMessageDirtyForScope(
			ctx,
			plan.MessageWrite.WorkspaceID,
			plan.MessageWrite.ScopeID,
			plan.MessageWrite.Seq,
			"message",
			plan.MessageWrite.CreatedAt,
		); err != nil {
			return MessageApplyResult{}, fmt.Errorf("apply message sync dirty scope: %w", err)
		}
	}
	if err := repos.Scopes.AdvanceMessageStats(ctx, plan.MessageWrite.ScopeID, plan.MessageWrite.Seq, plan.MessageWrite.WorkspaceGlobalSeq, plan.MessageWrite.CreatedAt, humanVisible, recentAuthorMemberID); err != nil {
		return MessageApplyResult{}, fmt.Errorf("apply scope counters: %w", err)
	}
	return result, nil
}

func applyMessageSidecarWrites(ctx context.Context, tx coredb.Tx, writes MessageSidecarWrites, writer MessageSidecarWriter) ([]attachment.AppAttachment, error) {
	if !writes.HasWrites() {
		return nil, nil
	}
	if writer == nil {
		return nil, fmt.Errorf("message sidecar writer is required")
	}
	var attachments []attachment.AppAttachment
	for _, claim := range writes.AttachmentClaims {
		if len(claim.AttachmentIDs) == 0 {
			continue
		}
		claimed, err := writer.ClaimAttachmentsForMessage(ctx, tx, claim)
		if err != nil {
			return nil, fmt.Errorf("apply attachment sidecar claim: %w", err)
		}
		attachments = append(attachments, claimed...)
	}
	for _, copyWrite := range writes.AttachmentCopies {
		if len(copyWrite.SourceMessageIDs) == 0 {
			continue
		}
		copied, err := writer.CopyMessageAttachments(ctx, tx, copyWrite)
		if err != nil {
			return nil, fmt.Errorf("apply attachment sidecar copy: %w", err)
		}
		attachments = append(attachments, copied...)
	}
	return attachments, nil
}

func validateMessagePlanSideEffectProfile(plan MessagePlan) error {
	profile := plan.SideEffectProfile
	if profile == "" {
		profile = MessagePlanSideEffectProfileScopeFanout
	}
	switch profile {
	case MessagePlanSideEffectProfileScopeFanout:
		if !hasScopeFanoutOutboxEvent(plan.OutboxEvents) {
			return fmt.Errorf("message plan side-effect profile %q requires a %q outbox event", profile, ScopeOutboxFanoutScope)
		}
	case MessagePlanSideEffectProfileProductScopeFanout:
		if !hasScopeFanoutOutboxEvent(plan.OutboxEvents) {
			return fmt.Errorf("message plan side-effect profile %q requires a %q outbox event", profile, ScopeOutboxFanoutScope)
		}
	case MessagePlanSideEffectProfileNoOutbox:
		if len(plan.OutboxEvents) > 0 {
			return fmt.Errorf("message plan side-effect profile %q must not include outbox events", profile)
		}
	default:
		return fmt.Errorf("unknown message plan side-effect profile %q", profile)
	}
	return nil
}

func hasScopeFanoutOutboxEvent(records []ScopeFanoutOutboxRecord) bool {
	for _, record := range records {
		if record.FanoutType == ScopeOutboxFanoutScope {
			return true
		}
	}
	return false
}

func validateInsertedMessagePlanScopeFanoutOutboxes(plan MessagePlan) error {
	profile := plan.SideEffectProfile
	if profile == "" {
		profile = MessagePlanSideEffectProfileScopeFanout
	}
	if profile != MessagePlanSideEffectProfileScopeFanout && profile != MessagePlanSideEffectProfileProductScopeFanout {
		return nil
	}
	for _, record := range plan.OutboxEvents {
		if record.FanoutType != ScopeOutboxFanoutScope {
			continue
		}
		if err := validateMessagePlanScopeFanoutOutbox(plan, record); err != nil {
			return err
		}
	}
	return nil
}

func validateMessagePlanScopeFanoutOutbox(plan MessagePlan, record ScopeFanoutOutboxRecord) error {
	messageID := strings.TrimSpace(plan.MessageWrite.ID)
	expected := BuildScopeMessageFanoutOutboxRecord(ScopeMessageFanoutOutboxRecordParams{
		WorkspaceID:   plan.MessageWrite.WorkspaceID,
		ScopeID:       plan.MessageWrite.ScopeID,
		MessageID:     messageID,
		DedupeKey:     strings.TrimSpace(record.DedupeKey),
		Payload:       record.Payload,
		CreatedAt:     record.CreatedAt,
		NextAttemptAt: record.NextAttemptAt,
	})
	if strings.TrimSpace(record.MessageID) != expected.MessageID ||
		strings.TrimSpace(record.EventRefType) != expected.EventRefType ||
		strings.TrimSpace(record.EventRefID) != expected.EventRefID {
		return fmt.Errorf("message plan scope fanout outbox must use the delivery intent message source builder")
	}
	if strings.TrimSpace(record.ScopeID) != strings.TrimSpace(plan.MessageWrite.ScopeID) ||
		strings.TrimSpace(record.WorkspaceID) != strings.TrimSpace(plan.MessageWrite.WorkspaceID) {
		return fmt.Errorf("message plan scope fanout outbox workspace and scope must match the message")
	}
	return nil
}

func applyScopeMessageWrite(ctx context.Context, store MessageStore, record ScopeMessageRecord) (ScopeMessageInsertResult, error) {
	if strings.TrimSpace(record.ClientMessageID) == "" {
		if err := store.Insert(ctx, record); err != nil {
			return ScopeMessageInsertResult{}, err
		}
		return ScopeMessageInsertResult{Inserted: true, Message: record}, nil
	}
	return store.InsertIdempotent(ctx, record)
}
