package message

import (
	"context"
	"strings"

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

// ContributionSourceResolutionKind explains which canonical model supplied a
// resolved contribution source without changing the stable browser/daemon DTO.
type ContributionSourceResolutionKind string

const (
	ContributionSourceResolutionEmpty        ContributionSourceResolutionKind = "empty"
	ContributionSourceResolutionAuthor       ContributionSourceResolutionKind = "author_source"
	ContributionSourceResolutionNativeLedger ContributionSourceResolutionKind = "native_ledger"
	ContributionSourceResolutionMetadata     ContributionSourceResolutionKind = "message_metadata"
)

// ContributionSourcePresentationFacts are the stable facts used by browser,
// socket, desktop, old-daemon, and diagnostics adapters when they render a
// message source. The source resolver owns this normalization so each surface
// does not independently interpret raw contribution source fields.
type ContributionSourcePresentationFacts struct {
	MessageID      string
	ResolutionKind string
	Kind           string
	RefType        string
	RefID          string
	RunID          string
	EventID        string
	EventType      string
	Label          string
}

// ResolvedContributionSource is the resolver contract shared by message
// history, browser, socket, desktop, old-daemon, and diagnostics projections.
// Source is the stable public DTO shape; the remaining fields are internal
// traceability facts for read-model diagnostics.
type ResolvedContributionSource struct {
	MessageID      string
	Source         ContributionSource
	ResolutionKind ContributionSourceResolutionKind
	Presentation   ContributionSourcePresentationFacts
}

func (resolved ResolvedContributionSource) Normalize() ResolvedContributionSource {
	resolved.MessageID = strings.TrimSpace(resolved.MessageID)
	resolved.Source = resolved.Source.Normalize()
	resolved.ResolutionKind = ContributionSourceResolutionKind(strings.TrimSpace(string(resolved.ResolutionKind)))
	resolved.Presentation = contributionSourcePresentationFacts(resolved.MessageID, resolved.ResolutionKind, resolved.Source)
	return resolved
}

// ResolveContributionSource is the store-neutral resolver contract for one
// canonical message source.
func ResolveContributionSource(messageID string, rawSource ContributionSource) ResolvedContributionSource {
	messageID = strings.TrimSpace(messageID)
	source := rawSource.Normalize()
	if source.IsZero() {
		return (ResolvedContributionSource{
			MessageID:      messageID,
			ResolutionKind: ContributionSourceResolutionEmpty,
		}).Normalize()
	}

	resolutionKind := ContributionSourceResolutionAuthor
	if source.RefType == ContributionSourceRefTypeServiceEventDelivery ||
		source.RefType == ContributionSourceRefTypeAutomationRun ||
		source.RefType == ContributionSourceRefTypeWorkflowRun {
		resolutionKind = ContributionSourceResolutionNativeLedger
	}
	return (ResolvedContributionSource{
		MessageID:      messageID,
		Source:         source,
		ResolutionKind: resolutionKind,
	}).Normalize()
}

func contributionSourcePresentationFacts(messageID string, resolutionKind ContributionSourceResolutionKind, source ContributionSource) ContributionSourcePresentationFacts {
	source = source.Normalize()
	return ContributionSourcePresentationFacts{
		MessageID:      strings.TrimSpace(messageID),
		ResolutionKind: strings.TrimSpace(string(resolutionKind)),
		Kind:           source.Kind,
		RefType:        source.RefType,
		RefID:          source.RefID,
		RunID:          source.RunID,
		EventID:        source.EventID,
		EventType:      source.EventType,
		Label:          source.Label,
	}
}

// ContributionSourceResolver is the single read-model entry point for turning
// canonical service-event, automation, and workflow ledgers into visible
// message contribution sources.
type ContributionSourceResolver struct {
	queryer coredb.Queryer
}

func NewContributionSourceResolver(queryer coredb.Queryer) ContributionSourceResolver {
	return ContributionSourceResolver{queryer: queryer}
}

// ResolveForMessages returns source projections keyed by message id. If several
// source rows point to the same visible message, the newest completed/attempted
// row wins so callers get stable current behavior while the migration replaces
// the backing ledger.
func (resolver ContributionSourceResolver) ResolveForMessages(ctx context.Context, workspaceID string, messageIDs []string) (map[string]ResolvedContributionSource, error) {
	workspaceID = strings.TrimSpace(workspaceID)
	messageIDs = normalizeContributionSourceMessageIDs(messageIDs)
	if resolver.queryer == nil || workspaceID == "" || len(messageIDs) == 0 {
		return map[string]ResolvedContributionSource{}, nil
	}
	nativeSources, err := resolveServiceEventDeliverySources(ctx, resolver.queryer, workspaceID, messageIDs)
	if err != nil {
		return nil, err
	}
	resolved := make(map[string]ResolvedContributionSource, len(nativeSources))
	for messageID, source := range nativeSources {
		resolved[messageID] = ResolveContributionSource(messageID, source)
	}
	workflowMessageIDs := contributionSourceUnresolvedMessageIDs(messageIDs, resolved)
	if len(workflowMessageIDs) > 0 {
		nativeWorkflowSources, err := resolveWorkflowRunSources(ctx, resolver.queryer, workspaceID, workflowMessageIDs)
		if err != nil {
			return nil, err
		}
		for messageID, source := range nativeWorkflowSources {
			if _, exists := resolved[messageID]; exists {
				continue
			}
			resolved[messageID] = ResolveContributionSource(messageID, source)
		}
	}
	scheduleMessageIDs := contributionSourceUnresolvedMessageIDs(messageIDs, resolved)
	if len(scheduleMessageIDs) > 0 {
		nativeScheduleSources, err := resolveAutomationRunSources(ctx, resolver.queryer, workspaceID, scheduleMessageIDs)
		if err != nil {
			return nil, err
		}
		for messageID, source := range nativeScheduleSources {
			if _, exists := resolved[messageID]; exists {
				continue
			}
			resolved[messageID] = ResolveContributionSource(messageID, source)
		}
	}
	return resolved, nil
}

// AttachToAppMessages applies default author-based sources first, then overlays
// resolver-backed source/run projections for messages created by service events
// or automation and metadata-backed projections for ordinary messages that carry
// product-specific contribution metadata such as review comments.
func (resolver ContributionSourceResolver) AttachToAppMessages(ctx context.Context, workspaceID string, messages []AppMessage) error {
	if len(messages) == 0 {
		return nil
	}
	messageIDs := make([]string, 0, len(messages))
	resolvedByMessageID := make(map[string]ResolvedContributionSource, len(messages))
	for i := range messages {
		ApplyDefaultContributionSourceToAppMessage(&messages[i])
		if trimmed := strings.TrimSpace(messages[i].ID); trimmed != "" {
			messageIDs = append(messageIDs, trimmed)
			resolvedByMessageID[trimmed] = ResolveContributionSource(trimmed, messages[i].Source)
		}
	}
	byMessageID, err := resolver.ResolveForMessages(ctx, workspaceID, messageIDs)
	if err != nil {
		return err
	}
	for messageID, source := range byMessageID {
		resolvedByMessageID[messageID] = source
	}
	for i := range messages {
		if source, ok := reviewCommentContributionSourceForAppMessage(messages[i]); ok {
			trimmed := strings.TrimSpace(messages[i].ID)
			resolved := ResolveContributionSource(trimmed, source)
			resolved.ResolutionKind = ContributionSourceResolutionMetadata
			resolvedByMessageID[trimmed] = resolved.Normalize()
		}
	}
	for i := range messages {
		if resolved, ok := resolvedByMessageID[strings.TrimSpace(messages[i].ID)]; ok {
			messages[i].Source = resolved.Source
		}
		// A workflow_run/schedule_run source identifies an automation-run message
		// whose frozen Human owner is the authorization/audit principal, not the
		// visible author. Overlay the Workflow/Schedule service author
		// so history/search/browser/socket reads never present the Human as the
		// automation message author.
		ApplyAutomationServiceAuthorToAppMessage(&messages[i])
	}
	return nil
}

// AttachToHistoryMessages applies the same source/run projection contract to
// agent-facing history/search/read-check rows that browser/socket reads use for
// AppMessage, so schedule, service-event, and review-comment provenance is
// visible on every API read surface.
func (resolver ContributionSourceResolver) AttachToHistoryMessages(ctx context.Context, workspaceID string, messages []HistoryMessage) error {
	if len(messages) == 0 {
		return nil
	}
	messageIDs := make([]string, 0, len(messages))
	resolvedByMessageID := make(map[string]ResolvedContributionSource, len(messages))
	for i := range messages {
		ApplyDefaultContributionSourceToHistoryMessage(&messages[i])
		if trimmed := strings.TrimSpace(messages[i].ID); trimmed != "" {
			messageIDs = append(messageIDs, trimmed)
			resolvedByMessageID[trimmed] = ResolveContributionSource(trimmed, messages[i].Source)
		}
	}
	byMessageID, err := resolver.ResolveForMessages(ctx, workspaceID, messageIDs)
	if err != nil {
		return err
	}
	for messageID, source := range byMessageID {
		resolvedByMessageID[messageID] = source
	}
	for i := range messages {
		if source, ok := reviewCommentContributionSourceForHistoryMessage(messages[i]); ok {
			trimmed := strings.TrimSpace(messages[i].ID)
			resolved := ResolveContributionSource(trimmed, source)
			resolved.ResolutionKind = ContributionSourceResolutionMetadata
			resolvedByMessageID[trimmed] = resolved.Normalize()
		}
	}
	for i := range messages {
		if resolved, ok := resolvedByMessageID[strings.TrimSpace(messages[i].ID)]; ok {
			messages[i].Source = resolved.Source
		}
		ApplyAutomationServiceAuthorToHistoryMessage(&messages[i])
	}
	return nil
}

func reviewCommentContributionSourceForAppMessage(appMessage AppMessage) (ContributionSource, bool) {
	payload, ok, err := ReviewFeedbackPayloadFromMetadata(appMessage.Metadata)
	if err != nil || !ok {
		return ContributionSource{}, false
	}
	return ContributionSourceForReviewComment(appMessage.ID, ReviewFeedbackContributionSourceLabel(payload)), true
}

func reviewCommentContributionSourceForHistoryMessage(historyMessage HistoryMessage) (ContributionSource, bool) {
	payload, ok, err := ReviewFeedbackPayloadFromMetadata(historyMessage.Metadata)
	if err != nil || !ok {
		return ContributionSource{}, false
	}
	return ContributionSourceForReviewComment(historyMessage.ID, ReviewFeedbackContributionSourceLabel(payload)), true
}

// AttachContributionSourcesForAppMessages keeps existing read paths on the
// central source resolver without exposing the resolver construction at each
// browser/history/socket call site.
func AttachContributionSourcesForAppMessages(ctx context.Context, queryer coredb.Queryer, workspaceID string, messages []AppMessage) error {
	return NewContributionSourceResolver(queryer).AttachToAppMessages(ctx, workspaceID, messages)
}

// AttachContributionSourcesForHistoryMessages keeps agent history/search/read
// paths on the central source resolver without leaking store construction into
// handler or DTO code.
func AttachContributionSourcesForHistoryMessages(ctx context.Context, queryer coredb.Queryer, workspaceID string, messages []HistoryMessage) error {
	return NewContributionSourceResolver(queryer).AttachToHistoryMessages(ctx, workspaceID, messages)
}

func normalizeContributionSourceMessageIDs(raw []string) []string {
	if len(raw) == 0 {
		return nil
	}
	seen := make(map[string]struct{}, len(raw))
	out := make([]string, 0, len(raw))
	for _, value := range raw {
		trimmed := strings.TrimSpace(value)
		if trimmed == "" {
			continue
		}
		if _, ok := seen[trimmed]; ok {
			continue
		}
		seen[trimmed] = struct{}{}
		out = append(out, trimmed)
	}
	return out
}

func contributionSourceUnresolvedMessageIDs(messageIDs []string, resolved map[string]ResolvedContributionSource) []string {
	if len(messageIDs) == 0 {
		return nil
	}
	out := make([]string, 0, len(messageIDs))
	for _, messageID := range messageIDs {
		trimmed := strings.TrimSpace(messageID)
		if trimmed == "" {
			continue
		}
		if _, ok := resolved[trimmed]; ok {
			continue
		}
		out = append(out, trimmed)
	}
	return out
}
