package app

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

	"github.com/tidbcloud/AgentTeam/internal/core/authidentity"
	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/core/registration"
	"github.com/tidbcloud/AgentTeam/internal/core/secrets"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/pinyinsearch"
	"github.com/tidbcloud/AgentTeam/internal/store/access"
	"github.com/tidbcloud/AgentTeam/internal/store/agentops"
	"github.com/tidbcloud/AgentTeam/internal/store/attachment"
	"github.com/tidbcloud/AgentTeam/internal/store/channel"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/orgmembership"
	"github.com/tidbcloud/AgentTeam/internal/store/plan"
	"github.com/tidbcloud/AgentTeam/internal/store/reaction"
	"github.com/tidbcloud/AgentTeam/internal/store/rewind"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceaccount"
	"github.com/tidbcloud/AgentTeam/internal/store/team"
	"github.com/tidbcloud/AgentTeam/internal/store/workspace"
)

func (store *Store) Reaction() *reaction.Store {
	return reaction.New(store.db, reaction.Options{
		LookupMessageChannelID: store.LookupMessageChannelID,
		AuthorizeMessageSidecarReadForUser: func(ctx context.Context, queryer coredb.QueryExecer, userID string, workspaceID string, messageID string, kind message.SidecarKind) error {
			return store.authorizeMessageSidecarReadForUser(ctx, queryer, userID, workspaceID, messageID, kind)
		},
		AuthorizeMessageSidecarReadForMember: func(ctx context.Context, queryer coredb.QueryExecer, memberID string, workspaceID string, messageID string, kind message.SidecarKind) error {
			return store.authorizeMessageSidecarReadForMember(ctx, queryer, memberID, workspaceID, messageID, kind)
		},
		ResolveAuthorizedHumanMessageTargetWithQueryer: func(ctx context.Context, queryer reaction.HumanMessageTargetQueryer, userID string, workspaceID string, channelID string, channelAction access.HumanAction, dmAction access.HumanAction, threadAction access.HumanAction) (access.HumanWorkspaceMember, message.ThreadAccess, error) {
			return store.resolveAuthorizedHumanMessageTargetWithQueryer(ctx, queryer, userID, workspaceID, channelID, channelAction, dmAction, threadAction)
		},
		LoadPublicMemberNamesByMemberID: func(ctx context.Context, queryer reaction.MemberIdentityQueryer, workspaceID string, memberIDs []string) (map[string]string, error) {
			return loadPublicMemberNamesByMemberID(ctx, queryer, workspaceID, memberIDs)
		},
	})
}

func (store *Store) AgentOps() *agentops.Store {
	return agentops.New(store.db, agentops.Options{})
}

func (store *Store) attachmentStore() *attachment.Store {
	return attachment.New(store.db, store.archiveStorage, store.mediaObjectStorage, store.hotMessageRetentionDays, store.AttachmentOptions())
}

func (store *Store) claimAttachmentsForMessage(ctx context.Context, tx coredb.Tx, workspaceID string, scopeID string, uploadedByMemberID string, messageID string, attachmentIDs []string) ([]attachment.AppAttachment, error) {
	return store.attachmentStore().InTx(tx).ClaimAttachmentsForMessage(ctx, workspaceID, scopeID, uploadedByMemberID, messageID, attachmentIDs)
}

func (store *Store) loadAttachmentsForMessages(ctx context.Context, workspaceID string, messageIDs []string) (map[string][]attachment.AppAttachment, error) {
	return store.attachmentStore().LoadAttachmentsForMessages(ctx, workspaceID, messageIDs)
}

func (store *Store) GetWorkspaceForUser(ctx context.Context, userID string, workspaceID string) (workspace.Summary, error) {
	membership, err := store.requireWorkspaceAccess(ctx, userID, workspaceID)
	if err != nil {
		return workspace.Summary{}, err
	}

	row := store.db.QueryRow(ctx, `
SELECT slug, name
FROM workspaces
WHERE id = $1
`, workspaceID)
	summary := workspace.Summary{
		WorkspaceID:   workspaceID,
		MemberID:      membership.Member.ID,
		MemberUserID:  userID,
		WorkspaceRole: membership.WorkspaceRole,
	}
	if err := row.Scan(&summary.Slug, &summary.Name); err != nil {
		if coredb.IsNotFound(err) {
			return workspace.Summary{}, ErrForbidden
		}
		return workspace.Summary{}, fmt.Errorf("load workspace summary: %w", err)
	}
	return summary, nil
}

