package message

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

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

// OpenReplyThreadForUser creates or loads a reply-thread channel for one human user.
func (txs TxStore) OpenReplyThreadForUser(ctx context.Context, userID string, workspaceID string, channelID string, parentMessageID string) (ReplyThreadSummary, error) {
	membership, err := access.LoadHumanWorkspaceMember(ctx, txs.tx, strings.TrimSpace(userID), strings.TrimSpace(workspaceID))
	if err != nil {
		return ReplyThreadSummary{}, mapAccessError(err)
	}

	opened, err := OpenReplyThreadForMember(ctx, txs.tx, membership.Member.ID, strings.TrimSpace(workspaceID), strings.TrimSpace(channelID), parentMessageID)
	if errors.Is(err, ErrNotFound) && txs.archiveStorage != nil {
		opened, err = txs.openArchivedReplyThreadForMember(ctx, membership.Member.ID, strings.TrimSpace(workspaceID), strings.TrimSpace(channelID), parentMessageID)
	}
	if err != nil {
		return ReplyThreadSummary{}, err
	}
	return opened, nil
}

// ListReplyThreadSummariesForUser lists reply-thread summaries visible from one parent channel or DM.
func (txs TxStore) ListReplyThreadSummariesForUser(ctx context.Context, userID string, workspaceID string, channelID string) (map[string]ReplyThreadSummary, error) {
	return txs.ListReplyThreadSummariesForUserWithLimit(ctx, userID, workspaceID, channelID, 0)
}

func (txs TxStore) ListReplyThreadSummariesForUserWithLimit(ctx context.Context, userID string, workspaceID string, channelID string, limit int) (map[string]ReplyThreadSummary, error) {
	membership, err := access.LoadHumanWorkspaceMember(ctx, txs.tx, strings.TrimSpace(userID), strings.TrimSpace(workspaceID))
	if err != nil {
		return nil, mapAccessError(err)
	}
	return ListReplyThreadSummariesForMemberWithLimit(ctx, txs.tx, membership.Member.ID, strings.TrimSpace(workspaceID), strings.TrimSpace(channelID), limit)
}

func (txs TxStore) openArchivedReplyThreadForMember(ctx context.Context, memberID string, workspaceID string, channelID string, parentMessageID string) (ReplyThreadSummary, error) {
	if txs.archiveStorage == nil {
		return ReplyThreadSummary{}, ErrNotFound
	}

	parent, err := ResolveReplyThreadParentForMember(ctx, txs.tx, strings.TrimSpace(memberID), strings.TrimSpace(workspaceID), strings.TrimSpace(channelID))
	if err != nil {
		return ReplyThreadSummary{}, err
	}

	parentScopeID := replyThreadParentScopeID(parent, channelID)
	exists, err := txs.archivedMessageExists(ctx, txs.tx, workspaceID, parentScopeID, parentMessageID)
	if err != nil {
		return ReplyThreadSummary{}, err
	}
	if !exists {
		return ReplyThreadSummary{}, ErrNotFound
	}

	shortID, ok := messageShortID8(parentMessageID)
	if !ok {
		return ReplyThreadSummary{}, domain.ErrInvalidArgument
	}
	if err := EnsureReplyThread(ctx, txs.tx, workspaceID, parentScopeID, parentMessageID, shortID); err != nil {
		return ReplyThreadSummary{}, err
	}
	return loadReplyThreadSummary(ctx, txs.tx, workspaceID, parentScopeID, parentMessageID)
}

