package app

import (
	"context"
	"fmt"
	"reflect"
	"strings"
	"time"

	"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"
)

// InstallAppForWorkspaceAdmin is the explicit privileged counterpart to a
// member installation request. It never writes or settles an ordinary
// member's approval row. Such a proposal is revalidated only if later approved.
// A request whose author has since become owner/admin is withdrawn first so a
// stale approval row cannot block the direct command forever.
func (store *Store) InstallAppForWorkspaceAdmin(
	ctx context.Context,
	userID string,
	workspaceID string,
	params InstallAppParams,
) (AppInstallationView, error) {
	adminMember, err := store.requireWorkspaceAdmin(ctx, userID, workspaceID)
	if err != nil {
		return AppInstallationView{}, err
	}
	prepared, err := prepareWorkspaceAppInstallationRequest(ctx, store.db, workspaceID, params)
	if err != nil {
		return AppInstallationView{}, err
	}
	if err := requireGenericAdminAppInstallAllowed(
		ctx,
		store.db,
		prepared,
	); err != nil {
		return AppInstallationView{}, err
	}

	var installationID string
	for {
		settlementOnly := false
		cleanupCtx, avatarCleanup := withAppAvatarMediaCleanup(ctx)
		if err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
			// Revalidate both catalog admission and the immutable version/config in
			// the write transaction. The definition row then provides the canonical
			// mutation lock shared with requests, decisions, and uninstall.
			currentPrepared, err := prepareWorkspaceAppInstallationRequest(ctx, tx, workspaceID, params)
			if err != nil {
				return err
			}
			if err := requireGenericAdminAppInstallAllowed(
				ctx,
				tx,
				currentPrepared,
			); err != nil {
				return err
			}
			if strings.TrimSpace(currentPrepared.version.Version) != strings.TrimSpace(prepared.version.Version) {
				return conflict("App version changed. Reload and install again.")
			}
			if !registry.IsProductApp(currentPrepared.app.ID, currentPrepared.app.Slug) {
				return badRequest("Custom Apps must be created through App setup.")
			}
			if err := authorizeAppInstallationRequest(ctx, tx, workspaceID, currentPrepared); err != nil {
				return err
			}
			now := time.Now().UTC()
			definition, err := store.syncAppServiceDefinition(ctx, tx, currentPrepared.app.ID, now)
			if err != nil {
				return err
			}
			if strings.TrimSpace(definition.ID) != strings.TrimSpace(currentPrepared.app.ID) {
				return conflict("App installation definition changed.")
			}
			pending, err := settlePrivilegedPendingAppInstallationRequestsInTx(cleanupCtx, tx, workspaceID, definition.ID, now)
			if err != nil {
				return err
			}
			if pending.SettledCount > 0 {
				settlementOnly = true
				return nil
			}
			currentAdmin, err := store.revalidateServiceInstallationManagementForUpdate(
				ctx,
				tx,
				userID,
				workspaceID,
				adminMember.Member.ID,
			)
			if err != nil {
				return err
			}
			installationID, err = store.installPreparedAppInTx(
				ctx,
				tx,
				userID,
				workspaceID,
				currentAdmin.Member.ID,
				currentPrepared,
				now,
			)
			return err
		}); err != nil {
			return AppInstallationView{}, err
		}
		store.finalizeAppAvatarMediaCleanup(ctx, avatarCleanup)
		if settlementOnly {
			continue
		}
		break
	}
	return store.appInstallationViewByID(ctx, workspaceID, installationID)
}

// requireGenericAdminAppInstallAllowed keeps provider-specific setup
// invariants out of the generic App installation path. A GitHub installation
// and its verified credentials must become durable in the same transaction;
// otherwise an administrator could create an active but unusable App by
// bypassing FinalizeGitHubAppSetupForWorkspaceAdmin.
func requireGenericAdminAppInstallAllowed(
	ctx context.Context,
	queryer coredb.QueryRower,
	prepared preparedAppInstallation,
) error {
	if strings.TrimSpace(prepared.app.ID) == registry.GitHubAppID {
		return badRequest("GitHub must be installed and configured through GitHub setup.")
	}
	return requireGenericAppSetupAllowed(
		ctx,
		queryer,
		prepared.app.ID,
	)
}

// settleOrLockAppConfigurationScope runs the workspace-wide privileged
// settlement for workspace installations (reporting whether the agentteam must
// restart settlement-only) or takes the canonical Definition mutation lock
// directly for personal installations, which never touch the workspace
// ledger.
func settleOrLockAppConfigurationScope(ctx context.Context, tx coredb.Tx, cleanupCtx context.Context, workspaceID string, definitionID string, ownershipKind string) (bool, error) {
	if strings.TrimSpace(ownershipKind) == serviceactor.OwnershipKindPersonal {
		if err := installation.LockAppInstallationMutationScope(ctx, tx, definitionID); err != nil {
			return false, err
		}
		return false, nil
	}
	pending, err := settlePrivilegedPendingAppInstallationRequestsInTx(cleanupCtx, tx, workspaceID, definitionID, time.Now().UTC())
	if err != nil {
		return false, err
	}
	return pending.SettledCount > 0, nil
}

