package app

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

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/app/installation"
	"github.com/tidbcloud/AgentTeam/internal/store/app/registry"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
)

func appMentionThreadMaterializationRequiresParentAuthority(params bindAppTargetParams) bool {
	if strings.TrimSpace(params.CreatedVia) != AppMentionCreatedViaThread || !params.EnsureMembership {
		return false
	}
	targetKind := strings.TrimSpace(params.Target.TargetKind)
	if targetKind != "thread" && targetKind != "task" {
		return false
	}
	permissionScopeID := strings.TrimSpace(params.Target.PermissionCheckScopeID)
	return permissionScopeID != "" && permissionScopeID != strings.TrimSpace(params.Target.DeliveryScopeID)
}

// lockAppMentionThreadTargetCandidate locks one globally sorted optimistic
// candidate and revalidates either its parent-derived authority or its explicit
// current-target authority. Callers must discover and sort the full candidate
// union before invoking this function so App locks are never category-ordered.
func lockAppMentionThreadTargetCandidate(
	ctx context.Context,
	store *Store,
	tx coredb.Tx,
	workspaceID string,
	candidate appMentionThreadTargetCandidate,
	target ServiceEventTarget,
) (memberID string, parentAuthorized bool, err error) {
	definitionID, err := lockAppMentionThreadMaterializationDefinitionFence(
		ctx, tx, workspaceID, candidate.ServiceInstallationID,
	)
	if err != nil {
		return "", false, err
	}
	if definitionID != strings.TrimSpace(candidate.ServiceDefinitionID) {
		return "", false, ErrNotFound
	}
	installation, err := store.serviceActorStore().InTx(tx).LoadInstallationForUpdate(
		ctx, strings.TrimSpace(candidate.ServiceInstallationID),
	)
	if err != nil {
		return "", false, err
	}
	if strings.TrimSpace(installation.WorkspaceID) != strings.TrimSpace(workspaceID) ||
		strings.TrimSpace(installation.ServiceDefinitionID) != definitionID ||
		strings.TrimSpace(installation.ServiceKind) != serviceactor.KindApp ||
		strings.TrimSpace(installation.Status) != serviceactor.StatusActive {
		return "", false, ErrNotFound
	}
	if _, err := requirePinnedAppEvent(installation, registry.AppEventMessageMention); err != nil {
		return "", false, err
	}
	serviceMember, err := store.serviceActorStore().InTx(tx).LoadMemberByInstallation(ctx, installation.ID)
	if err != nil {
		return "", false, err
	}
	memberID = strings.TrimSpace(serviceMember.MemberID)
	if strings.TrimSpace(serviceMember.Status) != serviceactor.StatusActive ||
		memberID == "" || memberID != strings.TrimSpace(candidate.ServiceMemberMemberID) {
		return "", false, ErrNotFound
	}
	if candidate.ParentAuthority {
		if parentErr := lockAppMentionThreadMaterializationParentAuthority(
			ctx, tx, installation, serviceMember, target,
		); parentErr == nil {
			return memberID, true, nil
		} else if !errors.Is(parentErr, ErrNotFound) {
			return "", false, parentErr
		}
	}
	if candidate.DirectTargetAuthority {
		if directErr := lockAppMentionThreadCurrentTargetAuthority(
			ctx, tx, installation, serviceMember, target,
		); directErr == nil {
			return memberID, false, nil
		} else if !errors.Is(directErr, ErrNotFound) {
			return "", false, directErr
		}
	}
	return "", false, ErrNotFound
}