// EnsureArchivedReplyThreadForAgentTarget creates or loads a reply-thread for an
// archived parent message targeted by an agent.
func (txs TxStore) EnsureArchivedReplyThreadForAgentTarget(ctx context.Context, actor MemberActor, reference domain.ThreadReference) (ReplyThreadSummary, ReplyThreadShape, error) {
	if txs.archiveStorage == nil {
		return ReplyThreadSummary{}, ReplyThreadShape{}, ErrNotFound
	}
	if strings.TrimSpace(reference.ShortID) == "" {
		return ReplyThreadSummary{}, ReplyThreadShape{}, domain.ErrInvalidArgument
	}

	parentReference := reference
	parentReference.ShortID = ""

	parent, err := ResolveAccessibleThread(ctx, txs.tx, actor, parentReference)
	if err != nil {
		return ReplyThreadSummary{}, ReplyThreadShape{}, err
	}

	parentScopeID := ScopeIDForThreadAccess(parent)
	parentMessageID, _, err := txs.resolveArchivedParentMessageIDByShortID(ctx, txs.tx, actor.WorkspaceID, parentScopeID, reference.ShortID)
	if err != nil {
		return ReplyThreadSummary{}, ReplyThreadShape{}, err
	}

	shortID, ok := messageShortID8(parentMessageID)
	if !ok {
		return ReplyThreadSummary{}, ReplyThreadShape{}, domain.ErrInvalidArgument
	}
	if err := EnsureReplyThread(ctx, txs.tx, actor.WorkspaceID, parentScopeID, parentMessageID, shortID); err != nil {
		return ReplyThreadSummary{}, ReplyThreadShape{}, err
	}
	summary, err := loadReplyThreadSummary(ctx, txs.tx, actor.WorkspaceID, parentScopeID, parentMessageID)
	if err != nil {
		return ReplyThreadSummary{}, ReplyThreadShape{}, err
	}

	shape, err := LoadReplyThreadShape(ctx, txs.tx, actor.WorkspaceID, summary.ThreadChannelID)
	if err != nil {
		return ReplyThreadSummary{}, ReplyThreadShape{}, err
	}
	if shape.ParentThreadType == domain.ThreadTypeDM {
		shape.PeerMemberID = parent.PeerMemberID
	}
	return summary, shape, nil
}

func (txs TxStore) archivedMessageExists(ctx context.Context, queryer coredb.QueryRower, workspaceID string, scopeID string, messageID string) (bool, error) {
	_, err := txs.loadArchivedMessageByID(ctx, queryer, workspaceID, scopeID, messageID)
	if err == nil {
		return true, nil
	}
	if errors.Is(err, ErrNotFound) {
		return false, nil
	}
	return false, err
}

func (txs TxStore) loadArchivedMessageByID(ctx context.Context, queryer coredb.QueryRower, workspaceID string, scopeID string, messageID string) (archive.Message, error) {
	trimmedMessageID := strings.TrimSpace(messageID)
	if trimmedMessageID == "" {
		return archive.Message{}, domain.ErrInvalidArgument
	}

	archivedUntilSeq, err := loadArchivedUntilSeq(ctx, queryer, scopeID)
	if err != nil {
		return archive.Message{}, err
	}
	if archivedUntilSeq <= 0 {
		return archive.Message{}, notFound("parent message not found")
	}

	indexRecord, ok, err := loadArchivedMessageIndexByID(ctx, queryer, workspaceID, scopeID, trimmedMessageID)
	if err != nil {
		return archive.Message{}, err
	}
	if ok {
		if indexRecord.Seq > archivedUntilSeq {
			return archive.Message{}, fmt.Errorf("archived message index seq %d exceeds archived_until_seq %d", indexRecord.Seq, archivedUntilSeq)
		}
		message, _, err := txs.readIndexedArchivedMessage(ctx, workspaceID, scopeID, indexRecord)
		if err != nil {
			return archive.Message{}, err
		}
		return message, nil
	}

	messages, err := txs.readLegacySingleArchivedLookupChunk(ctx, workspaceID, scopeID, archivedUntilSeq)
	if err != nil {
		return archive.Message{}, err
	}
	for _, message := range messages {
		if strings.TrimSpace(message.ID) == trimmedMessageID {
			return message, nil
		}
	}
	return archive.Message{}, notFound("parent message not found")
}

func (txs TxStore) loadArchivedReplyThreadParentAuthor(ctx context.Context, queryer coredb.QueryRower, workspaceID string, scopeID string, messageID string) (domain.MemberType, string, error) {
	if txs.archiveStorage == nil {
		return "", "", notFound("parent message not found")
	}
	message, err := txs.loadArchivedMessageByID(ctx, queryer, workspaceID, scopeID, messageID)
	if err != nil {
		return "", "", err
	}
	return domain.MemberType(strings.TrimSpace(message.SenderType)), strings.TrimSpace(message.SenderName), nil
}

