package tasktimeline

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

const (
	ActivityRefTypeTaskEvent = "task_event"
)

type ActorParticipantMode string

const (
	ActorParticipantModeNone             ActorParticipantMode = "none"
	ActorParticipantModeDefaultUnwatched ActorParticipantMode = "default_unwatched"
)

type TaskTimelinePlan struct {
	TypedActivityIntents []TypedActivityIntent
}

// TypedActivityIntent is the canonical write-side contract for task typed
// timeline events. Task mutations should finish their fact writes, then declare
// these intents; WritePlan owns event id allocation, timeline sequence
// reservation, actor/source projection, unread/inbox, and other timeline side
// effects.
type TypedActivityIntent struct {
	TaskID          string
	ActorRef        string
	EventType       string
	EventPayload    string
	SourceMessageID string
	// SuppressAttention keeps the durable Activity while declaring that a
	// higher-signal sibling in the same Task mutation owns unread/wake.
	SuppressAttention    bool
	ActorParticipantMode ActorParticipantMode
	CreatedAt            time.Time
}

type TypedActivityProjection struct {
	EventID              string
	WorkspaceID          string
	WorkspaceSeq         int64
	TaskID               string
	TaskScopeID          string
	ActorRef             string
	ActorMemberID        string
	EventType            string
	ActivityType         string
	EventPayload         string
	SourceMessageID      string
	SuppressAttention    bool
	Seq                  int64
	ActorParticipantMode ActorParticipantMode
	CreatedAt            time.Time
}

func NewTypedActivityIntent(taskID string, actorRef string, eventType string, eventPayload string, createdAt time.Time) TypedActivityIntent {
	return TypedActivityIntent{
		TaskID:               strings.TrimSpace(taskID),
		ActorRef:             strings.TrimSpace(actorRef),
		EventType:            strings.TrimSpace(eventType),
		EventPayload:         strings.TrimSpace(eventPayload),
		ActorParticipantMode: ActorParticipantModeDefaultUnwatched,
		CreatedAt:            createdAt,
	}
}

func WritePlan(ctx context.Context, tx coredb.Tx, plan TaskTimelinePlan) error {
	_, err := WritePlanWithResult(ctx, tx, plan)
	return err
}

type TaskTimelineWriteResult struct {
	TypedActivities []TypedActivityProjection
}

func WritePlanWithResult(ctx context.Context, tx coredb.Tx, plan TaskTimelinePlan) (TaskTimelineWriteResult, error) {
	typedActivities, err := typedActivityProjectionsFromIntents(plan.TypedActivityIntents)
	if err != nil {
		return TaskTimelineWriteResult{}, err
	}
	if len(typedActivities) == 0 {
		return TaskTimelineWriteResult{}, nil
	}
	if err := ensureTypedActivityActors(ctx, tx, typedActivities); err != nil {
		return TaskTimelineWriteResult{}, err
	}
	typedActivities, err = projectTypedActivities(ctx, tx, typedActivities)
	if err != nil {
		return TaskTimelineWriteResult{}, err
	}
	if err := projectTypedActivityAttention(ctx, tx, typedActivities); err != nil {
		return TaskTimelineWriteResult{}, err
	}
	return TaskTimelineWriteResult{TypedActivities: typedActivities}, nil
}

func typedActivityProjectionsFromIntents(intents []TypedActivityIntent) ([]TypedActivityProjection, error) {
	normalized := make([]TypedActivityProjection, 0, len(intents))
	for _, intent := range intents {
		intent.TaskID = strings.TrimSpace(intent.TaskID)
		intent.ActorRef = strings.TrimSpace(intent.ActorRef)
		intent.EventType = strings.TrimSpace(intent.EventType)
		intent.EventPayload = strings.TrimSpace(intent.EventPayload)
		intent.SourceMessageID = strings.TrimSpace(intent.SourceMessageID)
		actorParticipantMode, err := normalizeActorParticipantMode(intent.ActorParticipantMode)
		if err != nil {
			return nil, err
		}
		intent.ActorParticipantMode = actorParticipantMode
		if intent.TaskID == "" || intent.EventType == "" || intent.CreatedAt.IsZero() {
			return nil, fmt.Errorf("task timeline typed activity requires task, type, and created timestamp")
		}
		eventID, err := identifiers.NewEntityID("tev")
		if err != nil {
			return nil, fmt.Errorf("generate task timeline event id: %w", err)
		}
		normalized = append(normalized, TypedActivityProjection{
			EventID:              eventID,
			TaskID:               intent.TaskID,
			ActorRef:             intent.ActorRef,
			EventType:            intent.EventType,
			EventPayload:         intent.EventPayload,
			SourceMessageID:      intent.SourceMessageID,
			SuppressAttention:    intent.SuppressAttention,
			ActorParticipantMode: intent.ActorParticipantMode,
			CreatedAt:            intent.CreatedAt,
		})
	}
	return normalized, nil
}