func lockAppMentionThreadCurrentTargetAuthority(
	ctx context.Context,
	tx coredb.Tx,
	installation serviceactor.Installation,
	serviceMember serviceactor.Member,
	target ServiceEventTarget,
) error {
	workspaceID := strings.TrimSpace(installation.WorkspaceID)
	installationID := strings.TrimSpace(installation.ID)
	definitionID := strings.TrimSpace(installation.ServiceDefinitionID)
	serviceMemberID := strings.TrimSpace(serviceMember.ID)
	if workspaceID == "" || installationID == "" || definitionID == "" ||
		serviceMemberID == "" || strings.TrimSpace(serviceMember.MemberID) == "" {
		return ErrNotFound
	}
	var bindingID string
	if err := tx.QueryRow(ctx, `
SELECT binding.id
FROM service_target_bindings binding
JOIN service_event_subscriptions subscription
  ON subscription.workspace_id = binding.workspace_id
 AND subscription.service_definition_id = binding.service_definition_id
 AND subscription.service_installation_id = binding.service_installation_id
 AND subscription.service_member_id = binding.service_member_id
 AND subscription.service_target_binding_id = binding.id
 AND subscription.service_kind = binding.service_kind
 AND subscription.target_kind = binding.target_kind
 AND subscription.target_ref = binding.target_ref
 AND subscription.delivery_scope_id = binding.delivery_scope_id
 AND subscription.permission_check_scope_id = binding.permission_check_scope_id
 AND subscription.subscription_kind = $9
 AND subscription.event_filter_key = $10
 AND subscription.created_via = $13
 AND subscription.status = 'active'
 AND subscription.removed_at IS NULL
JOIN service_definitions definition
  ON definition.id = binding.service_definition_id
 AND definition.service_kind = binding.service_kind
LEFT JOIN extension_catalog_attributions attribution
  ON attribution.workspace_id = binding.workspace_id
 AND attribution.service_definition_id = binding.service_definition_id
WHERE binding.workspace_id = $1
  AND binding.service_installation_id = $2
  AND binding.service_definition_id = $3
  AND binding.service_member_id = $4
  AND binding.service_kind = 'app'
  AND binding.target_kind = $5
  AND binding.target_ref = $6
  AND binding.delivery_scope_id = $7
  AND binding.permission_check_scope_id = $8
  AND binding.created_via = $13
  AND binding.status = 'active'
  AND binding.removed_at IS NULL
  AND EXISTS (
      SELECT 1
      FROM service_grants read_grant
      WHERE read_grant.service_installation_id = binding.service_installation_id
        AND read_grant.scope = $11
        AND read_grant.revoked_at IS NULL
        AND (read_grant.expires_at IS NULL OR read_grant.expires_at > CURRENT_TIMESTAMP)
  )
  AND EXISTS (
      SELECT 1
      FROM service_grants write_grant
      WHERE write_grant.service_installation_id = binding.service_installation_id
        AND write_grant.scope = $12
        AND write_grant.revoked_at IS NULL
        AND (write_grant.expires_at IS NULL OR write_grant.expires_at > CURRENT_TIMESTAMP)
  )
  AND (
      attribution.service_definition_id IS NOT NULL
      OR (definition.id = 'app_github' AND LOWER(TRIM(definition.slug)) = 'github')
      OR (definition.id = 'app_gitea' AND LOWER(TRIM(definition.slug)) = 'gitea')
  )
LIMIT 1
FOR UPDATE
`, workspaceID, installationID, definitionID, serviceMemberID,
		strings.TrimSpace(target.TargetKind), strings.TrimSpace(target.TargetRef),
		strings.TrimSpace(target.DeliveryScopeID), strings.TrimSpace(target.PermissionCheckScopeID),
		serviceactor.SubscriptionKindAppMention, registry.AppEventMessageMention,
		AppMentionScopeRead, AppRuntimeScopeMessageWrite,
		AppMentionCreatedViaTarget).Scan(&bindingID); err != nil {
		if coredb.IsNotFound(err) {
			return ErrNotFound
		}
		return fmt.Errorf("lock App mention current target authority for membership repair: %w", err)
	}
	if strings.TrimSpace(bindingID) == "" {
		return ErrNotFound
	}
	return nil
}

// lockAppMentionThreadMaterializationDefinitionFence preserves the global App
// mutation lock order before the narrower installation lock is taken:
// definition -> workspace attribution (when present) -> installation. The
// initial installation lookup is intentionally non-locking; the locked load in
// lockAppMentionThreadTargetCandidate revalidates the same definition.
func lockAppMentionThreadMaterializationDefinitionFence(
	ctx context.Context,
	tx coredb.Tx,
	workspaceID string,
	installationID string,
) (string, error) {
	workspaceID = strings.TrimSpace(workspaceID)
	installationID = strings.TrimSpace(installationID)
	if workspaceID == "" || installationID == "" {
		return "", ErrNotFound
	}
	var definitionID string
	if err := tx.QueryRow(ctx, `
SELECT service_definition_id
FROM service_installations
WHERE id = $1
  AND workspace_id = $2
  AND service_kind = $3
LIMIT 1
`, installationID, workspaceID, serviceactor.KindApp).Scan(&definitionID); err != nil {
		if coredb.IsNotFound(err) {
			return "", ErrNotFound
		}
		return "", fmt.Errorf("load App mention installation definition fence: %w", err)
	}
	definitionID = strings.TrimSpace(definitionID)
	if err := installation.LockAppInstallationMutationScope(ctx, tx, definitionID); err != nil {
		return "", err
	}
	// Workspace-created Apps have an attribution row that visibility updates
	// lock before installation runtime. Product Apps intentionally have none;
	// their exact identity exception is revalidated with parent authority.
	if _, _, err := loadAppManagementAttribution(
		ctx, tx, workspaceID, definitionID, true,
	); err != nil {
		return "", err
	}
	return definitionID, nil
}