type workspaceInviteAcceptanceRecord struct {
	InviteID       string
	WorkspaceID    string
	OrganizationID string
	Slug           string
	Name           string
	Email          string
	Mobile         string
	InviterUserID  string
	AcceptedAt     *time.Time
	RevokedAt      *time.Time
	ExpiresAt      time.Time
}

type workspaceInviteUserTargets struct {
	Email  string
	Mobile string
}

func workspaceInviteTargetsForUser(user model.User) workspaceInviteUserTargets {
	var targets workspaceInviteUserTargets
	if user.HasVerifiedEmail() {
		email, err := authidentity.NormalizeStrictEmail(user.Email)
		if err != nil {
			email, err = authidentity.NormalizeEmail(user.Email)
		}
		if err == nil {
			targets.Email = email
		}
	}
	if user.HasVerifiedMobile() {
		mobile, err := authidentity.NormalizeMobile(user.Mobile)
		if err == nil {
			targets.Mobile = mobile
		}
	}
	return targets
}

func workspaceInviteMatchesUser(record workspaceInviteAcceptanceRecord, user model.User) bool {
	targets := workspaceInviteTargetsForUser(user)
	if email := strings.ToLower(strings.TrimSpace(record.Email)); email != "" {
		return targets.Email != "" && targets.Email == email
	}
	if mobile := strings.TrimSpace(record.Mobile); mobile != "" {
		normalizedMobile, err := authidentity.NormalizeMobile(mobile)
		if err != nil {
			return false
		}
		return targets.Mobile != "" && targets.Mobile == normalizedMobile
	}
	return false
}

func (store *Store) AcceptWorkspaceInvite(ctx context.Context, user model.User, workspaceID string, token string) (workspace.Summary, error) {
	tokenHash := secrets.HashToken(token)
	summary, found, err := store.acceptWorkspaceInvite(ctx, user, func(tx coredb.Tx) (workspaceInviteAcceptanceRecord, bool, error) {
		row := tx.QueryRow(ctx, `
SELECT i.id, w.id, w.organization_id, w.slug, w.name, i.email, i.mobile, i.created_by_user_id, i.accepted_at, i.revoked_at, i.expires_at
FROM workspace_invites i
JOIN workspaces w ON w.id = i.workspace_id
WHERE i.workspace_id = $1 AND i.token_hash = $2
FOR UPDATE
`, workspaceID, tokenHash)
		return scanWorkspaceInviteAcceptanceRecord(row)
	}, func(record workspaceInviteAcceptanceRecord) error {
		if !workspaceInviteMatchesUser(record, user) {
			return ErrForbidden
		}
		return nil
	}, func(tx coredb.Tx, _ workspaceInviteAcceptanceRecord) error {
		_, err := tx.Exec(ctx, `
UPDATE workspace_invites
SET accepted_at = NOW()
WHERE workspace_id = $1 AND token_hash = $2
`, workspaceID, tokenHash)
		return err
	})
	if err != nil {
		return workspace.Summary{}, err
	}
	if !found {
		return workspace.Summary{}, badRequest("invalid invite token")
	}
	return summary, nil
}

func (store *Store) AcceptWorkspaceInviteByIDIfExists(ctx context.Context, user model.User, inviteID string) (workspace.Summary, bool, error) {
	inviteID = strings.TrimSpace(inviteID)
	if inviteID == "" {
		return workspace.Summary{}, false, nil
	}
	return store.acceptWorkspaceInvite(ctx, user, func(tx coredb.Tx) (workspaceInviteAcceptanceRecord, bool, error) {
		row := tx.QueryRow(ctx, `
SELECT i.id, w.id, w.organization_id, w.slug, w.name, i.email, i.mobile, i.created_by_user_id, i.accepted_at, i.revoked_at, i.expires_at
FROM workspace_invites i
JOIN workspaces w ON w.id = i.workspace_id
WHERE i.id = $1
FOR UPDATE
`, inviteID)
		return scanWorkspaceInviteAcceptanceRecord(row)
	}, func(record workspaceInviteAcceptanceRecord) error {
		if record.AcceptedAt != nil {
			return badRequest("invite already accepted")
		}
		if !workspaceInviteMatchesUser(record, user) {
			return ErrForbidden
		}
		return nil
	}, func(tx coredb.Tx, record workspaceInviteAcceptanceRecord) error {
		_, err := tx.Exec(ctx, `
UPDATE workspace_invites
SET accepted_at = NOW()
WHERE id = $1
`, record.InviteID)
		return err
	})
}

