package app

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

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/pinyinsearch"
	"github.com/tidbcloud/AgentTeam/internal/store/access"
	"github.com/tidbcloud/AgentTeam/internal/store/app/registry"
	"github.com/tidbcloud/AgentTeam/internal/store/app/runtime"
	"github.com/tidbcloud/AgentTeam/internal/store/workspace"
)

type ChannelAddablePeopleOptions struct {
	Limit  int
	Cursor string
	Query  string
}

type ChannelAddablePeoplePage struct {
	Humans     []workspace.WorkspaceMember
	Services   []AppInstallationView
	HasMore    bool
	NextCursor string
}

type channelAddablePeopleCursor struct {
	Kind        string
	DisplayName string
	ID          string
}

func (store *Store) ListChannelAddablePeopleForUser(ctx context.Context, userID string, workspaceID string, channelID string, options ChannelAddablePeopleOptions) (ChannelAddablePeoplePage, error) {
	channelAccess, err := store.requireChannelInvite(ctx, userID, workspaceID, channelID)
	if err != nil {
		return ChannelAddablePeoplePage{}, err
	}
	if channelAccess.Channel.Type != "channel" {
		return ChannelAddablePeoplePage{}, badRequest("thread channels do not have direct members")
	}
	normalized := normalizeChannelAddablePeopleOptions(options)
	cursor, err := decodeChannelAddablePeopleCursor(normalized.Cursor)
	if err != nil {
		return ChannelAddablePeoplePage{}, err
	}
	queryPattern := "%" + strings.ToLower(normalized.Query) + "%"
	if normalized.Query == "" {
		queryPattern = ""
	}
	pinyinPattern := ""
	if normalized.Query != "" {
		normalizedQuery := pinyinsearch.NormalizeQuery(normalized.Query)
		if normalizedQuery != "" {
			pinyinPattern = "%" + normalizedQuery + "%"
		}
	}
	isAllChannel, err := appMentionDeliveryScopeIsAllChannel(ctx, store.db, workspaceID, channelID)
	if err != nil {
		return ChannelAddablePeoplePage{}, err
	}
	isWorkspaceAdmin := access.HasWorkspaceAdminRole(domain.WorkspaceRole(channelAccess.WorkspaceMember.WorkspaceRole))

	rows, err := store.db.Query(ctx, `
WITH addable_people AS (
    SELECT
        'human' AS kind,
        m.user_id AS id,
        m.id AS member_id,
        COALESCE(u.email, '') AS email,
        u.name AS name,
        COALESCE(NULLIF(u.display_name, ''), NULLIF(m.display_name, ''), m.id) AS display_name,
        u.avatar_url AS avatar_url,
        m.workspace_role AS role,
        m.created_at AS joined_at,
        '' AS installation_id,
        '' AS app_id,
        '' AS app_slug,
        '' AS app_name,
	        '' AS installed_version,
	        '' AS handle,
	        '' AS catalog_visibility,
	        NULL AS config,
	        NULL AS installation_metadata,
	        TRUE AS enabled,
        m.created_at AS created_at,
        m.updated_at AS updated_at,
        FALSE AS in_channel
    FROM members m
    JOIN users u ON u.id = m.user_id
    WHERE m.workspace_id = $1
      AND m.type = 'human'
      AND m.status = 'active'
      AND NOT EXISTS (
          SELECT 1
          FROM scope_memberships existing
          WHERE existing.scope_id = $2
            AND existing.member_id = m.id
            AND existing.visibility = 'public'
            AND existing.removed_at IS NULL
            AND existing.archived_at IS NULL
            AND existing.effective_to_seq IS NULL
      )
      AND ($3 = '' OR LOWER(CONCAT(
            m.id, ' ',
            COALESCE(NULLIF(m.display_name, ''), ''), ' ',
            COALESCE(NULLIF(u.name, ''), ''), ' ',
            COALESCE(NULLIF(u.display_name, ''), ''), ' ',
            COALESCE(NULLIF(u.email, ''), '')
      )) LIKE $3 OR ($12 <> '' AND (LOWER(m.display_name_pinyin) LIKE $12 OR LOWER(m.display_name_pinyin_initials) LIKE $12)))
    UNION ALL
    SELECT
        'service' AS kind,
        sm.member_id AS id,
        sm.member_id AS member_id,
        '' AS email,
        COALESCE(NULLIF(sm.handle, ''), sm.member_id) AS name,
        COALESCE(NULLIF(sm.display_name, ''), NULLIF(sd.name, ''), NULLIF(sm.handle, ''), sm.member_id) AS display_name,
        COALESCE(NULLIF(sm.avatar_url, ''), sd.avatar_url) AS avatar_url,
        '' AS role,
        sm.created_at AS joined_at,
        si.id AS installation_id,
        sd.id AS app_id,
        sd.slug AS app_slug,
        sd.name AS app_name,
        si.installed_version AS installed_version,
	        sm.handle AS handle,
	        CASE
	            WHEN attribution.service_definition_id IS NULL THEN ''
	            ELSE attribution.catalog_visibility
	        END AS catalog_visibility,
	        NULL AS config,
	        NULL AS installation_metadata,
        si.status = 'active' AS enabled,
        si.created_at AS created_at,
        si.updated_at AS updated_at,
        FALSE AS in_channel
	    FROM service_installations si
	    JOIN service_definitions sd
	      ON sd.id = si.service_definition_id
	     AND sd.service_kind = si.service_kind
	     AND LOWER(TRIM(sd.status)) = 'active'
	    JOIN service_members sm
	      ON sm.workspace_id = si.workspace_id
	     AND sm.service_definition_id = si.service_definition_id
	     AND sm.service_installation_id = si.id
	     AND sm.service_kind = si.service_kind
	     AND sm.status = 'active'
	    JOIN members visible_service_member
	      ON visible_service_member.id = sm.member_id
	     AND visible_service_member.workspace_id = si.workspace_id
	     AND visible_service_member.workspace_id = sm.workspace_id
	     AND visible_service_member.type = 'service'
	     AND visible_service_member.status = 'active'
	    LEFT JOIN extension_catalog_attributions attribution
	      ON attribution.workspace_id = si.workspace_id
	     AND attribution.service_definition_id = si.service_definition_id
	    WHERE si.workspace_id = $1
	      AND $4 = TRUE
	      AND si.service_kind = 'app'
	      AND si.status = 'active'
      AND NOT EXISTS (
          SELECT 1
          FROM scope_memberships existing
          WHERE existing.scope_id = $2
            AND existing.member_id = sm.member_id
            AND existing.visibility = 'public'
            AND existing.removed_at IS NULL
            AND existing.archived_at IS NULL
            AND existing.effective_to_seq IS NULL
      )
	      AND (
	          (
	                 (sd.id = 'app_github' AND LOWER(TRIM(sd.slug)) = 'github')
	              OR (sd.id = 'app_gitea' AND LOWER(TRIM(sd.slug)) = 'gitea')
	          )
	          OR (
	              attribution.service_definition_id IS NOT NULL
	              AND (
	                  $14 = TRUE
	                  OR attribution.catalog_visibility = 'public'
	                  OR EXISTS (
	              SELECT 1
	              FROM extension_definition_managers manager
	              WHERE manager.workspace_id = si.workspace_id
	                AND manager.service_definition_id = si.service_definition_id
	                AND manager.member_id = $13
	                AND manager.status = 'active'
	                AND manager.ended_at IS NULL
	                  )
	              )
	          )
	      )
      AND COALESCE(JSON_TYPE(JSON_EXTRACT(
          si.metadata,
          '$.manifest.capabilities.runtime'
      )), '') = 'OBJECT'
      AND JSON_CONTAINS(
          COALESCE(JSON_EXTRACT(
              si.metadata,
              '$.manifest.capabilities.runtime.events'
          ), JSON_ARRAY()),
          JSON_QUOTE($5)
      ) = 1
      AND EXISTS (
          SELECT 1
          FROM service_grants mention_grant
          WHERE mention_grant.service_installation_id = si.id
            AND mention_grant.scope = $6
            AND mention_grant.revoked_at IS NULL
            AND (mention_grant.expires_at IS NULL OR mention_grant.expires_at > CURRENT_TIMESTAMP)
      )
      AND EXISTS (
          SELECT 1
          FROM service_grants write_grant
          WHERE write_grant.service_installation_id = si.id
            AND write_grant.scope = $7
            AND write_grant.revoked_at IS NULL
            AND (write_grant.expires_at IS NULL OR write_grant.expires_at > CURRENT_TIMESTAMP)
      )
	      AND ($3 = '' OR LOWER(CONCAT(
            sm.member_id, ' ',
            COALESCE(NULLIF(sm.display_name, ''), ''), ' ',
            COALESCE(NULLIF(sm.handle, ''), ''), ' ',
            COALESCE(NULLIF(sd.name, ''), ''), ' ',
            COALESCE(NULLIF(sd.slug, ''), '')
      )) LIKE $3)
)
SELECT kind, id, member_id, email, name, display_name, avatar_url, role, joined_at,
       installation_id, app_id, app_slug, app_name, installed_version, handle,
       catalog_visibility, config, installation_metadata, enabled, created_at, updated_at, in_channel
FROM addable_people
WHERE (($8 = '' AND $9 = '' AND $10 = '') OR (kind > $8 OR (kind = $8 AND (display_name > $9 OR (display_name = $9 AND id > $10)))))
ORDER BY kind ASC, display_name ASC, id ASC
LIMIT $11
`, strings.TrimSpace(workspaceID), strings.TrimSpace(channelID), queryPattern, !isAllChannel, registry.AppEventMessageMention, AppMentionScopeRead, AppRuntimeScopeMessageWrite, cursor.Kind, cursor.DisplayName, cursor.ID, normalized.Limit+1, pinyinPattern, strings.TrimSpace(channelAccess.WorkspaceMember.Member.ID), isWorkspaceAdmin)
	if err != nil {
		return ChannelAddablePeoplePage{}, fmt.Errorf("list channel addable people: %w", err)
	}
	defer rows.Close()

	type item struct {
		kind        string
		id          string
		displayName string
		inChannel   bool
		human       workspace.WorkspaceMember
		service     AppInstallationView
	}
	items := make([]item, 0, normalized.Limit+1)
	for rows.Next() {
		var item item
		var config []byte
		var installationMetadata []byte
		if err := rows.Scan(
			&item.kind,
			&item.id,
			&item.human.MemberID,
			&item.human.Email,
			&item.human.Name,
			&item.displayName,
			&item.human.AvatarURL,
			&item.human.Role,
			&item.human.JoinedAt,
			&item.service.ID,
			&item.service.AppID,
			&item.service.AppSlug,
			&item.service.AppName,
			&item.service.InstalledVersion,
			&item.service.Handle,
			&item.service.CatalogVisibility,
			&config,
			&installationMetadata,
			&item.service.Enabled,
			&item.service.CreatedAt,
			&item.service.UpdatedAt,
			&item.inChannel,
		); err != nil {
			return ChannelAddablePeoplePage{}, fmt.Errorf("scan channel addable person: %w", err)
		}
		inChannel := item.inChannel
		switch item.kind {
		case "human":
			item.human.UserID = item.id
			item.human.DisplayName = item.displayName
			item.human.InChannel = &inChannel
			item.human.LegacyTargetKeys = legacyTargetKeys(item.human.MemberID, item.human.Name)
			item.human.AccessSources = []string{}
		case "service":
			item.service.MemberID = item.human.MemberID
			item.service.AvatarURL = item.human.AvatarURL
			// Channel candidates are an identity-only projection. The query still
			// validates the pinned App capability and grants, but configuration,
			// connections, and runtime transport metadata are deliberately absent.
			item.service.Config = nil
			item.service.Connections = []runtime.ConnectionView{}
			item.service.RuntimeCapability = nil
			item.service.InChannel = &inChannel
		default:
			return ChannelAddablePeoplePage{}, fmt.Errorf("invalid channel addable people kind %q", item.kind)
		}
		items = append(items, item)
	}
	if err := rows.Err(); err != nil {
		return ChannelAddablePeoplePage{}, fmt.Errorf("iterate channel addable people: %w", err)
	}

	hasMore := len(items) > normalized.Limit
	if hasMore {
		items = items[:normalized.Limit]
	}
	page := ChannelAddablePeoplePage{
		Humans:   make([]workspace.WorkspaceMember, 0),
		Services: make([]AppInstallationView, 0),
		HasMore:  hasMore,
	}
	for _, item := range items {
		if item.kind == "human" {
			page.Humans = append(page.Humans, item.human)
		} else {
			page.Services = append(page.Services, item.service)
		}
	}
	if hasMore && len(items) > 0 {
		last := items[len(items)-1]
		page.NextCursor, err = encodeChannelAddablePeopleCursor(channelAddablePeopleCursor{Kind: last.kind, DisplayName: last.displayName, ID: last.id})
		if err != nil {
			return ChannelAddablePeoplePage{}, err
		}
	}
	return page, nil
}

