package app

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/privateagent"
)

type appendMessageOptions struct {
	AttachmentIDs            []string
	AllowControlledBroadcast bool
	// AllowWorkflowContextMetadata is an internal-only capability bit. Keep it
	// false for every public/app/Agent authored write; trusted workflow effect
	// writers opt in before reaching this final conversation-write boundary.
	AllowWorkflowContextMetadata bool
	MentionHints                 []message.MessageMentionHint
	MessageType                  string
	ExplicitRoutingPlan          *message.AgentRoutingPlan
	Metadata                     map[string]any
	Source                       message.ContributionSource
	// RoutingPrincipalMemberID is the member whose authority resolves mentions
	// and routing. It may differ from the durable service/app author.
	RoutingPrincipalMemberID string
	ClientMessageID          string
	// IdempotencyPayloadDigest is an optional trusted digest of the immutable
	// authored payload. Workflow callback writes use it because the ordinary
	// message transaction materializes a visible canonical mention and derived
	// responder metadata before persistence; those server-derived bytes must not
	// change the clientMessageId identity of the authored effect.
	IdempotencyPayloadDigest string
	InterruptPolicy          string
	Freshness                message.SendMessageFreshness
	// NotifyPrincipalMemberID is an optional human principal for source-adapter
	// wake/notification policy. It must not replace the message author.
	NotifyPrincipalMemberID    string
	IdempotencySubjectMemberID string
	OwnerQuoteSourceMessageIDs []string
	ForceScopeID               string
	ForceMessageID             string
	ForceWorkspaceGlobalSeq    int64
	AllocateWorkspaceGlobalSeq bool
	CreatedAt                  time.Time
	// SuppressAgentRouting keeps App notification messages visible to Human
	// scope members without creating Agent durable/wake delivery intents.
	SuppressAgentRouting bool
}

// messageAppendLifecycle makes every caller of the lowest shared conversation
// append seam declare who will finalize its durable payload and first
// post-commit delivery. The executable producer inventory owns the complete
// callsite set, so a new producer cannot silently select an existing policy.
type messageAppendLifecycle uint8

const (
	messageAppendCallerOwned messageAppendLifecycle = iota + 1
	messageAppendAppServiceContracted
	messageAppendStoreOwnedRecoverable
)

func (lifecycle messageAppendLifecycle) valid() bool {
	switch lifecycle {
	case messageAppendCallerOwned, messageAppendAppServiceContracted, messageAppendStoreOwnedRecoverable:
		return true
	default:
		return false
	}
}

func (store *Store) appendMessageToThread(ctx context.Context, tx coredb.Tx, authorMemberID string, authorType domain.MemberType, target message.ThreadAccess, content string, attachmentIDs []string, options appendMessageOptions, lifecycle messageAppendLifecycle) (message.MessageDelivery, error) {
	result, err := store.appendMessageToThreadResult(ctx, tx, authorMemberID, authorType, target, content, attachmentIDs, options, lifecycle)
	if err != nil {
		return message.MessageDelivery{}, err
	}
	if result.State == message.SendStateHeld {
		return message.MessageDelivery{}, badRequest("message send was held by freshness gate")
	}
	return result.Delivery, nil
}

func (store *Store) appendMessageToThreadResult(ctx context.Context, tx coredb.Tx, authorMemberID string, authorType domain.MemberType, target message.ThreadAccess, content string, attachmentIDs []string, options appendMessageOptions, lifecycle messageAppendLifecycle) (message.SendMessageResult, error) {
	if !lifecycle.valid() {
		return message.SendMessageResult{}, fmt.Errorf("message append lifecycle policy is required")
	}
	if err := message.ValidateWorkflowContextMetadata(options.Metadata, options.AllowWorkflowContextMetadata); err != nil {
		return message.SendMessageResult{}, badRequest(err.Error())
	}
	normalizedSource, err := options.Source.NormalizeForWrite()
	if err != nil {
		return message.SendMessageResult{}, mapMessagingError(err)
	}
	options.Source = normalizedSource
	if message.SystemCardKind(options.Metadata) != "" {
		return message.SendMessageResult{}, badRequest("system card message metadata is retired")
	}
	messageSemantics := message.ClassifyMessageSemantics(options.MessageType, options.Metadata)
	if !messageSemantics.CurrentWriteAllowed() {
		return message.SendMessageResult{}, badRequest("message type is not allowed for active writes")
	}

	explicitPlan := options.ExplicitRoutingPlan
	result, err := store.privateAgentProjectionStore().WriteConversationMessageResult(ctx, tx, privateagent.WriteConversationMessageParams{
		AuthorMemberID:             authorMemberID,
		AuthorType:                 authorType,
		Target:                     target,
		Content:                    content,
		AttachmentIDs:              attachmentIDs,
		CreatedAt:                  options.CreatedAt,
		OwnerQuoteSourceMessageIDs: options.OwnerQuoteSourceMessageIDs,
		AllowControlledBroadcast:   options.AllowControlledBroadcast,
		MentionHints:               options.MentionHints,
		MessageType:                options.MessageType,
		ExplicitRoutingPlan:        explicitPlan,
		Metadata:                   options.Metadata,
		Source:                     options.Source,
		RoutingPrincipalMemberID:   options.RoutingPrincipalMemberID,
		ClientMessageID:            options.ClientMessageID,
		IdempotencyPayloadDigest:   options.IdempotencyPayloadDigest,
		InterruptPolicy:            options.InterruptPolicy,
		Freshness:                  options.Freshness,
		NotifyPrincipalMemberID:    options.NotifyPrincipalMemberID,
		IdempotencySubjectMemberID: options.IdempotencySubjectMemberID,
		ForceScopeID:               options.ForceScopeID,
		ForceMessageID:             options.ForceMessageID,
		ForceWorkspaceGlobalSeq:    options.ForceWorkspaceGlobalSeq,
		AllocateWorkspaceGlobalSeq: options.AllocateWorkspaceGlobalSeq,
	})
	if err == nil && !options.Source.IsZero() && result.Delivery.AppMessage != nil {
		result.Delivery.AppMessage.Source = options.Source.Normalize()
	}
	return result, err
}

func appChannelIDForTarget(target message.ThreadAccess) string {
	trimmedChannelID := strings.TrimSpace(target.ChannelID)
	if trimmedChannelID != "" {
		return trimmedChannelID
	}
	return strings.TrimSpace(target.DMThreadID)
}
