package app

import (
	"context"
	"fmt"
	"github.com/tidbcloud/AgentTeam/internal/store/app/registry"
	"strings"

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

// requireAppApprovalRequester keeps the request ledger member-only. Workspace
// owners/admins have explicit direct commands and must never be able to create
// a self-approval request by calling the request endpoints manually.
func requireAppApprovalRequester(workspaceMember access.HumanWorkspaceMember) error {
	if access.HasWorkspaceAdminRole(domain.WorkspaceRole(workspaceMember.WorkspaceRole)) {
		return ErrForbidden
	}
	return nil
}

// revalidateAppApprovalRequesterForUpdate closes the role-transition race
// between the request endpoint's early access check and the durable request
// write. The canonical membership row is locked for the rest of the request
// transaction so a member cannot be promoted to owner/admin while a pending
// self-approval request is being admitted.
func (store *Store) revalidateAppApprovalRequesterForUpdate(
	ctx context.Context,
	queryer coredb.QueryRower,
	userID string,
	workspaceID string,
	expected access.HumanWorkspaceMember,
) (access.HumanWorkspaceMember, error) {
	current, err := store.loadAuthorizedHumanWorkspaceMemberForUpdate(
		ctx,
		queryer,
		userID,
		workspaceID,
		workspaceAccessAction,
	)
	if err != nil {
		return access.HumanWorkspaceMember{}, err
	}
	currentUserID := ""
	if current.Member.UserID != nil {
		currentUserID = strings.TrimSpace(*current.Member.UserID)
	}
	expectedUserID := ""
	if expected.Member.UserID != nil {
		expectedUserID = strings.TrimSpace(*expected.Member.UserID)
	}
	if strings.TrimSpace(current.Member.ID) != strings.TrimSpace(expected.Member.ID) ||
		strings.TrimSpace(current.Member.WorkspaceID) != strings.TrimSpace(workspaceID) ||
		currentUserID != strings.TrimSpace(userID) ||
		expectedUserID != strings.TrimSpace(userID) ||
		strings.TrimSpace(expected.Member.WorkspaceID) != strings.TrimSpace(workspaceID) {
		return access.HumanWorkspaceMember{}, conflict("App installation requester membership changed.")
	}
	if err := requireAppApprovalRequester(current); err != nil {
		return access.HumanWorkspaceMember{}, err
	}
	return current, nil
}

// authorizeServiceInstallationManagement keeps direct installation lifecycle
// changes behind the workspace service-installer permission. Requesters and
// App definition creators do not gain management authority.
func authorizeServiceInstallationManagement(workspaceMember access.HumanWorkspaceMember) error {
	if err := access.AuthorizeHumanAction(workspaceMember, workspaceInstallServiceAction); err != nil {
		return ErrForbidden
	}
	return nil
}

// revalidateServiceInstallationManagementForUpdate closes the gap between a
// public surface's early authorization/read validation and its final write.
// Callers take their stable App-definition/request lock first, then lock the
// current human membership before any narrower installation/runtime row.
func (store *Store) revalidateServiceInstallationManagementForUpdate(
	ctx context.Context,
	queryer coredb.QueryRower,
	userID string,
	workspaceID string,
	expectedMemberID string,
) (access.HumanWorkspaceMember, error) {
	current, err := store.loadAuthorizedHumanWorkspaceMemberForUpdate(ctx, queryer, userID, workspaceID, workspaceInstallServiceAction)
	if err != nil {
		return access.HumanWorkspaceMember{}, err
	}
	if strings.TrimSpace(current.Member.ID) != strings.TrimSpace(expectedMemberID) {
		return access.HumanWorkspaceMember{}, conflict("App installation administrator membership changed.")
	}
	return current, nil
}

// authorizeAppInstallationManagement preserves administrator-only management
// for ordinary/product Apps. A workspace-created App is identified by its
// workspace catalog attribution and can be managed by an active App manager
// or a workspace owner/admin.
func (store *Store) authorizeAppInstallationManagement(
	ctx context.Context,
	queryer coredb.QueryRower,
	workspaceID string,
	workspaceMember access.HumanWorkspaceMember,
	serviceDefinitionID string,
) error {
	_, attributed, err := loadAppManagementAttribution(
		ctx,
		queryer,
		workspaceID,
		serviceDefinitionID,
		false,
	)
	if err != nil {
		return err
	}
	if !attributed {
		return authorizeServiceInstallationManagement(workspaceMember)
	}
	if access.HasWorkspaceAdminRole(domain.WorkspaceRole(workspaceMember.WorkspaceRole)) {
		return nil
	}
	managed, err := hasActiveAppManager(
		ctx,
		queryer,
		workspaceID,
		serviceDefinitionID,
		workspaceMember.Member.ID,
		false,
	)
	if err != nil {
		return err
	}
	if !managed {
		return ErrNotFound
	}
	return nil
}

// revalidateAppInstallationManagementForUpdate is the transaction-safe form
// of authorizeAppInstallationManagement. Callers hold the stable App
// definition fence first; this method then locks the current human membership
// and the App attribution before any narrower installation/runtime write.
func (store *Store) revalidateAppInstallationManagementForUpdate(
	ctx context.Context,
	queryer coredb.QueryRower,
	userID string,
	workspaceID string,
	expectedMemberID string,
	serviceDefinitionID string,
) (access.HumanWorkspaceMember, error) {
	currentMember, err := store.loadAuthorizedHumanWorkspaceMemberForUpdate(
		ctx,
		queryer,
		userID,
		workspaceID,
		workspaceAccessAction,
	)
	if err != nil {
		return access.HumanWorkspaceMember{}, err
	}
	if strings.TrimSpace(currentMember.Member.ID) != strings.TrimSpace(expectedMemberID) {
		return access.HumanWorkspaceMember{}, conflict("App installation manager membership changed.")
	}

	_, attributed, err := loadAppManagementAttribution(
		ctx,
		queryer,
		workspaceID,
		serviceDefinitionID,
		true,
	)
	if err != nil {
		return access.HumanWorkspaceMember{}, err
	}
	if !attributed {
		if err := authorizeServiceInstallationManagement(currentMember); err != nil {
			return access.HumanWorkspaceMember{}, err
		}
		return currentMember, nil
	}
	if access.HasWorkspaceAdminRole(domain.WorkspaceRole(currentMember.WorkspaceRole)) {
		return currentMember, nil
	}
	managed, err := hasActiveAppManager(
		ctx,
		queryer,
		workspaceID,
		serviceDefinitionID,
		currentMember.Member.ID,
		true,
	)
	if err != nil {
		return access.HumanWorkspaceMember{}, err
	}
	if !managed {
		return access.HumanWorkspaceMember{}, ErrNotFound
	}
	return currentMember, nil
}

func loadAppManagementAttribution(
	ctx context.Context,
	queryer coredb.QueryRower,
	workspaceID string,
	serviceDefinitionID string,
	forUpdate bool,
) (appManagementAttribution, bool, error) {
	query := `
SELECT COALESCE(created_by_member_id, ''), catalog_visibility
FROM extension_catalog_attributions
WHERE workspace_id = $1
  AND service_definition_id = $2
LIMIT 1
`
	if forUpdate {
		query += "FOR UPDATE\n"
	}
	var attribution appManagementAttribution
	if err := queryer.QueryRow(
		ctx,
		query,
		strings.TrimSpace(workspaceID),
		strings.TrimSpace(serviceDefinitionID),
	).Scan(&attribution.CreatorMemberID, &attribution.CatalogVisibility); err != nil {
		if coredb.IsNotFound(err) {
			return appManagementAttribution{}, false, nil
		}
		return appManagementAttribution{}, false, fmt.Errorf("load App management attribution: %w", err)
	}
	attribution.CreatorMemberID = strings.TrimSpace(attribution.CreatorMemberID)
	attribution.CatalogVisibility = strings.ToLower(strings.TrimSpace(attribution.CatalogVisibility))
	if attribution.CatalogVisibility != registry.CatalogVisibilityPrivate &&
		attribution.CatalogVisibility != registry.CatalogVisibilityPublic {
		return appManagementAttribution{}, false, fmt.Errorf("load App management attribution: invalid catalog visibility %q", attribution.CatalogVisibility)
	}
	return attribution, true, nil
}

type appManagementAttribution struct {
	CreatorMemberID   string
	CatalogVisibility string
}
