package app

import (
	"context"
	"fmt"
	"slices"
	"strings"
	"time"

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

const (
	// PrivilegedAppRequestDecisionReason records that a request was withdrawn
	// because its requester now manages App changes directly.
	PrivilegedAppRequestDecisionReason = "Withdrawn by system: workspace administrators use direct App management."
)

// IsPrivilegedAppRequestWithdrawalDecision identifies only system-settled
// privileged proposals. Requiring self-decision identity prevents an ordinary
// human rejection that happens to reuse either reason from being suppressed.
func IsPrivilegedAppRequestWithdrawalDecision(request AppInstallationRequestView) bool {
	if strings.TrimSpace(request.Status) != AppInstallationRequestStatusRejected ||
		strings.TrimSpace(request.RequestedByMemberID) == "" ||
		strings.TrimSpace(request.DecidedByMemberID) != strings.TrimSpace(request.RequestedByMemberID) {
		return false
	}
	return strings.TrimSpace(request.DecisionReason) == PrivilegedAppRequestDecisionReason
}

// SettlePrivilegedAppInstallationRequest handles a request whose author was
// promoted after submit. It
// locks the canonical App definition/request, rechecks the current requester,
// and atomically rejects the obsolete proposal plus any pending reviewer or
// applicant-receipt card.
func (store *Store) SettlePrivilegedAppInstallationRequest(
	ctx context.Context,
	workspaceID string,
	requestID string,
) (AppInstallationRequestView, bool, error) {
	workspaceID = strings.TrimSpace(workspaceID)
	requestID = strings.TrimSpace(requestID)
	if workspaceID == "" || requestID == "" {
		return AppInstallationRequestView{}, false, badRequest("workspaceId and requestId are required")
	}
	var request AppInstallationRequestView
	settled := false
	cleanupCtx, avatarCleanup := withAppAvatarMediaCleanup(ctx)
	if err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		var err error
		request, err = loadAppInstallationRequestForUpdate(ctx, tx, workspaceID, requestID)
		if err != nil {
			return err
		}
		// A pending notification transaction can start before the privileged
		// settlement lock, then commit its v1 card after the first withdrawal
		// pass. Re-running the card cleanup for the canonical sentinel decision
		// makes this boundary idempotent and lets the projector close that race.
		if IsPrivilegedAppRequestWithdrawalDecision(request) {
			return withdrawPrivilegedAppRequestPendingCardsInTx(ctx, tx, request, time.Now().UTC())
		}
		settled, err = settlePrivilegedAppInstallationRequestInTx(cleanupCtx, tx, request, time.Now().UTC())
		if err != nil || !settled {
			return err
		}
		request, err = loadAppInstallationRequest(ctx, tx, workspaceID, requestID)
		return err
	}); err != nil {
		return AppInstallationRequestView{}, false, err
	}
	store.finalizeAppAvatarMediaCleanup(ctx, avatarCleanup)
	return request, settled, nil
}

type privilegedPendingAppRequestSettlement struct {
	SettledCount int
}

type pendingAppRequestIdentity struct {
	requestID         string
	requesterMemberID string
}