func loadReplyThreadSummary(ctx context.Context, queryer coredb.Queryer, workspaceID string, parentScopeID string, parentMessageID string) (ReplyThreadSummary, error) {
	row := queryer.QueryRow(ctx, `
WITH parent_scope AS (
    SELECT id
    FROM scopes
    WHERE workspace_id = $1
      AND id = $2
      AND status = 'active'
      AND archived_at IS NULL
    LIMIT 1
)
SELECT child.id, child.visible_message_count, child.visible_last_message_at,
       COALESCE((
         SELECT sm.content FROM scope_messages sm
         WHERE sm.workspace_id = child.workspace_id AND sm.scope_id = child.id
         ORDER BY sm.seq DESC LIMIT 1
       ), '') AS last_reply_preview,
       COALESCE((
         SELECT m.type FROM scope_messages sm
         JOIN members m ON m.id = sm.author_member_id AND m.workspace_id = sm.workspace_id
         WHERE sm.workspace_id = child.workspace_id AND sm.scope_id = child.id
         ORDER BY sm.seq DESC LIMIT 1
       ), '') AS last_reply_author_type,
       COALESCE((
         SELECT CASE
           WHEN m.type = 'human' THEN COALESCE(NULLIF(u.display_name, ''), NULLIF(m.display_name, ''), NULLIF(u.name, ''), '')
           WHEN m.type = 'agent' THEN COALESCE(NULLIF(m.display_name, ''), NULLIF(a.name, ''), '')
           ELSE ''
         END
         FROM scope_messages sm
         JOIN members m ON m.id = sm.author_member_id AND m.workspace_id = sm.workspace_id
         LEFT JOIN users u ON m.type = 'human' AND u.id = m.user_id
         LEFT JOIN agents a ON m.type = 'agent' AND a.workspace_id = m.workspace_id AND a.member_id = m.id
         WHERE sm.workspace_id = child.workspace_id AND sm.scope_id = child.id
         ORDER BY sm.seq DESC LIMIT 1
       ), '') AS last_reply_author,
       COALESCE((
         SELECT CASE
           WHEN m.type = 'human' THEN COALESCE(u.id, '')
           WHEN m.type = 'agent' THEN COALESCE(a.id, '')
           ELSE ''
         END
         FROM scope_messages sm
         JOIN members m ON m.id = sm.author_member_id AND m.workspace_id = sm.workspace_id
         LEFT JOIN users u ON m.type = 'human' AND u.id = m.user_id
         LEFT JOIN agents a ON m.type = 'agent' AND a.workspace_id = m.workspace_id AND a.member_id = m.id
         WHERE sm.workspace_id = child.workspace_id AND sm.scope_id = child.id
         ORDER BY sm.seq DESC LIMIT 1
       ), '') AS last_reply_author_id
FROM parent_scope
JOIN scopes child
  ON child.workspace_id = $1
 AND child.parent_scope_id = parent_scope.id
 AND child.root_message_id = $3
 AND child.type = 'thread'
 AND child.status = 'active'
 AND child.archived_at IS NULL
LIMIT 1
`, strings.TrimSpace(workspaceID), strings.TrimSpace(parentScopeID), strings.TrimSpace(parentMessageID))

	var threadChannelID string
	var replyCount int64
	var lastReplyAt *time.Time
	var lastReplyPreview string
	var lastReplyAuthor string
	var lastReplyAuthorID string
	var lastReplyAuthorType string
	if err := row.Scan(&threadChannelID, &replyCount, &lastReplyAt, &lastReplyPreview, &lastReplyAuthorType, &lastReplyAuthor, &lastReplyAuthorID); err != nil {
		if coredb.IsNotFound(err) {
			return ReplyThreadSummary{}, ErrNotFound
		}
		return ReplyThreadSummary{}, fmt.Errorf("load reply thread summary: %w", err)
	}

	participantMap, authorMap, err := loadReplyThreadParticipantAndAuthorDetailsMaps(ctx, queryer, []string{threadChannelID})
	if err != nil {
		return ReplyThreadSummary{}, err
	}
	participants := participantMap[threadChannelID]
	replyAuthors := authorMap[threadChannelID]
	return ReplyThreadSummary{
		ThreadChannelID:     threadChannelID,
		ReplyCount:          replyCount,
		LastReplyAt:         lastReplyAt,
		LastReplyPreview:    lastReplyPreview,
		LastReplyAuthor:     lastReplyAuthor,
		LastReplyAuthorID:   lastReplyAuthorID,
		LastReplyAuthorType: lastReplyAuthorType,
		ParticipantIDs:      replyThreadParticipantIDsFromDetails(participants),
		Participants:        participants,
		ReplyAuthorIDs:      ReplyThreadRecentAuthorIDsFromDetails(replyAuthors),
		ReplyAuthors:        replyAuthors,
	}, nil
}

