package app

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

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
)

const (
	taskSourceThreadContextMaxLimit   = 20
	taskSourceThreadContextProvenance = "source_thread_bounded_window"
)

// BuildTaskSourceThreadContext assembles a bounded, recipient-authorized source
// thread window for task-agent dispatch metadata. It fails closed by omitting
// the context when the source thread cannot be authorized for every recipient.
func (store *Store) BuildTaskSourceThreadContext(ctx context.Context, request message.TaskSourceThreadContextRequest) (map[string]any, error) {
	workspaceID := strings.TrimSpace(request.WorkspaceID)
	sourceThreadChannelID := strings.TrimSpace(request.SourceThreadChannelID)
	if workspaceID == "" || sourceThreadChannelID == "" {
		return nil, nil
	}

	recipients := uniqueSourceThreadContextRecipients(request.Recipients)
	if len(recipients) == 0 {
		return nil, nil
	}

	limit := normalizeTaskSourceThreadContextLimit(request.Limit)
	parentChannelID := strings.TrimSpace(request.ParentChannelID)
	threadID := ""
	recipientVisibilities := make([]message.OwnerPrivateContextVisibility, 0, len(recipients))
	messageStore := message.New(store.db, store.archiveStorage, store.hotMessageRetentionDays, message.Options{})
	for _, recipient := range recipients {
		recipientThreadID, authorized, err := store.loadAuthorizedTaskSourceThreadID(ctx, workspaceID, sourceThreadChannelID, parentChannelID, recipient)
		if err != nil {
			return nil, err
		}
		if !authorized {
			return nil, nil
		}
		if threadID == "" {
			threadID = recipientThreadID
		} else if threadID != recipientThreadID {
			return nil, nil
		}
		visibility, err := messageStore.LoadOwnerPrivateContextVisibility(ctx, workspaceID, recipient.AgentMemberID, recipientThreadID)
		if err != nil {
			return nil, err
		}
		recipientVisibilities = append(recipientVisibilities, visibility)
	}
	if strings.TrimSpace(threadID) == "" {
		return nil, nil
	}

	parentRecord, hasParent, err := store.loadTaskSourceThreadParentRecord(ctx, workspaceID, parentChannelID, strings.TrimSpace(request.ParentMessageID), recipients, recipientVisibilities)
	if err != nil {
		return nil, err
	}

	fetchLimit := message.OwnerPrivateHistoryBatchLimit(limit + 1)
	threadRecords, hasMore, err := messageStore.ReadThreadHistoryRecords(ctx, workspaceID, threadID, fetchLimit, nil, nil)
	if err != nil {
		if errors.Is(err, message.ErrForbidden) || errors.Is(err, message.ErrNotFound) {
			return nil, nil
		}
		return nil, err
	}
	threadRecords = filterRecordsVisibleToAllRecipients(threadRecords, recipientVisibilities)

	records := make([]sourceThreadContextRecord, 0, limit)
	if hasParent {
		records = append(records, sourceThreadContextRecord{record: parentRecord, provenance: "parent_message"})
	}

	threadSlots := limit - len(records)
	truncated := hasMore
	if threadSlots < 0 {
		threadSlots = 0
	}
	if threadSlots == 0 {
		truncated = truncated || len(threadRecords) > 0
		threadRecords = nil
	} else if len(threadRecords) > threadSlots {
		truncated = true
		threadRecords = threadRecords[len(threadRecords)-threadSlots:]
	}
	for _, record := range threadRecords {
		if sourceThreadContextContainsRecord(records, record.MessageID) {
			continue
		}
		records = append(records, sourceThreadContextRecord{record: record, provenance: "source_thread_recent"})
	}
	if len(records) == 0 {
		return nil, nil
	}

	memberIDs := make([]string, 0, len(records))
	for _, record := range records {
		memberIDs = append(memberIDs, record.record.AuthorMemberID)
	}
	publicNames, err := loadPublicMemberNamesByMemberID(ctx, store.db, workspaceID, memberIDs)
	if err != nil {
		return nil, err
	}

	messages := make([]any, 0, len(records))
	var oldestSeq int64
	var oldestMessageID string
	for _, sourceRecord := range records {
		record := sourceRecord.record
		if sourceRecord.provenance != "parent_message" && (oldestSeq == 0 || (record.Seq > 0 && record.Seq < oldestSeq)) {
			oldestSeq = record.Seq
			oldestMessageID = record.MessageID
		}
		displayName := publicMemberNameForMemberID(record.AuthorMemberID, publicNames)
		authorProfile := message.AuthorProfile{
			ActorType:   strings.TrimSpace(record.AuthorType),
			MemberID:    strings.TrimSpace(record.AuthorMemberID),
			TargetID:    firstNonEmpty(displayName, record.AuthorMemberID),
			DisplayName: firstNonEmpty(displayName, record.AuthorMemberID),
		}.Normalize()
		messages = append(messages, map[string]any{
			"messageId":     strings.TrimSpace(record.MessageID),
			"seq":           record.Seq,
			"authorProfile": authorProfile,
			"content":       strings.TrimSpace(record.Content),
			"createdAt":     record.CreatedAt,
			"provenance":    sourceRecord.provenance,
		})
	}

	sourceThreadRef := strings.TrimSpace(request.SourceThreadRef)
	payload := map[string]any{
		"sourceThreadChannelId": sourceThreadChannelID,
		"sourceThreadRef":       sourceThreadRef,
		"parentChannelId":       strings.TrimSpace(request.ParentChannelID),
		"parentMessageId":       strings.TrimSpace(request.ParentMessageID),
		"limit":                 limit,
		"truncated":             truncated,
		"cursor": map[string]any{
			"beforeSeq":       oldestSeq,
			"oldestMessageId": strings.TrimSpace(oldestMessageID),
		},
		"provenance": taskSourceThreadContextProvenance,
		"messages":   messages,
	}
	if sourceThreadRef == "" {
		delete(payload, "sourceThreadRef")
	}
	if strings.TrimSpace(request.ParentChannelID) == "" {
		delete(payload, "parentChannelId")
	}
	if strings.TrimSpace(request.ParentMessageID) == "" {
		delete(payload, "parentMessageId")
	}
	return payload, nil
}

