package taskboard

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"strings"
	"time"

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

type taskReadMessageCursor struct {
	ID          string
	Seq         int64
	GlobalSeq   int64
	ActivitySeq int64
	CreatedAt   time.Time
	Valid       bool
}

func (txs TxStore) MarkTaskRead(ctx context.Context, request MarkTaskReadRequest) (MarkTaskReadResponse, error) {
	workspaceID, err := request.Context.requireWorkspaceID()
	if err != nil {
		return MarkTaskReadResponse{}, err
	}
	userID, _, err := request.Context.requireUser()
	if err != nil {
		return MarkTaskReadResponse{}, err
	}

	tx := txs.tx
	task, err := txs.resolveTaskReadLifecycleCandidate(ctx, tx, workspaceID, request.Board, request.Task)
	if err != nil {
		return MarkTaskReadResponse{}, err
	}
	// TOCTOU fence: lock the canonical Task row (deriving the board Channel
	// server-side) then lock/revalidate the full human Channel authority under
	// the shared lock order, so a concurrent revoke / archive / effective-close /
	// Channel delete / member deactivate / Task move / Task delete serializes
	// deterministically and authority-loss-first fails closed with zero read-state
	// side effects.
	if _, err := humanTaskFencerFor(userID, workspaceID, taskReadChannelAction).lockExistingTaskChannel(ctx, tx, workspaceID, task.ID); err != nil {
		return MarkTaskReadResponse{}, err
	}
	active, err := message.LockActiveTaskScopeForMutation(ctx, tx, workspaceID, taskReadScopeID(task))
	if err != nil {
		return MarkTaskReadResponse{}, err
	}
	if !active {
		return MarkTaskReadResponse{}, ErrNotFound
	}
	task, err = txs.taskByID(ctx, tx, workspaceID, task.ID)
	if err != nil {
		return MarkTaskReadResponse{}, err
	}
	memberID, err := loadTaskReadMemberID(ctx, tx, workspaceID, userID)
	if err != nil {
		return MarkTaskReadResponse{}, err
	}
	taskScopeID := taskReadScopeID(task)
	latest, err := loadLatestTaskReadMessageCursor(ctx, tx, workspaceID, taskScopeID)
	if err != nil {
		return MarkTaskReadResponse{}, err
	}
	latestActivitySeq, err := loadLatestTaskReadActivitySeq(ctx, tx, workspaceID, taskScopeID)
	if err != nil {
		return MarkTaskReadResponse{}, err
	}
	now := time.Now().UTC()
	if err := ensureTaskScopeReadMembership(ctx, tx, taskScopeID, memberID, latest.Seq, latest.GlobalSeq, now); err != nil {
		return MarkTaskReadResponse{}, err
	}
	if latestActivitySeq > 0 {
		if err := message.WriteActivityReadStateByGlobalSeq(ctx, tx, memberID, taskScopeID, latestActivitySeq, now); err != nil {
			return MarkTaskReadResponse{}, fmt.Errorf("advance task scope activity read cursor: %w", err)
		}
	}
	if err := message.ClearConversationManualUnreadState(ctx, tx, memberID, taskScopeID, now); err != nil {
		return MarkTaskReadResponse{}, err
	}

	readState, err := loadTaskReadStateForUser(ctx, tx, workspaceID, userID, task)
	if err != nil {
		return MarkTaskReadResponse{}, err
	}
	task.ReadState = &readState
	return MarkTaskReadResponse{Task: task, ReadState: readState}, nil
}

func (txs TxStore) resolveTaskReadLifecycleCandidate(
	ctx context.Context,
	tx coredb.QueryExecer,
	workspaceID string,
	board BoardRef,
	taskRef TaskRef,
) (Task, error) {
	if taskID := strings.TrimSpace(taskRef.TaskID); taskID != "" {
		return txs.taskByID(ctx, tx, workspaceID, taskID)
	}
	channelID, err := board.requireChannelID()
	if err != nil {
		return Task{}, err
	}
	taskNumber, err := taskRef.requireTaskNumber()
	if err != nil {
		return Task{}, err
	}
	return txs.taskByNumber(ctx, tx, workspaceID, channelID, taskNumber)
}

func attachTaskReadStateForUser(ctx context.Context, queryer coredb.QueryRower, workspaceID string, userID string, task *Task) error {
	if task == nil {
		return nil
	}
	readState, err := loadTaskReadStateForUser(ctx, queryer, workspaceID, userID, *task)
	if err != nil {
		return err
	}
	task.ReadState = &readState
	return nil
}