func normalizeChannelAddablePeopleOptions(options ChannelAddablePeopleOptions) ChannelAddablePeopleOptions {
	limit := options.Limit
	if limit <= 0 {
		limit = 50
	}
	if limit > 100 {
		limit = 100
	}
	return ChannelAddablePeopleOptions{Limit: limit, Cursor: strings.TrimSpace(options.Cursor), Query: strings.TrimSpace(options.Query)}
}

func encodeChannelAddablePeopleCursor(cursor channelAddablePeopleCursor) (string, error) {
	normalized, err := normalizeChannelAddablePeopleCursor(cursor)
	if err != nil {
		return "", err
	}
	values := url.Values{}
	values.Set("kind", normalized.Kind)
	values.Set("displayName", normalized.DisplayName)
	values.Set("id", normalized.ID)
	return base64.RawURLEncoding.EncodeToString([]byte(values.Encode())), nil
}

func decodeChannelAddablePeopleCursor(token string) (channelAddablePeopleCursor, error) {
	trimmed := strings.TrimSpace(token)
	if trimmed == "" {
		return channelAddablePeopleCursor{}, nil
	}
	payload, err := base64.RawURLEncoding.DecodeString(trimmed)
	if err != nil {
		return channelAddablePeopleCursor{}, badRequest("invalid channel addable people cursor")
	}
	values, err := url.ParseQuery(string(payload))
	if err != nil {
		return channelAddablePeopleCursor{}, badRequest("invalid channel addable people cursor")
	}
	return normalizeChannelAddablePeopleCursor(channelAddablePeopleCursor{
		Kind:        values.Get("kind"),
		DisplayName: values.Get("displayName"),
		ID:          values.Get("id"),
	})
}

func normalizeChannelAddablePeopleCursor(cursor channelAddablePeopleCursor) (channelAddablePeopleCursor, error) {
	kind := strings.TrimSpace(cursor.Kind)
	switch kind {
	case "human", "service":
	default:
		return channelAddablePeopleCursor{}, badRequest("invalid channel addable people cursor")
	}
	id := strings.TrimSpace(cursor.ID)
	if id == "" {
		return channelAddablePeopleCursor{}, badRequest("invalid channel addable people cursor")
	}
	return channelAddablePeopleCursor{
		Kind:        kind,
		DisplayName: strings.TrimSpace(cursor.DisplayName),
		ID:          id,
	}, nil
}