// ConfigureAppInstallationForWorkspaceAdmin applies ordinary App manifest
// config directly for a current workspace owner/admin or an active external
// App administrator. Provider credentials continue to use their
// provider-specific direct commands; this command only changes the
// version-pinned generic manifest config.
func (store *Store) ConfigureAppInstallationForWorkspaceAdmin(
	ctx context.Context,
	userID string,
	workspaceID string,
	installationID string,
	params ConfigureAppInstallationParams,
) (AppInstallationView, error) {
	workspaceMember, err := store.requireWorkspaceAccess(ctx, userID, workspaceID)
	if err != nil {
		return AppInstallationView{}, err
	}
	trimmedInstallationID := strings.TrimSpace(installationID)
	if trimmedInstallationID == "" {
		return AppInstallationView{}, badRequest("installationId is required")
	}
	if params.ExpectedBaseConfig == nil {
		return AppInstallationView{}, badRequest("expectedBaseConfig is required")
	}
	summary, err := store.serviceActorStore().LoadInstallationSummaryByKind(
		ctx,
		strings.TrimSpace(workspaceID),
		trimmedInstallationID,
		serviceactor.KindApp,
	)
	if err != nil {
		return AppInstallationView{}, err
	}
	if err := store.authorizeInstallationManagementFromSummary(ctx, store.db, workspaceID, workspaceMember, summary); err != nil {
		return AppInstallationView{}, err
	}

	for {
		settlementOnly := false
		cleanupCtx, avatarCleanup := withAppAvatarMediaCleanup(ctx)
		if err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
			var definitionID, ownershipKind string
			if err := tx.QueryRow(ctx, `
SELECT service_definition_id, COALESCE(ownership_kind, 'workspace')
FROM service_installations
WHERE id = $1
  AND workspace_id = $2
  AND service_kind = $3
LIMIT 1
	`, trimmedInstallationID, strings.TrimSpace(workspaceID), serviceactor.KindApp).Scan(&definitionID, &ownershipKind); err != nil {
				if coredb.IsNotFound(err) {
					return ErrNotFound
				}
				return fmt.Errorf("load App definition for direct configuration: %w", err)
			}
			if strings.TrimSpace(definitionID) != strings.TrimSpace(summary.Definition.ID) {
				return conflict("App installation changed before direct configuration.")
			}
			settledOnly, err := settleOrLockAppConfigurationScope(ctx, tx, cleanupCtx, workspaceID, definitionID, ownershipKind)
			if err != nil {
				return err
			}
			if settledOnly {
				settlementOnly = true
				return nil
			}
			if _, err := store.revalidateInstallationManagementForUpdate(
				ctx,
				tx,
				userID,
				workspaceID,
				workspaceMember.Member.ID,
				trimmedInstallationID,
				definitionID,
			); err != nil {
				return err
			}

			installation, err := store.serviceActorStore().InTx(tx).LoadInstallationForUpdate(ctx, trimmedInstallationID)
			if err != nil {
				return err
			}
			if strings.TrimSpace(installation.WorkspaceID) != strings.TrimSpace(workspaceID) ||
				strings.TrimSpace(installation.ServiceDefinitionID) != strings.TrimSpace(definitionID) ||
				strings.TrimSpace(installation.ServiceKind) != serviceactor.KindApp ||
				strings.TrimSpace(installation.Status) != serviceactor.StatusActive {
				return ErrNotFound
			}
			baseConfig := registry.JSONObjectOrEmpty(decodeJSONObjectMap(installation.Config))
			if !reflect.DeepEqual(baseConfig, registry.JSONObjectOrEmpty(params.ExpectedBaseConfig)) {
				return conflict("App configuration changed after it was opened. Reload and configure again.")
			}
			prepared, err := prepareInstalledAppConfigurationAtVersion(
				ctx,
				tx,
				installation.ServiceDefinitionID,
				installation.InstalledVersion,
				params.Config,
			)
			if err != nil {
				return err
			}
			if reflect.DeepEqual(baseConfig, registry.JSONObjectOrEmpty(decodeJSONObjectMap(prepared.configJSON))) {
				return nil
			}
			return applyPreparedAppConfigurationInTx(
				ctx,
				tx,
				workspaceID,
				trimmedInstallationID,
				prepared,
				time.Now().UTC(),
			)
		}); err != nil {
			return AppInstallationView{}, err
		}
		store.finalizeAppAvatarMediaCleanup(ctx, avatarCleanup)
		if settlementOnly {
			continue
		}
		break
	}
	return store.appInstallationViewByID(ctx, workspaceID, trimmedInstallationID)
}