func loadTaskReadStateForUser(ctx context.Context, queryer coredb.QueryRower, workspaceID string, userID string, task Task) (TaskReadState, error) {
	memberID, err := loadTaskReadMemberID(ctx, queryer, workspaceID, userID)
	if err != nil {
		return TaskReadState{}, err
	}
	taskScopeID := taskReadScopeID(task)
	latest, err := loadLatestTaskReadMessageCursor(ctx, queryer, workspaceID, taskScopeID)
	if err != nil {
		return TaskReadState{}, err
	}
	membershipCursor, err := loadTaskScopeMembershipReadCursor(ctx, queryer, taskScopeID, memberID)
	if err != nil {
		return TaskReadState{}, err
	}
	var manualUnread bool
	var manualUnreadGeneration int64
	var watchState string
	if err := queryer.QueryRow(ctx, `
SELECT (cms.manual_unread_at IS NOT NULL), COALESCE(cms.unread_generation, 0), COALESCE(sns.task_watch_state, '')
FROM (SELECT 1) seed
LEFT JOIN conversation_member_states cms ON cms.scope_id = $1 AND cms.member_id = $2
LEFT JOIN scope_notification_settings sns ON sns.scope_id = $1 AND sns.member_id = $2
`, taskScopeID, memberID).Scan(&manualUnread, &manualUnreadGeneration, &watchState); err != nil {
		return TaskReadState{}, fmt.Errorf("load task read preferences: %w", err)
	}
	if manualUnreadGeneration < 0 {
		return TaskReadState{}, fmt.Errorf("invalid task manual unread generation")
	}
	if strings.TrimSpace(watchState) != message.HumanScopeSubscriptionStateActive {
		watchState = message.HumanScopeSubscriptionStateMute
	}
	readState := TaskReadState{
		ManualUnread:           manualUnread,
		LastMessageReadSeq:     membershipCursor.Seq,
		LastActivityReadSeq:    membershipCursor.ActivitySeq,
		ManualUnreadGeneration: manualUnreadGeneration,
		WatchState:             watchState,
	}
	if latest.Valid {
		latestAt := latest.CreatedAt
		readState.LatestEventID = latest.ID
		readState.LatestEventCreatedAt = &latestAt
	}
	if membershipCursor.Valid {
		lastRead, err := loadTaskReadMessageCursorAtOrBeforeSeq(ctx, queryer, workspaceID, taskScopeID, membershipCursor.Seq)
		if err != nil {
			return TaskReadState{}, err
		}
		if lastRead.Valid {
			lastReadAt := lastRead.CreatedAt
			readState.LastReadEventID = lastRead.ID
			readState.LastReadEventCreatedAt = &lastReadAt
		}
	}
	return readState, nil
}

func loadTaskReadMemberID(ctx context.Context, queryer coredb.QueryRower, workspaceID string, userID string) (string, error) {
	membership, err := access.LoadHumanWorkspaceMember(ctx, queryer, userID, workspaceID)
	if err != nil {
		return "", fmt.Errorf("load task read member: %w", err)
	}
	memberID := strings.TrimSpace(membership.Member.ID)
	if memberID == "" {
		return "", fmt.Errorf("load task read member: member id is empty")
	}
	return memberID, nil
}

func taskReadScopeID(task Task) string {
	if taskScopeID := strings.TrimSpace(task.TaskScopeID); taskScopeID != "" {
		return taskScopeID
	}
	return taskScopeID(task.ID)
}