// settlePrivilegedPendingAppInstallationRequestsInTx scans every pending
// proposal under the canonical definition lock. It locks requester membership
// rows in stable id order, and settles every current owner/admin (including
// inactive members). Ordinary proposals are intentionally left untouched: they
// are independent review work, not a lock on direct workspace administration.
func settlePrivilegedPendingAppInstallationRequestsInTx(
	ctx context.Context,
	tx coredb.Tx,
	workspaceID string,
	serviceDefinitionID string,
	now time.Time,
) (privilegedPendingAppRequestSettlement, error) {
	if err := installation.LockAppInstallationMutationScope(ctx, tx, serviceDefinitionID); err != nil {
		return privilegedPendingAppRequestSettlement{}, err
	}
	rows, err := tx.Query(ctx, `
SELECT id, requested_by_member_id
FROM service_installation_requests
WHERE workspace_id = $1
  AND service_definition_id = $2
  AND service_kind = 'app'
  AND status = 'pending'
  AND approval_required = TRUE
ORDER BY created_at ASC, id ASC
FOR UPDATE
`, strings.TrimSpace(workspaceID), strings.TrimSpace(serviceDefinitionID))
	if err != nil {
		return privilegedPendingAppRequestSettlement{}, fmt.Errorf("list pending privileged App requests: %w", err)
	}
	identities := make([]pendingAppRequestIdentity, 0)
	requesterSet := make(map[string]struct{})
	for rows.Next() {
		var identity pendingAppRequestIdentity
		if err := rows.Scan(&identity.requestID, &identity.requesterMemberID); err != nil {
			rows.Close()
			return privilegedPendingAppRequestSettlement{}, fmt.Errorf("scan pending privileged App request: %w", err)
		}
		identity.requestID = strings.TrimSpace(identity.requestID)
		identity.requesterMemberID = strings.TrimSpace(identity.requesterMemberID)
		identities = append(identities, identity)
		if identity.requesterMemberID != "" {
			requesterSet[identity.requesterMemberID] = struct{}{}
		}
	}
	if err := rows.Err(); err != nil {
		rows.Close()
		return privilegedPendingAppRequestSettlement{}, fmt.Errorf("iterate pending privileged App requests: %w", err)
	}
	rows.Close()

	requesterIDs := make([]string, 0, len(requesterSet))
	for requesterID := range requesterSet {
		requesterIDs = append(requesterIDs, requesterID)
	}
	slices.Sort(requesterIDs)
	privilegedByRequester := make(map[string]bool, len(requesterIDs))
	for _, requesterID := range requesterIDs {
		var workspaceRole string
		err := tx.QueryRow(ctx, `
SELECT workspace_role
FROM members
WHERE id = $1
  AND workspace_id = $2
  AND type = 'human'
LIMIT 1
FOR UPDATE
`, requesterID, strings.TrimSpace(workspaceID)).Scan(&workspaceRole)
		if coredb.IsNotFound(err) {
			continue
		}
		if err != nil {
			return privilegedPendingAppRequestSettlement{}, fmt.Errorf("lock App requester's current workspace role: %w", err)
		}
		privilegedByRequester[requesterID] = access.HasWorkspaceAdminRole(domain.WorkspaceRole(strings.TrimSpace(workspaceRole)))
	}

	result := privilegedPendingAppRequestSettlement{}
	for _, identity := range identities {
		if !privilegedByRequester[identity.requesterMemberID] {
			continue
		}
		request, err := loadAppInstallationRequest(ctx, tx, workspaceID, identity.requestID)
		if err != nil {
			return privilegedPendingAppRequestSettlement{}, err
		}
		if err := settleKnownPrivilegedAppInstallationRequestInTx(ctx, tx, request, now); err != nil {
			return privilegedPendingAppRequestSettlement{}, err
		}
		result.SettledCount++
	}
	return result, nil
}

func settlePrivilegedAppInstallationRequestInTx(
	ctx context.Context,
	tx coredb.Tx,
	request AppInstallationRequestView,
	now time.Time,
) (bool, error) {
	if strings.TrimSpace(request.Status) != AppInstallationRequestStatusPending {
		return false, nil
	}
	if !request.ApprovalRequired {
		return false, nil
	}
	var workspaceRole string
	err := tx.QueryRow(ctx, `
SELECT workspace_role
FROM members
WHERE id = $1
  AND workspace_id = $2
  AND type = 'human'
LIMIT 1
FOR UPDATE
`, strings.TrimSpace(request.RequestedByMemberID), strings.TrimSpace(request.WorkspaceID)).Scan(&workspaceRole)
	if coredb.IsNotFound(err) {
		return false, nil
	}
	if err != nil {
		return false, fmt.Errorf("load App requester's current workspace role: %w", err)
	}
	if !access.HasWorkspaceAdminRole(domain.WorkspaceRole(strings.TrimSpace(workspaceRole))) {
		return false, nil
	}
	if err := settleKnownPrivilegedAppInstallationRequestInTx(ctx, tx, request, now); err != nil {
		return false, err
	}
	return true, nil
}

func settleKnownPrivilegedAppInstallationRequestInTx(
	ctx context.Context,
	tx coredb.Tx,
	request AppInstallationRequestView,
	now time.Time,
) error {
	return settleKnownAppInstallationRequestAsSystemRejectedInTx(
		ctx, tx, request, PrivilegedAppRequestDecisionReason, now,
	)
}

