package message

import (
	"context"
	"encoding/base64"
	"fmt"
	"net/url"
	"strconv"
	"strings"

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

const (
	TaskWatcherPreviewSize = 3
	// Task watcher pages stay bounded for both preview and explicit pagination.
	DefaultTaskWatcherPageSize = 50
	MaxTaskWatcherPageSize     = 100
	taskWatcherCursorVersion   = 1
	// The envelope covers two utf8mb4 varchar(255) cursor fields.
	maxTaskWatcherCursorBytes = 8192
	maxTaskWatcherSortBytes   = 1020
	maxTaskWatcherMemberBytes = 1020
)

type taskWatcherCursor struct {
	Version     int
	DisplayName string
	ActorType   string
	MemberID    string
}

type visibleTaskWatcherRow struct {
	Profile AuthorProfile
	SortKey string
}

// visibleTaskWatcherProfilesSQL returns a bounded explicit-Watch roster plus
// independent totals for Watch state and current Task scope membership. Explicit
// Watch state comes only from the authoritative task_watch_state field; scope
// membership establishes current participation but never infers watcher state.
//
// The AuthorProfile projection intentionally matches the authoritative member
// presentation in scope_api.go. Keeping eligibility, lifecycle, presentation,
// total count, and the bounded page in one statement gives the caller a
// consistent snapshot. Task readability is authorized by the API's narrow
// Task gate; every authorized viewer sees the same complete explicit roster,
// including private participants, while this projection exposes no ownership
// or runtime metadata.
const visibleTaskWatcherProfilesSQL = `
WITH current_task_members AS (
  SELECT DISTINCT
    m.id AS member_id,
    m.type AS actor_type,
    CASE
      WHEN m.type = 'human' THEN COALESCE(u.id, '')
      WHEN m.type = 'agent' THEN COALESCE(a.id, '')
      ELSE ''
    END AS principal_id,
    CASE
      WHEN m.type = 'human' THEN m.id
      WHEN m.type = 'agent' THEN COALESCE(NULLIF(a.name, ''), NULLIF(m.display_name, ''), m.id)
      ELSE m.id
    END AS target_id,
    CASE
      WHEN m.type = 'human' THEN COALESCE(NULLIF(u.display_name, ''), NULLIF(m.display_name, ''), m.id)
      WHEN m.type = 'agent' THEN COALESCE(NULLIF(m.display_name, ''), NULLIF(a.name, ''), m.id)
      ELSE COALESCE(NULLIF(m.display_name, ''), m.id)
    END AS display_name,
    CASE
      WHEN m.type = 'human' THEN COALESCE(u.avatar_url, '')
      WHEN m.type = 'agent' THEN COALESCE(a.avatar_url, '')
      ELSE ''
    END AS avatar_url,
    COALESCE(sns.task_watch_state, '') AS task_watch_state,
    COALESCE(mac.id, '') AS agent_machine_id,
    LOWER(TRIM(
      CASE
        WHEN m.type = 'human' THEN COALESCE(NULLIF(u.display_name, ''), NULLIF(m.display_name, ''), m.id)
        WHEN m.type = 'agent' THEN COALESCE(NULLIF(m.display_name, ''), NULLIF(a.name, ''), m.id)
        ELSE COALESCE(NULLIF(m.display_name, ''), m.id)
      END
    )) AS sort_display_name
  FROM scopes task_scope
  JOIN scope_memberships sm
    ON sm.scope_id = task_scope.id
   AND sm.removed_at IS NULL
   AND sm.archived_at IS NULL
   AND sm.effective_to_seq IS NULL
  JOIN members m
    ON m.id = sm.member_id
   AND m.workspace_id = task_scope.workspace_id
   AND m.status = 'active'
  LEFT JOIN scope_notification_settings sns
    ON sns.scope_id = sm.scope_id
   AND sns.member_id = sm.member_id
  LEFT JOIN users u
    ON m.type = 'human'
   AND u.id = m.user_id
  LEFT JOIN agents a
    ON m.type = 'agent'
   AND a.member_id = m.id
   AND a.workspace_id = task_scope.workspace_id
   AND a.status <> 'archived'
  LEFT JOIN machines mac
    ON m.type = 'agent'
   AND mac.id = a.machine_id
   AND mac.workspace_id = a.workspace_id
   AND mac.status = 'active'
  WHERE task_scope.workspace_id = $1
    AND task_scope.id = $2
    AND task_scope.type = 'task'
),
visible_task_watchers AS (
  SELECT
    member_id,
    actor_type,
    principal_id,
    target_id,
    display_name,
    avatar_url,
    sort_display_name
  FROM current_task_members
  WHERE (
      (
        actor_type = 'human'
        AND principal_id <> ''
        AND task_watch_state = 'active'
      )
      OR (
        actor_type = 'agent'
        AND principal_id <> ''
        AND agent_machine_id <> ''
        AND task_watch_state = 'auto-wake'
      )
    )
),
visible_task_watcher_page AS (
  SELECT
    member_id,
    actor_type,
    principal_id,
    target_id,
    display_name,
    avatar_url,
    sort_display_name
  FROM visible_task_watchers
  WHERE $3 = ''
     OR sort_display_name > $3
     OR (sort_display_name = $3 AND actor_type > $4)
     OR (sort_display_name = $3 AND actor_type = $4 AND member_id > $5)
  ORDER BY sort_display_name ASC, actor_type ASC, member_id ASC
  LIMIT $6
),
visible_task_watcher_total AS (
  SELECT COUNT(*) AS watcher_count
  FROM visible_task_watchers
),
current_task_member_total AS (
  SELECT COUNT(*) AS member_count
  FROM current_task_members
)
SELECT
  COALESCE(page.member_id, ''),
  COALESCE(page.actor_type, ''),
  COALESCE(page.principal_id, ''),
  COALESCE(page.target_id, ''),
  COALESCE(page.display_name, ''),
  COALESCE(page.avatar_url, ''),
  COALESCE(page.sort_display_name, ''),
  watcher_total.watcher_count,
  member_total.member_count
FROM visible_task_watcher_total watcher_total
CROSS JOIN current_task_member_total member_total
LEFT JOIN visible_task_watcher_page page ON 1 = 1
ORDER BY page.sort_display_name ASC, page.actor_type ASC, page.member_id ASC
`

// LoadVisibleTaskWatcherPage returns explicit Task watchers after the caller's
// narrow readable-Task gate; Task roles never infer Watch state.
func LoadVisibleTaskWatcherPage(
	ctx context.Context,
	queryer coredb.Queryer,
	workspaceID string,
	taskScopeID string,
	cursor string,
	limit int,
) (TaskWatcherPage, error) {
	cursorKey, err := decodeTaskWatcherCursor(cursor)
	if err != nil {
		return TaskWatcherPage{}, err
	}
	pageSize := normalizeTaskWatcherPageSize(limit)
	rows, err := queryer.Query(
		ctx,
		visibleTaskWatcherProfilesSQL,
		strings.TrimSpace(workspaceID),
		strings.TrimSpace(taskScopeID),
		cursorKey.DisplayName,
		cursorKey.ActorType,
		cursorKey.MemberID,
		pageSize+1,
	)
	if err != nil {
		return TaskWatcherPage{}, fmt.Errorf("load visible task watcher page: %w", err)
	}
	defer rows.Close()

	items := make([]visibleTaskWatcherRow, 0, pageSize+1)
	watcherCount := 0
	memberCount := 0
	for rows.Next() {
		var item visibleTaskWatcherRow
		if err := rows.Scan(
			&item.Profile.MemberID,
			&item.Profile.ActorType,
			&item.Profile.PrincipalID,
			&item.Profile.TargetID,
			&item.Profile.DisplayName,
			&item.Profile.AvatarURL,
			&item.SortKey,
			&watcherCount,
			&memberCount,
		); err != nil {
			return TaskWatcherPage{}, fmt.Errorf("scan visible task watcher page: %w", err)
		}
		item.Profile = item.Profile.Normalize()
		// The LEFT JOIN produces one sentinel row when the requested page is
		// empty so watcherCount remains available even after the last cursor.
		if item.Profile.MemberID == "" {
			continue
		}
		if !item.Profile.Valid() {
			continue
		}
		items = append(items, item)
	}
	if err := rows.Err(); err != nil {
		return TaskWatcherPage{}, fmt.Errorf("iterate visible task watcher page: %w", err)
	}

	hasMore := len(items) > pageSize
	if hasMore {
		items = items[:pageSize]
	}
	profiles := make([]AuthorProfile, 0, len(items))
	for _, item := range items {
		profiles = append(profiles, item.Profile)
	}
	nextCursor := ""
	if hasMore && len(items) > 0 {
		last := items[len(items)-1]
		nextCursor = encodeTaskWatcherCursor(taskWatcherCursor{
			Version:     taskWatcherCursorVersion,
			DisplayName: last.SortKey,
			ActorType:   last.Profile.ActorType,
			MemberID:    last.Profile.MemberID,
		})
	}
	return TaskWatcherPage{
		Watchers:     profiles,
		WatcherCount: watcherCount,
		MemberCount:  memberCount,
		HasMore:      hasMore,
		NextCursor:   nextCursor,
	}, nil
}

func normalizeTaskWatcherPageSize(limit int) int {
	if limit <= 0 {
		return DefaultTaskWatcherPageSize
	}
	if limit > MaxTaskWatcherPageSize {
		return MaxTaskWatcherPageSize
	}
	return limit
}

func encodeTaskWatcherCursor(cursor taskWatcherCursor) string {
	values := url.Values{}
	values.Set("v", strconv.Itoa(cursor.Version))
	values.Set("d", cursor.DisplayName)
	values.Set("t", cursor.ActorType)
	values.Set("m", cursor.MemberID)
	return base64.RawURLEncoding.EncodeToString([]byte(values.Encode()))
}

func decodeTaskWatcherCursor(raw string) (taskWatcherCursor, error) {
	trimmed := strings.TrimSpace(raw)
	if trimmed == "" {
		return taskWatcherCursor{}, nil
	}
	if len(trimmed) > maxTaskWatcherCursorBytes {
		return taskWatcherCursor{}, badRequest("invalid task watcher cursor")
	}
	decoded, err := base64.RawURLEncoding.Strict().DecodeString(trimmed)
	if err != nil ||
		len(decoded) > maxTaskWatcherCursorBytes ||
		base64.RawURLEncoding.EncodeToString(decoded) != trimmed {
		return taskWatcherCursor{}, badRequest("invalid task watcher cursor")
	}
	values, err := url.ParseQuery(string(decoded))
	if err != nil ||
		len(values) != 4 ||
		len(values["v"]) != 1 ||
		len(values["d"]) != 1 ||
		len(values["t"]) != 1 ||
		len(values["m"]) != 1 {
		return taskWatcherCursor{}, badRequest("invalid task watcher cursor")
	}
	version, err := strconv.Atoi(values.Get("v"))
	if err != nil {
		return taskWatcherCursor{}, badRequest("invalid task watcher cursor")
	}
	cursor := taskWatcherCursor{
		Version:     version,
		DisplayName: values.Get("d"),
		ActorType:   values.Get("t"),
		MemberID:    values.Get("m"),
	}
	if cursor.Version != taskWatcherCursorVersion ||
		cursor.DisplayName == "" ||
		len(cursor.DisplayName) > maxTaskWatcherSortBytes ||
		(cursor.ActorType != AuthorActorTypeHuman && cursor.ActorType != AuthorActorTypeAgent) ||
		strings.TrimSpace(cursor.MemberID) != cursor.MemberID ||
		cursor.MemberID == "" ||
		len(cursor.MemberID) > maxTaskWatcherMemberBytes {
		return taskWatcherCursor{}, badRequest("invalid task watcher cursor")
	}
	return cursor, nil
}