func ensureTaskScopeReadMembership(ctx context.Context, execer coredb.QueryExecer, taskScopeID string, memberID string, seq int64, globalSeq int64, updatedAt time.Time) error {
	trimmedTaskScopeID := strings.TrimSpace(taskScopeID)
	trimmedMemberID := strings.TrimSpace(memberID)
	if trimmedTaskScopeID == "" || trimmedMemberID == "" {
		return fmt.Errorf("task scope read membership requires scope and member")
	}
	if updatedAt.IsZero() {
		updatedAt = time.Now().UTC()
	}
	commandTag, err := execer.Exec(ctx, `
UPDATE scope_memberships
SET unread_generation = CASE
        WHEN GREATEST(last_read_seq, $3) <> last_read_seq
          OR GREATEST(last_read_global_seq, $4) <> last_read_global_seq
          OR removed_at IS NOT NULL
          OR archived_at IS NOT NULL THEN unread_generation + 1
        ELSE unread_generation
    END,
    last_read_seq = GREATEST(last_read_seq, $3),
    last_read_global_seq = GREATEST(last_read_global_seq, $4),
    updated_at = CASE
        WHEN GREATEST(last_read_seq, $3) <> last_read_seq
          OR GREATEST(last_read_global_seq, $4) <> last_read_global_seq
          OR removed_at IS NOT NULL
          OR archived_at IS NOT NULL THEN $5
        ELSE updated_at
    END,
    removed_at = NULL,
    archived_at = NULL
WHERE scope_id = $1
  AND member_id = $2
  AND effective_to_seq IS NULL
  AND (
      last_read_seq < $3
      OR last_read_global_seq < $4
      OR removed_at IS NOT NULL
      OR archived_at IS NOT NULL
  )
`, trimmedTaskScopeID, trimmedMemberID, seq, globalSeq, updatedAt)
	if err != nil {
		return fmt.Errorf("advance task scope read membership: %w", err)
	}
	if commandTag.RowsAffected() != 0 {
		return nil
	}
	var openMembershipExists int
	if err := execer.QueryRow(ctx, `
SELECT 1
FROM scope_memberships
WHERE scope_id = $1
  AND member_id = $2
  AND effective_to_seq IS NULL
LIMIT 1
`, trimmedTaskScopeID, trimmedMemberID).Scan(&openMembershipExists); err != nil && !errors.Is(err, sql.ErrNoRows) {
		return fmt.Errorf("check task scope read membership after update: %w", err)
	}
	if openMembershipExists != 0 {
		return nil
	}
	if _, err := execer.Exec(ctx, `
INSERT INTO scope_memberships (
  scope_id,
  member_id,
  visibility,
  owner_member_id,
  last_read_seq,
  last_read_global_seq,
  effective_from_seq,
  effective_to_seq,
  authority_from_at,
  authority_to_at,
  created_at,
  updated_at,
  removed_at,
  archived_at
) VALUES ($1, $2, 'public', NULL, $3, $4, 1, NULL, LEAST($5, CURRENT_TIMESTAMP(6)), NULL, $5, $5, NULL, NULL)
ON DUPLICATE KEY UPDATE
  visibility = VALUES(visibility),
  owner_member_id = VALUES(owner_member_id),
  unread_generation = CASE
    WHEN GREATEST(scope_memberships.last_read_seq, VALUES(last_read_seq)) <> scope_memberships.last_read_seq
      OR GREATEST(scope_memberships.last_read_global_seq, VALUES(last_read_global_seq)) <> scope_memberships.last_read_global_seq
      OR scope_memberships.effective_to_seq IS NOT NULL
      OR scope_memberships.removed_at IS NOT NULL
      OR scope_memberships.archived_at IS NOT NULL THEN scope_memberships.unread_generation + 1
    ELSE scope_memberships.unread_generation
  END,
  last_read_seq = GREATEST(scope_memberships.last_read_seq, VALUES(last_read_seq)),
  last_read_global_seq = GREATEST(scope_memberships.last_read_global_seq, VALUES(last_read_global_seq)),
  effective_to_seq = VALUES(effective_to_seq),
  authority_from_at = CASE
    WHEN scope_memberships.removed_at IS NOT NULL
      OR scope_memberships.archived_at IS NOT NULL
      OR scope_memberships.effective_to_seq IS NOT NULL THEN VALUES(authority_from_at)
    ELSE COALESCE(scope_memberships.authority_from_at, scope_memberships.created_at)
  END,
  authority_to_at = CASE
    WHEN scope_memberships.removed_at IS NOT NULL
      OR scope_memberships.archived_at IS NOT NULL
      OR scope_memberships.effective_to_seq IS NOT NULL THEN NULL
    ELSE scope_memberships.authority_to_at
  END,
  updated_at = CASE
    WHEN GREATEST(scope_memberships.last_read_seq, VALUES(last_read_seq)) <> scope_memberships.last_read_seq
      OR GREATEST(scope_memberships.last_read_global_seq, VALUES(last_read_global_seq)) <> scope_memberships.last_read_global_seq
      OR scope_memberships.effective_to_seq IS NOT NULL
      OR scope_memberships.removed_at IS NOT NULL
      OR scope_memberships.archived_at IS NOT NULL THEN VALUES(updated_at)
    ELSE scope_memberships.updated_at
  END,
  removed_at = VALUES(removed_at),
  archived_at = VALUES(archived_at)
`, trimmedTaskScopeID, trimmedMemberID, seq, globalSeq, updatedAt); err != nil {
		return fmt.Errorf("ensure task scope read membership: %w", err)
	}
	return nil
}

