package message

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

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

func ResolveReplyThreadParticipantID(ctx context.Context, tx coredb.Tx, authorMemberID string, authorType domain.MemberType) (string, error) {
	trimmedAuthorMemberID := strings.TrimSpace(authorMemberID)
	if trimmedAuthorMemberID == "" {
		return "", fmt.Errorf("author member id is required")
	}

	switch authorType {
	case domain.MemberTypeHuman:
		row := tx.QueryRow(ctx, `SELECT user_id FROM members WHERE id = $1 AND type = 'human' AND status = 'active'`, trimmedAuthorMemberID)
		var userID string
		if err := row.Scan(&userID); err != nil {
			if coredb.IsNotFound(err) {
				return "", notFound("member not found")
			}
			return "", fmt.Errorf("load reply thread participant user id: %w", err)
		}
		trimmedUserID := strings.TrimSpace(userID)
		if trimmedUserID == "" {
			return "", fmt.Errorf("invalid user id for member")
		}
		return trimmedUserID, nil
	case domain.MemberTypeAgent:
		row := tx.QueryRow(ctx, `SELECT id FROM agents WHERE member_id = $1`, trimmedAuthorMemberID)
		var agentID string
		if err := row.Scan(&agentID); err != nil {
			if coredb.IsNotFound(err) {
				return "", notFound("agent not found")
			}
			return "", fmt.Errorf("load reply thread participant agent id: %w", err)
		}
		trimmedAgentID := strings.TrimSpace(agentID)
		if trimmedAgentID == "" {
			return "", fmt.Errorf("invalid agent id for member")
		}
		return trimmedAgentID, nil
	default:
		return "", fmt.Errorf("invalid author type %q", authorType)
	}
}

func LoadReplyThreadStatsForUpdate(ctx context.Context, tx coredb.Tx, threadChannelID string, parentMessageID string) (int64, *time.Time, error) {
	row := tx.QueryRow(ctx, `
SELECT child.visible_message_count, child.visible_last_message_at
FROM scopes child
WHERE child.id = $1
  AND child.root_message_id = $2
  AND child.type = 'thread'
  AND child.status = 'active'
  AND child.archived_at IS NULL
`, strings.TrimSpace(threadChannelID), strings.TrimSpace(parentMessageID))

	var replyCount int64
	var lastReplyAt *time.Time
	if err := row.Scan(&replyCount, &lastReplyAt); err != nil {
		if coredb.IsNotFound(err) {
			return 0, nil, notFound("reply thread not found")
		}
		return 0, nil, fmt.Errorf("load reply thread stats: %w", err)
	}
	return replyCount, lastReplyAt, nil
}

func LoadReplyThreadSummaryForUpdate(ctx context.Context, tx coredb.Tx, threadChannelID string, parentMessageID string) (int64, *time.Time, []string, error) {
	replyCount, lastReplyAt, err := LoadReplyThreadStatsForUpdate(ctx, tx, threadChannelID, parentMessageID)
	if err != nil {
		return 0, nil, nil, err
	}
	participantIDs, err := LoadReplyThreadParticipantIDs(ctx, tx, threadChannelID, parentMessageID)
	if err != nil {
		return 0, nil, nil, err
	}
	return replyCount, lastReplyAt, participantIDs, nil
}

func LoadReplyThreadParticipantIDs(ctx context.Context, queryer coredb.Queryer, threadChannelID string, parentMessageID string) ([]string, error) {
	return loadReplyThreadParticipantIDs(ctx, queryer, threadChannelID, parentMessageID)
}

func LoadReplyThreadParticipantMap(ctx context.Context, queryer coredb.Queryer, threadChannelIDs []string) (map[string][]string, error) {
	return loadReplyThreadParticipantMap(ctx, queryer, threadChannelIDs)
}

func LoadReplyThreadParticipantDetailsMap(ctx context.Context, queryer coredb.Queryer, threadChannelIDs []string) (map[string][]ReplyThreadParticipant, error) {
	return loadReplyThreadParticipantDetailsMap(ctx, queryer, threadChannelIDs)
}

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

