package app

import (
	"context"
	"fmt"
	"github.com/tidbcloud/AgentTeam/internal/pinyinsearch"
	"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/model"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
)

const generatedMemberInsertAttempts = 4

func insertGeneratedMember(
	newMemberID func() (string, error),
	insert func(memberID string) error,
	generateContext string,
	insertContext string,
) (string, error) {
	var duplicateErr error
	for attempt := 0; attempt < generatedMemberInsertAttempts; attempt++ {
		memberID, err := newMemberID()
		if err != nil {
			return "", fmt.Errorf("%s: %w", generateContext, err)
		}
		if err := insert(memberID); err != nil {
			if uniqueViolationMatches(err, "PRIMARY") {
				duplicateErr = err
				continue
			}
			return "", fmt.Errorf("%s: %w", insertContext, err)
		}
		return memberID, nil
	}
	return "", fmt.Errorf("%s after %d generated id collisions: %w", insertContext, generatedMemberInsertAttempts, duplicateErr)
}

type preparedAppInstallation struct {
	app        model.InstallableAppRecord
	version    model.InstallableAppVersionRecord
	configJSON []byte
}

func prepareAppInstallation(ctx context.Context, queryer coredb.Queryer, params InstallAppParams) (preparedAppInstallation, error) {
	return prepareAppInstallationAtVersion(ctx, queryer, params.AppID, params.AppSlug, "", params.Config)
}

func prepareWorkspaceAppInstallationRequest(
	ctx context.Context,
	queryer coredb.Queryer,
	workspaceID string,
	params InstallAppParams,
) (preparedAppInstallation, error) {
	app, err := installation.LoadWorkspaceInstallableApp(
		ctx,
		queryer,
		workspaceID,
		params.AppID,
		params.AppSlug,
	)
	if err != nil {
		return preparedAppInstallation{}, err
	}
	return prepareAppRecordAtVersion(ctx, queryer, app, app.LatestVersion, params.Config)
}

func prepareAppInstallationAtVersion(ctx context.Context, queryer coredb.Queryer, appID string, appSlug string, versionID string, config map[string]any) (preparedAppInstallation, error) {
	app, err := installation.LoadInstallableApp(ctx, queryer, appID, appSlug)
	if err != nil {
		return preparedAppInstallation{}, err
	}
	return prepareAppRecordAtVersion(ctx, queryer, app, versionID, config)
}

func prepareInstalledAppConfigurationAtVersion(ctx context.Context, queryer coredb.Queryer, appID string, versionID string, config map[string]any) (preparedAppInstallation, error) {
	var app model.InstallableAppRecord
	if err := queryer.QueryRow(ctx, `
SELECT id, slug, name
FROM service_definitions
WHERE id = $1
`, strings.TrimSpace(appID)).Scan(&app.ID, &app.Slug, &app.Name); err != nil {
		if coredb.IsNotFound(err) {
			return preparedAppInstallation{}, ErrNotFound
		}
		return preparedAppInstallation{}, fmt.Errorf("load installed App definition: %w", err)
	}
	return prepareAppRecordAtVersion(ctx, queryer, app, versionID, config)
}

func prepareAppRecordAtVersion(ctx context.Context, queryer coredb.Queryer, app model.InstallableAppRecord, versionID string, config map[string]any) (preparedAppInstallation, error) {
	if err := validateProviderAppManifestConfig(app.ID, config); err != nil {
		return preparedAppInstallation{}, err
	}
	version, err := installation.LoadInstallableAppVersion(ctx, queryer, app.ID, versionID, app.LatestVersion)
	if err != nil {
		return preparedAppInstallation{}, err
	}
	configValue := registry.JSONObjectOrEmpty(config)
	if err := registry.ValidateJSONObjectAgainstManifestSchema("config", version.ConfigSchema, configValue); err != nil {
		return preparedAppInstallation{}, err
	}
	configJSON, err := installation.MarshalJSONObject(configValue, "config")
	if err != nil {
		return preparedAppInstallation{}, err
	}
	return preparedAppInstallation{
		app:        app,
		version:    version,
		configJSON: configJSON,
	}, nil
}

