package fanout

import (
	"context"
	"fmt"
	"sort"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	messaging "github.com/tidbcloud/AgentTeam/internal/service/message"
	"github.com/tidbcloud/AgentTeam/internal/store/identity"
	storemessage "github.com/tidbcloud/AgentTeam/internal/store/message"
	runruntime "github.com/tidbcloud/AgentTeam/internal/workflow/orchestrator"
)

// WorkflowMessageOwnerResolver resolves the human identity that owns a
// workflow-triggered send_message effect.
type WorkflowMessageOwnerResolver interface {
	FindUserByID(ctx context.Context, userID string) (identity.User, error)
}

// WorkflowRunMessageWriter executes first-class Run message effects through
// the ordinary human message + mention fanout path. It deliberately uses the
// frozen Invocation owner. The ordinary message store fences that write to the
// frozen member inside the same transaction that creates the message and
// fanout receipt, so a deleted/rebound identity cannot leak a side effect under
// another human.
type WorkflowRunMessageWriter struct {
	owners     WorkflowMessageOwnerResolver
	targets    WorkflowRunMessageTargetResolver
	responders WorkflowRunCallbackResponderResolver
	messaging  HumanMessageWriter
	fanout     DeliveryFanout
	receipts   WorkflowRunMessageReceiptResolver
}

// WorkflowRunMessageTargetResolver resolves either a legacy author-facing
// target (for example #all) or a first-class existing scope into the canonical
// destination expected by the ordinary Human message writer. Keeping legacy
// target interpretation outside the reducer preserves its frozen effect
// request; the scope form is validated, authorized, and reserved without
// creating or reinterpreting topology.
type WorkflowRunMessageTargetResolver interface {
	ResolveWorkflowRunMessageTargetScope(ctx context.Context, workspaceID string, ownerUserID string, authorMemberID string, targetRef string, clientMessageID string) (domain.ResolvedMessageTarget, error)
	ResolveWorkflowRunMessageScope(ctx context.Context, workspaceID string, ownerUserID string, authorMemberID string, scope domain.MessageScopeValue, clientMessageID string) (domain.ResolvedMessageTarget, error)
}

// WorkflowRunMessageReceiptResolver recovers the exact wake recipients and
// delivery scope from the committed scope-fanout outbox when the ordinary
// message writer returns a clientMessageId replay. Human-message replay
// intentionally does not re-route mentions, so callback authorization must
// use this durable receipt rather than current Agent membership or a second
// mention inference pass.
type WorkflowRunMessageReceiptResolver interface {
	LoadWorkflowRunMessageReceipt(context.Context, runruntime.MessageRequest, string) (runruntime.MessageResult, error)
}

// WorkflowRunCallbackResponderResolver resolves the canonical callback Agent
// for a rendered send_message.callback request before delivery, using the same
// trusted AgentRef/destination-visibility chain as the ordinary message
// transaction without committing a message.
type WorkflowRunCallbackResponderResolver interface {
	ResolveWorkflowRunCallbackResponderID(context.Context, string, string, string, string, *domain.MessageScopeValue, string, string) (string, error)
}

func NewWorkflowRunMessageWriter(owners WorkflowMessageOwnerResolver, targets WorkflowRunMessageTargetResolver, dependencies Dependencies) WorkflowRunMessageWriter {
	return WorkflowRunMessageWriter{
		owners:    owners,
		targets:   targets,
		messaging: dependencies.HumanMessages,
		fanout: NewDeliveryFanout(dependencies.Hub, dependencies.StreamWriter, dependencies.Logger).
			WithReplyThreadRootPreloader(dependencies.ReplyThreadRootPreloader).
			WithScopeFanoutMarker(dependencies.ScopeFanoutMarker).
			WithTaskScopeLifecycleGuard(dependencies.TaskScopeLifecycle).
			WithUnreadRuntime(dependencies.UnreadRuntime).
			WithUnreadDeltaSink(dependencies.UnreadDeltaSink),
	}
}

func (writer WorkflowRunMessageWriter) WithReceiptResolver(resolver WorkflowRunMessageReceiptResolver) WorkflowRunMessageWriter {
	writer.receipts = resolver
	return writer
}

// WithResponderResolver wires the trusted pre-delivery callback responder
// resolver used to freeze the workspace+Agent execution capacity.
func (writer WorkflowRunMessageWriter) WithResponderResolver(resolver WorkflowRunCallbackResponderResolver) WorkflowRunMessageWriter {
	writer.responders = resolver
	return writer
}