func resolveReplyThreadParticipantMemberIDs(ctx context.Context, queryer coredb.Queryer, participantIDs []string) ([]string, error) {
	normalizedParticipantIDs := NormalizeReplyThreadParticipantIDs(participantIDs)
	if len(normalizedParticipantIDs) == 0 {
		return nil, nil
	}

	requestedParticipantIDsCTE, requestedParticipantIDArgs, _, err := coredb.BuildIndexedStringValuesCTE("requested_participant_ids", "participant_id", "ordinality", normalizedParticipantIDs, 1)
	if err != nil {
		return nil, fmt.Errorf("build reply thread participant resolver: %w", err)
	}

	rows, err := queryer.Query(ctx, fmt.Sprintf(`
WITH %s
SELECT COALESCE(agent.member_id, human.id, direct_member.id, '') AS member_id
FROM requested_participant_ids
LEFT JOIN agents agent
  ON agent.id = requested_participant_ids.participant_id
LEFT JOIN members human
  ON human.user_id = requested_participant_ids.participant_id
 AND human.type = 'human'
 AND human.status = 'active'
LEFT JOIN members direct_member
  ON direct_member.id = requested_participant_ids.participant_id
 AND direct_member.status = 'active'
WHERE COALESCE(agent.member_id, human.id, direct_member.id, '') <> ''
ORDER BY requested_participant_ids.ordinality ASC
`, requestedParticipantIDsCTE), requestedParticipantIDArgs...)
	if err != nil {
		return nil, fmt.Errorf("resolve reply thread participant members: %w", err)
	}
	defer rows.Close()

	memberIDs := make([]string, 0, len(normalizedParticipantIDs))
	seen := make(map[string]struct{}, len(normalizedParticipantIDs))
	for rows.Next() {
		var memberID string
		if err := rows.Scan(&memberID); err != nil {
			return nil, fmt.Errorf("scan reply thread participant member: %w", err)
		}
		trimmedMemberID := strings.TrimSpace(memberID)
		if trimmedMemberID == "" {
			continue
		}
		if _, exists := seen[trimmedMemberID]; exists {
			continue
		}
		seen[trimmedMemberID] = struct{}{}
		memberIDs = append(memberIDs, trimmedMemberID)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate reply thread participant members: %w", err)
	}
	return memberIDs, nil
}

func ReplaceReplyThreadParticipants(ctx context.Context, tx coredb.Tx, threadChannelID string, participantIDs []string) error {
	trimmedThreadChannelID := strings.TrimSpace(threadChannelID)
	if trimmedThreadChannelID == "" {
		return fmt.Errorf("threadChannelID is required")
	}
	return ensureReplyThreadParticipantIDProjections(ctx, tx, trimmedThreadChannelID, participantIDs, time.Now().UTC())
}

