package channel

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/access"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/plan"
)

// ManagedIMChannelListOptions is the bounded continuation input for the
// Agent-managed plain-IM channel list. Cursor is a decimal offset in V1.
type ManagedIMChannelListOptions struct {
	Limit  int
	Cursor string
}

// ManagedIMChannelPage is one store-projected page of channels after the
// channel-management carrier filter has been applied.
type ManagedIMChannelPage struct {
	Channels   []message.ChannelSummary
	HasMore    bool
	NextCursor string
	TotalCount int
}

func (store *Store) ListChannelsForUser(ctx context.Context, userID string, workspaceID string) ([]message.ChannelSummary, error) {
	memberID, err := store.requireWorkspaceAccess(ctx, userID, workspaceID)
	if err != nil {
		return nil, err
	}
	channels, err := message.ListChannelsForMember(ctx, store.db, memberID, workspaceID)
	if err != nil {
		return nil, mapMessagingError(err)
	}
	return channels, nil
}

const managedIMChannelsForMemberFromSQL = `
FROM scopes s
JOIN scope_channel_profiles p
  ON p.scope_id = s.id
 AND p.workspace_id = s.workspace_id
JOIN (
    SELECT
        scope_id,
        member_id
    FROM scope_memberships
    WHERE member_id = $2
      AND visibility = 'public'
      AND removed_at IS NULL
      AND effective_to_seq IS NULL
    GROUP BY scope_id, member_id
) sm
  ON sm.scope_id = s.id
 AND sm.member_id = $2
LEFT JOIN workspace_teams wt
  ON wt.workspace_id = s.workspace_id
 AND wt.channel_id = s.id
 AND wt.status = 'active'
WHERE s.workspace_id = $1
  AND s.type = 'channel'
  AND s.status = 'active'
  AND s.archived_at IS NULL
  AND p.subsystem = 'im'
  AND TRIM(COALESCE(wt.id, '')) = ''
  AND (p.slug = 'all' OR p.owner_member_id IS NOT NULL OR wt.id IS NOT NULL)
`

// ListManagedIMChannelsForUserPage returns only the plain IM channels visible
// to one workspace user. Unlike the app-compatible list projection, this
// purpose-specific path applies the Agent Exec carrier filter before
// LIMIT/OFFSET so every continuation page does bounded store work.
func (store *Store) ListManagedIMChannelsForUserPage(
	ctx context.Context,
	userID string,
	workspaceID string,
	options ManagedIMChannelListOptions,
) (ManagedIMChannelPage, error) {
	memberID, err := store.requireWorkspaceAccess(ctx, userID, workspaceID)
	if err != nil {
		return ManagedIMChannelPage{}, err
	}
	limit := options.Limit
	if limit <= 0 {
		limit = message.DefaultChannelMemberListLimit
	}
	if limit > message.MaxChannelMemberListLimit {
		limit = message.MaxChannelMemberListLimit
	}
	offset, err := decodeOffsetCursor(options.Cursor)
	if err != nil {
		return ManagedIMChannelPage{}, err
	}

	// Count and page share one SQL statement so every supported backend uses one
	// read snapshot (PostgreSQL's default READ COMMITTED would otherwise permit
	// two statements in one transaction to observe different channel sets).
	// The LEFT JOIN yields one sentinel row when the requested offset is beyond
	// the end, preserving TotalCount without a second query.
	rows, err := store.db.Query(ctx, `
WITH managed_im_channels AS (
    SELECT
        s.id AS channel_id,
        p.slug AS slug,
        COALESCE(NULLIF(p.display_name, ''), p.slug) AS display_name,
        COALESCE(p.description, '') AS description,
        COALESCE(NULLIF(p.visibility, ''), 'private') AS visibility
`+managedIMChannelsForMemberFromSQL+`
),
paged_managed_im_channels AS (
    SELECT channel_id, slug, display_name, description, visibility
    FROM managed_im_channels
    ORDER BY slug ASC, channel_id ASC
    LIMIT $3 OFFSET $4
),
managed_im_channel_count AS (
    SELECT COUNT(*) AS total_count
    FROM managed_im_channels
)
SELECT
    paged.channel_id IS NOT NULL AS has_channel,
    COALESCE(paged.channel_id, ''),
    COALESCE(paged.slug, ''),
    COALESCE(paged.display_name, ''),
    COALESCE(paged.description, ''),
    COALESCE(paged.visibility, ''),
    counted.total_count
FROM managed_im_channel_count counted
LEFT JOIN paged_managed_im_channels paged ON TRUE
ORDER BY paged.slug ASC, paged.channel_id ASC
`, workspaceID, memberID, limit+1, offset)
	if err != nil {
		return ManagedIMChannelPage{}, fmt.Errorf("list managed IM channels: %w", err)
	}
	defer rows.Close()

	channels := make([]message.ChannelSummary, 0, limit+1)
	totalCount := 0
	for rows.Next() {
		var channel message.ChannelSummary
		var hasChannel bool
		if err := rows.Scan(
			&hasChannel,
			&channel.ChannelID,
			&channel.Slug,
			&channel.DisplayName,
			&channel.Description,
			&channel.Visibility,
			&totalCount,
		); err != nil {
			return ManagedIMChannelPage{}, fmt.Errorf("scan managed IM channel: %w", err)
		}
		if hasChannel {
			channels = append(channels, channel)
		}
	}
	if err := rows.Err(); err != nil {
		return ManagedIMChannelPage{}, fmt.Errorf("iterate managed IM channels: %w", err)
	}
	hasMore := len(channels) > limit
	if hasMore {
		channels = channels[:limit]
	}
	nextCursor := ""
	if hasMore {
		nextCursor = strconv.Itoa(offset + limit)
	}
	return ManagedIMChannelPage{
		Channels:   channels,
		HasMore:    hasMore,
		NextCursor: nextCursor,
		TotalCount: totalCount,
	}, nil
}