// ResolveCallbackResponder implements runruntime.CallbackResponderResolver. It
// resolves the canonical callback Agent through the same trusted chain the
// ordinary message transaction uses, without committing a message, so the
// frozen responder can gate the (workspace, Agent) execution capacity.
func (writer WorkflowRunMessageWriter) ResolveCallbackResponder(ctx context.Context, request runruntime.MessageRequest) (string, error) {
	if err := request.Validate(); err != nil {
		return "", err
	}
	if writer.owners == nil {
		return "", fmt.Errorf("workflow Run callback owner resolver is not configured")
	}
	if writer.responders == nil {
		return "", fmt.Errorf("workflow Run callback responder resolver is not configured")
	}
	owner, err := writer.owners.FindUserByID(ctx, strings.TrimSpace(request.OwnerUserID))
	if err != nil {
		return "", err
	}
	var scope *domain.MessageScopeValue
	if request.Scope != nil {
		copied := *request.Scope
		scope = &copied
	}
	return writer.responders.ResolveWorkflowRunCallbackResponderID(
		ctx,
		strings.TrimSpace(request.WorkspaceID),
		strings.TrimSpace(owner.ID),
		strings.TrimSpace(request.AuthorMemberID),
		strings.TrimSpace(request.TargetRef),
		scope,
		strings.TrimSpace(request.ResponderAgentRef),
		strings.TrimSpace(request.ClientMessageID),
	)
}

// RecoverRunMessageReceipt reconstructs a receipt after the native message
// transaction committed and bound a MessageID but the dispatcher lost the
// writer response. This path intentionally bypasses owner, member, target, and
// message writers: those are mutable routing inputs and are no longer relevant
// once the immutable outbox receipt exists.
func (writer WorkflowRunMessageWriter) RecoverRunMessageReceipt(ctx context.Context, request runruntime.MessageRequest, messageID string) (runruntime.MessageResult, error) {
	if err := request.Validate(); err != nil {
		return runruntime.MessageResult{}, err
	}
	messageID = strings.TrimSpace(messageID)
	if messageID == "" {
		return runruntime.MessageResult{}, fmt.Errorf("workflow Run bound message id is required")
	}
	if writer.receipts == nil {
		return runruntime.MessageResult{}, fmt.Errorf("workflow Run message receipt resolver is not configured")
	}
	receipt, err := writer.receipts.LoadWorkflowRunMessageReceipt(ctx, request, messageID)
	if err != nil {
		return runruntime.MessageResult{}, err
	}
	receipt = receipt.Normalize()
	if receipt.MessageID != messageID {
		return runruntime.MessageResult{}, fmt.Errorf("workflow Run message recovery receipt identity mismatch")
	}
	if err := receipt.Validate(); err != nil {
		return runruntime.MessageResult{}, fmt.Errorf("workflow Run message recovery receipt is invalid: %w", err)
	}
	return receipt, nil
}