func ensureReplyThreadParticipantIDProjections(ctx context.Context, tx coredb.Tx, threadChannelID string, participantIDs []string, participatedAt time.Time) error {
	normalizedParticipantIDs := NormalizeReplyThreadParticipantIDs(participantIDs)
	memberIDs, err := resolveReplyThreadParticipantMemberIDs(ctx, tx, normalizedParticipantIDs)
	if err != nil {
		return err
	}
	unsubscribedMemberIDs, err := loadExplicitlyUnsubscribedThreadMemberIDSet(ctx, tx, threadChannelID, memberIDs)
	if err != nil {
		return err
	}
	now := participatedAt.UTC()
	if now.IsZero() {
		now = time.Now().UTC()
	}

	parentInactiveMemberIDs := make(map[string]struct{})
	for _, memberID := range memberIDs {
		if _, unsubscribed := unsubscribedMemberIDs[memberID]; unsubscribed {
			// Preserve the explicit unsubscribe (do not project), but still run
			// the parent-authority fence. A historical removed interval may
			// coexist with a later active child interval, so skipping the fence
			// here would also keep that stale child membership in desired.
			fenceErr := withChannelReplyThreadFence(ctx, tx, threadChannelID, memberID, func(coredb.QueryExecer) error {
				return nil
			})
			if errors.Is(fenceErr, ErrScopeParentMembershipInactive) {
				parentInactiveMemberIDs[memberID] = struct{}{}
			}
			if err := SilenceDerivedReplyThreadParticipantFence(fenceErr); err != nil {
				return err
			}
			continue
		}
		// This participant set is a derived summary of historical reply authors,
		// not an authority grant. A participant who no longer has an active
		// parent-channel membership must be omitted rather than revived or
		// allowed to block the current legitimate writer. The direct author
		// subscription path remains fail-closed later in message finalization.
		projectionErr := ensureReplyThreadParticipantProjectionState(ctx, tx, threadChannelID, memberID, "auto-wake", now)
		if errors.Is(projectionErr, ErrScopeParentMembershipInactive) {
			parentInactiveMemberIDs[memberID] = struct{}{}
		}
		if err := SilenceDerivedReplyThreadParticipantFence(projectionErr); err != nil {
			return err
		}
	}

	rows, err := tx.Query(ctx, `
SELECT thread_membership.member_id
FROM scope_memberships thread_membership
JOIN members thread_member
  ON thread_member.id = thread_membership.member_id
 AND thread_member.type IN ('human', 'agent')
WHERE thread_membership.scope_id = $1
  AND thread_membership.removed_at IS NULL
  AND thread_membership.archived_at IS NULL
  AND thread_membership.effective_to_seq IS NULL
ORDER BY thread_membership.member_id ASC
`, threadChannelID)
	if err != nil {
		return fmt.Errorf("load active reply thread participants: %w", err)
	}

	desired := make(map[string]struct{}, len(memberIDs))
	for _, memberID := range memberIDs {
		if _, parentInactive := parentInactiveMemberIDs[memberID]; parentInactive {
			continue
		}
		desired[memberID] = struct{}{}
	}
	memberIDsToRemove := make([]string, 0)
	for rows.Next() {
		var memberID string
		if err := rows.Scan(&memberID); err != nil {
			rows.Close()
			return fmt.Errorf("scan active reply thread participant: %w", err)
		}
		trimmedMemberID := strings.TrimSpace(memberID)
		if _, keep := desired[trimmedMemberID]; keep {
			continue
		}
		if trimmedMemberID == "" {
			continue
		}
		memberIDsToRemove = append(memberIDsToRemove, trimmedMemberID)
	}
	if err := rows.Err(); err != nil {
		rows.Close()
		return fmt.Errorf("iterate active reply thread participants: %w", err)
	}
	rows.Close()

	if err := removeReplyThreadParticipants(ctx, tx, threadChannelID, memberIDsToRemove, now); err != nil {
		return err
	}
	return nil
}

func removeReplyThreadParticipants(ctx context.Context, execer CommandTagExecer, threadChannelID string, memberIDs []string, removedAt time.Time) error {
	normalizedMemberIDs := normalizeNonEmptyStrings(memberIDs)
	if len(normalizedMemberIDs) == 0 {
		return nil
	}

	removedMemberIDsCTE, removedMemberIDArgs, _, err := coredb.BuildStringValuesCTE("removed_member_ids", "member_id", normalizedMemberIDs, 3)
	if err != nil {
		return fmt.Errorf("build reply thread participant removal: %w", err)
	}
	args := []any{strings.TrimSpace(threadChannelID), removedAt.UTC()}
	args = append(args, removedMemberIDArgs...)

	if _, err := execer.Exec(ctx, fmt.Sprintf(`
WITH %s
UPDATE scope_memberships
SET unread_generation = unread_generation + 1,
    removed_at = $2,
    archived_at = COALESCE(archived_at, $2),
    updated_at = $2
WHERE scope_id = $1
  AND member_id IN (SELECT member_id FROM removed_member_ids)
  AND removed_at IS NULL
  AND archived_at IS NULL
  AND effective_to_seq IS NULL
`, removedMemberIDsCTE), args...); err != nil {
		return fmt.Errorf("remove reply thread participants: %w", err)
	}
	return nil
}