func (store *Store) acceptWorkspaceInvite(
	ctx context.Context,
	user model.User,
	loadInvite func(coredb.Tx) (workspaceInviteAcceptanceRecord, bool, error),
	validateIdentity func(workspaceInviteAcceptanceRecord) error,
	markAccepted func(coredb.Tx, workspaceInviteAcceptanceRecord) error,
) (workspace.Summary, bool, error) {
	var summary workspace.Summary
	var found bool
	if err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		record, ok, err := loadInvite(tx)
		if err != nil {
			return err
		}
		if !ok {
			return nil
		}
		found = true
		if record.RevokedAt != nil {
			return badRequest("invite revoked")
		}
		if time.Now().UTC().After(record.ExpiresAt) {
			return badRequest("invite expired")
		}
		if validateIdentity != nil {
			if err := validateIdentity(record); err != nil {
				return err
			}
		}
		if _, err := orgmembership.EnsureWorkspaceInviteMemberInTx(ctx, tx, orgmembership.WorkspaceInviteMemberParams{
			OrganizationID: record.OrganizationID,
			UserID:         user.ID,
			InviterUserID:  record.InviterUserID,
			CheckOrganizationQuotaInTx: func(ctx context.Context, tx coredb.Tx, _ string, metric plan.MetricKey, delta int64) error {
				return store.checkWorkspaceQuotaInTx(ctx, tx, record.WorkspaceID, metric, delta)
			},
		}); err != nil {
			return err
		}

		membershipRow := tx.QueryRow(ctx, `
SELECT id, workspace_role, status
FROM members
WHERE workspace_id = $1 AND user_id = $2 AND type = 'human'
`, record.WorkspaceID, user.ID)
		var memberID string
		var workspaceRole string
		var memberStatus string
		membershipErr := membershipRow.Scan(&memberID, &workspaceRole, &memberStatus)
		if membershipErr != nil && !coredb.IsNotFound(membershipErr) {
			return fmt.Errorf("load existing workspace membership: %w", membershipErr)
		}
		if coredb.IsNotFound(membershipErr) || memberStatus != "active" {
			hasSeat, err := appHasActiveOrganizationSeat(ctx, tx, record.OrganizationID, user.ID)
			if err != nil {
				return err
			}
			if !hasSeat {
				if err := store.checkWorkspaceQuotaInTx(ctx, tx, record.WorkspaceID, plan.MetricSeats, 1); err != nil {
					return err
				}
			}
		}
		if coredb.IsNotFound(membershipErr) {
			now := time.Now().UTC()
			role := string(domain.WorkspaceRoleMember)
			crossDomainDisplayName := displayNameFromUser(user)
			crossDomainPinyinFull, crossDomainPinyinInitials := pinyinsearch.Variants(crossDomainDisplayName)
			memberID, err = insertGeneratedMember(identifiers.NewMemberID, func(memberID string) error {
				_, err := tx.Exec(ctx, `
INSERT INTO members (id, workspace_id, type, display_name, display_name_pinyin, display_name_pinyin_initials, user_id, workspace_role, status, created_at, updated_at)
VALUES ($1, $2, 'human', $3, $4, $5, $6, $7, 'active', $8, $8)
`, memberID, record.WorkspaceID, crossDomainDisplayName, crossDomainPinyinFull, crossDomainPinyinInitials, user.ID, role, now)
				return err
			}, "generate invite member id", "insert invited member")
			if err != nil {
				return err
			}
			workspaceRole = role
		} else if memberStatus != "active" {
			now := time.Now().UTC()
			if _, err := tx.Exec(ctx, `
UPDATE members
SET status = 'active', updated_at = $3
WHERE workspace_id = $1 AND user_id = $2 AND type = 'human'
`, record.WorkspaceID, user.ID, now); err != nil {
				return fmt.Errorf("reactivate invited member: %w", err)
			}
		}
		allChannelID, err := lookupAllChannelID(ctx, tx, record.WorkspaceID)
		if err != nil {
			return err
		}
		if err := store.ensureChannelScopeParticipantProjection(ctx, tx, allChannelID, memberID, "", time.Now().UTC()); err != nil {
			return fmt.Errorf("ensure #all for invited member: %w", err)
		}
		if err := serviceaccount.New(store.db).EnsureHumanAudienceMembershipsInTx(
			ctx,
			tx,
			record.WorkspaceID,
			memberID,
			time.Now().UTC(),
		); err != nil {
			return fmt.Errorf("ensure service account audience for invited member: %w", err)
		}
		if record.AcceptedAt == nil {
			if err := markAccepted(tx, record); err != nil {
				return fmt.Errorf("mark invite accepted: %w", err)
			}
		}
		summary = workspace.Summary{
			WorkspaceID:   record.WorkspaceID,
			Slug:          record.Slug,
			Name:          record.Name,
			MemberID:      memberID,
			MemberUserID:  user.ID,
			WorkspaceRole: workspaceRole,
			AllChannelID:  allChannelID,
		}
		return nil
	}); err != nil {
		return workspace.Summary{}, found, err
	}
	return summary, found, nil
}