func normalizeActorParticipantMode(mode ActorParticipantMode) (ActorParticipantMode, error) {
	switch mode {
	case "", ActorParticipantModeDefaultUnwatched:
		return ActorParticipantModeDefaultUnwatched, nil
	case ActorParticipantModeNone:
		return ActorParticipantModeNone, nil
	default:
		return "", fmt.Errorf("unknown task timeline actor participant mode %q", mode)
	}
}

func ensureTypedActivityActors(ctx context.Context, execer coredb.QueryExecer, events []TypedActivityProjection) error {
	seen := make(map[string]struct{}, len(events))
	for _, event := range events {
		if event.ActorParticipantMode != ActorParticipantModeDefaultUnwatched || event.ActorRef == "" {
			continue
		}
		key := event.TaskID + "\x00" + event.ActorRef
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		if err := ensureTaskScopeActorParticipantDefaultUnwatched(ctx, execer, event.TaskID, event.ActorRef, event.CreatedAt); err != nil {
			return err
		}
	}
	return nil
}

func projectTypedActivities(ctx context.Context, tx coredb.Tx, events []TypedActivityProjection) ([]TypedActivityProjection, error) {
	pendingRowsSQL, args := typedActivityProjectionRowsSQL(events)
	reserveResult, err := tx.Exec(ctx, `
UPDATE scopes task_scope
JOIN (
  SELECT t.task_scope_id, COUNT(*) AS event_count
  FROM (`+pendingRowsSQL+`) pending
  JOIN tasks t
    ON t.id = pending.task_id
  GROUP BY t.task_scope_id
) scope_counts
  ON scope_counts.task_scope_id = task_scope.id
SET task_scope.latest_activity_seq = task_scope.latest_activity_seq + scope_counts.event_count
WHERE task_scope.type = 'task'
  AND task_scope.status = 'active'
`, args...)
	if err != nil {
		return nil, fmt.Errorf("reserve task timeline typed activity seqs: %w", err)
	}
	if reserveResult.RowsAffected() == 0 {
		return nil, storeerr.NotFound("task timeline target not found")
	}
	if err := allocateWorkspaceActivitySequences(ctx, tx, events); err != nil {
		return nil, err
	}

	pendingRowsSQL, args = typedActivityProjectionRowsSQL(events)
	if _, err := tx.Exec(ctx, `
INSERT INTO scope_activity_events (
  id,
  workspace_id,
  scope_id,
  seq,
  activity_type,
  activity_ref_type,
  activity_ref_id,
  activity_global_seq,
  workspace_seq,
  source_message_id,
  actor_member_id,
  reason,
  payload,
  created_at,
  suppressed_at
)
SELECT
  projection.event_id,
  projection.workspace_id,
  projection.task_scope_id,
  projection.seq,
  projection.activity_type,
  'task_event',
  projection.event_id,
  projection.seq,
  projection.workspace_seq,
  NULLIF(projection.source_message_id, ''),
  projection.actor_member_id,
  'task_event',
  projection.payload,
  projection.created_at,
  NULL
FROM (
  SELECT
    pending.event_id,
    tb.workspace_id,
    t.task_scope_id,
    workspace_counter.last_sequence
      - COUNT(*) OVER (PARTITION BY tb.workspace_id)
      + ROW_NUMBER() OVER (PARTITION BY tb.workspace_id ORDER BY pending.created_at ASC, pending.event_id ASC)
      AS workspace_seq,
    task_scope.latest_activity_seq
      - COUNT(*) OVER (PARTITION BY t.task_scope_id)
      + ROW_NUMBER() OVER (PARTITION BY t.task_scope_id ORDER BY pending.created_at ASC, pending.event_id ASC) AS seq,
    CONCAT('task.', pending.event_type) AS activity_type,
    actor_member.id AS actor_member_id,
    pending.source_message_id,
    pending.payload,
    pending.created_at
  FROM (`+pendingRowsSQL+`) pending
  JOIN tasks t
    ON t.id = pending.task_id
  JOIN task_boards tb
    ON tb.id = t.board_id
  JOIN scopes task_channel
    ON task_channel.id = tb.channel_id
   AND task_channel.workspace_id = tb.workspace_id
   AND task_channel.type = 'channel'
   AND task_channel.status = 'active'
   AND task_channel.archived_at IS NULL
   AND t.channel_id = tb.channel_id
  JOIN scopes task_scope
    ON task_scope.id = t.task_scope_id
   AND task_scope.workspace_id = tb.workspace_id
   AND task_scope.type = 'task'
   AND task_scope.status = 'active'
   AND task_scope.archived_at IS NULL
  LEFT JOIN members actor_member
    ON actor_member.id = NULLIF(pending.actor_ref, '')
  JOIN scope_activity_workspace_counters workspace_counter
    ON workspace_counter.workspace_id = tb.workspace_id
) projection
	`, args...); err != nil {
		return nil, fmt.Errorf("insert task timeline typed activity projections: %w", err)
	}
	projected, err := loadProjectedTypedActivities(ctx, tx, events)
	if err != nil {
		return nil, err
	}
	return projected, nil
}

