package app

import (
	"context"
	"fmt"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
)

type agentTargetBasis struct {
	Visibility    string
	OwnerMemberID string
}

func (basis agentTargetBasis) Private() bool {
	return strings.TrimSpace(basis.Visibility) == "private" && strings.TrimSpace(basis.OwnerMemberID) != ""
}

type agentTargetResolveOptions struct {
	Role              agentTargetResolutionRole
	CreatePlainDM     bool
	CreateReplyThread bool
}

type agentChannelRosterTarget struct {
	ChannelID string
	ThreadID  string
	Slug      string
}

// resolveAgentThreadTarget is the single agent target-basis resolver for
// read/write/review surfaces. Public and private channel roster rows both
// resolve here; operation-specific policy is applied after the returned basis.
func (store *Store) resolveAgentThreadTarget(ctx context.Context, queryer coredb.QueryExecer, authenticatedAgent agent.AuthenticatedAgent, request agentScopeTargetRequest, options agentTargetResolveOptions) (message.ThreadAccess, agentTargetBasis, error) {
	if strings.TrimSpace(request.Team.TeamID) != "" {
		return store.resolveAgentTeamThreadTarget(ctx, queryer, authenticatedAgent, request, options)
	}

	reference := request.Reference
	if reference.Type != domain.ThreadTypeChannel {
		target, err := resolveAgentDMTarget(ctx, queryer, authenticatedAgent, request, options)
		if err != nil {
			return message.ThreadAccess{}, agentTargetBasis{}, err
		}
		return target, agentTargetBasis{}, nil
	}

	host, basis, err := store.resolveAgentChannelRosterTarget(ctx, queryer, authenticatedAgent, reference.ChannelSlug)
	if err != nil {
		return message.ThreadAccess{}, agentTargetBasis{}, err
	}
	if strings.TrimSpace(reference.ShortID) == "" {
		return message.ThreadAccess{
			ThreadID:    strings.TrimSpace(host.ThreadID),
			ThreadType:  domain.ThreadTypeChannel,
			ChannelID:   strings.TrimSpace(host.ChannelID),
			ChannelName: strings.TrimSpace(host.Slug),
		}, basis, nil
	}

	var target message.ThreadAccess
	if options.CreateReplyThread {
		target, err = message.ResolveOrEnsureChannelReplyThreadTarget(ctx, queryer, authenticatedAgent.WorkspaceID, host.ChannelID, host.Slug, reference.ShortID)
	} else {
		target, err = resolveExistingAgentChannelReplyThreadTarget(ctx, queryer, authenticatedAgent.WorkspaceID, host.ChannelID, host.Slug, reference.ShortID)
	}
	if err != nil {
		return message.ThreadAccess{}, agentTargetBasis{}, mapMessagingError(err)
	}
	return target, basis, nil
}

func (store *Store) resolveAgentTeamThreadTarget(ctx context.Context, queryer coredb.QueryExecer, authenticatedAgent agent.AuthenticatedAgent, request agentScopeTargetRequest, options agentTargetResolveOptions) (message.ThreadAccess, agentTargetBasis, error) {
	host, basis, err := store.resolveAgentTeamChannelRosterTarget(ctx, queryer, authenticatedAgent, request.Team.TeamID)
	if err != nil {
		return message.ThreadAccess{}, agentTargetBasis{}, err
	}
	if strings.TrimSpace(request.Team.ShortID) == "" {
		return message.ThreadAccess{
			ThreadID:    strings.TrimSpace(host.ThreadID),
			ThreadType:  domain.ThreadTypeChannel,
			ChannelID:   strings.TrimSpace(host.ChannelID),
			ChannelName: strings.TrimSpace(host.Slug),
		}, basis, nil
	}

	var target message.ThreadAccess
	if options.CreateReplyThread {
		target, err = message.ResolveOrEnsureChannelReplyThreadTarget(ctx, queryer, authenticatedAgent.WorkspaceID, host.ChannelID, host.Slug, request.Team.ShortID)
	} else {
		target, err = resolveExistingAgentChannelReplyThreadTarget(ctx, queryer, authenticatedAgent.WorkspaceID, host.ChannelID, host.Slug, request.Team.ShortID)
	}
	if err != nil {
		return message.ThreadAccess{}, agentTargetBasis{}, mapMessagingError(err)
	}
	return target, basis, nil
}