func scanWorkspaceInviteAcceptanceRecord(row coredb.Row) (workspaceInviteAcceptanceRecord, bool, error) {
	var record workspaceInviteAcceptanceRecord
	var email sql.NullString
	var mobile sql.NullString
	if err := row.Scan(&record.InviteID, &record.WorkspaceID, &record.OrganizationID, &record.Slug, &record.Name, &email, &mobile, &record.InviterUserID, &record.AcceptedAt, &record.RevokedAt, &record.ExpiresAt); err != nil {
		if coredb.IsNotFound(err) {
			return workspaceInviteAcceptanceRecord{}, false, nil
		}
		return workspaceInviteAcceptanceRecord{}, false, fmt.Errorf("load invite: %w", err)
	}
	if email.Valid {
		record.Email = email.String
	}
	if mobile.Valid {
		record.Mobile = mobile.String
	}
	return record, true, nil
}

func displayNameFromUser(user model.User) string {
	if displayName := strings.TrimSpace(user.DisplayName); displayName != "" {
		return displayName
	}
	if name := strings.TrimSpace(user.Name); name != "" {
		return name
	}
	if email := strings.TrimSpace(user.Email); email != "" {
		return displayNameFromEmail(email)
	}
	if mobile := strings.TrimSpace(user.Mobile); mobile != "" {
		return authidentity.RedactMobile(mobile)
	}
	return "user"
}

func displayNameFromEmail(email string) string {
	localPart := strings.TrimSpace(strings.SplitN(email, "@", 2)[0])
	if localPart == "" {
		return "user"
	}
	return localPart
}

func appHasActiveOrganizationSeat(ctx context.Context, queryer coredb.QueryRower, organizationID string, userID string) (bool, error) {
	row := queryer.QueryRow(ctx, `
SELECT COUNT(*)
FROM (
  SELECT om.user_id AS user_id
  FROM organization_members om
  WHERE om.organization_id = $1
    AND om.user_id = $2
    AND om.status = 'active'
  UNION
  SELECT m.user_id AS user_id
  FROM members m
  JOIN workspaces w ON w.id = m.workspace_id
  WHERE w.organization_id = $1
    AND m.user_id = $2
    AND m.type = 'human'
    AND m.status = 'active'
) seats
`, organizationID, userID)
	var count int64
	if err := row.Scan(&count); err != nil {
		return false, fmt.Errorf("check active organization seat: %w", err)
	}
	return count > 0, nil
}

func loadReplyThreadParticipantAndAuthorDetailsMaps(ctx context.Context, queryer coredb.Queryer, threadChannelIDs []string) (map[string][]message.ReplyThreadParticipant, map[string][]message.ReplyThreadParticipant, error) {
	participantMap, authorMap, err := message.LoadReplyThreadParticipantAndAuthorDetailsMaps(ctx, queryer, threadChannelIDs)
	return participantMap, authorMap, mapMessagingError(err)
}