func allocateWorkspaceActivitySequences(ctx context.Context, tx coredb.Tx, events []TypedActivityProjection) error {
	pendingRowsSQL, args := typedActivityProjectionRowsSQL(events)
	rows, err := tx.Query(ctx, `
SELECT tb.workspace_id, COUNT(*) AS event_count
FROM (`+pendingRowsSQL+`) pending
JOIN tasks t
  ON t.id = pending.task_id
JOIN task_boards tb
  ON tb.id = t.board_id
JOIN scopes task_channel
  ON task_channel.id = tb.channel_id
 AND task_channel.workspace_id = tb.workspace_id
 AND task_channel.type = 'channel'
 AND task_channel.status = 'active'
 AND task_channel.archived_at IS NULL
 AND t.channel_id = tb.channel_id
GROUP BY tb.workspace_id
ORDER BY tb.workspace_id ASC
`, args...)
	if err != nil {
		return fmt.Errorf("load task timeline workspace activity counts: %w", err)
	}

	type workspaceEventCount struct {
		workspaceID string
		count       int64
	}
	counts := make([]workspaceEventCount, 0, 1)
	for rows.Next() {
		var item workspaceEventCount
		if err := rows.Scan(&item.workspaceID, &item.count); err != nil {
			rows.Close()
			return fmt.Errorf("scan task timeline workspace activity count: %w", err)
		}
		counts = append(counts, item)
	}
	if err := rows.Err(); err != nil {
		rows.Close()
		return fmt.Errorf("iterate task timeline workspace activity counts: %w", err)
	}
	rows.Close()

	for _, item := range counts {
		if _, err := tx.Exec(ctx, `
INSERT IGNORE INTO scope_activity_workspace_counters (
  workspace_id, last_sequence, updated_at
) VALUES ($1, 0, CURRENT_TIMESTAMP(6))
`, item.workspaceID); err != nil {
			return fmt.Errorf("initialize task timeline workspace activity counter: %w", err)
		}
		result, err := tx.Exec(ctx, `
UPDATE scope_activity_workspace_counters
SET last_sequence = last_sequence + $2,
    updated_at = CURRENT_TIMESTAMP(6)
WHERE workspace_id = $1
`, item.workspaceID, item.count)
		if err != nil {
			return fmt.Errorf("allocate task timeline workspace activity sequences: %w", err)
		}
		if result.RowsAffected() != 1 {
			return fmt.Errorf(
				"allocate task timeline workspace activity sequences: workspace %q counter update affected %d rows",
				item.workspaceID,
				result.RowsAffected(),
			)
		}
	}
	return nil
}