func resolveAgentDMTarget(ctx context.Context, queryer coredb.QueryExecer, authenticatedAgent agent.AuthenticatedAgent, request agentScopeTargetRequest, options agentTargetResolveOptions) (message.ThreadAccess, error) {
	actor := messagingActor(authenticatedAgent)
	if request.Reference.Type == domain.ThreadTypeDM && strings.TrimSpace(request.Reference.ShortID) == "" && options.CreatePlainDM {
		// Keep resolve-channel compatible with older daemon upload flows: a plain
		// dm:@member target is allowed to create the DM scope before upload.
		return message.EnsureDMThread(ctx, queryer, actor, request.Reference.PeerMemberID)
	}
	if options.Role == agentTargetResolutionRoleWrite {
		target, err := message.ResolveSendTarget(ctx, queryer, actor, request.Raw, "")
		if err != nil {
			return message.ThreadAccess{}, mapMessagingError(err)
		}
		return target, nil
	}
	target, err := resolveAccessibleThread(ctx, queryer, authenticatedAgent, request.Reference)
	if err != nil {
		return message.ThreadAccess{}, err
	}
	return target, nil
}

func (store *Store) resolveAgentChannelRosterTarget(ctx context.Context, queryer coredb.QueryRower, authenticatedAgent agent.AuthenticatedAgent, channelSlug string) (agentChannelRosterTarget, agentTargetBasis, error) {
	host, err := loadAgentTargetChannelBySlug(ctx, queryer, authenticatedAgent.WorkspaceID, channelSlug)
	if err != nil {
		return agentChannelRosterTarget{}, agentTargetBasis{}, err
	}
	basis, err := loadAgentTargetChannelRosterBasis(ctx, queryer, authenticatedAgent, host.ChannelID)
	if err != nil {
		return agentChannelRosterTarget{}, agentTargetBasis{}, err
	}
	return host, basis, nil
}

func (store *Store) resolveAgentTeamChannelRosterTarget(ctx context.Context, queryer coredb.QueryRower, authenticatedAgent agent.AuthenticatedAgent, teamID string) (agentChannelRosterTarget, agentTargetBasis, error) {
	host, err := loadAgentTargetChannelByTeamID(ctx, queryer, authenticatedAgent.WorkspaceID, teamID)
	if err != nil {
		return agentChannelRosterTarget{}, agentTargetBasis{}, err
	}
	basis, err := loadAgentTargetChannelRosterBasis(ctx, queryer, authenticatedAgent, host.ChannelID)
	if err != nil {
		return agentChannelRosterTarget{}, agentTargetBasis{}, err
	}
	return host, basis, nil
}

func loadAgentTargetChannelBySlug(ctx context.Context, queryer coredb.QueryRower, workspaceID string, channelSlug string) (agentChannelRosterTarget, error) {
	row := queryer.QueryRow(ctx, `
SELECT s.id, s.id, p.slug
FROM scopes s
JOIN scope_channel_profiles p
  ON p.scope_id = s.id
WHERE s.workspace_id = $1
  AND s.type = 'channel'
  AND s.status = 'active'
  AND s.archived_at IS NULL
  AND p.slug = $2
LIMIT 1
`, strings.TrimSpace(workspaceID), strings.TrimSpace(channelSlug))
	var host agentChannelRosterTarget
	if err := row.Scan(&host.ChannelID, &host.ThreadID, &host.Slug); err != nil {
		if coredb.IsNotFound(err) {
			return agentChannelRosterTarget{}, ErrNotFound
		}
		return agentChannelRosterTarget{}, fmt.Errorf("resolve agent target channel: %w", err)
	}
	return host, nil
}

func loadAgentTargetChannelByTeamID(ctx context.Context, queryer coredb.QueryRower, workspaceID string, teamID string) (agentChannelRosterTarget, error) {
	row := queryer.QueryRow(ctx, `
SELECT s.id, s.id, COALESCE(NULLIF(p.slug, ''), wt.id)
FROM workspace_teams wt
JOIN scopes s
  ON s.workspace_id = wt.workspace_id
 AND s.id = wt.channel_id
 AND s.type = 'channel'
 AND s.status = 'active'
 AND s.archived_at IS NULL
JOIN scope_channel_profiles p
  ON p.workspace_id = s.workspace_id
 AND p.scope_id = s.id
 AND COALESCE(NULLIF(p.subsystem, ''), 'im') = 'team'
WHERE wt.workspace_id = $1
  AND wt.id = $2
  AND wt.status = 'active'
LIMIT 1
`, strings.TrimSpace(workspaceID), strings.TrimSpace(teamID))
	var host agentChannelRosterTarget
	if err := row.Scan(&host.ChannelID, &host.ThreadID, &host.Slug); err != nil {
		if coredb.IsNotFound(err) {
			return agentChannelRosterTarget{}, ErrNotFound
		}
		return agentChannelRosterTarget{}, fmt.Errorf("resolve agent target Team channel: %w", err)
	}
	return host, nil
}