func loadArchivedUntilSeq(ctx context.Context, queryer coredb.QueryRower, scopeID string) (int64, error) {
	row := queryer.QueryRow(ctx, `
SELECT archived_until_seq
FROM scopes
WHERE id = $1
  AND status = 'active'
  AND archived_at IS NULL
`, strings.TrimSpace(scopeID))

	var archivedUntilSeq int64
	if err := row.Scan(&archivedUntilSeq); err != nil {
		if coredb.IsNotFound(err) {
			return 0, ErrNotFound
		}
		return 0, fmt.Errorf("load archived_until_seq: %w", err)
	}
	return archivedUntilSeq, nil
}

func (txs TxStore) resolveArchivedParentMessageIDByShortID(ctx context.Context, tx coredb.Tx, workspaceID string, parentScopeID string, shortID string) (string, string, error) {
	if txs.archiveStorage == nil {
		return "", "", notFound("parent message not found")
	}

	normalized, lowBound, highBound, err := resolveReplyThreadShortIDBounds(shortID)
	if err != nil {
		return "", "", err
	}

	archivedUntilSeq, err := loadArchivedUntilSeq(ctx, tx, parentScopeID)
	if err != nil {
		if errors.Is(err, ErrNotFound) {
			return "", "", notFound("parent thread not found")
		}
		return "", "", err
	}
	if archivedUntilSeq <= 0 {
		return "", "", notFound("parent message not found")
	}

	indexRecord, ok, err := resolveArchivedMessageIndexByShortID(ctx, tx, workspaceID, parentScopeID, lowBound, highBound)
	if err != nil {
		return "", "", err
	}
	if ok {
		if indexRecord.Seq > archivedUntilSeq {
			return "", "", fmt.Errorf("archived message index seq %d exceeds archived_until_seq %d", indexRecord.Seq, archivedUntilSeq)
		}
		message, shortID8, err := txs.readIndexedArchivedMessage(ctx, workspaceID, parentScopeID, indexRecord)
		if err != nil {
			return "", "", err
		}
		if !strings.HasPrefix(shortID8, normalized) {
			return "", "", fmt.Errorf("archived message index short id %q does not match requested prefix %q", shortID8, normalized)
		}
		return message.ID, shortID8, nil
	}

	matchID := ""
	matchShortID := ""
	messages, err := txs.readLegacySingleArchivedLookupChunk(ctx, workspaceID, parentScopeID, archivedUntilSeq)
	if err != nil {
		return "", "", err
	}
	for _, message := range messages {
		shortID8, ok := messageShortID8(message.ID)
		if !ok {
			continue
		}
		if !strings.HasPrefix(shortID8, normalized) {
			continue
		}
		if matchID != "" && message.ID != matchID {
			return "", "", conflict("thread identifier is ambiguous")
		}
		matchID = message.ID
		matchShortID = shortID8
		if len(normalized) == 8 {
			break
		}
	}
	if matchID == "" {
		return "", "", notFound("parent message not found")
	}
	return matchID, matchShortID, nil
}