// Provider configuration never enters generic App manifest
// config. Provider-specific request handling persists the approved provider
// state separately while the App installation remains at canonical {}.
func validateProviderAppManifestConfig(appID string, config map[string]any) error {
	if len(registry.JSONObjectOrEmpty(config)) == 0 {
		return nil
	}
	switch strings.TrimSpace(appID) {
	case registry.GitHubAppID, registry.GiteaAppID:
		return badRequest("Provider App manifest config must be empty.")
	default:
		return nil
	}
}

func (store *Store) installPreparedAppInTx(ctx context.Context, tx coredb.Tx, userID string, workspaceID string, createdByMemberID string, prepared preparedAppInstallation, now time.Time) (string, error) {
	installer := preparedAppInstaller{store: store}
	return installer.installInstance(
		ctx,
		tx,
		userID,
		workspaceID,
		createdByMemberID,
		prepared,
		appInstallationInstanceIdentity{
			InstanceKey:              serviceactor.DefaultInstanceKey,
			Handle:                   prepared.app.Slug,
			DisplayName:              prepared.app.Name,
			JoinAllChannelMembership: true,
		},
		now,
	)
}

type appInstallationInstanceIdentity struct {
	InstanceKey              string
	Handle                   string
	DisplayName              string
	JoinAllChannelMembership bool
}

type preparedAppInstaller struct {
	store *Store
}

func (installer preparedAppInstaller) installInstance(
	ctx context.Context,
	tx coredb.Tx,
	userID string,
	workspaceID string,
	createdByMemberID string,
	prepared preparedAppInstallation,
	identity appInstallationInstanceIdentity,
	now time.Time,
) (string, error) {
	identity, now, err := normalizeAppInstallationInstanceIdentity(identity, now)
	if err != nil {
		return "", err
	}
	return installation.InstallPreparedInstance(
		ctx, tx, installer.store.serviceActorStore().InTx(tx), workspaceID, createdByMemberID,
		installation.PreparedInstance{
			DefinitionID:  prepared.app.ID,
			Version:       prepared.version.Version,
			VersionScopes: prepared.version.Scopes,
			ConfigJSON:    prepared.configJSON,
		},
		installation.InstanceIdentity{
			InstanceKey: identity.InstanceKey,
			Handle:      identity.Handle,
			DisplayName: identity.DisplayName,
		},
		installation.InstallCallbacks{
			EnsureChannelMembership: func(ctx context.Context, tx coredb.Tx, workspaceID string, memberID string, now time.Time) error {
				if !identity.JoinAllChannelMembership || prepared.version.RuntimeCapabilitySupported {
					// Runtime-capability hosts and explicit non-joiners stay
					// out of the all-channel membership.
					return nil
				}
				allChannelID, err := lookupAllChannelID(ctx, tx, workspaceID)
				if err != nil {
					return err
				}
				return installer.store.ensureChannelMembership(ctx, tx, allChannelID, memberID, now)
			},
			DeleteWorkspaceState: func(ctx context.Context, tx coredb.Tx, existing serviceactor.Installation, now time.Time) error {
				seed, err := installation.LoadDefinitionSeed(ctx, tx, existing.ServiceDefinitionID)
				if err != nil {
					return err
				}
				member, err := installer.store.serviceActorStore().InTx(tx).LoadMemberByInstallation(ctx, existing.ID)
				if err != nil {
					return err
				}
				workspaceOwnedReplacement := strings.TrimSpace(existing.OwnershipKind) != serviceactor.OwnershipKindPersonal
				return installer.store.deleteAppInstallationWorkspaceState(ctx, tx, model.StoredAppInstallation{
					ID:                existing.ID,
					AppID:             existing.ServiceDefinitionID,
					AppSlug:           seed.Slug,
					AppName:           seed.Name,
					WorkspaceID:       existing.WorkspaceID,
					InstanceKey:       existing.InstanceKey,
					MemberID:          member.MemberID,
					Handle:            member.Handle,
					MemberDisplayName: member.DisplayName,
					InstalledVersion:  existing.InstalledVersion,
					Config:            existing.Config,
					Enabled:           false,
					CreatedAt:         existing.CreatedAt,
					UpdatedAt:         now,
				}, workspaceOwnedReplacement, now)
			},
			Reconcile: installer.store.RunWorkspaceAppCatalogReconcileHook,
		},
		now,
	)
}