func BumpReplyThreadSummary(ctx context.Context, tx coredb.Tx, threadChannelID string, parentMessageID string, lastReplyAt time.Time, participantID string) (ReplyThreadUpdated, error) {
	trimmedThreadChannelID := strings.TrimSpace(threadChannelID)
	if trimmedThreadChannelID == "" {
		return ReplyThreadUpdated{}, fmt.Errorf("threadChannelID is required")
	}
	trimmedParentMessageID := strings.TrimSpace(parentMessageID)
	if trimmedParentMessageID == "" {
		return ReplyThreadUpdated{}, fmt.Errorf("parentMessageID is required")
	}
	trimmedParticipantID := strings.TrimSpace(participantID)

	replyCount, computedLastReplyAt, existingParticipantIDs, err := LoadReplyThreadSummaryForUpdate(ctx, tx, trimmedThreadChannelID, trimmedParentMessageID)
	if err != nil {
		return ReplyThreadUpdated{}, err
	}

	updatedParticipantIDs := MergeReplyThreadParticipantIDs(existingParticipantIDs, []string{trimmedParticipantID})
	lastReplyAtValue := lastReplyAt.UTC()
	if computedLastReplyAt != nil && !computedLastReplyAt.IsZero() {
		lastReplyAtValue = computedLastReplyAt.UTC()
	}
	if err := ReplaceReplyThreadParticipants(ctx, tx, trimmedThreadChannelID, updatedParticipantIDs); err != nil {
		return ReplyThreadUpdated{}, err
	}
	participantMap, authorMap, err := loadReplyThreadParticipantAndAuthorDetailsMaps(ctx, tx, []string{trimmedThreadChannelID})
	if err != nil {
		return ReplyThreadUpdated{}, err
	}
	participants := participantMap[trimmedThreadChannelID]
	replyAuthors := authorMap[trimmedThreadChannelID]

	return ReplyThreadUpdated{
		ParentMessageID: trimmedParentMessageID,
		ThreadChannelID: trimmedThreadChannelID,
		ReplyCount:      replyCount,
		LastReplyAt:     &lastReplyAtValue,
		ParticipantIDs:  replyThreadParticipantIDsFromDetails(participants),
		Participants:    participants,
		ReplyAuthorIDs:  ReplyThreadRecentAuthorIDsFromDetails(replyAuthors),
		ReplyAuthors:    replyAuthors,
	}, nil
}

func AddReplyThreadParticipantIDs(ctx context.Context, tx coredb.Tx, threadChannelID string, parentMessageID string, participantIDs []string) error {
	trimmedThreadChannelID := strings.TrimSpace(threadChannelID)
	trimmedParentMessageID := strings.TrimSpace(parentMessageID)
	if trimmedThreadChannelID == "" || trimmedParentMessageID == "" {
		return fmt.Errorf("reply thread participant context is incomplete")
	}

	normalizedParticipantIDs := make([]string, 0, len(participantIDs))
	seen := make(map[string]struct{}, len(participantIDs))
	for _, participantID := range participantIDs {
		trimmedParticipantID := strings.TrimSpace(participantID)
		if trimmedParticipantID == "" {
			continue
		}
		if _, exists := seen[trimmedParticipantID]; exists {
			continue
		}
		seen[trimmedParticipantID] = struct{}{}
		normalizedParticipantIDs = append(normalizedParticipantIDs, trimmedParticipantID)
	}
	if len(normalizedParticipantIDs) == 0 {
		return nil
	}

	if _, _, err := LoadReplyThreadStatsForUpdate(ctx, tx, trimmedThreadChannelID, trimmedParentMessageID); err != nil {
		return err
	}
	existingParticipantIDs, err := LoadReplyThreadParticipantIDs(ctx, tx, trimmedThreadChannelID, trimmedParentMessageID)
	if err != nil {
		return err
	}

	mergedParticipantIDs := MergeReplyThreadParticipantIDs(existingParticipantIDs, normalizedParticipantIDs)
	if replyThreadParticipantIDsEqual(existingParticipantIDs, mergedParticipantIDs) {
		return nil
	}
	return ReplaceReplyThreadParticipants(ctx, tx, trimmedThreadChannelID, mergedParticipantIDs)
}

func replyThreadParticipantIDsEqual(a []string, b []string) bool {
	normalizedA := NormalizeReplyThreadParticipantIDs(a)
	normalizedB := NormalizeReplyThreadParticipantIDs(b)
	if len(normalizedA) != len(normalizedB) {
		return false
	}
	for i := range normalizedA {
		if normalizedA[i] != normalizedB[i] {
			return false
		}
	}
	return true
}