func (writer WorkflowRunMessageWriter) WriteRunMessage(ctx context.Context, request runruntime.MessageRequest) (runruntime.MessageResult, error) {
	if err := request.Validate(); err != nil {
		return runruntime.MessageResult{}, err
	}
	if writer.owners == nil {
		return runruntime.MessageResult{}, fmt.Errorf("workflow Run message owner resolver is not configured")
	}
	if writer.messaging == nil {
		return runruntime.MessageResult{}, fmt.Errorf("workflow Run message service is not configured")
	}
	if writer.targets == nil {
		return runruntime.MessageResult{}, fmt.Errorf("workflow Run message target resolver is not configured")
	}
	owner, err := writer.owners.FindUserByID(ctx, strings.TrimSpace(request.OwnerUserID))
	if err != nil {
		return runruntime.MessageResult{}, err
	}
	resolvedTarget, err := writer.resolveDestination(ctx, request, owner.ID)
	if err != nil {
		return runruntime.MessageResult{}, err
	}
	deliveryScope, err := domain.MessageScopeValueFromResolvedTarget(resolvedTarget)
	if err != nil {
		return runruntime.MessageResult{}, fmt.Errorf("workflow Run message destination resolver returned invalid scope: %w", err)
	}
	if request.StartThread && deliveryScope.Kind != domain.MessageScopeKindChannel {
		return runruntime.MessageResult{}, fmt.Errorf("workflow Run message startThread requires a Channel root destination")
	}
	appMessage, delivery, err := SendAndFanoutHumanMessage(
		ctx, writer.messaging, writer.fanout, owner,
		request.WorkspaceID, deliveryScope.ScopeID, request.Content, nil,
		workflowRunMessageSendOptions(request, deliveryScope),
	)
	if err != nil {
		return runruntime.MessageResult{}, err
	}
	messageID := strings.TrimSpace(delivery.MessageID)
	if messageID == "" {
		messageID = strings.TrimSpace(appMessage.ID)
	}
	if messageID == "" {
		return runruntime.MessageResult{}, fmt.Errorf("workflow Run message writer returned no message id")
	}
	agents := make([]string, 0, len(delivery.WakeRecipients))
	seen := make(map[string]struct{}, len(delivery.WakeRecipients))
	for _, recipient := range delivery.WakeRecipients {
		agentID := strings.TrimSpace(recipient.AgentID)
		if agentID == "" {
			continue
		}
		if _, ok := seen[agentID]; ok {
			continue
		}
		seen[agentID] = struct{}{}
		agents = append(agents, agentID)
	}
	sort.Strings(agents)
	callbackResponderAgentID, err := storemessage.WorkflowCallbackResponderAgentID(appMessage.Metadata)
	if err != nil {
		return runruntime.MessageResult{}, err
	}
	var recoveredReceipt *runruntime.MessageResult
	if request.Callback && delivery.IdempotentReplay && len(agents) == 0 {
		receipt, receiptErr := writer.RecoverRunMessageReceipt(ctx, request, messageID)
		if receiptErr != nil {
			return runruntime.MessageResult{}, receiptErr
		}
		if *receipt.DeliveryScope != deliveryScope {
			return runruntime.MessageResult{}, fmt.Errorf("workflow Run callback message replay receipt scope does not match reserved destination")
		}
		agents = receipt.WakeAgentIDs
		callbackResponderAgentID = receipt.CallbackResponderAgentID
		recoveredReceipt = &receipt
	}
	callbackResponderAgentID, err = runruntime.ResolveCallbackResponderAuthority(
		request.Callback,
		callbackResponderAgentID,
		agents,
	)
	if err != nil {
		return runruntime.MessageResult{}, err
	}
	threadScope, err := workflowRunStartedThreadScope(request.StartThread, messageID, delivery)
	if err != nil {
		return runruntime.MessageResult{}, err
	}
	// The frozen minimal member context was atomically persisted into the
	// message metadata by the ordinary write transaction after it authorized the
	// responder against the frozen Run Team. Recovery reuses the durable outbox
	// receipt's frozen context when the send was an idempotent replay. Reading
	// the frozen snapshot rather than re-querying mutable Team/member state
	// guarantees callback/retry/replay and recovery reuse the exact first-frozen
	// value.
	frozenMemberContext, err := workflowFrozenMemberContext(appMessage, recoveredReceipt)
	if err != nil {
		return runruntime.MessageResult{}, err
	}
	return runruntime.MessageResult{
		MessageID: messageID, WakeAgentIDs: agents, CallbackResponderAgentID: callbackResponderAgentID,
		DeliveryScope: &deliveryScope, ThreadScope: threadScope,
		IdempotentReplay: delivery.IdempotentReplay, TeamMemberContext: frozenMemberContext,
	}, nil
}

// workflowFrozenMemberContext decodes the frozen member context persisted in
// workflow message metadata. A recovered durable receipt takes precedence on
// idempotent replay; otherwise the committed message metadata is read directly.
// A missing value is accepted for non-Team or legacy messages; malformed
// present data fails closed so a corrupted snapshot can never be re-frozen.
func workflowFrozenMemberContext(appMessage storemessage.AppMessage, recovered *runruntime.MessageResult) (*runruntime.CallbackTeamMemberContext, error) {
	if recovered != nil && recovered.TeamMemberContext != nil {
		return recovered.TeamMemberContext, nil
	}
	wire, err := storemessage.WorkflowTeamMemberContextFromMetadata(appMessage.Metadata)
	if err != nil {
		return nil, err
	}
	if wire == nil {
		return nil, nil
	}
	return &runruntime.CallbackTeamMemberContext{
		TeamID:          wire.TeamID,
		TeamHandle:      wire.TeamHandle,
		TeamDisplayName: wire.TeamDisplayName,
		TeamPurpose:     wire.TeamPurpose,
		TeamMemberID:    wire.TeamMemberID,
		Role:            wire.Role,
		RoleLabel:       wire.RoleLabel,
		RoleOverlay:     wire.RoleOverlay,
	}, nil
}