// lockAppMentionThreadMaterializationParentAuthority is the post-installation-
// lock half of dynamic thread materialization. Visibility cleanup takes the
// same installation lock before removing parent and child runtime, so exactly
// one side wins: a child created first is cleaned, while cleanup committed
// first makes this recheck fail without recreating membership or runtime.
func lockAppMentionThreadMaterializationParentAuthority(
	ctx context.Context,
	tx coredb.Tx,
	installation serviceactor.Installation,
	serviceMember serviceactor.Member,
	target ServiceEventTarget,
) error {
	workspaceID := strings.TrimSpace(installation.WorkspaceID)
	installationID := strings.TrimSpace(installation.ID)
	definitionID := strings.TrimSpace(installation.ServiceDefinitionID)
	serviceMemberID := strings.TrimSpace(serviceMember.ID)
	visibleMemberID := strings.TrimSpace(serviceMember.MemberID)
	parentScopeID := strings.TrimSpace(target.PermissionCheckScopeID)
	if workspaceID == "" || installationID == "" || definitionID == "" ||
		serviceMemberID == "" || visibleMemberID == "" || parentScopeID == "" {
		return ErrNotFound
	}
	var bindingID string
	if err := tx.QueryRow(ctx, `
SELECT binding.id
FROM scope_memberships parent_membership
JOIN scopes parent_scope
  ON parent_scope.id = parent_membership.scope_id
 AND parent_scope.workspace_id = $1
 AND parent_scope.status = 'active'
 AND parent_scope.type IN ('channel', 'dm', 'task')
JOIN service_target_bindings binding
  ON binding.workspace_id = $1
 AND binding.service_definition_id = $3
 AND binding.service_installation_id = $2
 AND binding.service_member_id = $4
 AND binding.service_kind = 'app'
 AND binding.target_ref = parent_membership.scope_id
 AND binding.delivery_scope_id = parent_membership.scope_id
 AND binding.permission_check_scope_id = parent_membership.scope_id
 AND binding.status = 'active'
 AND binding.removed_at IS NULL
JOIN service_event_subscriptions subscription
  ON subscription.workspace_id = binding.workspace_id
 AND subscription.service_definition_id = binding.service_definition_id
 AND subscription.service_installation_id = binding.service_installation_id
 AND subscription.service_member_id = binding.service_member_id
 AND subscription.service_target_binding_id = binding.id
 AND subscription.service_kind = binding.service_kind
 AND subscription.target_kind = binding.target_kind
 AND subscription.target_ref = binding.target_ref
 AND subscription.delivery_scope_id = binding.delivery_scope_id
 AND subscription.permission_check_scope_id = binding.permission_check_scope_id
 AND subscription.subscription_kind = $7
 AND subscription.event_filter_key = $8
 AND subscription.status = 'active'
 AND subscription.removed_at IS NULL
JOIN service_definitions definition
  ON definition.id = binding.service_definition_id
 AND definition.service_kind = binding.service_kind
LEFT JOIN extension_catalog_attributions attribution
  ON attribution.workspace_id = binding.workspace_id
 AND attribution.service_definition_id = binding.service_definition_id
WHERE parent_membership.scope_id = $6
  AND parent_membership.member_id = $5
  AND parent_membership.visibility = 'public'
  AND parent_membership.removed_at IS NULL
  AND parent_membership.archived_at IS NULL
  AND parent_membership.effective_to_seq IS NULL
  AND (
      attribution.service_definition_id IS NOT NULL
      OR (definition.id = 'app_github' AND LOWER(TRIM(definition.slug)) = 'github')
      OR (definition.id = 'app_gitea' AND LOWER(TRIM(definition.slug)) = 'gitea')
  )
LIMIT 1
FOR UPDATE
`, workspaceID, installationID, definitionID, serviceMemberID, visibleMemberID, parentScopeID, serviceactor.SubscriptionKindAppMention, registry.AppEventMessageMention).Scan(&bindingID); err != nil {
		if coredb.IsNotFound(err) {
			return ErrNotFound
		}
		return fmt.Errorf("lock App mention parent authority for thread materialization: %w", err)
	}
	if strings.TrimSpace(bindingID) == "" {
		return ErrNotFound
	}
	return nil
}
