package message

import (
	"strings"
	"time"
)

const (
	ScopeMessageReasonDirectMention       = "direct_mention"
	ScopeMessageReasonControlledBroadcast = "controlled_broadcast"
	ScopeMessageReasonExplicitRecipient   = "explicit_recipient"
)

const (
	ScopeOutboxFanoutScope = "scope_fanout"
)

// MessagePlanSideEffectProfile makes the transaction side-effect contract
// explicit instead of inferring it from whether OutboxEvents happens to be
// empty. ApplyMessagePlanTx still performs no runtime wake, websocket, or
// waiter work directly; this profile only describes durable facts written in
// the transaction.
type MessagePlanSideEffectProfile string

const (
	// MessagePlanSideEffectProfileScopeFanout is the ordinary message profile:
	// the transaction writes the message-owned facts plus at least one
	// scope_fanout outbox job for post-commit delivery side effects.
	MessagePlanSideEffectProfileScopeFanout MessagePlanSideEffectProfile = "scope_fanout"

	// MessagePlanSideEffectProfileProductScopeFanout writes the authoritative
	// message and scope fanout outbox while excluding ordinary search and
	// sync/unread projections.
	MessagePlanSideEffectProfileProductScopeFanout MessagePlanSideEffectProfile = "product_scope_fanout"

	// MessagePlanSideEffectProfileNoOutbox is reserved for characterized
	// no-outbox paths that still need the authoritative message facts, search
	// projection, sync dirty marker, and scope counters.
	MessagePlanSideEffectProfileNoOutbox MessagePlanSideEffectProfile = "no_outbox"
)

// MessagePlanner compiles a send intent into a MessagePlan. It intentionally
// performs no SQL and is not the current production write owner; production
// send paths must apply durable facts through ApplyMessagePlanTx until the
// planner migration is explicit and complete.
type MessagePlanner struct {
	policy PolicyEngine
	now    func() time.Time
}

func NewMessagePlanner(policy PolicyEngine) MessagePlanner {
	return NewMessagePlannerWithClock(policy, time.Now)
}

func NewMessagePlannerWithClock(policy PolicyEngine, now func() time.Time) MessagePlanner {
	if now == nil {
		now = time.Now
	}
	return MessagePlanner{policy: policy, now: now}
}

type MessagePlanRequest struct {
	Intent       MessageIntent
	Actor        ActorContext
	Scope        ScopeContext
	LiveMentions []MentionTarget

	ControlledBroadcastRecipients []MentionTarget
	ExplicitRecipients            []MentionTarget
	TaskContextIngressRefs        []ScopeTaskContextIngressRef
}

type MessageIntent struct {
	WorkspaceID                string
	TargetScopeID              string
	ParentScopeID              string
	MessageID                  string
	ClientMessageID            string
	NotifyPrincipalMemberID    string
	IdempotencySubjectMemberID string
	IdempotencyPayloadDigest   string
	Seq                        int64
	WorkspaceGlobalSeq         int64
	MessageType                string
	Content                    string
	Metadata                   []byte
	SourceRefs                 []SourceRef
	ExplicitContext            bool
	CreatedAt                  time.Time
}

type ActorContext struct {
	MemberID      string
	ActorType     string
	OwnerMemberID string
	Membership    *ScopeMembershipRecord
}

type ScopeContext struct {
	TargetScopeID string
	ParentScopeID string
	RootMessageID string
}

type SourceRef struct {
	SourceScopeID   string
	SourceSeq       int64
	SourceMessageID string
	SourceKind      string
	SourceDedupeKey string
}

type MentionTarget struct {
	MemberID         string
	MemberType       string
	OwnerMemberID    string
	Grant            *ScopePrivateGrantRecord
	ParentMembership *ScopeMembershipRecord
	TargetMembership *ScopeMembershipRecord
}

type MessagePlan struct {
	MessageWrite           ScopeMessageRecord
	SourceWrites           []ScopeMessageSourceRecord
	SidecarWrites          MessageSidecarWrites
	MembershipMutations    []ScopeMembershipRecord
	NotificationMutations  []ScopeNotificationSettingRecord
	TaskContextIngressRefs []ScopeTaskContextIngressRef
	SideEffectProfile      MessagePlanSideEffectProfile
	OutboxEvents           []ScopeFanoutOutboxRecord
	Explain                []ScopePlanExplain
}