func (store *Store) CreateChannelForUser(ctx context.Context, userID string, workspaceID string, name string) (message.ChannelSummary, error) {
	memberID, err := store.requireWorkspaceAccess(ctx, userID, workspaceID)
	if err != nil {
		return message.ChannelSummary{}, err
	}
	tx, err := store.db.BeginTx(ctx, coredb.TxOptions{})
	if err != nil {
		return message.ChannelSummary{}, fmt.Errorf("begin create channel tx: %w", err)
	}
	defer rollbackTx(tx, ctx)
	summary, err := store.CreateChannelInTx(ctx, tx, workspaceID, memberID, name, "", "private")
	if err != nil {
		return message.ChannelSummary{}, err
	}
	if err := tx.Commit(ctx); err != nil {
		return message.ChannelSummary{}, fmt.Errorf("commit create channel: %w", err)
	}
	return summary, nil
}

// createChannelInTx performs the channel create on the caller's transaction so
// Agent Exec can record its idempotency receipt in the same transaction. It
// does not commit; the caller owns the transaction boundary. ownerMemberID is
// the resolved channel owner (the creating Human for the browser path, the
// Agent's resolved Human owner for Agent Exec).
func (store *Store) CreateChannelInTx(ctx context.Context, tx coredb.Tx, workspaceID string, ownerMemberID string, name string, description string, visibility string) (message.ChannelSummary, error) {
	displayName := strings.TrimSpace(name)
	if displayName == "" {
		return message.ChannelSummary{}, badRequest("channel name is required")
	}
	normalizedVisibility := strings.ToLower(strings.TrimSpace(visibility))
	if normalizedVisibility != "public" {
		normalizedVisibility = "private"
	}

	channelID, err := identifiers.NewEntityID("chn")
	if err != nil {
		return message.ChannelSummary{}, fmt.Errorf("generate channel id: %w", err)
	}
	slug := deriveChannelSlug(displayName, channelID)
	if err := domain.ValidateChannelSlug(slug); err != nil {
		return message.ChannelSummary{}, err
	}
	if err := store.checkWorkspaceQuotaInTx(ctx, tx, workspaceID, plan.MetricChannels, 1); err != nil {
		return message.ChannelSummary{}, err
	}
	now := time.Now().UTC()
	repos := message.NewScopeRepositories(tx)
	if err := repos.Scopes.UpsertScope(ctx, message.ScopeRecord{
		ID:                channelID,
		WorkspaceID:       workspaceID,
		Type:              string(domain.ThreadTypeChannel),
		IdentityKey:       "channel:" + channelID,
		Status:            "active",
		CreatedByMemberID: ownerMemberID,
		CreatedAt:         now,
		UpdatedAt:         now,
	}); err != nil {
		return message.ChannelSummary{}, fmt.Errorf("insert channel scope: %w", err)
	}
	insertProfile := func() error {
		_, err := tx.Exec(ctx, `
INSERT INTO scope_channel_profiles (
  scope_id, workspace_id, slug, display_name, description, visibility,
  owner_member_id, is_all_channel, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, FALSE, $8, $8)
`, channelID, workspaceID, slug, displayName, description, normalizedVisibility, ownerMemberID, now)
		return err
	}
	if err := insertProfile(); err != nil {
		if coredb.IsUniqueViolation(err) {
			released, releaseErr := releaseArchivedChannelSlugForCreate(ctx, tx, workspaceID, slug, now)
			if releaseErr != nil {
				return message.ChannelSummary{}, releaseErr
			}
			if !released {
				return message.ChannelSummary{}, channelSlugConflict()
			}
			if retryErr := insertProfile(); retryErr != nil {
				if coredb.IsUniqueViolation(retryErr) {
					return message.ChannelSummary{}, channelSlugConflict()
				}
				return message.ChannelSummary{}, fmt.Errorf("insert channel scope profile: %w", retryErr)
			}
		} else {
			return message.ChannelSummary{}, fmt.Errorf("insert channel scope profile: %w", err)
		}
	}
	if err := store.ensureChannelScopeParticipantProjection(ctx, tx, channelID, ownerMemberID, "no-wake", now); err != nil {
		return message.ChannelSummary{}, fmt.Errorf("insert creator channel participant projection: %w", err)
	}
	return message.ChannelSummary{
		ChannelID:         channelID,
		Slug:              slug,
		DisplayName:       displayName,
		Description:       description,
		Visibility:        normalizedVisibility,
		Joined:            true,
		CanInvite:         true,
		CanManage:         true,
		SubscriptionState: message.HumanScopeSubscriptionStateActive,
	}, nil
}