func (store *Store) syncReplyThreadSubscriptions(ctx context.Context, tx coredb.Tx, authorMemberID string, authorType domain.MemberType, threadChannelID string, parentMessageID string, mentionedMemberIDs []string, agentMentionMemberIDs []string, currentMessageSeq int64, currentMessageGlobalSeq int64, subscribedAt time.Time) error {
	trimmedAuthorMemberID := strings.TrimSpace(authorMemberID)
	trimmedThreadChannelID := strings.TrimSpace(threadChannelID)
	trimmedParentMessageID := strings.TrimSpace(parentMessageID)
	if trimmedAuthorMemberID == "" || trimmedThreadChannelID == "" || trimmedParentMessageID == "" {
		return fmt.Errorf("reply thread subscription context is incomplete")
	}

	if _, err := store.ensureReplyThreadScopeProjection(ctx, tx, trimmedThreadChannelID); err != nil {
		return err
	}
	if err := message.EnsureReplyThreadParentDMAgentParticipant(ctx, tx, trimmedThreadChannelID, subscribedAt); err != nil {
		return err
	}

	parentAuthorMemberID, err := store.loadReplyThreadParentAuthorMemberID(ctx, tx, trimmedThreadChannelID, trimmedParentMessageID)
	if err != nil {
		return err
	}
	if parentAuthorMemberID != "" && parentAuthorMemberID != trimmedAuthorMemberID {
		if err := message.SilenceDerivedReplyThreadParticipantFence(message.EnsureReplyThreadParticipantProjection(ctx, tx, trimmedThreadChannelID, parentAuthorMemberID, "auto-wake", subscribedAt)); err != nil {
			return err
		}
	}

	if authorType == domain.MemberTypeHuman {
		if err := message.ActivateThreadSubscriptionAtSeq(ctx, tx, trimmedAuthorMemberID, trimmedThreadChannelID, currentMessageSeq, subscribedAt); err != nil {
			return err
		}
	}

	rootMentionMemberIDs, err := store.loadReplyThreadRootMentionRecipientMemberIDs(ctx, tx, trimmedParentMessageID)
	if err != nil {
		return err
	}

	for _, memberID := range mergeReplyThreadSubscriptionMemberIDs(mentionedMemberIDs, agentMentionMemberIDs) {
		if err := message.SilenceDerivedReplyThreadParticipantFence(message.ActivateThreadSubscriptionWithInitialReadAtSeq(ctx, tx, memberID, trimmedThreadChannelID, currentMessageSeq-1, currentMessageGlobalSeq-1, currentMessageSeq, subscribedAt)); err != nil {
			return err
		}
	}
	for _, memberID := range mergeReplyThreadSubscriptionMemberIDs(rootMentionMemberIDs) {
		if err := message.SilenceDerivedReplyThreadParticipantFence(message.ActivateThreadSubscriptionWithInitialReadIfNotExplicitlyUnsubscribedAtSeq(ctx, tx, memberID, trimmedThreadChannelID, currentMessageSeq-1, currentMessageGlobalSeq-1, currentMessageSeq, subscribedAt)); err != nil {
			return err
		}
	}

	return nil
}

func (store *Store) loadReplyThreadRootMentionRecipientMemberIDs(ctx context.Context, tx coredb.Tx, parentMessageID string) ([]string, error) {
	trimmedParentMessageID := strings.TrimSpace(parentMessageID)
	if trimmedParentMessageID == "" {
		return nil, nil
	}

	var scopeID string
	var workspaceID string
	var authorMemberID string
	var content string
	var rawMetadata []byte
	err := tx.QueryRow(ctx, `
SELECT workspace_id, scope_id, author_member_id, content, metadata
FROM scope_messages
WHERE id = $1
LIMIT 1
`, trimmedParentMessageID).Scan(&workspaceID, &scopeID, &authorMemberID, &content, &rawMetadata)
	if err != nil {
		if coredb.IsNotFound(err) {
			return nil, nil
		}
		return nil, fmt.Errorf("load reply thread root message mentions: %w", err)
	}
	metadata, err := message.DecodeMessageMetadata(rawMetadata)
	if err != nil {
		return nil, err
	}
	recipients, err := message.LoadScopeMentionedAgentRecipientsWithMetadata(ctx, tx, strings.TrimSpace(scopeID), strings.TrimSpace(authorMemberID), content, metadata)
	if err != nil {
		return nil, fmt.Errorf("load reply thread root mention recipients: %w", err)
	}
	memberIDs := make([]string, 0, len(recipients))
	for _, recipient := range recipients {
		if memberID := strings.TrimSpace(recipient.AgentMemberID); memberID != "" {
			memberIDs = append(memberIDs, memberID)
		}
	}
	privateRecipients, err := store.resolveOwnerPrivateRecipientsForMessage(ctx, tx, strings.TrimSpace(workspaceID), message.ThreadAccess{
		ThreadType: domain.ThreadTypeChannel,
		ChannelID:  strings.TrimSpace(scopeID),
	}, strings.TrimSpace(authorMemberID), domain.MemberTypeHuman, content, ownerPrivateRecipientResolutionOptions{
		IncludeDirectMentions: true,
	})
	if err != nil {
		return nil, fmt.Errorf("load reply thread root private mention recipients: %w", err)
	}
	for _, recipient := range privateRecipients.DirectMentionRecipients {
		if memberID := strings.TrimSpace(recipient.AgentMemberID); memberID != "" {
			memberIDs = append(memberIDs, memberID)
		}
	}
	teamRecipients, err := store.loadReplyThreadRootTeamMentionRecipientMemberIDs(ctx, tx, strings.TrimSpace(workspaceID), strings.TrimSpace(scopeID), strings.TrimSpace(authorMemberID), metadata)
	if err != nil {
		return nil, fmt.Errorf("load reply thread root team mention recipients: %w", err)
	}
	memberIDs = append(memberIDs, teamRecipients...)
	return mergeReplyThreadSubscriptionMemberIDs(memberIDs), nil
}