type sourceThreadContextRecord struct {
	record     model.ThreadMessageRecord
	provenance string
}

func uniqueSourceThreadContextRecipients(recipients []message.AgentRecipient) []message.AgentRecipient {
	normalized := make([]message.AgentRecipient, 0, len(recipients))
	seen := make(map[string]struct{}, len(recipients))
	for _, recipient := range recipients {
		memberID := strings.TrimSpace(recipient.AgentMemberID)
		if memberID == "" {
			continue
		}
		if _, ok := seen[memberID]; ok {
			continue
		}
		seen[memberID] = struct{}{}
		recipient.AgentMemberID = memberID
		recipient.AgentID = strings.TrimSpace(recipient.AgentID)
		normalized = append(normalized, recipient)
	}
	return normalized
}

func normalizeTaskSourceThreadContextLimit(limit int) int {
	if limit <= 0 {
		return message.TaskSourceThreadContextDefaultLimit
	}
	if limit > taskSourceThreadContextMaxLimit {
		return taskSourceThreadContextMaxLimit
	}
	return limit
}

func (store *Store) loadAuthorizedTaskSourceThreadID(ctx context.Context, workspaceID string, sourceThreadChannelID string, parentChannelID string, recipient message.AgentRecipient) (string, bool, error) {
	if strings.TrimSpace(parentChannelID) == "" {
		return "", false, nil
	}
	authorized, err := store.canRecipientReadTaskSourceParentChannel(ctx, workspaceID, parentChannelID, recipient)
	if err != nil {
		return "", false, err
	}
	if !authorized {
		return "", false, nil
	}
	row := store.db.QueryRow(ctx, `
SELECT source_scope.id
FROM scopes source_scope
JOIN scopes parent_scope
  ON parent_scope.id = source_scope.parent_scope_id
 AND parent_scope.workspace_id = source_scope.workspace_id
 AND parent_scope.type = 'channel'
 AND parent_scope.status = 'active'
JOIN scope_messages root_message
  ON root_message.id = source_scope.root_message_id
 AND root_message.scope_id = parent_scope.id
 AND root_message.workspace_id = source_scope.workspace_id
WHERE source_scope.workspace_id = $1
  AND source_scope.id = $2
  AND source_scope.type = 'thread'
  AND source_scope.status = 'active'
  AND parent_scope.id = $3
`, strings.TrimSpace(workspaceID), strings.TrimSpace(sourceThreadChannelID), strings.TrimSpace(parentChannelID))
	var threadID string
	if err := row.Scan(&threadID); err != nil {
		if coredb.IsNotFound(err) {
			return "", false, nil
		}
		return "", false, fmt.Errorf("load source thread id: %w", err)
	}
	return strings.TrimSpace(threadID), true, nil
}