func loadLatestTaskReadMessageCursor(ctx context.Context, queryer coredb.QueryRower, workspaceID string, taskScopeID string) (taskReadMessageCursor, error) {
	var cursor taskReadMessageCursor
	if err := queryer.QueryRow(ctx, `
SELECT id, seq, workspace_global_seq, created_at
FROM scope_messages
WHERE workspace_id = $1
  AND scope_id = $2
  AND message_type = $3
ORDER BY seq DESC
LIMIT 1
`, strings.TrimSpace(workspaceID), strings.TrimSpace(taskScopeID), taskScopeMessageTypeDiscussion).Scan(&cursor.ID, &cursor.Seq, &cursor.GlobalSeq, &cursor.CreatedAt); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return taskReadMessageCursor{}, nil
		}
		return taskReadMessageCursor{}, fmt.Errorf("load latest task scope message cursor: %w", err)
	}
	cursor.Valid = true
	return cursor, nil
}

func loadLatestTaskReadActivitySeq(ctx context.Context, queryer coredb.QueryRower, workspaceID string, taskScopeID string) (int64, error) {
	var seq int64
	if err := queryer.QueryRow(ctx, `
SELECT seq
FROM scope_activity_events
WHERE workspace_id = $1
  AND scope_id = $2
  AND suppressed_at IS NULL
ORDER BY seq DESC
LIMIT 1
`, strings.TrimSpace(workspaceID), strings.TrimSpace(taskScopeID)).Scan(&seq); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return 0, nil
		}
		return 0, fmt.Errorf("load latest task scope activity cursor: %w", err)
	}
	if seq < 0 {
		return 0, fmt.Errorf("invalid latest task scope activity cursor")
	}
	return seq, nil
}

func loadTaskReadMessageCursorAtOrBeforeSeq(ctx context.Context, queryer coredb.QueryRower, workspaceID string, taskScopeID string, seq int64) (taskReadMessageCursor, error) {
	if seq <= 0 {
		return taskReadMessageCursor{}, nil
	}
	var cursor taskReadMessageCursor
	if err := queryer.QueryRow(ctx, `
SELECT id, seq, workspace_global_seq, created_at
FROM scope_messages
WHERE workspace_id = $1
  AND scope_id = $2
  AND message_type = $3
  AND seq <= $4
ORDER BY seq DESC
LIMIT 1
`, strings.TrimSpace(workspaceID), strings.TrimSpace(taskScopeID), taskScopeMessageTypeDiscussion, seq).Scan(&cursor.ID, &cursor.Seq, &cursor.GlobalSeq, &cursor.CreatedAt); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return taskReadMessageCursor{}, nil
		}
		return taskReadMessageCursor{}, fmt.Errorf("load task scope read message cursor: %w", err)
	}
	cursor.Valid = true
	return cursor, nil
}

func loadTaskScopeMembershipReadCursor(ctx context.Context, queryer coredb.QueryRower, taskScopeID string, memberID string) (taskReadMessageCursor, error) {
	var cursor taskReadMessageCursor
	if err := queryer.QueryRow(ctx, `
SELECT last_read_seq, last_read_global_seq, last_activity_global_seq
FROM scope_memberships
WHERE scope_id = $1
  AND member_id = $2
  AND effective_to_seq IS NULL
  AND removed_at IS NULL
  AND archived_at IS NULL
LIMIT 1
`, strings.TrimSpace(taskScopeID), strings.TrimSpace(memberID)).Scan(&cursor.Seq, &cursor.GlobalSeq, &cursor.ActivitySeq); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return taskReadMessageCursor{}, nil
		}
		return taskReadMessageCursor{}, fmt.Errorf("load task scope membership read cursor: %w", err)
	}
	cursor.Valid = cursor.Seq > 0 || cursor.GlobalSeq > 0 || cursor.ActivitySeq > 0
	return cursor, nil
}