func workflowRunMessageSendOptions(request runruntime.MessageRequest, deliveryScope domain.MessageScopeValue) messaging.SendOptions {
	var taskWatchHandoff *storemessage.WorkflowCallbackTaskWatchHandoff
	if request.TaskWatchHandoff != nil {
		taskWatchHandoff = &storemessage.WorkflowCallbackTaskWatchHandoff{
			TaskScopeID:         request.TaskWatchHandoff.TaskScope.ScopeID,
			PredecessorAgentIDs: append([]string(nil), request.TaskWatchHandoff.CausalPredecessorAgentIDs...),
		}
	}
	// A recovery nudge is a separate wake message for an already-awaiting
	// callback; it must not bind to the awaiting effect (whose frozen request
	// carries the original clientMessageId). Keep the typed workflow_run source
	// for canonical automation identity and projection/replay, while the
	// internal skip bit suppresses only the new effect binding.
	source := storemessage.ContributionSourceForWorkflowRun(storemessage.WorkflowRunContributionSourceParams{
		RunID: request.RunID, EffectID: request.EffectID, PlanID: request.PlanID,
		WorkflowName: request.WorkflowName, SourceKind: request.SourceKind,
	})
	source.SkipWorkflowRunMessageBinding = request.SkipWorkflowRunMessageBinding
	return messaging.SendOptions{
		ClientMessageID:                          request.ClientMessageID,
		ExpectedAuthorMemberID:                   request.AuthorMemberID,
		Source:                                   source,
		AllowWorkflowContextMetadata:             true,
		RequireSingleWorkflowCallbackResponder:   request.Callback,
		WorkflowCallbackResponderAgentRef:        strings.TrimSpace(request.ResponderAgentRef),
		WorkflowRunTeamID:                        strings.TrimSpace(request.TeamID),
		WorkflowCallbackExpectedResponderAgentID: strings.TrimSpace(request.ExpectedCallbackResponderAgentID),
		WorkflowCallbackTaskWatchHandoff:         taskWatchHandoff,
		StartAgentThread:                         request.StartThread,
		Metadata: storemessage.BuildWorkflowContextMetadata(storemessage.WorkflowContextMetadataParams{
			WorkspaceID:   request.WorkspaceID,
			PlanID:        request.PlanID,
			PlanVersionID: request.PlanVersionID,
			RunID:         request.RunID,
			// The effect id identifies the exact callback attempt. A stale
			// message therefore cannot complete a later retry of this node.
			EffectID:          request.EffectID,
			NodeID:            request.NodeID,
			SourceKind:        request.SourceKind,
			DeliveryScope:     &deliveryScope,
			CallbackExpected:  request.Callback,
			ResponderAgentRef: request.ResponderAgentRef,
			Presentation:      request.Presentation,
		}),
	}
}

func workflowRunStartedThreadScope(startThread bool, messageID string, delivery storemessage.MessageDelivery) (*domain.MessageScopeValue, error) {
	if !startThread {
		return nil, nil
	}
	if delivery.StartedReplyThread == nil {
		return nil, fmt.Errorf("workflow Run startThread message writer returned no started reply thread receipt")
	}
	if strings.TrimSpace(delivery.StartedReplyThread.RootMessageID) != messageID {
		return nil, fmt.Errorf("workflow Run startThread message writer returned a mismatched reply thread root")
	}
	scope, err := domain.NormalizeMessageScopeValue(domain.MessageScopeValue{
		Kind:    domain.MessageScopeKindThread,
		ScopeID: strings.TrimSpace(delivery.StartedReplyThread.ThreadChannelID),
	})
	if err != nil {
		return nil, fmt.Errorf("workflow Run startThread message writer returned an invalid reply thread scope: %w", err)
	}
	return &scope, nil
}

func (writer WorkflowRunMessageWriter) resolveDestination(
	ctx context.Context,
	request runruntime.MessageRequest,
	ownerUserID string,
) (domain.ResolvedMessageTarget, error) {
	if request.Scope != nil {
		resolved, err := writer.targets.ResolveWorkflowRunMessageScope(
			ctx,
			strings.TrimSpace(request.WorkspaceID),
			strings.TrimSpace(ownerUserID),
			strings.TrimSpace(request.AuthorMemberID),
			*request.Scope,
			strings.TrimSpace(request.ClientMessageID),
		)
		if err != nil {
			return domain.ResolvedMessageTarget{}, fmt.Errorf(
				"resolve workflow Run message scope %s:%s: %w",
				request.Scope.Kind, request.Scope.ScopeID, err,
			)
		}
		return resolved, nil
	}
	resolved, err := writer.targets.ResolveWorkflowRunMessageTargetScope(
		ctx,
		strings.TrimSpace(request.WorkspaceID),
		strings.TrimSpace(ownerUserID),
		strings.TrimSpace(request.AuthorMemberID),
		strings.TrimSpace(request.TargetRef),
		strings.TrimSpace(request.ClientMessageID),
	)
	if err != nil {
		return domain.ResolvedMessageTarget{}, fmt.Errorf(
			"resolve workflow Run message target %q: %w",
			strings.TrimSpace(request.TargetRef),
			err,
		)
	}
	return resolved, nil
}

var _ runruntime.MessageWriter = WorkflowRunMessageWriter{}
var _ runruntime.MessageReceiptRecoverer = WorkflowRunMessageWriter{}
var _ runruntime.CallbackResponderResolver = WorkflowRunMessageWriter{}