func (store *Store) loadReplyThreadRootTeamMentionRecipientMemberIDs(ctx context.Context, tx coredb.Tx, workspaceID string, scopeID string, authorMemberID string, metadata map[string]any) ([]string, error) {
	if strings.TrimSpace(workspaceID) == "" || strings.TrimSpace(scopeID) == "" || strings.TrimSpace(authorMemberID) == "" {
		return nil, nil
	}
	persistedMentions := message.ExtractPersistedMentionMetadata(metadata)
	if len(persistedMentions) == 0 {
		return nil, nil
	}

	hints := make([]message.MessageMentionHint, 0, len(persistedMentions))
	tokens := make([]string, 0, len(persistedMentions))
	seenTokens := make(map[string]struct{}, len(persistedMentions))
	for _, mention := range persistedMentions {
		if strings.ToLower(strings.TrimSpace(mention.Type)) != "team" {
			continue
		}
		teamID := strings.TrimSpace(mention.ID)
		token := strings.ToLower(strings.TrimSpace(mention.Token))
		if teamID == "" || token == "" {
			continue
		}
		hints = append(hints, message.MessageMentionHint{
			Type:  "team",
			ID:    teamID,
			Token: token,
		})
		if _, ok := seenTokens[token]; ok {
			continue
		}
		seenTokens[token] = struct{}{}
		tokens = append(tokens, token)
	}
	if len(tokens) == 0 {
		return nil, nil
	}

	routing, err := resolveTeamMentionRecipientsForUser(ctx, team.New(store.db).InTx(tx), workspaceID, scopeID, authorMemberID, hints, tokens, nil)
	if err != nil {
		return nil, err
	}
	memberIDs := make([]string, 0, len(routing.recipients))
	for _, recipient := range routing.recipients {
		if memberID := strings.TrimSpace(recipient.AgentMemberID); memberID != "" {
			memberIDs = append(memberIDs, memberID)
		}
	}
	return mergeReplyThreadSubscriptionMemberIDs(memberIDs), nil
}

func mergeReplyThreadSubscriptionMemberIDs(groups ...[]string) []string {
	seen := make(map[string]struct{})
	merged := make([]string, 0)
	for _, group := range groups {
		for _, raw := range group {
			memberID := strings.TrimSpace(raw)
			if memberID == "" {
				continue
			}
			if _, ok := seen[memberID]; ok {
				continue
			}
			seen[memberID] = struct{}{}
			merged = append(merged, memberID)
		}
	}
	return merged
}

func (store *Store) loadReplyThreadParentAuthorMemberID(ctx context.Context, tx coredb.Tx, threadChannelID string, parentMessageID string) (string, error) {
	row := tx.QueryRow(ctx, `
SELECT COALESCE(m.author_member_id, '')
FROM scopes reply_scope
LEFT JOIN scope_messages m
  ON m.id = reply_scope.root_message_id
 AND m.scope_id = reply_scope.parent_scope_id
WHERE reply_scope.id = $1
  AND reply_scope.root_message_id = $2
  AND reply_scope.type = 'thread'
  AND reply_scope.status = 'active'
`, strings.TrimSpace(threadChannelID), strings.TrimSpace(parentMessageID))

	var authorMemberID string
	if err := row.Scan(&authorMemberID); err != nil {
		if coredb.IsNotFound(err) {
			return "", nil
		}
		return "", fmt.Errorf("load parent message author for thread subscription: %w", err)
	}
	return strings.TrimSpace(authorMemberID), nil
}