// MessageSidecarWrites describes message-owned sidecar deltas that must be
// applied inside the same transaction as the authoritative message row. Sidecar
// stores own their physical rows/blob storage, but the message plan owns the
// append/mutation moment so callers cannot write message sidecars out-of-band.
type MessageSidecarWrites struct {
	AttachmentClaims []AttachmentClaimWrite
	AttachmentCopies []AttachmentCopyWrite
}

func (writes MessageSidecarWrites) HasWrites() bool {
	return len(writes.AttachmentClaims) > 0 || len(writes.AttachmentCopies) > 0
}

type AttachmentClaimWrite struct {
	WorkspaceID        string
	ScopeID            string
	UploadedByMemberID string
	MessageID          string
	AttachmentIDs      []string
}

type AttachmentCopyWrite struct {
	WorkspaceID        string
	ScopeID            string
	UploadedByMemberID string
	MessageID          string
	SourceMessageIDs   []string
}

type ScopeTaskContextIngressRef struct {
	ScopeID     string
	TaskID      string
	ExecutionID string
	RecipientID string
	CreatedAt   time.Time
}

type ScopePlanExplain struct {
	MemberID            string
	Reason              string
	Allowed             bool
	AuthorizationSource string
	DenyReason          string
}

func (planner MessagePlanner) Plan(request MessagePlanRequest) MessagePlan {
	now := normalizedPlanTime(request.Intent.CreatedAt, planner.now)
	targetScopeID := firstNonEmpty(request.Intent.TargetScopeID, request.Scope.TargetScopeID)
	parentScopeID := firstNonEmpty(request.Intent.ParentScopeID, request.Scope.ParentScopeID)
	messageID := strings.TrimSpace(request.Intent.MessageID)
	workspaceID := strings.TrimSpace(request.Intent.WorkspaceID)
	actorMemberID := strings.TrimSpace(request.Actor.MemberID)

	plan := MessagePlan{
		MessageWrite: ScopeMessageRecord{
			ID:                         messageID,
			WorkspaceID:                workspaceID,
			ScopeID:                    targetScopeID,
			Seq:                        request.Intent.Seq,
			WorkspaceGlobalSeq:         request.Intent.WorkspaceGlobalSeq,
			AuthorMemberID:             actorMemberID,
			AuthorType:                 strings.TrimSpace(request.Actor.ActorType),
			NotifyPrincipalMemberID:    firstNonEmpty(request.Intent.NotifyPrincipalMemberID, actorMemberID),
			IdempotencySubjectMemberID: firstNonEmpty(request.Intent.IdempotencySubjectMemberID, actorMemberID),
			ClientMessageID:            strings.TrimSpace(request.Intent.ClientMessageID),
			IdempotencyPayloadDigest:   strings.TrimSpace(request.Intent.IdempotencyPayloadDigest),
			MessageType:                strings.TrimSpace(request.Intent.MessageType),
			Content:                    request.Intent.Content,
			Metadata:                   append([]byte(nil), request.Intent.Metadata...),
			CreatedAt:                  now,
			UpdatedAt:                  now,
		},
		SourceWrites:      make([]ScopeMessageSourceRecord, 0, len(request.Intent.SourceRefs)),
		SideEffectProfile: MessagePlanSideEffectProfileScopeFanout,
	}
	plan.OutboxEvents = append(plan.OutboxEvents, BuildScopeMessageFanoutOutboxRecord(ScopeMessageFanoutOutboxRecordParams{
		WorkspaceID:   workspaceID,
		ScopeID:       targetScopeID,
		MessageID:     messageID,
		DedupeKey:     "scope:" + targetScopeID + ":message:" + messageID + ":fanout:" + ScopeOutboxFanoutScope,
		CreatedAt:     now,
		NextAttemptAt: now,
	}))

	for index, source := range request.Intent.SourceRefs {
		plan.SourceWrites = append(plan.SourceWrites, ScopeMessageSourceRecord{
			TargetScopeID:   targetScopeID,
			MessageID:       messageID,
			Ordinal:         index,
			SourceScopeID:   strings.TrimSpace(source.SourceScopeID),
			SourceSeq:       source.SourceSeq,
			SourceMessageID: strings.TrimSpace(source.SourceMessageID),
			SourceKind:      strings.TrimSpace(source.SourceKind),
			SourceDedupeKey: strings.TrimSpace(source.SourceDedupeKey),
			CreatedAt:       now,
		})
	}

	mode := ResolveRoutingMentionMode(RoutingMentionModeRequest{
		ExplicitContext: request.Intent.ExplicitContext,
		SourceCount:     len(request.Intent.SourceRefs),
	})
	if mode == ScopeRoutingMentionModeNormal || mode == ScopeRoutingMentionModeSingleForward {
		planner.addMentionTargets(&plan, request, request.LiveMentions, ScopeMessageReasonDirectMention, false, now, targetScopeID, parentScopeID, workspaceID, messageID)
	}
	planner.addMentionTargets(&plan, request, request.ControlledBroadcastRecipients, ScopeMessageReasonControlledBroadcast, false, now, targetScopeID, parentScopeID, workspaceID, messageID)
	planner.addMentionTargets(&plan, request, request.ExplicitRecipients, ScopeMessageReasonExplicitRecipient, true, now, targetScopeID, parentScopeID, workspaceID, messageID)

	for _, ref := range request.TaskContextIngressRefs {
		ref.ScopeID = firstNonEmpty(ref.ScopeID, targetScopeID)
		if ref.CreatedAt.IsZero() {
			ref.CreatedAt = now
		}
		plan.TaskContextIngressRefs = append(plan.TaskContextIngressRefs, ref)
	}

	return plan
}