func loadProjectedTypedActivities(ctx context.Context, queryer coredb.Queryer, events []TypedActivityProjection) ([]TypedActivityProjection, error) {
	eventIDs := make([]string, 0, len(events))
	byID := make(map[string]TypedActivityProjection, len(events))
	for _, event := range events {
		eventID := strings.TrimSpace(event.EventID)
		if eventID == "" {
			continue
		}
		eventIDs = append(eventIDs, eventID)
		byID[eventID] = event
	}
	if len(eventIDs) == 0 {
		return events, nil
	}
	requestedEvents, args, _, err := coredb.BuildStringValuesCTE("requested_events", "event_id", eventIDs, 1)
	if err != nil {
		return nil, err
	}
	rows, err := queryer.Query(ctx, fmt.Sprintf(`
WITH %s
SELECT
  ae.id,
  ae.workspace_id,
  ae.workspace_seq,
  ae.scope_id,
  ae.seq,
  ae.activity_type,
  COALESCE(ae.actor_member_id, '')
FROM requested_events requested
JOIN scope_activity_events ae
  ON ae.id = requested.event_id
ORDER BY ae.seq, ae.id
`, requestedEvents), args...)
	if err != nil {
		return nil, fmt.Errorf("load task timeline typed activity projections: %w", err)
	}
	defer rows.Close()

	projected := make([]TypedActivityProjection, 0, len(events))
	for rows.Next() {
		var eventID string
		var workspaceID string
		var workspaceSeq int64
		var taskScopeID string
		var seq int64
		var activityType string
		var actorMemberID string
		if err := rows.Scan(&eventID, &workspaceID, &workspaceSeq, &taskScopeID, &seq, &activityType, &actorMemberID); err != nil {
			return nil, fmt.Errorf("scan task timeline typed activity projection: %w", err)
		}
		event, ok := byID[strings.TrimSpace(eventID)]
		if !ok {
			continue
		}
		event.WorkspaceID = strings.TrimSpace(workspaceID)
		event.WorkspaceSeq = workspaceSeq
		event.TaskScopeID = strings.TrimSpace(taskScopeID)
		event.Seq = seq
		event.ActivityType = strings.TrimSpace(activityType)
		event.ActorMemberID = strings.TrimSpace(actorMemberID)
		projected = append(projected, event)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate task timeline typed activity projections: %w", err)
	}
	return projected, nil
}

func typedActivityProjectionRowsSQL(events []TypedActivityProjection) (string, []any) {
	var rows strings.Builder
	args := make([]any, 0, len(events)*7)
	param := 1
	for index, event := range events {
		if index > 0 {
			rows.WriteString("\n  UNION ALL\n")
		}
		payload := marshalTypedActivityPayload(event)
		rows.WriteString("  SELECT $")
		rows.WriteString(strconv.Itoa(param))
		rows.WriteString(" AS event_id, $")
		rows.WriteString(strconv.Itoa(param + 1))
		rows.WriteString(" AS task_id, $")
		rows.WriteString(strconv.Itoa(param + 2))
		rows.WriteString(" AS actor_ref, $")
		rows.WriteString(strconv.Itoa(param + 3))
		rows.WriteString(" AS event_type, $")
		rows.WriteString(strconv.Itoa(param + 4))
		rows.WriteString(" AS payload, $")
		rows.WriteString(strconv.Itoa(param + 5))
		rows.WriteString(" AS source_message_id, $")
		rows.WriteString(strconv.Itoa(param + 6))
		rows.WriteString(" AS created_at")
		args = append(args, event.EventID, event.TaskID, event.ActorRef, event.EventType, payload, event.SourceMessageID, event.CreatedAt)
		param += 7
	}
	return rows.String(), args
}

func marshalTypedActivityPayload(event TypedActivityProjection) []byte {
	taskEvent := map[string]any{
		"eventId": event.EventID,
		"taskId":  event.TaskID,
		"type":    event.EventType,
		"payload": event.EventPayload,
	}
	if event.ActorRef != "" {
		taskEvent["actorRef"] = event.ActorRef
	}
	payload, err := model.MarshalJSONObject("task timeline activity payload", map[string]any{
		"task": map[string]string{
			"id":      event.TaskID,
			"scopeId": canonicalTaskScopeID(event.TaskID),
		},
		"taskEvent": taskEvent,
	})
	if err != nil {
		// All fields are primitive JSON values, so JSON marshaling cannot fail in practice.
		return []byte(`{}`)
	}
	return payload
}

func canonicalTaskScopeID(taskID string) string {
	return strings.TrimSpace(taskID)
}

