package taskboard

import (
	"context"
	"errors"
	"fmt"
	"strings"

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

func (txs TxStore) resolveTaskCollaboratorTargetForUser(ctx context.Context, queryer coredb.QueryExecer, userID string, workspaceID string, taskRef TaskRef, scopeRef TaskCollaboratorScopeRef, action access.HumanAction) (taskCollaboratorTarget, string, error) {
	membership, err := access.LoadHumanWorkspaceMember(ctx, queryer, userID, workspaceID)
	if errors.Is(err, access.ErrForbidden) {
		return taskCollaboratorTarget{}, "", ErrForbidden
	}
	if err != nil {
		return taskCollaboratorTarget{}, "", fmt.Errorf("load task collaborator actor: %w", err)
	}
	target, err := txs.resolveTaskCollaboratorTarget(ctx, queryer, workspaceID, taskRef, scopeRef)
	if err != nil {
		return taskCollaboratorTarget{}, "", err
	}
	if err := txs.authorizeTaskCollaboratorScopeAccess(ctx, queryer, userID, workspaceID, target, action); err != nil {
		return taskCollaboratorTarget{}, "", err
	}
	return target, membership.Member.ID, nil
}

// resolveTaskCollaboratorTarget resolves the collaborator scope target and its
// authoritative execution owner. The owner is server-derived, never the viewer:
//   - task:   the task collaborator anchor (execution_owner_member_id with the
//     creator private-agent / created-by fallback);
//   - thread: a client-supplied owner when present, else natural owner
//     (creator → root-message author → scope execution → parent channel);
//     the Topic root-message author wins over any stale execution row.
//   - channel: a client-supplied owner when present, else the canonical natural
//     owner (task_scope_executions → channel profile owner → creator).
//
// An explicitly supplied owner is honored so workspace-admin direct-add and the
// agent-collaborators surface keep their existing semantics; a non-owner viewer
// is never used as an owner fallback, and the list surface must work with no
// owner query parameter at all.
func (txs TxStore) resolveTaskCollaboratorTarget(ctx context.Context, queryer coredb.QueryExecer, workspaceID string, taskRef TaskRef, scopeRef TaskCollaboratorScopeRef) (taskCollaboratorTarget, error) {
	scopeType := strings.TrimSpace(scopeRef.ScopeType)
	if scopeType == "" && strings.TrimSpace(taskRef.TaskID) != "" {
		scopeType = taskCollaboratorScopeTask
	}
	if scopeType == "" {
		return taskCollaboratorTarget{}, badRequest("collaborator scopeType is required")
	}
	normalizedScopeType, err := normalizeTaskCollaboratorScope(scopeType)
	if err != nil {
		return taskCollaboratorTarget{}, err
	}

	switch normalizedScopeType {
	case taskCollaboratorScopeTask:
		taskID, err := taskRef.requireTaskID()
		if err != nil {
			return taskCollaboratorTarget{}, err
		}
		anchor, err := txs.loadTaskCollaboratorTaskAnchor(ctx, queryer, workspaceID, taskID)
		if err != nil {
			return taskCollaboratorTarget{}, err
		}
		return taskCollaboratorTarget{
			WorkspaceID:            workspaceID,
			ScopeType:              taskCollaboratorScopeTask,
			ScopeID:                anchor.TaskID,
			ExecutionOwnerMemberID: anchor.ExecutionOwnerMemberID,
			TaskID:                 anchor.TaskID,
			TaskScopeID:            anchor.TaskScopeID,
			ChannelID:              anchor.ChannelID,
			ThreadChannelID:        anchor.SourceThreadChannelID,
		}, nil
	case taskCollaboratorScopeThread:
		scopeID := strings.TrimSpace(scopeRef.ScopeID)
		if scopeID == "" {
			return taskCollaboratorTarget{}, badRequest("scopeId is required")
		}
		executionOwnerMemberID, err := txs.resolveCollaboratorScopeOwnerMemberID(
			ctx, queryer, workspaceID, taskCollaboratorScopeThread, scopeID, scopeRef.ExecutionOwnerMemberID,
		)
		if err != nil {
			return taskCollaboratorTarget{}, err
		}
		return taskCollaboratorTarget{
			WorkspaceID:            workspaceID,
			ScopeType:              taskCollaboratorScopeThread,
			ScopeID:                scopeID,
			ExecutionOwnerMemberID: executionOwnerMemberID,
			ThreadChannelID:        scopeID,
		}, nil
	case taskCollaboratorScopeChannel:
		scopeID := strings.TrimSpace(scopeRef.ScopeID)
		if scopeID == "" {
			return taskCollaboratorTarget{}, badRequest("scopeId is required")
		}
		executionOwnerMemberID, err := txs.resolveCollaboratorScopeOwnerMemberID(
			ctx, queryer, workspaceID, taskCollaboratorScopeChannel, scopeID, scopeRef.ExecutionOwnerMemberID,
		)
		if err != nil {
			return taskCollaboratorTarget{}, err
		}
		return taskCollaboratorTarget{
			WorkspaceID:            workspaceID,
			ScopeType:              taskCollaboratorScopeChannel,
			ScopeID:                scopeID,
			ExecutionOwnerMemberID: executionOwnerMemberID,
			ChannelID:              scopeID,
		}, nil
	default:
		return taskCollaboratorTarget{}, badRequest("invalid collaborator scopeType")
	}
}

// resolveCollaboratorScopeOwnerMemberID returns the authoritative execution owner
// for a thread/channel collaborator scope. An explicitly supplied owner is
// honored (preserving workspace-admin direct-add and the agent-collaborators
// surface); otherwise the canonical natural owner is derived server-side via
// resolveScopeNaturalOwner. The viewer is never used as an owner fallback.
func (txs TxStore) resolveCollaboratorScopeOwnerMemberID(ctx context.Context, queryer coredb.QueryExecer, workspaceID string, scopeType string, scopeID string, explicitOwnerMemberID string) (string, error) {
	if owner := strings.TrimSpace(explicitOwnerMemberID); owner != "" {
		return owner, nil
	}
	owner, _, err := txs.resolveScopeNaturalOwner(ctx, queryer, workspaceID, scopeType, scopeID)
	if err != nil {
		return "", err
	}
	if owner == "" {
		return "", badRequest("executionOwnerMemberId is required")
	}
	return owner, nil
}

func (txs TxStore) authorizeTaskCollaboratorScopeAccess(ctx context.Context, queryer rowQueryer, userID string, workspaceID string, target taskCollaboratorTarget, action access.HumanAction) error {
	switch target.ScopeType {
	case taskCollaboratorScopeTask:
		_, err := txs.loadAuthorizedUserTaskChannelAccess(ctx, queryer, userID, workspaceID, target.ChannelID, action)
		return err
	case taskCollaboratorScopeThread:
		_, err := access.LoadAuthorizedHumanThreadAccess(ctx, queryer, userID, workspaceID, target.ScopeID, taskReadThreadAction)
		if errors.Is(err, access.ErrForbidden) {
			return ErrForbidden
		}
		if errors.Is(err, access.ErrNotFound) {
			return ErrNotFound
		}
		if err != nil {
			return fmt.Errorf("load authorized task collaborator thread access: %w", err)
		}
		return nil
	case taskCollaboratorScopeChannel:
		_, err := txs.loadAuthorizedUserTaskChannelAccess(ctx, queryer, userID, workspaceID, target.ScopeID, action)
		return err
	default:
		return badRequest("invalid collaborator scopeType")
	}
}

func (txs TxStore) authorizeTaskCollaboratorManager(ctx context.Context, queryer rowQueryer, workspaceID string, actorMemberID string, executionOwnerMemberID string) error {
	if strings.TrimSpace(actorMemberID) == strings.TrimSpace(executionOwnerMemberID) {
		return nil
	}
	row := queryer.QueryRow(ctx, `
SELECT workspace_role
FROM members
WHERE workspace_id = $1 AND id = $2 AND type = 'human' AND status = 'active'
`, workspaceID, actorMemberID)
	var workspaceRole string
	if err := row.Scan(&workspaceRole); err != nil {
		if coredb.IsNotFound(err) {
			return ErrForbidden
		}
		return fmt.Errorf("load task collaborator manager role: %w", err)
	}
	if access.HasWorkspaceAdminRole(domain.WorkspaceRole(workspaceRole)) {
		return nil
	}
	return ErrForbidden
}

type taskCollaboratorAppIdentity struct {
	AppID string
	Slug  string
	IsApp bool
}

func loadActiveTaskCollaboratorAppIdentity(
	ctx context.Context,
	queryer rowQueryer,
	workspaceID string,
	memberID string,
) (taskCollaboratorAppIdentity, error) {
	var identity taskCollaboratorAppIdentity
	if err := queryer.QueryRow(ctx, `
SELECT definition.id,
       definition.slug,
       COALESCE(JSON_TYPE(JSON_EXTRACT(installation.metadata, '$.manifest.capabilities.runtime')), '') = 'OBJECT'
FROM service_members service_member
JOIN members visible_member
  ON visible_member.id = service_member.member_id
 AND visible_member.workspace_id = service_member.workspace_id
 AND visible_member.type = 'service'
 AND visible_member.status = 'active'
JOIN service_installations installation
  ON installation.id = service_member.service_installation_id
 AND installation.workspace_id = service_member.workspace_id
 AND installation.service_definition_id = service_member.service_definition_id
 AND installation.service_kind = service_member.service_kind
 AND installation.status = 'active'
JOIN service_definitions definition
  ON definition.id = installation.service_definition_id
 AND definition.service_kind = installation.service_kind
 AND definition.status = 'active'
WHERE service_member.workspace_id = $1
  AND service_member.member_id = $2
  AND service_member.service_kind = 'app'
  AND service_member.status = 'active'
LIMIT 1
`, strings.TrimSpace(workspaceID), strings.TrimSpace(memberID)).Scan(
		&identity.AppID,
		&identity.Slug,
		&identity.IsApp,
	); err != nil {
		if coredb.IsNotFound(err) {
			return taskCollaboratorAppIdentity{}, ErrNotFound
		}
		return taskCollaboratorAppIdentity{}, fmt.Errorf("load task collaborator App identity: %w", err)
	}
	identity.AppID = strings.TrimSpace(identity.AppID)
	identity.Slug = strings.TrimSpace(identity.Slug)
	if identity.AppID == "" || identity.Slug == "" {
		return taskCollaboratorAppIdentity{}, ErrNotFound
	}
	return identity, nil
}

func isTaskCollaboratorProductApp(identity taskCollaboratorAppIdentity) bool {
	return (identity.AppID == "app_github" && identity.Slug == "github") ||
		(identity.AppID == "app_gitea" && identity.Slug == "gitea")
}

// authorizeAppServiceTaskCollaboratorForUser keeps workspace-created Apps out
// of the task/agent collaborator authority surface unless the acting Human can
// actually manage that App. Public catalog visibility authorizes discovery and
// channel invitation only; it is deliberately irrelevant here. Exact product
// Apps and non-App App services retain their existing collaborator behavior.
func (txs TxStore) authorizeAppServiceTaskCollaboratorForUser(
	ctx context.Context,
	queryer rowQueryer,
	userID string,
	workspaceID string,
	actorMemberID string,
	collaboratorMemberID string,
) error {
	identity, err := loadActiveTaskCollaboratorAppIdentity(
		ctx, queryer, workspaceID, collaboratorMemberID,
	)
	if err != nil {
		return err
	}
	if isTaskCollaboratorProductApp(identity) || !identity.IsApp {
		return nil
	}

	// Match App management lock order: stable definition fence, current Human,
	// workspace attribution, then active manager. This closes manager-removal,
	// role-change, uninstall, and catalog-visibility races before task authority
	// is granted.
	var lockedDefinitionID string
	if err := queryer.QueryRow(ctx, `
SELECT id
FROM service_definitions
WHERE id = $1
  AND service_kind = 'app'
LIMIT 1
FOR UPDATE
`, identity.AppID).Scan(&lockedDefinitionID); err != nil {
		if coredb.IsNotFound(err) {
			return ErrNotFound
		}
		return fmt.Errorf("lock task collaborator App definition: %w", err)
	}
	currentIdentity, err := loadActiveTaskCollaboratorAppIdentity(
		ctx, queryer, workspaceID, collaboratorMemberID,
	)
	if err != nil {
		return err
	}
	if currentIdentity != identity || strings.TrimSpace(lockedDefinitionID) != identity.AppID {
		return ErrNotFound
	}

	actor, err := access.LoadHumanWorkspaceMemberForUpdate(
		ctx, queryer, strings.TrimSpace(userID), strings.TrimSpace(workspaceID),
	)
	if errors.Is(err, access.ErrForbidden) {
		return ErrForbidden
	}
	if err != nil {
		return fmt.Errorf("revalidate task collaborator App actor: %w", err)
	}
	if strings.TrimSpace(actor.Member.ID) != strings.TrimSpace(actorMemberID) {
		return ErrForbidden
	}

	var catalogVisibility string
	if err := queryer.QueryRow(ctx, `
SELECT catalog_visibility
FROM extension_catalog_attributions
WHERE workspace_id = $1
  AND service_definition_id = $2
LIMIT 1
FOR UPDATE
`, strings.TrimSpace(workspaceID), identity.AppID).Scan(&catalogVisibility); err != nil {
		if coredb.IsNotFound(err) {
			return ErrNotFound
		}
		return fmt.Errorf("lock task collaborator App attribution: %w", err)
	}
	switch strings.ToLower(strings.TrimSpace(catalogVisibility)) {
	case "private", "public":
	default:
		return ErrNotFound
	}
	if access.HasWorkspaceAdminRole(domain.WorkspaceRole(actor.WorkspaceRole)) {
		return nil
	}

	var managerID string
	if err := queryer.QueryRow(ctx, `
SELECT id
FROM extension_definition_managers
WHERE workspace_id = $1
  AND service_definition_id = $2
  AND member_id = $3
  AND status = 'active'
  AND ended_at IS NULL
LIMIT 1
FOR UPDATE
`, strings.TrimSpace(workspaceID), identity.AppID, strings.TrimSpace(actorMemberID)).Scan(&managerID); err != nil {
		if coredb.IsNotFound(err) {
			return ErrNotFound
		}
		return fmt.Errorf("lock task collaborator App manager: %w", err)
	}
	if strings.TrimSpace(managerID) == "" {
		return ErrNotFound
	}
	return nil
}

func (txs TxStore) loadTaskCollaboratorTaskAnchor(ctx context.Context, queryer rowQueryer, workspaceID string, taskID string) (taskCollaboratorTaskAnchor, error) {
	row := queryer.QueryRow(ctx, `
SELECT
    t.id,
    t.task_scope_id,
    tb.channel_id,
    COALESCE(NULLIF(t.execution_owner_member_id, ''), NULLIF(creator_private_agent.owner_member_id, ''), NULLIF(t.created_by_ref, ''), t.created_by_member_id),
    COALESCE(t.source_thread_channel_id, '')
FROM tasks t
JOIN scopes task_scope
  ON task_scope.id = t.task_scope_id
 AND task_scope.workspace_id = $1
 AND task_scope.type = 'task'
 AND task_scope.status = 'active'
 AND task_scope.archived_at IS NULL
JOIN task_boards tb
  ON tb.id = t.board_id
 AND tb.workspace_id = $1
JOIN scopes channel_scope
  ON channel_scope.id = tb.channel_id
 AND channel_scope.workspace_id = $1
 AND channel_scope.type = 'channel'
 AND channel_scope.status = 'active'
 AND channel_scope.archived_at IS NULL
LEFT JOIN scope_memberships creator_private_agent
  ON creator_private_agent.scope_id = tb.channel_id
 AND creator_private_agent.member_id = COALESCE(NULLIF(t.created_by_ref, ''), t.created_by_member_id)
 AND creator_private_agent.visibility = 'private'
 AND creator_private_agent.owner_member_id IS NOT NULL
WHERE t.channel_id = tb.channel_id
  AND t.id = $2
`, workspaceID, taskID)
	var anchor taskCollaboratorTaskAnchor
	anchor.WorkspaceID = workspaceID
	if err := row.Scan(&anchor.TaskID, &anchor.TaskScopeID, &anchor.ChannelID, &anchor.ExecutionOwnerMemberID, &anchor.SourceThreadChannelID); err != nil {
		if coredb.IsNotFound(err) {
			return taskCollaboratorTaskAnchor{}, ErrNotFound
		}
		return taskCollaboratorTaskAnchor{}, fmt.Errorf("load task collaborator task anchor: %w", err)
	}
	return anchor, nil
}

func (txs TxStore) ensureMemberCanReadTaskCollaboratorScope(ctx context.Context, queryer rowQueryer, workspaceID string, target taskCollaboratorTarget, memberID string) error {
	switch target.ScopeType {
	case taskCollaboratorScopeTask:
		return ensureMemberCanReadScope(ctx, queryer, target.ChannelID, memberID)
	case taskCollaboratorScopeChannel:
		if err := ensureTaskCollaboratorScopeType(ctx, queryer, workspaceID, target.ScopeID, "channel"); err != nil {
			return err
		}
		return ensureMemberCanReadScope(ctx, queryer, target.ScopeID, memberID)
	case taskCollaboratorScopeThread:
		return ensureMemberCanReadThreadScope(ctx, queryer, workspaceID, target.ScopeID, memberID)
	default:
		return badRequest("invalid collaborator scopeType")
	}
}

func ensureTaskCollaboratorScopeType(ctx context.Context, queryer rowQueryer, workspaceID string, scopeID string, scopeType string) error {
	row := queryer.QueryRow(ctx, `
SELECT type
FROM scopes
WHERE workspace_id = $1
  AND id = $2
  AND status = 'active'
`, workspaceID, scopeID)
	var actualType string
	if err := row.Scan(&actualType); err != nil {
		if coredb.IsNotFound(err) {
			return ErrNotFound
		}
		return fmt.Errorf("load task collaborator scope: %w", err)
	}
	if strings.TrimSpace(actualType) != strings.TrimSpace(scopeType) {
		return badRequest(scopeType + " collaborator scope requires a " + scopeType)
	}
	return nil
}

func ensureMemberCanReadScope(ctx context.Context, queryer rowQueryer, scopeID string, memberID string) error {
	row := queryer.QueryRow(ctx, `
SELECT 1
FROM scope_memberships
WHERE scope_id = $1
  AND member_id = $2
  AND visibility = 'public'
  AND removed_at IS NULL
`, scopeID, memberID)
	var marker int
	if err := row.Scan(&marker); err != nil {
		if coredb.IsNotFound(err) {
			return ErrForbidden
		}
		return fmt.Errorf("load member scope visibility: %w", err)
	}
	return nil
}

func ensureMemberCanReadThreadScope(ctx context.Context, queryer rowQueryer, workspaceID string, threadChannelID string, memberID string) error {
	row := queryer.QueryRow(ctx, `
SELECT 1
FROM scopes thread_scope
JOIN scope_memberships sm
  ON sm.member_id = $3
 AND sm.visibility = 'public'
 AND sm.removed_at IS NULL
 AND (sm.scope_id = thread_scope.id OR sm.scope_id = thread_scope.parent_scope_id)
WHERE thread_scope.workspace_id = $1
  AND thread_scope.id = $2
  AND thread_scope.type = 'thread'
  AND thread_scope.status = 'active'
LIMIT 1
`, workspaceID, threadChannelID, memberID)
	var marker int
	if err := row.Scan(&marker); err != nil {
		if coredb.IsNotFound(err) {
			return ErrForbidden
		}
		return fmt.Errorf("load task collaborator thread scope visibility: %w", err)
	}
	return nil
}

func normalizeTaskCollaboratorScope(scopeType string) (string, error) {
	switch strings.TrimSpace(scopeType) {
	case taskCollaboratorScopeTask:
		return taskCollaboratorScopeTask, nil
	case taskCollaboratorScopeThread:
		return taskCollaboratorScopeThread, nil
	case taskCollaboratorScopeChannel:
		return taskCollaboratorScopeChannel, nil
	default:
		return "", badRequest("invalid collaborator scopeType")
	}
}

func normalizeTaskCollaboratorRole(role string) (string, error) {
	switch strings.TrimSpace(role) {
	case "", taskCollaboratorRoleContributor:
		return taskCollaboratorRoleContributor, nil
	default:
		return "", badRequest("invalid collaborator role")
	}
}