func (planner MessagePlanner) addMentionTargets(plan *MessagePlan, request MessagePlanRequest, targets []MentionTarget, messageReason string, explicitRecipient bool, now time.Time, targetScopeID string, parentScopeID string, workspaceID string, messageID string) {
	for _, target := range targets {
		memberID := strings.TrimSpace(target.MemberID)
		if memberID == "" {
			continue
		}
		memberType := strings.TrimSpace(target.MemberType)
		if memberType == "" {
			memberType = ScopeActorHuman
		}
		if !mentionTargetIsActiveMember(target, targetScopeID, request.Intent.Seq) {
			plan.Explain = append(plan.Explain, ScopePlanExplain{
				MemberID:   memberID,
				Reason:     messageReason,
				Allowed:    false,
				DenyReason: "target_not_in_scope",
			})
			continue
		}

		var authority PrivateAuthorityDecision
		if memberType == ScopeActorPrivateAgent {
			decision := planner.policy.EvaluateMentionPolicy(MentionPolicyRequest{
				ActorMemberID:          request.Actor.MemberID,
				ActorType:              request.Actor.ActorType,
				ActorOwnerMemberID:     request.Actor.OwnerMemberID,
				TargetMemberID:         memberID,
				TargetType:             memberType,
				TargetScopeID:          targetScopeID,
				ParentScopeID:          parentScopeID,
				TargetPrivateOwnerID:   target.OwnerMemberID,
				TargetParentMembership: target.ParentMembership,
				TargetMembership:       target.TargetMembership,
				ExplicitRecipient:      explicitRecipient,
				Grant:                  target.Grant,
				ActorMembership:        request.Actor.Membership,
			})
			if !decision.Allowed {
				plan.Explain = append(plan.Explain, ScopePlanExplain{
					MemberID:   memberID,
					Reason:     messageReason,
					Allowed:    false,
					DenyReason: decision.DenyReason,
				})
				continue
			}
			authority = decision.Authority
		}

		plan.Explain = append(plan.Explain, ScopePlanExplain{
			MemberID:            memberID,
			Reason:              messageReason,
			Allowed:             true,
			AuthorizationSource: authority.AuthorizationSource,
		})
	}
}

func mentionTargetIsActiveMember(target MentionTarget, targetScopeID string, messageSeq int64) bool {
	if target.TargetMembership == nil {
		return false
	}
	membership := *target.TargetMembership
	if strings.TrimSpace(membership.ScopeID) != strings.TrimSpace(targetScopeID) {
		return false
	}
	if strings.TrimSpace(membership.MemberID) != strings.TrimSpace(target.MemberID) {
		return false
	}
	return membershipCoversMessage(membership, messageSeq)
}

func normalizedPlanTime(createdAt time.Time, now func() time.Time) time.Time {
	if !createdAt.IsZero() {
		return createdAt
	}
	return now()
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if trimmed := strings.TrimSpace(value); trimmed != "" {
			return trimmed
		}
	}
	return ""
}