func normalizeAppInstallationInstanceIdentity(
	identity appInstallationInstanceIdentity,
	now time.Time,
) (appInstallationInstanceIdentity, time.Time, error) {
	if now.IsZero() {
		now = time.Now().UTC()
	} else {
		now = now.UTC()
	}
	identity.InstanceKey = strings.TrimSpace(identity.InstanceKey)
	identity.Handle = strings.TrimSpace(identity.Handle)
	identity.DisplayName = strings.TrimSpace(identity.DisplayName)
	if identity.InstanceKey == "" {
		return identity, now,
			badRequest("App installation instance key is required.")
	}
	if identity.Handle == "" {
		return identity, now,
			badRequest("App installation handle is required.")
	}
	if identity.DisplayName == "" {
		return identity, now,
			badRequest("App installation display name is required.")
	}
	return identity, now, nil
}

func (store *Store) UninstallAppInstallationForUser(ctx context.Context, userID string, workspaceID string, installationID string) (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")
	}
	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
	}
	if summary.Installation.OwnershipKind == serviceactor.OwnershipKindPersonal {
		return store.uninstallPersonalInstallationForUser(ctx, userID, workspaceID, workspaceMember, summary)
	}

	var result appInstallationUninstallResult
	for {
		settlementOnly := false
		cleanupCtx, avatarCleanup := withAppAvatarMediaCleanup(ctx)
		if err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
			// Resolve the immutable definition id without taking the installation row
			// lock. Every mutation first takes the shared definition lock, so request
			// admission, decisions, direct provider writes, and uninstall all use
			// definition -> narrower rows in the same order.
			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
	`, trimmedInstallationID, strings.TrimSpace(workspaceID), serviceactor.KindApp).Scan(&definitionID); err != nil {
				if coredb.IsNotFound(err) {
					return ErrNotFound
				}
				return fmt.Errorf("load App definition for uninstall: %w", err)
			}
			if strings.TrimSpace(definitionID) != strings.TrimSpace(summary.Definition.ID) {
				return conflict("App installation changed before uninstall.")
			}
			pending, err := settlePrivilegedPendingAppInstallationRequestsInTx(cleanupCtx, tx, workspaceID, definitionID, time.Now().UTC())
			if err != nil {
				return err
			}
			if isGitHubApp(definitionID) {
				if err := deleteExpiredGitHubSetupVerificationsInTx(
					ctx, tx, workspaceID, time.Now().UTC().Add(-githubSetupVerificationTTL),
				); err != nil {
					return err
				}
				if err := requireNoGitHubSetupVerificationInTx(ctx, tx, workspaceID); err != nil {
					return err
				}
			}
			if pending.SettledCount > 0 {
				settlementOnly = true
				return nil
			}
			currentWorkspaceMember, err := store.revalidateInstallationManagementForUpdate(
				ctx,
				tx,
				userID,
				workspaceID,
				workspaceMember.Member.ID,
				trimmedInstallationID,
				definitionID,
			)
			if err != nil {
				return err
			}
			var uninstallErr error
			result, uninstallErr = store.uninstallAppInstallationInTx(
				cleanupCtx,
				tx,
				workspaceID,
				trimmedInstallationID,
				definitionID,
				userID,
				currentWorkspaceMember.Member.ID,
				nil,
			)
			return uninstallErr
		}); err != nil {
			return AppInstallationView{}, err
		}
		store.finalizeAppAvatarMediaCleanup(ctx, avatarCleanup)
		if settlementOnly {
			continue
		}
		break
	}
	store.finalizeAppInstallationUninstall(ctx, result)
	return result.view, nil
}

func blockPullRequestSubscriptionsBeforeAppInstallationDelete(ctx context.Context, tx coredb.Tx, workspaceID string, installationID string, appSlug string, now time.Time) error {
	if !isPullRequestSubscriptionProviderSlug(appSlug) {
		return nil
	}
	provider := normalizePullRequestSubscriptionProvider(appSlug)
	serviceDefinitionID, err := pullRequestSubscriptionProviderAppID(provider)
	if err != nil {
		return err
	}
	// Blocking is installation-scoped: sibling installations of the same
	// provider App (workspace or other members' personal installs) keep
	// their subscriptions.
	subscriptions, err := loadServiceEventSubscriptionsForInstallationBlock(ctx, tx, strings.TrimSpace(workspaceID), serviceDefinitionID, strings.TrimSpace(installationID))
	if err != nil {
		return err
	}
	for _, subscription := range subscriptions {
		metadata := decodeServiceEventSubscriptionJSONMap(subscription.Metadata)
		connectionRef := serviceEventSubscriptionStoredConnectionRef(subscription, metadata)
		if _, err := mutateServiceEventSubscriptionConnectionBlockRow(ctx, tx, subscription, metadata, true, now, connectionRef); err != nil {
			return err
		}
	}
	return nil
}

func (store *Store) deleteAppInstallationWorkspaceState(ctx context.Context, tx coredb.Tx, storedInstallation model.StoredAppInstallation, workspaceOwned bool, now time.Time) error {
	if err := store.RunWorkspaceAppCatalogReconcileHook(ctx, tx, storedInstallation.WorkspaceID, now); err != nil {
		return err
	}
	// Workspace-wide provider state belongs to the workspace installation:
	// personal installations never delete shared credentials or the GitHub
	// workspace claim, which sibling installations still depend on.
	if workspaceOwned {
		if strings.TrimSpace(storedInstallation.AppID) == giteaAppID {
			if _, err := tx.Exec(ctx, `DELETE FROM service_provider_configs WHERE workspace_id = $1 AND provider_kind = 'gitea'`, strings.TrimSpace(storedInstallation.WorkspaceID)); err != nil {
				return fmt.Errorf("delete Gitea credentials with App installation: %w", err)
			}
		}
		if strings.TrimSpace(storedInstallation.AppID) == registry.GitHubAppID {
			if _, err := tx.Exec(ctx, `DELETE FROM service_provider_configs WHERE workspace_id = $1 AND provider_kind = 'github'`, strings.TrimSpace(storedInstallation.WorkspaceID)); err != nil {
				return fmt.Errorf("delete GitHub credentials with App installation: %w", err)
			}
			if err := releaseActiveGitHubAppClaimForWorkspaceInTx(ctx, tx, storedInstallation.WorkspaceID); err != nil {
				return err
			}
		}
	}
	if err := store.markMemberScopeProjectionsRemoved(ctx, tx, storedInstallation.MemberID, now); err != nil {
		return err
	}
	if err := markAppCollaboratorMetadataLeft(ctx, tx, storedInstallation.MemberID, now); err != nil {
		return err
	}
	if _, err := tx.Exec(ctx, `
UPDATE members
SET status = 'inactive',
    updated_at = $3
WHERE workspace_id = $1
  AND id = $2
  AND type = 'service'
`, storedInstallation.WorkspaceID, storedInstallation.MemberID, now); err != nil {
		return fmt.Errorf("deactivate app service member: %w", err)
	}
	return nil
}

func markAppCollaboratorMetadataLeft(ctx context.Context, execer coredb.QueryExecer, memberID string, leftAt time.Time) error {
	trimmedMemberID := strings.TrimSpace(memberID)
	if trimmedMemberID == "" {
		return nil
	}
	now := leftAt.UTC()
	if now.IsZero() {
		now = time.Now().UTC()
	}
	if _, err := execer.Exec(ctx, `
UPDATE task_scope_participant_metadata
SET left_at = $2,
    active_key = id,
    updated_at = $2
WHERE member_id = $1
  AND removed_at IS NULL
  AND left_at IS NULL
`, trimmedMemberID, now); err != nil {
		return fmt.Errorf("mark app collaborator metadata left: %w", err)
	}
	return nil
}

func (store *Store) appInstallationViewByID(ctx context.Context, workspaceID string, installationID string) (AppInstallationView, error) {
	return store.installationFromServiceActor(ctx, workspaceID, strings.TrimSpace(installationID))
}

// AppInstallationViewByID exposes the app-domain installation view loader for
// integration tests and narrow provider adapters.
func (store *Store) AppInstallationViewByID(ctx context.Context, workspaceID string, installationID string) (AppInstallationView, error) {
	return store.appInstallationViewByID(ctx, workspaceID, installationID)
}

func lookupAllChannelID(ctx context.Context, queryer coredb.QueryRower, workspaceID string) (string, error) {
	row := queryer.QueryRow(ctx, `
SELECT p.scope_id
FROM scope_channel_profiles p
JOIN scopes s ON s.id = p.scope_id
WHERE p.workspace_id = $1
  AND p.is_all_channel = TRUE
  AND s.type = 'channel'
  AND s.status = 'active'
  AND s.archived_at IS NULL
LIMIT 1
`, strings.TrimSpace(workspaceID))

	var channelID string
	if err := row.Scan(&channelID); err != nil {
		if coredb.IsNotFound(err) {
			return "", ErrNotFound
		}
		return "", fmt.Errorf("load #all channel: %w", err)
	}
	return channelID, nil
}

func (store *Store) ensureChannelMembership(ctx context.Context, tx coredb.Tx, channelID string, memberID string, createdAt time.Time) error {
	if err := store.ensureChannelScopeParticipantProjection(ctx, tx, channelID, memberID, "no-wake", createdAt); err != nil {
		return err
	}
	return nil
}

func syncServiceMemberProfile(ctx context.Context, tx coredb.Tx, workspaceID string, memberID string, displayName string, updatedAt time.Time) error {
	trimmedDisplayName := strings.TrimSpace(displayName)
	pinyinFull, pinyinInitials := pinyinsearch.Variants(trimmedDisplayName)
	result, err := tx.Exec(ctx, `
UPDATE members
SET display_name = $4,
    display_name_pinyin = $5,
    display_name_pinyin_initials = $6,
    status = 'active',
    updated_at = $3
WHERE workspace_id = $1
  AND id = $2
  AND type = 'service'
`, strings.TrimSpace(workspaceID), strings.TrimSpace(memberID), updatedAt, trimmedDisplayName, pinyinFull, pinyinInitials)
	if err != nil {
		return fmt.Errorf("update app service member: %w", err)
	}
	if result.RowsAffected() != 0 {
		return nil
	}

	var exists int
	row := tx.QueryRow(ctx, `
SELECT 1
FROM members
WHERE workspace_id = $1
  AND id = $2
  AND type = 'service'
LIMIT 1
`, strings.TrimSpace(workspaceID), strings.TrimSpace(memberID))
	if err := row.Scan(&exists); err != nil {
		if coredb.IsNotFound(err) {
			return ErrNotFound
		}
		return fmt.Errorf("verify app service member: %w", err)
	}
	return nil
}