func loadAgentTargetChannelRosterBasis(ctx context.Context, queryer coredb.QueryRower, authenticatedAgent agent.AuthenticatedAgent, channelID string) (agentTargetBasis, error) {
	row := queryer.QueryRow(ctx, `
SELECT sm.visibility, COALESCE(sm.owner_member_id, '')
FROM scope_memberships sm
LEFT JOIN agents a
  ON a.workspace_id = $3
 AND a.id = $4
 AND a.member_id = sm.member_id
LEFT JOIN machines mac
  ON mac.workspace_id = a.workspace_id
 AND mac.id = a.machine_id
LEFT JOIN members owner_member
  ON owner_member.workspace_id = a.workspace_id
 AND owner_member.user_id = mac.created_by_user_id
 AND owner_member.type = 'human'
 AND owner_member.status = 'active'
WHERE sm.scope_id = $1
  AND sm.member_id = $2
  AND sm.visibility IN ('public', 'private')
  AND sm.removed_at IS NULL
  AND sm.archived_at IS NULL
  AND sm.effective_to_seq IS NULL
  AND (
       sm.owner_member_id IS NULL
       OR EXISTS (
          SELECT 1
          FROM scope_memberships owner_channel_membership
          WHERE owner_channel_membership.scope_id = sm.scope_id
            AND owner_channel_membership.member_id = sm.owner_member_id
            AND owner_channel_membership.removed_at IS NULL
            AND owner_channel_membership.archived_at IS NULL
            AND owner_channel_membership.effective_to_seq IS NULL
       )
  )
  AND (
       sm.visibility = 'public'
       OR (
          sm.visibility = 'private'
          AND sm.owner_member_id IS NOT NULL
          AND sm.owner_member_id = owner_member.id
       )
  )
ORDER BY CASE WHEN sm.visibility = 'private' THEN 0 ELSE 1 END, sm.created_at DESC, sm.effective_from_seq DESC
LIMIT 1
`, strings.TrimSpace(channelID), strings.TrimSpace(authenticatedAgent.MemberID), strings.TrimSpace(authenticatedAgent.WorkspaceID), strings.TrimSpace(authenticatedAgent.AgentID))
	var basis agentTargetBasis
	if err := row.Scan(&basis.Visibility, &basis.OwnerMemberID); err != nil {
		if coredb.IsNotFound(err) {
			return agentTargetBasis{}, ErrForbidden
		}
		return agentTargetBasis{}, fmt.Errorf("load agent target channel roster basis: %w", err)
	}
	return basis, nil
}

func resolveExistingAgentChannelReplyThreadTarget(ctx context.Context, queryer coredb.Queryer, workspaceID string, parentChannelID string, parentChannelName string, shortID string) (message.ThreadAccess, error) {
	normalized := strings.ToLower(strings.TrimSpace(shortID))
	if normalized == "" {
		return message.ThreadAccess{}, badRequest("invalid thread identifier")
	}
	rows, err := queryer.Query(ctx, `
SELECT thread_scope.id,
       COALESCE(thread_scope.root_message_id, ''),
       COALESCE(thread_scope.root_message_short_id, '')
FROM scopes thread_scope
LEFT JOIN scope_channel_profiles thread_profile
  ON thread_profile.workspace_id = thread_scope.workspace_id
 AND thread_profile.scope_id = thread_scope.id
WHERE thread_scope.workspace_id = $1
  AND thread_scope.parent_scope_id = $2
  AND thread_scope.type = 'thread'
  AND thread_scope.status = 'active'
  AND thread_scope.archived_at IS NULL
  AND (
      thread_scope.root_message_short_id = $3
      OR LOWER(COALESCE(thread_profile.slug, '')) = CONCAT('thread-', $3)
  )
ORDER BY thread_scope.id ASC
LIMIT 2
`, strings.TrimSpace(workspaceID), strings.TrimSpace(parentChannelID), normalized)
	if err != nil {
		return message.ThreadAccess{}, fmt.Errorf("resolve agent channel reply thread: %w", err)
	}
	defer rows.Close()

	matches := make([]message.ThreadAccess, 0, 2)
	for rows.Next() {
		var threadChannelID string
		var parentMessageID string
		var canonicalShortID string
		if err := rows.Scan(&threadChannelID, &parentMessageID, &canonicalShortID); err != nil {
			return message.ThreadAccess{}, fmt.Errorf("scan agent channel reply thread: %w", err)
		}
		matches = append(matches, message.ThreadAccess{
			ThreadID:   strings.TrimSpace(threadChannelID),
			ThreadType: domain.ThreadTypeChannel,
			ChannelID:  strings.TrimSpace(threadChannelID),
			ReplyThread: &message.ReplyThreadTarget{
				ParentMessageID:   strings.TrimSpace(parentMessageID),
				ShortID:           strings.TrimSpace(canonicalShortID),
				ParentChannelID:   strings.TrimSpace(parentChannelID),
				ParentChannelName: strings.TrimSpace(parentChannelName),
			},
		})
	}
	if err := rows.Err(); err != nil {
		return message.ThreadAccess{}, fmt.Errorf("iterate agent channel reply thread matches: %w", err)
	}

	switch len(matches) {
	case 0:
		return message.ThreadAccess{}, ErrNotFound
	case 1:
		return matches[0], nil
	default:
		return message.ThreadAccess{}, conflict("thread identifier is ambiguous")
	}
}
