package im

import (
	"context"
	"fmt"
	"time"

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

// ArchiveIndexedConversation archives one existing member-local Inbox
// projection and appends its stream event atomically. Unlike source-backed
// conversation actions, this operation deliberately does not require a live
// scope: after a source object is hard-deleted, the exact member-local index row
// is the only remaining object the member can dismiss.
func (store *Store) ArchiveIndexedConversation(ctx context.Context, request IndexedConversationArchiveRequest) (StreamEvent, ConversationIndexItem, error) {
	if err := store.Validate(); err != nil {
		return StreamEvent{}, ConversationIndexItem{}, err
	}
	workspaceID, memberID, err := requireWorkspaceMember(request.WorkspaceID, request.MemberID)
	if err != nil {
		return StreamEvent{}, ConversationIndexItem{}, err
	}
	conversationID := normalizedString(request.ConversationID)
	if conversationID == "" {
		return StreamEvent{}, ConversationIndexItem{}, fmt.Errorf("conversation id is required")
	}

	var event StreamEvent
	var conversation ConversationIndexItem
	err = coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		// Read once without taking the index-row lock so this operation keeps the
		// same lock order as every other stream projection write: stream event
		// first, conversation index second. The later FOR UPDATE read refreshes
		// the projection before it is persisted.
		snapshot, err := loadIndexedConversationForArchive(ctx, tx, workspaceID, memberID, conversationID, false)
		if err != nil {
			return err
		}

		event = request.Event
		if event.CreatedAt.IsZero() {
			event.CreatedAt = time.Now().UTC()
		} else {
			event.CreatedAt = event.CreatedAt.UTC()
		}
		projectionKind, _ := snapshot.Projection["kind"].(string)
		snapshot = archivedConversationIndexItem(snapshot, event.CreatedAt)
		payload := make(map[string]any, len(event.Payload)+1)
		for key, value := range event.Payload {
			payload[key] = value
		}
		payload["conversation"] = archivedConversationEventProjection(snapshot, projectionKind)
		event.Payload = payload
		event.ScopeID = snapshot.ScopeID
		event.ConversationID = snapshot.ConversationID
		event, err = appendMemberWorkspaceEventTx(ctx, tx, workspaceID, memberID, event)
		if err != nil {
			return err
		}
		if err := persistConversationHiddenMarkerIfSourceExists(ctx, tx, workspaceID, memberID, snapshot.ScopeID, event.CreatedAt); err != nil {
			return err
		}

		conversation, err = loadIndexedConversationForArchive(ctx, tx, workspaceID, memberID, conversationID, true)
		if err != nil {
			return err
		}
		conversation = archivedConversationIndexItem(conversation, event.CreatedAt)
		conversation.LastEventSeq = event.EventSeq
		conversation.Projection = mergeConversationProjection(conversation.Projection, conversation)

		if err := upsertConversationTx(ctx, tx, workspaceID, memberID, conversation); err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		return StreamEvent{}, ConversationIndexItem{}, err
	}
	return event, conversation, nil
}

func archivedConversationIndexItem(item ConversationIndexItem, archivedAt time.Time) ConversationIndexItem {
	if item.ArchivedAt == nil {
		value := archivedAt.UTC()
		item.ArchivedAt = &value
	}
	item.Projection = mergeConversationProjection(item.Projection, item)
	item.Projection["sidebarHidden"] = true
	return item
}

func archivedConversationEventProjection(item ConversationIndexItem, projectionKind string) map[string]any {
	projection := make(map[string]any, len(item.Projection)+3)
	for key, value := range item.Projection {
		projection[key] = value
	}
	if projectionKind = frontendConversationProjectionKind(item, projectionKind); projectionKind != "" {
		projection["kind"] = projectionKind
	}
	// Persisted Inbox projections intentionally omit volatile unread fields;
	// realtime ConversationItem consumers require unreadCount, and an archived
	// row must clear every unread signal in other tabs and replayed clients.
	projection["unreadCount"] = int64(0)
	projection["manualUnread"] = false
	projection["hasUnread"] = false
	return projection
}

