package taskboard

import (
	"context"
	"fmt"
	"strings"

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

const (
	taskTimelineSourceKindActivity = "activity"
	taskTimelineSourceKindMessage  = "message"
)

type taskTimelineReadQueryer interface {
	QueryRow(context.Context, string, ...any) coredb.Row
	Query(context.Context, string, ...any) (coredb.Rows, error)
}

type taskTimelineReadOptions struct {
	skipDiscussionReactions bool
	viewerMemberID          string
	viewerType              string
}

// TaskTimelineReadService is the single read-side projection boundary for task
// timeline surfaces. Message-backed task content is read from scope_messages,
// typed task facts are read from task-owned activity projections, and duplicate
// projections are deduped here rather than scattered across API/DTO/UI code.
type TaskTimelineReadService struct {
	queryer taskTimelineReadQueryer
}

func NewTaskTimelineReadService(queryer taskTimelineReadQueryer) TaskTimelineReadService {
	return TaskTimelineReadService{queryer: queryer}
}

// LoadTimelinePage applies the message-domain readable-row authority
// to message-backed Task timeline entries. Task activity remains state/projection
// data, while discussion bodies and their sidecars are returned only when the
// viewer can read the canonical Task parent-Channel source chain.
func (service TaskTimelineReadService) LoadTimelinePage(ctx context.Context, workspaceID string, taskID string, viewerMemberID string, viewerType string, cursor TaskTimelineCursor, limit int) (TaskTimelinePage, error) {
	return service.loadTimelinePage(ctx, workspaceID, taskID, cursor, limit, taskTimelineReadOptions{
		viewerMemberID: strings.TrimSpace(viewerMemberID),
		viewerType:     strings.TrimSpace(viewerType),
	})
}

func (service TaskTimelineReadService) loadTimelinePage(ctx context.Context, workspaceID string, taskID string, cursor TaskTimelineCursor, limit int, options taskTimelineReadOptions) (TaskTimelinePage, error) {
	activityPage, err := service.loadActivityPage(ctx, workspaceID, taskID, cursor, limit)
	if err != nil {
		return TaskTimelinePage{}, err
	}
	if len(activityPage.Events) == 0 {
		return TaskTimelinePage{Entries: []TaskTimelineEntry{}}, nil
	}

	discussionIDs := make([]string, 0, len(activityPage.Events))
	for _, event := range activityPage.Events {
		if event.Type != taskEventDiscussionAdded || event.SourceKind != taskTimelineSourceKindMessage {
			continue
		}
		messageID := event.SourceMessageID
		if messageID == "" {
			messageID = event.Payload
		}
		if messageID != "" {
			discussionIDs = append(discussionIDs, messageID)
		}
	}
	discussionsByID, err := loadTaskDiscussionEntriesByIDMapWithOptions(ctx, service.queryer, workspaceID, taskID, discussionIDs, taskDiscussionReadOptions{
		skipReactions:  options.skipDiscussionReactions,
		viewerMemberID: options.viewerMemberID,
		viewerType:     options.viewerType,
	})
	if err != nil {
		return TaskTimelinePage{}, err
	}

	entries := make([]TaskTimelineEntry, 0, len(activityPage.Events))
	for _, event := range activityPage.Events {
		entry := TaskTimelineEntry{
			ID:        event.ID,
			Kind:      TaskTimelineEntryKindActivity,
			CreatedAt: event.CreatedAt,
		}
		if event.Type == taskEventDiscussionAdded && event.SourceKind == taskTimelineSourceKindMessage {
			messageID := event.SourceMessageID
			if messageID == "" {
				messageID = event.Payload
			}
			if discussion, ok := discussionsByID[messageID]; ok {
				discussionCopy := discussion
				entry.Kind = TaskTimelineEntryKindDiscussion
				entry.Discussion = &discussionCopy
				entries = append(entries, entry)
			}
			// An unreadable message-backed activity is not a safe activity
			// fallback: its message id and existence are themselves sensitive.
			continue
		}
		eventCopy := event
		entry.Activity = &eventCopy
		entries = append(entries, entry)
	}

	return TaskTimelinePage{
		Entries:    entries,
		HasMore:    activityPage.HasMore,
		NextCursor: activityPage.NextCursor,
	}, nil
}

type taskActivityPage struct {
	Events     []TaskActivityEvent
	HasMore    bool
	NextCursor TaskTimelineCursor
}

func (service TaskTimelineReadService) loadActivityPage(ctx context.Context, workspaceID string, taskID string, cursor TaskTimelineCursor, limit int) (taskActivityPage, error) {
	pageSize := normalizeTaskTimelinePageSize(limit)
	cursorValue, err := normalizeTaskTimelineCursor(cursor)
	if err != nil {
		return taskActivityPage{}, err
	}

	query, args := taskTimelineActivityPageQuery(taskID, cursorValue, pageSize+1, workspaceID)
	rows, err := service.queryer.Query(ctx, query, args...)
	if err != nil {
		return taskActivityPage{}, fmt.Errorf("query task timeline activity: %w", err)
	}
	defer rows.Close()

	events := make([]TaskActivityEvent, 0, pageSize+1)
	for rows.Next() {
		var event TaskActivityEvent
		var actor taskActorProjection
		if err := rows.Scan(
			&event.ID,
			&event.Seq,
			&event.SourceKind,
			&event.SourceMessageID,
			&event.SourceActivityID,
			&event.ActorRef,
			&actor.MemberType,
			&actor.MemberID,
			&actor.UserID,
			&actor.UserDisplayName,
			&actor.UserAvatarURL,
			&actor.MemberDisplayName,
			&actor.UserName,
			&actor.AgentID,
			&actor.AgentName,
			&actor.AgentAvatarURL,
			&actor.ServiceID,
			&actor.ServiceKind,
			&actor.ServiceHandle,
			&actor.ServiceDisplayName,
			&actor.ServiceAvatarURL,
			&actor.ServiceDefinitionID,
			&actor.ServiceDefinitionSlug,
			&actor.ServiceDefinitionName,
			&actor.ServiceDefinitionAvatarURL,
			&event.Type,
			&event.Payload,
			&event.CreatedAt,
		); err != nil {
			return taskActivityPage{}, fmt.Errorf("scan task timeline activity: %w", err)
		}
		event.AuthorProfile = taskActivityAuthorProfile(event.ActorRef, actor)
		event.Source = taskActivityContributionSource(event)
		event.ActorRef = taskActivityCompatActorRefFromAuthorProfile(event.AuthorProfile, event.ActorRef)
		event.ActorDisplayName = taskActivityCompatActorDisplayNameFromAuthorProfile(event.AuthorProfile, actor.displayNameOrEmpty())
		events = append(events, event)
	}
	if err := rows.Err(); err != nil {
		return taskActivityPage{}, fmt.Errorf("iterate task timeline activity: %w", err)
	}
	if len(events) == 0 {
		return taskActivityPage{Events: []TaskActivityEvent{}}, nil
	}

	hasMore := len(events) > pageSize
	if hasMore {
		events = events[:pageSize]
	}
	nextCursor := TaskTimelineCursor{}
	if hasMore {
		lastEvent := events[len(events)-1]
		cursorSeq := lastEvent.Seq
		if lastEvent.SourceKind == taskTimelineSourceKindMessage && lastEvent.SourceActivityID == "" {
			// A legacy discussion without the canonical source_message_id
			// projection cannot safely use its message-local seq as the unified
			// timeline continuation. Keep emitting a v1 cursor for that edge.
			cursorSeq = 0
		}
		nextCursor, err = taskTimelineCursorFromItem(cursorSeq, lastEvent.CreatedAt, lastEvent.ID)
		if err != nil {
			return taskActivityPage{}, err
		}
	}

	return taskActivityPage{
		Events:     events,
		HasMore:    hasMore,
		NextCursor: nextCursor,
	}, nil
}

func taskTimelineActivityPageQuery(taskID string, cursor TaskTimelineCursor, limit int, workspaceIDs ...string) (string, []any) {
	workspaceID := ""
	if len(workspaceIDs) > 0 {
		workspaceID = workspaceIDs[0]
	}
	return tasktimeline.BuildReadPageQuery(taskID, tasktimeline.ReadCursor{
		Seq:       cursor.Seq,
		CreatedAt: cursor.CreatedAt,
		ID:        cursor.ID,
	}, limit, workspaceID)
}

func taskActivityContributionSource(event TaskActivityEvent) message.ContributionSource {
	profile := event.AuthorProfile.Normalize()
	kind := taskActivityContributionKindForAuthor(profile)
	if kind == "" {
		return message.ContributionSource{}
	}
	refType := message.ContributionSourceRefTypeTaskActivity
	refID := firstTaskString(event.SourceActivityID, event.ID)
	eventID := refID
	if event.SourceKind == taskTimelineSourceKindMessage && event.SourceMessageID != "" {
		refType = message.ContributionSourceRefTypeMessage
		refID = event.SourceMessageID
		eventID = firstTaskString(event.SourceActivityID, event.ID)
	}
	if refID == "" {
		return message.ContributionSource{}
	}
	return message.ContributionSource{
		Kind:      kind,
		RefType:   refType,
		RefID:     refID,
		EventID:   eventID,
		EventType: event.Type,
		Label:     profile.DisplayName,
	}.Normalize()
}

func taskActivityContributionKindForAuthor(profile message.AuthorProfile) string {
	switch profile.Normalize().ActorType {
	case message.AuthorActorTypeHuman:
		return message.ContributionSourceKindHumanCompose
	case message.AuthorActorTypeAgent:
		return message.ContributionSourceKindAgentRuntime
	case message.AuthorActorTypeService:
		return ""
	default:
		return ""
	}
}

func taskActivityCompatActorRefFromAuthorProfile(profile message.AuthorProfile, fallback string) string {
	profile = profile.Normalize()
	if !profile.Valid() {
		return fallback
	}
	return firstTaskString(profile.MemberID, profile.PrincipalID, profile.TargetID, fallback)
}

func taskActivityCompatActorDisplayNameFromAuthorProfile(profile message.AuthorProfile, fallback string) string {
	profile = profile.Normalize()
	if !profile.Valid() {
		return fallback
	}
	return firstTaskString(profile.DisplayName, fallback)
}