func ensureTaskScopeActorParticipantDefaultUnwatched(ctx context.Context, execer coredb.QueryExecer, taskID string, actorMemberID string, createdAt time.Time) error {
	trimmedActorMemberID := strings.TrimSpace(actorMemberID)
	if trimmedActorMemberID == "" {
		return nil
	}
	taskScopeID, err := loadTaskScopeIDForParticipantNotification(ctx, execer, taskID)
	if err != nil {
		return err
	}
	return ensureTaskScopeParticipantDefaultUnwatched(ctx, execer, taskScopeID, trimmedActorMemberID, createdAt)
}

func loadTaskScopeIDForParticipantNotification(ctx context.Context, queryer coredb.Queryer, taskID string) (string, error) {
	trimmedTaskID := strings.TrimSpace(taskID)
	if trimmedTaskID == "" {
		return "", nil
	}
	var taskScopeID string
	if err := queryer.QueryRow(ctx, `
SELECT task_scope_id
FROM tasks
WHERE id = $1
`, trimmedTaskID).Scan(&taskScopeID); err != nil {
		if coredb.IsNotFound(err) {
			return "", nil
		}
		return "", fmt.Errorf("load task scope for participant notification: %w", err)
	}
	return strings.TrimSpace(taskScopeID), nil
}

func ensureTaskScopeParticipantDefaultUnwatched(ctx context.Context, execer coredb.QueryExecer, scopeID string, memberID string, createdAt time.Time) error {
	if strings.TrimSpace(scopeID) == "" || strings.TrimSpace(memberID) == "" {
		return nil
	}
	state, err := taskScopeDefaultUnwatchedNotificationStateForMember(ctx, execer, memberID)
	if err != nil {
		return err
	}
	return ensureTaskScopeParticipantNotification(ctx, execer, scopeID, memberID, state, createdAt)
}

func ensureTaskScopeParticipantNotification(ctx context.Context, execer coredb.QueryExecer, scopeID string, memberID string, state string, createdAt time.Time) error {
	trimmedScopeID := strings.TrimSpace(scopeID)
	trimmedMemberID := strings.TrimSpace(memberID)
	if trimmedScopeID == "" || trimmedMemberID == "" {
		return nil
	}
	if err := message.EnsureScopeMembership(ctx, execer, trimmedScopeID, trimmedMemberID, "public", "", "", 0, 0, createdAt); err != nil {
		return err
	}
	trimmedState := strings.TrimSpace(state)
	if trimmedState == "" {
		return nil
	}
	now := createdAt.UTC()
	if now.IsZero() {
		now = time.Now().UTC()
	}
	if _, err := execer.Exec(ctx, `
INSERT INTO scope_notification_settings (
  scope_id, member_id, state, task_watch_state,
  created_at, updated_at, unread_generation
) VALUES ($1, $2, $3, $3, $4, $4, 1)
ON DUPLICATE KEY UPDATE
  unread_generation = CASE
    WHEN task_watch_state IS NULL THEN unread_generation + 1
    ELSE unread_generation
  END,
  updated_at = CASE
    WHEN task_watch_state IS NULL THEN VALUES(updated_at)
    ELSE updated_at
  END,
  task_watch_state = COALESCE(task_watch_state, VALUES(task_watch_state))
`, trimmedScopeID, trimmedMemberID, trimmedState, now); err != nil {
		return fmt.Errorf("insert task scope default notification setting: %w", err)
	}
	return nil
}

func taskScopeDefaultUnwatchedNotificationStateForMember(ctx context.Context, queryer coredb.Queryer, memberID string) (string, error) {
	isAgent, err := taskScopeMemberIsAgent(ctx, queryer, memberID)
	if err != nil {
		return "", err
	}
	if isAgent {
		return "no-wake", nil
	}
	return "mute", nil
}

func taskScopeMemberIsAgent(ctx context.Context, queryer coredb.Queryer, memberID string) (bool, error) {
	var memberType string
	if err := queryer.QueryRow(ctx, `
SELECT type
FROM members
WHERE id = $1
`, strings.TrimSpace(memberID)).Scan(&memberType); err != nil {
		return false, fmt.Errorf("load task scope notification member type: %w", err)
	}
	return strings.TrimSpace(memberType) == string(domain.MemberTypeAgent), nil
}