// settleKnownAppInstallationRequestAsSystemRejectedInTx terminalizes a
// pending approval whose authority has been superseded by a trusted lifecycle
// transition. The caller must already hold the App definition mutation lock
// and the request row lock. It preserves a redacted audit row while withdrawing
// stale interactive cards so a system-settled request cannot remain actionable.
func settleKnownAppInstallationRequestAsSystemRejectedInTx(
	ctx context.Context,
	tx coredb.Tx,
	request AppInstallationRequestView,
	decisionReason string,
	now time.Time,
) error {
	resolvedBaseConfigJSON, err := model.MarshalJSONObject(
		"resolved service installation request base config",
		appInstallationRequestResolvedBaseConfig(request),
	)
	if err != nil {
		return fmt.Errorf("redact privileged service installation request base config: %w", err)
	}
	resolvedRequestedConfig, err := appInstallationRequestTerminalRequestedConfig(request)
	if err != nil {
		return fmt.Errorf("resolve privileged service installation request config: %w", err)
	}
	resolvedRequestedConfigJSON, err := model.MarshalJSONObject(
		"resolved service installation request config",
		resolvedRequestedConfig,
	)
	if err != nil {
		return fmt.Errorf("redact privileged service installation request config: %w", err)
	}
	result, err := tx.Exec(ctx, `
UPDATE service_installation_requests
SET status = $1,
    decided_by_member_id = requested_by_member_id,
    decision_reason = $2,
    base_config = $3,
    requested_config = $4,
    updated_at = $5,
    decided_at = $5
WHERE id = $6
  AND workspace_id = $7
  AND status = 'pending'
`, AppInstallationRequestStatusRejected, strings.TrimSpace(decisionReason),
		resolvedBaseConfigJSON, resolvedRequestedConfigJSON, now.UTC(),
		strings.TrimSpace(request.ID), strings.TrimSpace(request.WorkspaceID))
	if err != nil {
		return fmt.Errorf("withdraw superseded App installation request: %w", err)
	}
	if result.RowsAffected() != 1 {
		return conflict("App installation request decision changed.")
	}
	if isGitHubApp(request.AppID) && strings.TrimSpace(request.RequestKind) == AppInstallationRequestKindInstall {
		if err := releaseGitHubAppSetupClaimByTokenInTx(ctx, tx, request.WorkspaceID, request.ID); err != nil {
			return err
		}
	}
	if err := withdrawPrivilegedAppRequestPendingCardsInTx(ctx, tx, request, now); err != nil {
		return err
	}
	return deleteTerminalWorkspaceAppCatalogStateInTx(ctx, tx, request)
}

func withdrawPrivilegedAppRequestPendingCardsInTx(
	ctx context.Context,
	tx coredb.Tx,
	request AppInstallationRequestView,
	now time.Time,
) error {
	if _, err := tx.Exec(ctx, `
UPDATE service_notifications
SET status = 'withdrawn',
    revision = revision + 1,
    business_object_version = 2,
    withdrawn_at = $1,
    updated_at = $1
WHERE workspace_id = $2
  AND business_object_namespace = 'agentteam.approval'
  AND business_object_type = 'app_installation_request'
  AND business_object_id = $3
  AND recipient_basis_kind IN ('app_installation_reviewer', 'app_installation_applicant')
  AND content_kind = 'interactive_card'
  AND status = 'active'
  AND business_object_version = 1
`, now.UTC(), strings.TrimSpace(request.WorkspaceID), strings.TrimSpace(request.ID)); err != nil {
		return fmt.Errorf("withdraw privileged App request pending notifications: %w", err)
	}
	if _, err := tx.Exec(ctx, `
UPDATE service_notification_cards card
JOIN service_notifications notification
  ON notification.id = card.service_notification_id
SET card.state = 'withdrawn',
    card.card_revision = card.card_revision + 1,
    card.updated_at = $1
WHERE notification.workspace_id = $2
  AND notification.business_object_namespace = 'agentteam.approval'
  AND notification.business_object_type = 'app_installation_request'
  AND notification.business_object_id = $3
  AND notification.recipient_basis_kind IN ('app_installation_reviewer', 'app_installation_applicant')
  AND notification.content_kind = 'interactive_card'
  AND notification.status = 'withdrawn'
  AND notification.business_object_version = 2
  AND card.state = 'active'
`, now.UTC(), strings.TrimSpace(request.WorkspaceID), strings.TrimSpace(request.ID)); err != nil {
		return fmt.Errorf("withdraw privileged App request pending cards: %w", err)
	}
	return nil
}