func (store *Store) canRecipientReadTaskSourceParentChannel(ctx context.Context, workspaceID string, parentChannelID string, recipient message.AgentRecipient) (bool, error) {
	workspaceID = strings.TrimSpace(workspaceID)
	parentChannelID = strings.TrimSpace(parentChannelID)
	memberID := strings.TrimSpace(recipient.AgentMemberID)
	agentID := strings.TrimSpace(recipient.AgentID)
	if workspaceID == "" || parentChannelID == "" || memberID == "" || agentID == "" {
		return false, nil
	}
	access, err := message.NewSQLVisibleScopeAuthorizer(store.db).CanReadVisibleScopesForAgent(
		ctx,
		workspaceID,
		memberID,
		agentID,
		[]string{parentChannelID},
	)
	if err != nil {
		return false, fmt.Errorf("check source thread parent channel access: %w", err)
	}
	decision, ok := access[parentChannelID]
	return ok && decision.ScopeType == "channel" && decision.Readable, nil
}

func (store *Store) loadTaskSourceThreadParentRecord(ctx context.Context, workspaceID string, parentChannelID string, parentMessageID string, recipients []message.AgentRecipient, visibilities []message.OwnerPrivateContextVisibility) (model.ThreadMessageRecord, bool, error) {
	if parentChannelID == "" || parentMessageID == "" {
		return model.ThreadMessageRecord{}, false, nil
	}
	for _, recipient := range recipients {
		authorized, err := store.canRecipientReadTaskSourceParentChannel(ctx, workspaceID, parentChannelID, recipient)
		if err != nil {
			return model.ThreadMessageRecord{}, false, err
		}
		if !authorized {
			return model.ThreadMessageRecord{}, false, nil
		}
	}

	row := store.db.QueryRow(ctx, fmt.Sprintf(`
SELECT sm.id, sm.scope_id, COALESCE(NULLIF(scope.type, ''), ''), COALESCE(scope.parent_scope_id, ''), sm.seq, sm.workspace_global_seq, sm.author_member_id, sm.author_type, sm.content, sm.metadata, sm.created_at
FROM scope_messages sm
LEFT JOIN scopes scope
  ON scope.id = sm.scope_id
WHERE sm.workspace_id = $1
  AND sm.scope_id = $2
  AND sm.id = $3
  AND %s
`, humanVisibleMessageTypeSQL("sm")), workspaceID, parentChannelID, parentMessageID)
	var record model.ThreadMessageRecord
	var rawMetadata []byte
	if err := row.Scan(&record.MessageID, &record.ScopeID, &record.ScopeKind, &record.ParentScopeID, &record.Seq, &record.WorkspaceGlobalSeq, &record.AuthorMemberID, &record.AuthorType, &record.Content, &rawMetadata, &record.CreatedAt); err != nil {
		if coredb.IsNotFound(err) {
			return model.ThreadMessageRecord{}, false, nil
		}
		return model.ThreadMessageRecord{}, false, fmt.Errorf("load source thread parent message: %w", err)
	}
	metadata, err := message.DecodeMessageMetadata(rawMetadata)
	if err != nil {
		return model.ThreadMessageRecord{}, false, err
	}
	record.Metadata = metadata
	if !recordVisibleToAllRecipients(record, visibilities) {
		return model.ThreadMessageRecord{}, false, nil
	}
	return record, true, nil
}

func filterRecordsVisibleToAllRecipients(records []model.ThreadMessageRecord, visibilities []message.OwnerPrivateContextVisibility) []model.ThreadMessageRecord {
	if len(records) == 0 {
		return nil
	}
	filtered := make([]model.ThreadMessageRecord, 0, len(records))
	for _, record := range records {
		if recordVisibleToAllRecipients(record, visibilities) {
			filtered = append(filtered, record)
		}
	}
	return filtered
}

func recordVisibleToAllRecipients(record model.ThreadMessageRecord, visibilities []message.OwnerPrivateContextVisibility) bool {
	if strings.TrimSpace(record.AuthorMemberID) == "" || len(visibilities) == 0 {
		return false
	}
	for _, visibility := range visibilities {
		if !message.OwnerPrivateContextHistoryRecordVisible(record, visibility) {
			return false
		}
	}
	return true
}

func sourceThreadContextContainsRecord(records []sourceThreadContextRecord, messageID string) bool {
	trimmedMessageID := strings.TrimSpace(messageID)
	if trimmedMessageID == "" {
		return false
	}
	for _, record := range records {
		if strings.TrimSpace(record.record.MessageID) == trimmedMessageID {
			return true
		}
	}
	return false
}