func frontendConversationProjectionKind(item ConversationIndexItem, projectionKind string) string {
	projectionKind = normalizedString(projectionKind)
	if projectionKind == "" {
		projectionKind = normalizedString(item.Kind)
	}
	switch projectionKind {
	case "replyThread", "humanDm", "agentDm", "serviceDm", "serviceAccount":
		return projectionKind
	case "thread":
		return "replyThread"
	case "service_account":
		return "serviceAccount"
	case "dm":
		switch normalizedString(fmt.Sprint(item.Projection["peerType"])) {
		case "human", "user":
			return "humanDm"
		case "agent":
			return "agentDm"
		case "service":
			return "serviceDm"
		}
	}
	return projectionKind
}

func loadIndexedConversationForArchive(ctx context.Context, queryer coredb.QueryRower, workspaceID string, memberID string, conversationID string, lock bool) (ConversationIndexItem, error) {
	query := `
SELECT workspace_id, member_id, conversation_id, scope_id, kind, sort_at,
       last_event_seq, pinned_at, archived_at,
       projection_json, created_at, updated_at,
       CASE
         WHEN kind = 'task' THEN COALESCE((
           SELECT CASE WHEN sns.task_watch_state = 'active' THEN 'active' ELSE 'mute' END
           FROM scope_notification_settings sns
           WHERE sns.scope_id = im_conversation_index.scope_id
             AND sns.member_id = im_conversation_index.member_id
           LIMIT 1
         ), 'mute')
         ELSE ''
       END AS subscription_state
FROM im_conversation_index
WHERE workspace_id = $1
  AND member_id = $2
  AND conversation_id = $3
  AND kind IN ('channel', 'dm', 'thread', 'task', 'service_account', 'replyThread', 'humanDm', 'agentDm', 'serviceDm', 'serviceAccount')
`
	if lock {
		query += " FOR UPDATE"
	}
	row := queryer.QueryRow(ctx, query, workspaceID, memberID, conversationID)
	item, err := scanConversationIndexItem(row)
	if err != nil {
		if coredb.IsNotFound(err) {
			return ConversationIndexItem{}, fmt.Errorf("conversation not found: %w", err)
		}
		return ConversationIndexItem{}, err
	}
	return item, nil
}

// persistConversationHiddenMarkerIfSourceExists keeps a source-backed
// conversation hidden across later projection refreshes. Hard-deleted sources
// deliberately match no rows, leaving the member-local index as the sole
// durable authority. The exact index row was already checked by the caller, so
// this never grants access to a live source that the member could not see.
func persistConversationHiddenMarkerIfSourceExists(ctx context.Context, execer coredb.QueryExecer, workspaceID string, memberID string, scopeID string, hiddenAt time.Time) error {
	scopeID = normalizedString(scopeID)
	if scopeID == "" {
		return nil
	}
	_, err := execer.Exec(ctx, `
INSERT INTO conversation_member_states (
  scope_id, member_id, pinned_at, manual_unread_at, manual_unread_seq,
  hidden_at, hidden_through_seq, hidden_through_activity_seq, created_at, updated_at
)
SELECT s.id, m.id, NULL, NULL, NULL,
       $4, COALESCE(s.latest_seq, 0), COALESCE(s.latest_activity_seq, 0), $4, $4
FROM scopes s
JOIN members m
  ON m.id = $2
 AND m.workspace_id = s.workspace_id
 AND m.status = 'active'
WHERE s.workspace_id = $1
  AND s.id = $3
  AND s.status = 'active'
  AND s.archived_at IS NULL
ON DUPLICATE KEY UPDATE
  hidden_at = VALUES(hidden_at),
  hidden_through_seq = VALUES(hidden_through_seq),
  hidden_through_activity_seq = VALUES(hidden_through_activity_seq),
  updated_at = VALUES(updated_at)
`, workspaceID, memberID, scopeID, hiddenAt.UTC())
	if err != nil {
		return fmt.Errorf("persist conversation hidden marker: %w", err)
	}
	return nil
}