func releaseArchivedChannelSlugForCreate(ctx context.Context, execer coredb.QueryExecer, workspaceID string, slug string, updatedAt time.Time) (bool, error) {
	trimmedWorkspaceID := strings.TrimSpace(workspaceID)
	trimmedSlug := strings.TrimSpace(slug)
	if trimmedWorkspaceID == "" || trimmedSlug == "" {
		return false, nil
	}

	row := execer.QueryRow(ctx, `
SELECT p.scope_id
FROM scope_channel_profiles p
JOIN scopes s
  ON s.workspace_id = p.workspace_id
 AND s.id = p.scope_id
WHERE p.workspace_id = $1
  AND p.slug = $2
  AND s.type = 'channel'
  AND s.status = 'archived'
LIMIT 1
`, trimmedWorkspaceID, trimmedSlug)
	var archivedScopeID string
	if err := row.Scan(&archivedScopeID); err != nil {
		if coredb.IsNotFound(err) {
			return false, nil
		}
		return false, fmt.Errorf("load archived channel slug holder: %w", err)
	}

	now := updatedAt.UTC()
	if now.IsZero() {
		now = time.Now().UTC()
	}
	result, err := execer.Exec(ctx, `
UPDATE scope_channel_profiles
SET slug = $3,
    updated_at = $4
WHERE workspace_id = $1
  AND scope_id = $2
  AND slug = $5
`, trimmedWorkspaceID, archivedScopeID, ArchivedScopeProfileSlug(archivedScopeID), now, trimmedSlug)
	if err != nil {
		return false, fmt.Errorf("release archived channel slug holder: %w", err)
	}
	return result.RowsAffected() > 0, nil
}

func (store *Store) JoinChannelForUser(ctx context.Context, userID string, workspaceID string, channelID string) error {
	memberID, err := store.requireWorkspaceAccess(ctx, userID, workspaceID)
	if err != nil {
		return err
	}
	channel, err := lookupChannel(ctx, store.db, workspaceID, channelID)
	if err != nil {
		return err
	}
	if channel.Type != "channel" {
		return badRequest("channel cannot be joined")
	}
	if channel.Slug != "all" && channel.OwnerMemberID == nil {
		return ErrNotFound
	}
	if channel.Visibility != "public" {
		return ErrNotFound
	}
	now := time.Now().UTC()
	if err := store.ensureChannelScopeParticipantProjection(ctx, store.db, channelID, memberID, "no-wake", now); err != nil {
		return err
	}
	return nil
}

func (store *Store) LeaveChannelForUser(ctx context.Context, userID string, workspaceID string, channelID string) error {
	memberID, err := store.requireWorkspaceAccess(ctx, userID, workspaceID)
	if err != nil {
		return err
	}
	channel, err := lookupChannel(ctx, store.db, workspaceID, channelID)
	if err != nil {
		return err
	}
	if channel.Type != "channel" {
		return badRequest("channel cannot be left")
	}
	if channel.Slug == "all" {
		return badRequest("cannot leave #all")
	}
	if strings.TrimSpace(channel.TeamID) != "" {
		return badRequest("agent team channels are managed by the team")
	}
	if channel.OwnerMemberID != nil && *channel.OwnerMemberID == memberID {
		return badRequest("channel owner must transfer ownership before leaving")
	}
	if err := store.markScopeParticipantProjectionRemoved(ctx, store.db, channelID, memberID, time.Now().UTC()); err != nil {
		return err
	}
	if err := store.removePrivateEntriesForOwnerHostChannel(ctx, store.db, memberID, channelID, time.Now().UTC()); err != nil {
		return err
	}
	return nil
}

func lookupChannel(ctx context.Context, queryer coredb.QueryRower, workspaceID string, channelID string) (access.Channel, error) {
	channel, err := access.LookupChannel(ctx, queryer, workspaceID, channelID)
	if err != nil {
		if err == access.ErrNotFound {
			return access.Channel{}, ErrNotFound
		}
		return access.Channel{}, err
	}
	return channel, nil
}

func mapMessagingError(err error) error {
	switch {
	case err == nil:
		return nil
	case err == message.ErrNotFound:
		return ErrNotFound
	case err == message.ErrForbidden:
		return ErrForbidden
	default:
		return err
	}
}

func rollbackTx(tx coredb.Tx, ctx context.Context) {
	_ = tx.Rollback(ctx)
}