func writeThreadReadStateByGlobalSeqWithExecer(ctx context.Context, execer channel.CommandTagExecer, memberID string, threadID string, lastReadSeq int64, lastReadGlobalSeq int64, updatedAt time.Time) error {
	return message.WriteThreadReadStateByGlobalSeq(ctx, execer, memberID, threadID, lastReadSeq, lastReadGlobalSeq, updatedAt)
}

func writeThreadOwnerContextDismissStateWithExecer(ctx context.Context, execer channel.CommandTagExecer, memberID string, threadID string, ownerContextDismissedThroughGlobalSeq int64) error {
	return message.WriteThreadOwnerContextDismissState(ctx, execer, memberID, threadID, ownerContextDismissedThroughGlobalSeq)
}

func loadThreadOwnerContextDismissedThroughGlobalSeq(ctx context.Context, queryer coredb.QueryRower, memberID string, threadID string) (int64, error) {
	return message.LoadThreadOwnerContextDismissedThroughGlobalSeq(ctx, queryer, memberID, threadID)
}

func buildThreadTimelineReadQuery(scopeID string, viewerMemberID string, limit int, beforeSeq int64) (string, []any) {
	return message.BuildThreadTimelineReadQuery(scopeID, viewerMemberID, limit, beforeSeq)
}

func preferredDisplayName(values ...string) string {
	return registration.PreferredDisplayName(values...)
}

func legacyTargetKeys(preferred string, aliases ...string) []string {
	return registration.LegacyTargetKeys(preferred, aliases...)
}

func messageMentionContextForTarget(target message.ThreadAccess) message.MentionContext {
	return message.MentionContextForTarget(target)
}

func writeScopeParticipantReadStateForTargetWithExecer(ctx context.Context, execer coredb.QueryExecer, target message.ThreadAccess, memberID string, lastReadSeq int64, lastReadGlobalSeq int64, updatedAt time.Time, exact bool) error {
	scopeID := message.ScopeIDForThreadAccess(target)
	if strings.TrimSpace(scopeID) == "" {
		return nil
	}
	_, err := execer.Exec(ctx, `
UPDATE scope_memberships
SET unread_generation = CASE
        WHEN (CASE WHEN $6 THEN $3 ELSE GREATEST(last_read_seq, $3) END) <> last_read_seq
          OR (CASE WHEN $6 THEN $4 ELSE GREATEST(last_read_global_seq, $4) END) <> last_read_global_seq THEN unread_generation + 1
        ELSE unread_generation
    END,
    last_read_seq = CASE WHEN $6 THEN $3 ELSE GREATEST(last_read_seq, $3) END,
    last_read_global_seq = CASE WHEN $6 THEN $4 ELSE GREATEST(last_read_global_seq, $4) END,
    updated_at = $5
WHERE member_id = $1 AND scope_id = $2 AND removed_at IS NULL AND effective_to_seq IS NULL
`, strings.TrimSpace(memberID), strings.TrimSpace(scopeID), lastReadSeq, lastReadGlobalSeq, updatedAt.UTC(), exact)
	if err != nil {
		return fmt.Errorf("update target scope membership read state: %w", err)
	}
	return nil
}

func (store *Store) RewindOptions() rewind.Options {
	return rewind.Options{
		LookupMessageChannelID: store.LookupMessageChannelID,
		ResolveAuthorizedHumanMessageTarget: func(ctx context.Context, userID string, workspaceID string, channelID string) (access.HumanWorkspaceMember, message.ThreadAccess, error) {
			return store.resolveAuthorizedHumanMessageTarget(ctx, userID, workspaceID, channelID, channelReadAction, dmReadAction, threadReadAction)
		},
		RequireAgentOperation: func(ctx context.Context, userID string, workspaceID string, agentID string) (access.HumanAgentAccess, error) {
			return store.requireAgentOperation(ctx, userID, workspaceID, agentID)
		},
		ReplaceReplyThreadParticipants: message.ReplaceReplyThreadParticipants,
		MergeReplyThreadParticipantIDs: message.MergeReplyThreadParticipantIDs,
	}
}