func (txs TxStore) readIndexedArchivedMessage(ctx context.Context, workspaceID string, scopeID string, record archivedMessageIndexRecord) (archive.Message, string, error) {
	if txs.archiveStorage == nil {
		return archive.Message{}, "", notFound("parent message not found")
	}
	chunkStartSeq, err := archive.ChunkStartSeq(record.Seq)
	if err != nil {
		return archive.Message{}, "", fmt.Errorf("archived message index seq %d: %w", record.Seq, err)
	}
	chunkMessages, err := txs.archiveStorage.ReadChunk(ctx, workspaceID, scopeID, chunkStartSeq)
	if err != nil {
		if errors.Is(err, archive.ErrChunkNotFound) {
			return archive.Message{}, "", fmt.Errorf("archived message index points to missing chunk %d", chunkStartSeq)
		}
		return archive.Message{}, "", fmt.Errorf("read archived chunk %d: %w", chunkStartSeq, err)
	}
	offset := record.Seq - chunkStartSeq
	if offset < 0 || offset >= int64(len(chunkMessages)) {
		return archive.Message{}, "", fmt.Errorf("archived message index seq %d is outside chunk %d", record.Seq, chunkStartSeq)
	}
	message := chunkMessages[offset]
	if message.Seq != record.Seq {
		return archive.Message{}, "", fmt.Errorf("archived message index seq %d does not match archive seq %d", record.Seq, message.Seq)
	}
	if strings.TrimSpace(message.ID) != strings.TrimSpace(record.MessageID) {
		return archive.Message{}, "", fmt.Errorf("archived message index points to message %q but archive has %q at seq %d", record.MessageID, message.ID, record.Seq)
	}
	shortID8, ok := messageShortID8(message.ID)
	if !ok {
		return archive.Message{}, "", fmt.Errorf("invalid archive message id %q", message.ID)
	}
	if record.MessageShortID != "" && shortID8 != record.MessageShortID {
		return archive.Message{}, "", fmt.Errorf("archived message index short id %q does not match archive short id %q", record.MessageShortID, shortID8)
	}
	return message, shortID8, nil
}

func (txs TxStore) readLegacySingleArchivedLookupChunk(ctx context.Context, workspaceID string, scopeID string, archivedUntilSeq int64) ([]archive.Message, error) {
	if txs.archiveStorage == nil {
		return nil, notFound("parent message not found")
	}
	if archivedUntilSeq <= 0 {
		return nil, notFound("parent message not found")
	}
	if archivedUntilSeq > archive.ChunkMessageCount {
		return nil, fmt.Errorf("archived message index missing for scope %s; refusing unbounded archive scan", strings.TrimSpace(scopeID))
	}
	messages, err := txs.archiveStorage.ReadChunk(ctx, workspaceID, scopeID, 1)
	if err != nil {
		if errors.Is(err, archive.ErrChunkNotFound) {
			return nil, notFound("parent message not found")
		}
		return nil, fmt.Errorf("read archived chunk 1: %w", err)
	}
	return messages, nil
}

func resolveReplyThreadShortIDBounds(shortID string) (normalized string, low string, high string, _ error) {
	trimmed := strings.TrimSpace(shortID)
	if trimmed == "" {
		return "", "", "", badRequest("invalid thread identifier")
	}
	normalized = strings.ToLower(trimmed)
	if len(normalized) < 6 || len(normalized) > 8 {
		return "", "", "", badRequest("invalid thread identifier")
	}
	for i := 0; i < len(normalized); i++ {
		b := normalized[i]
		switch {
		case b >= '0' && b <= '9':
		case b >= 'a' && b <= 'f':
		default:
			return "", "", "", badRequest("invalid thread identifier")
		}
	}
	low = normalized + strings.Repeat("0", 8-len(normalized))
	high = normalized + strings.Repeat("f", 8-len(normalized))
	return normalized, low, high, nil
}

func messageShortID8(messageID string) (string, bool) {
	trimmed := strings.TrimSpace(messageID)
	if len(trimmed) < 8 {
		return "", false
	}
	shortID := strings.ToLower(trimmed[:8])
	for i := 0; i < len(shortID); i++ {
		b := shortID[i]
		switch {
		case b >= '0' && b <= '9':
		case b >= 'a' && b <= 'f':
		default:
			return "", false
		}
	}
	return shortID, true
}

func mapAccessError(err error) error {
	if err == nil {
		return nil
	}
	if errors.Is(err, access.ErrForbidden) {
		return ErrForbidden
	}
	if errors.Is(err, access.ErrNotFound) {
		return ErrNotFound
	}
	return err
}
