package app

import (
	"context"
	"database/sql"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/app/registry"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

const (
	AppMentionScopeRead            = "app.mentions.read"
	AppNotificationScopeWrite      = "app.notifications.write"
	AppNotificationActionScopeRead = "app.notification_actions.read"
	AppRuntimeScopeMessageRead     = "messages.read"
	AppRuntimeScopeMessageWrite    = "messages.write"
	AppRuntimeScopeThreadRead      = "threads.read"

	AppMentionExternalResourceType = "agentteam_message_mention"
	AppMentionDeliveryPolicy       = "app_runtime_delivery"
	AppMentionServiceConnectionRef = "app_runtime"
	AppMentionCreatedViaChannel    = "channel_membership"
	AppMentionCreatedViaTarget     = "target_membership"
	AppMentionCreatedViaThread     = "thread_direct_mention"
	AppMentionRemovedViaChannel    = "channel_membership_removed"
)

// AppTargetBindingResult summarizes the canonical target membership, binding,
// and subscription created for a App service member in a AgentTeam target.
type AppTargetBindingResult struct {
	Target       ServiceEventTarget
	BindingID    string
	Subscription serviceactor.EventSubscription
}

// BindAppNotificationBuiltinTeamChannel gives a registered built-in App the
// narrow scope binding required to publish an interactive notification into
// one active Team-owned channel. Callers cannot use this boundary to bind an
// arbitrary scope: the channel is resolved exclusively through the Team row,
// and the built-in registration, pinned manifest, grants, installation, and
// service member are all revalidated in the binding transaction.
func (store *Store) BindAppNotificationBuiltinTeamChannel(
	ctx context.Context,
	identityID string,
	workspaceID string,
	teamID string,
) (AppTargetBindingResult, error) {
	principal, err := store.AppNotificationBuiltinPrincipal(ctx, workspaceID, identityID)
	if err != nil {
		return AppTargetBindingResult{}, err
	}
	if _, allowed := principal.service.AllowedTargetKinds[AppNotificationTargetScope]; !allowed {
		return AppTargetBindingResult{}, forbidden("built-in App is not allowed to target bound scopes")
	}
	workspaceID = strings.TrimSpace(workspaceID)
	teamID = strings.TrimSpace(teamID)
	if workspaceID == "" || teamID == "" {
		return AppTargetBindingResult{}, badRequest("workspaceId and teamId are required")
	}

	var result AppTargetBindingResult
	err = coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		actors := store.serviceActorStore().InTx(tx)
		installation, err := actors.LoadInstallationForUpdate(ctx, principal.ServiceInstallationID)
		if err != nil {
			return err
		}
		if installation.WorkspaceID != workspaceID ||
			installation.Status != serviceactor.StatusActive ||
			installation.ServiceKind != serviceactor.KindApp {
			return storeerr.ErrNotFound
		}
		capability, err := requirePinnedRuntimeCapability(installation)
		if err != nil {
			return err
		}
		if !capability.Notifications.Supported ||
			!stringSliceContains(capability.Notifications.Targets, registry.AppNotificationTargetBoundScope) {
			return forbidden("built-in App manifest does not allow bound scope notifications")
		}
		if err := requireAppInstallationScopes(ctx, tx, installation.ID, []string{
			AppRuntimeScopeMessageWrite,
			AppNotificationScopeWrite,
			AppNotificationActionScopeRead,
		}); err != nil {
			return err
		}
		serviceMember, err := actors.LoadMemberByInstallation(ctx, installation.ID)
		if err != nil {
			return err
		}
		if serviceMember.ID != principal.ServiceMemberID ||
			serviceMember.Status != serviceactor.StatusActive {
			return forbidden("built-in App service member is not active")
		}

		var channelID string
		var latestSeq, latestGlobalSeq int64
		if err := tx.QueryRow(ctx, `
SELECT team.channel_id, scope.latest_seq, scope.latest_global_seq
FROM workspace_teams team
JOIN scopes scope
  ON scope.workspace_id = team.workspace_id
 AND scope.id = team.channel_id
 AND scope.type = 'channel'
 AND scope.status = 'active'
JOIN scope_channel_profiles profile
  ON profile.workspace_id = team.workspace_id
 AND profile.scope_id = team.channel_id
 AND profile.subsystem = 'team'
WHERE team.workspace_id = $1
  AND team.id = $2
  AND team.status = 'active'
  AND team.channel_id IS NOT NULL
  AND team.channel_id <> ''
LIMIT 1
FOR UPDATE
`, workspaceID, teamID).Scan(&channelID, &latestSeq, &latestGlobalSeq); err != nil {
			if coredb.IsNotFound(err) {
				return storeerr.NotFound("active Team channel not found")
			}
			return fmt.Errorf("resolve built-in App Team channel: %w", err)
		}
		channelID = strings.TrimSpace(channelID)
		now := time.Now().UTC()
		if err := message.EnsureScopeMembership(
			ctx, tx, channelID, serviceMember.MemberID, "public", "", "no-wake",
			latestSeq, latestGlobalSeq, now,
		); err != nil {
			return fmt.Errorf("ensure built-in App Team channel membership: %w", err)
		}
		target, err := ResolveServiceEventTargetForScope(
			ctx, tx, workspaceID, serviceMember.MemberID, channelID,
		)
		if err != nil {
			return err
		}
		binding, err := store.upsertAppNotificationTargetBindingInTx(
			ctx,
			tx,
			installation,
			serviceMember,
			target,
			AppNotificationExecutionActor{
				Type: appNotificationExecutionActorInternalService,
				ID:   strings.TrimSpace(identityID),
			},
			now,
		)
		if err != nil {
			return err
		}
		result = AppTargetBindingResult{Target: target, BindingID: binding.ID}
		return nil
	})
	if err != nil {
		return AppTargetBindingResult{}, err
	}
	return result, nil
}

func (store *Store) bindAppInstallationTargetForUser(ctx context.Context, userID string, workspaceID string, installationID string, targetScopeID string, createdVia string) (AppTargetBindingResult, error) {
	workspaceMember, err := store.loadAuthorizedHumanWorkspaceMember(ctx, store.db, userID, workspaceID, workspaceAccessAction)
	if err != nil {
		return AppTargetBindingResult{}, err
	}
	var result AppTargetBindingResult
	if err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		var err error
		target, err := ResolveServiceEventTargetForScope(ctx, tx, workspaceID, workspaceMember.Member.ID, targetScopeID)
		if err != nil {
			return err
		}
		result, err = store.bindAppInstallationTargetInTx(ctx, tx, bindAppTargetParams{
			WorkspaceID:        workspaceID,
			ActorMemberID:      workspaceMember.Member.ID,
			ActorType:          "human",
			ActorID:            userID,
			InstallationID:     installationID,
			Target:             target,
			CreatedVia:         firstNonEmpty(strings.TrimSpace(createdVia), AppMentionCreatedViaTarget),
			EnsureMembership:   true,
			SubscriptionStatus: serviceactor.EventSubscriptionStatusActive,
			Now:                time.Now().UTC(),
		})
		return err
	}); err != nil {
		return AppTargetBindingResult{}, err
	}
	return result, nil
}

type bindAppTargetParams struct {
	WorkspaceID        string
	ActorMemberID      string
	ActorType          string
	ActorID            string
	InstallationID     string
	Target             ServiceEventTarget
	CreatedVia         string
	EnsureMembership   bool
	SubscriptionStatus string
	Now                time.Time
}

func (store *Store) bindAppInstallationTargetInTx(ctx context.Context, tx coredb.Tx, params bindAppTargetParams) (AppTargetBindingResult, error) {
	trimmedWorkspaceID := strings.TrimSpace(params.WorkspaceID)
	trimmedActorMemberID := strings.TrimSpace(params.ActorMemberID)
	trimmedInstallationID := strings.TrimSpace(params.InstallationID)
	if trimmedWorkspaceID == "" || trimmedActorMemberID == "" || trimmedInstallationID == "" {
		return AppTargetBindingResult{}, badRequest("workspaceId, actor member, and installationId are required")
	}
	if err := validateServiceEventTarget(params.Target); err != nil {
		return AppTargetBindingResult{}, err
	}
	now := params.Now.UTC()
	if now.IsZero() {
		now = time.Now().UTC()
	}
	txServiceActor := store.serviceActorStore().InTx(tx)
	installation, err := txServiceActor.LoadInstallationForUpdate(ctx, trimmedInstallationID)
	if err != nil {
		return AppTargetBindingResult{}, err
	}
	if strings.TrimSpace(installation.WorkspaceID) != trimmedWorkspaceID {
		return AppTargetBindingResult{}, storeerr.ErrNotFound
	}
	if strings.TrimSpace(installation.ServiceKind) != serviceactor.KindApp || strings.TrimSpace(installation.Status) != serviceactor.StatusActive {
		return AppTargetBindingResult{}, badRequest("App installation is not active")
	}
	if _, err := requirePinnedAppEvent(installation, registry.AppEventMessageMention); err != nil {
		return AppTargetBindingResult{}, err
	}
	if err := requireAppInstallationScopes(ctx, tx, installation.ID, []string{AppMentionScopeRead, AppRuntimeScopeMessageWrite}); err != nil {
		return AppTargetBindingResult{}, err
	}
	serviceMember, err := txServiceActor.LoadMemberByInstallation(ctx, installation.ID)
	if err != nil {
		return AppTargetBindingResult{}, err
	}
	if strings.TrimSpace(serviceMember.Status) != serviceactor.StatusActive {
		return AppTargetBindingResult{}, badRequest("App service member is not active")
	}
	if appMentionThreadMaterializationRequiresParentAuthority(params) {
		if err := lockAppMentionThreadMaterializationParentAuthority(
			ctx, tx, installation, serviceMember, params.Target,
		); err != nil {
			return AppTargetBindingResult{}, err
		}
	}
	if params.EnsureMembership {
		if err := message.EnsureScopeMembership(ctx, tx, params.Target.DeliveryScopeID, serviceMember.MemberID, "public", "", "no-wake", 0, 0, now); err != nil {
			return AppTargetBindingResult{}, fmt.Errorf("ensure App target scope membership: %w", err)
		}
	}
	scopeSnapshotJSON, err := marshalServiceEventSubscriptionJSONMap(params.Target.ScopeSnapshot)
	if err != nil {
		return AppTargetBindingResult{}, err
	}
	bindingMetadataJSON, err := model.MarshalJSONObject("app target binding metadata", map[string]any{
		"event_type":                  registry.AppEventMessageMention,
		"subscription_kind":           serviceactor.SubscriptionKindAppMention,
		"service_connection_ref":      AppMentionServiceConnectionRef,
		"visible_membership_scope_id": params.Target.DeliveryScopeID,
	})
	if err != nil {
		return AppTargetBindingResult{}, err
	}
	actor := ServiceEventActor{
		ActorType:        strings.TrimSpace(params.ActorType),
		ActorID:          strings.TrimSpace(params.ActorID),
		MemberID:         trimmedActorMemberID,
		AuthorizerUserID: trimmedActorMemberID,
	}
	binding, err := txServiceActor.UpsertTargetBinding(ctx, serviceactor.TargetBindingUpsert{
		WorkspaceID:              trimmedWorkspaceID,
		ServiceDefinitionID:      installation.ServiceDefinitionID,
		ServiceInstallationID:    installation.ID,
		ServiceMemberID:          serviceMember.ID,
		ServiceKind:              serviceactor.KindApp,
		TargetKind:               params.Target.TargetKind,
		TargetRef:                params.Target.TargetRef,
		DeliveryScopeID:          params.Target.DeliveryScopeID,
		PermissionCheckScopeID:   params.Target.PermissionCheckScopeID,
		AuthoritySubjectMemberID: serviceEventAuthoritySubjectMemberID(actor, params.Target),
		DeliveryContract:         serviceEventDeliveryContractJSON(actor, params.Target),
		CreatedByMemberID:        trimmedActorMemberID,
		CreatedByActorType:       firstNonEmpty(strings.TrimSpace(params.ActorType), "human"),
		CreatedByActorID:         strings.TrimSpace(params.ActorID),
		CreatedVia:               firstNonEmpty(strings.TrimSpace(params.CreatedVia), AppMentionCreatedViaTarget),
		ScopeSnapshot:            scopeSnapshotJSON,
		Metadata:                 bindingMetadataJSON,
		ActiveBindingKey:         appMentionTargetBindingKey(trimmedWorkspaceID, installation.ID, params.Target.TargetKind, params.Target.TargetRef),
		Now:                      now,
	})
	if err != nil {
		return AppTargetBindingResult{}, err
	}
	eventFiltersJSON, err := model.MarshalJSONObject("app mention subscription filters", map[string]any{
		"events": []string{registry.AppEventMessageMention},
	})
	if err != nil {
		return AppTargetBindingResult{}, err
	}
	subscriptionMetadataJSON, err := model.MarshalJSONObject("app mention subscription metadata", map[string]any{
		"event_type":             registry.AppEventMessageMention,
		"source":                 "direct_mention",
		"visible_membership":     true,
		"service_connection_ref": AppMentionServiceConnectionRef,
	})
	if err != nil {
		return AppTargetBindingResult{}, err
	}
	status := strings.TrimSpace(params.SubscriptionStatus)
	if status == "" {
		status = serviceactor.EventSubscriptionStatusActive
	}
	subscription, _, err := txServiceActor.UpsertEventSubscription(ctx, serviceactor.EventSubscriptionUpsert{
		WorkspaceID:            trimmedWorkspaceID,
		ServiceDefinitionID:    installation.ServiceDefinitionID,
		ServiceInstallationID:  installation.ID,
		ServiceMemberID:        serviceMember.ID,
		ServiceConnectionRef:   AppMentionServiceConnectionRef,
		ServiceTargetBindingID: binding.ID,
		ServiceKind:            serviceactor.KindApp,
		SubscriptionKind:       serviceactor.SubscriptionKindAppMention,
		TargetKind:             params.Target.TargetKind,
		TargetRef:              params.Target.TargetRef,
		DeliveryScopeID:        params.Target.DeliveryScopeID,
		PermissionCheckScopeID: params.Target.PermissionCheckScopeID,
		OwnerMemberID:          trimmedActorMemberID,
		AuthorizerMemberID:     trimmedActorMemberID,
		CreatedByActorType:     firstNonEmpty(strings.TrimSpace(params.ActorType), "human"),
		CreatedByActorID:       strings.TrimSpace(params.ActorID),
		CreatedVia:             firstNonEmpty(strings.TrimSpace(params.CreatedVia), AppMentionCreatedViaTarget),
		ExternalResourceType:   AppMentionExternalResourceType,
		ExternalResourceKey:    appMentionExternalResourceKey(params.Target),
		ExternalResourceRef:    registry.AppEventMessageMention,
		EventFilterKey:         registry.AppEventMessageMention,
		EventFilters:           eventFiltersJSON,
		DeliveryPolicy:         AppMentionDeliveryPolicy,
		NotifyPrincipal:        false,
		Status:                 status,
		ActiveSubscriptionKey:  appMentionSubscriptionKey(trimmedWorkspaceID, installation.ID, params.Target.TargetKind, params.Target.TargetRef),
		Metadata:               subscriptionMetadataJSON,
		Now:                    now,
	})
	if err != nil {
		return AppTargetBindingResult{}, err
	}
	return AppTargetBindingResult{Target: params.Target, BindingID: binding.ID, Subscription: subscription}, nil
}

func (store *Store) removeAppInstallationTargetInTx(ctx context.Context, tx coredb.Tx, workspaceID string, actorMemberID string, serviceMemberID string, target ServiceEventTarget, reason string, now time.Time) error {
	trimmedWorkspaceID := strings.TrimSpace(workspaceID)
	trimmedActorMemberID := strings.TrimSpace(actorMemberID)
	trimmedServiceMemberID := strings.TrimSpace(serviceMemberID)
	if trimmedWorkspaceID == "" || trimmedActorMemberID == "" || trimmedServiceMemberID == "" {
		return nil
	}
	if err := validateServiceEventTarget(target); err != nil {
		return err
	}
	if now.IsZero() {
		now = time.Now().UTC()
	} else {
		now = now.UTC()
	}
	installation, _, ok, err := loadActiveAppInstallationByServiceMemberID(ctx, tx, trimmedWorkspaceID, trimmedServiceMemberID)
	if err != nil || !ok {
		return err
	}
	return store.removeLockedAppInstallationTargetInTx(ctx, tx, trimmedWorkspaceID, trimmedActorMemberID, installation, target, reason, now)
}

func (store *Store) removeLockedAppInstallationTargetInTx(
	ctx context.Context,
	tx coredb.Tx,
	workspaceID string,
	actorMemberID string,
	installation serviceactor.Installation,
	target ServiceEventTarget,
	reason string,
	now time.Time,
) error {
	trimmedWorkspaceID := strings.TrimSpace(workspaceID)
	trimmedActorMemberID := strings.TrimSpace(actorMemberID)
	if trimmedWorkspaceID == "" || trimmedActorMemberID == "" ||
		strings.TrimSpace(installation.ID) == "" ||
		strings.TrimSpace(installation.WorkspaceID) != trimmedWorkspaceID ||
		strings.TrimSpace(installation.ServiceKind) != serviceactor.KindApp ||
		strings.TrimSpace(installation.Status) != serviceactor.StatusActive {
		return nil
	}
	if err := validateServiceEventTarget(target); err != nil {
		return err
	}
	if now.IsZero() {
		now = time.Now().UTC()
	} else {
		now = now.UTC()
	}
	txServiceActor := store.serviceActorStore().InTx(tx)
	subscriptions, err := txServiceActor.ListEventSubscriptionsForUpdate(ctx, serviceactor.EventSubscriptionListQuery{
		WorkspaceID:           trimmedWorkspaceID,
		ServiceInstallationID: installation.ID,
		SubscriptionKind:      serviceactor.SubscriptionKindAppMention,
		TargetKind:            target.TargetKind,
		TargetRef:             target.TargetRef,
		Statuses:              []string{serviceactor.EventSubscriptionStatusActive, serviceactor.EventSubscriptionStatusPaused, serviceactor.EventSubscriptionStatusBlocked},
	})
	if err != nil {
		return err
	}
	for _, subscription := range subscriptions {
		if _, err := txServiceActor.RemoveEventSubscription(ctx, trimmedWorkspaceID, subscription.ID, now); err != nil {
			return err
		}
	}
	_, _, err = txServiceActor.DisableTargetBinding(ctx, trimmedWorkspaceID, target.TargetKind, target.TargetRef, installation.ID, firstNonEmpty(strings.TrimSpace(reason), "target membership removed"), now)
	return err
}

func loadActiveAppServiceInstallationByServiceMemberID(
	ctx context.Context,
	queryer coredb.QueryRower,
	workspaceID string,
	serviceMemberID string,
	forUpdate bool,
) (serviceactor.Installation, serviceactor.Member, bool, bool, error) {
	query := `
SELECT installation.id, installation.workspace_id, installation.service_definition_id,
       installation.service_kind, installation.status, installation.installed_version,
       installation.config, installation.metadata, COALESCE(installation.created_by_member_id, ''),
       installation.created_at, installation.updated_at, installation.disabled_at,
       service_member.id, service_member.workspace_id, service_member.service_definition_id,
       service_member.service_installation_id, service_member.member_id, service_member.service_kind,
       service_member.handle, service_member.display_name, service_member.avatar_url,
       service_member.status, service_member.created_at, service_member.updated_at
FROM service_installations installation
JOIN service_members service_member
	ON service_member.workspace_id = installation.workspace_id
 AND service_member.service_definition_id = installation.service_definition_id
 AND service_member.service_installation_id = installation.id
 AND service_member.service_kind = installation.service_kind
 AND service_member.status = 'active'
JOIN members visible_member
  ON visible_member.id = service_member.member_id
 AND visible_member.workspace_id = installation.workspace_id
 AND visible_member.workspace_id = service_member.workspace_id
 AND visible_member.type = 'service'
 AND visible_member.status = 'active'
WHERE installation.workspace_id = $1
  AND installation.service_kind = $3
  AND installation.status = 'active'
  AND service_member.member_id = $2
LIMIT 1
	`
	if forUpdate {
		query += "FOR UPDATE\n"
	}
	row := queryer.QueryRow(ctx, query, strings.TrimSpace(workspaceID), strings.TrimSpace(serviceMemberID), serviceactor.KindApp)
	installation, member, err := scanAppInstallationMember(row)
	if err != nil {
		if coredb.IsNotFound(err) {
			return serviceactor.Installation{}, serviceactor.Member{}, false, false, nil
		}
		return serviceactor.Installation{}, serviceactor.Member{}, false, false, fmt.Errorf("load App service installation by member: %w", err)
	}
	capability, err := pinnedRuntimeCapability(installation)
	if err != nil {
		return serviceactor.Installation{}, serviceactor.Member{}, false, false, err
	}
	return installation, member, true, capability.Supported, nil
}

func loadActiveAppInstallationByServiceMemberID(ctx context.Context, queryer coredb.QueryRower, workspaceID string, serviceMemberID string) (serviceactor.Installation, serviceactor.Member, bool, error) {
	installation, member, found, isApp, err := loadActiveAppServiceInstallationByServiceMemberID(
		ctx,
		queryer,
		workspaceID,
		serviceMemberID,
		true,
	)
	if err != nil || !found || !isApp {
		return serviceactor.Installation{}, serviceactor.Member{}, false, err
	}
	return installation, member, true, nil
}

func requireAppInstallationScopes(ctx context.Context, queryer coredb.QueryRower, installationID string, required []string) error {
	for _, scope := range required {
		trimmedScope := strings.TrimSpace(scope)
		if trimmedScope == "" {
			continue
		}
		var count int
		if err := queryer.QueryRow(ctx, `
SELECT COUNT(*)
FROM service_grants
WHERE service_installation_id = $1
  AND scope = $2
  AND revoked_at IS NULL
  AND (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)
`, strings.TrimSpace(installationID), trimmedScope).Scan(&count); err != nil {
			return fmt.Errorf("check App service grant %s: %w", trimmedScope, err)
		}
		if count == 0 {
			return forbidden("App installation is missing required service grant")
		}
	}
	return nil
}

func appMentionTargetBindingKey(workspaceID string, installationID string, targetKind string, targetRef string) string {
	return pullRequestSubscriptionKey("app-target", workspaceID, installationID, targetKind, targetRef)
}

func appMentionSubscriptionKey(workspaceID string, installationID string, targetKind string, targetRef string) string {
	return pullRequestSubscriptionKey("app-mention", workspaceID, installationID, targetKind, targetRef)
}

func appMentionExternalResourceKey(target ServiceEventTarget) string {
	return strings.Join([]string{strings.TrimSpace(target.TargetKind), strings.TrimSpace(target.TargetRef), registry.AppEventMessageMention}, ":")
}

func scanAppInstallationMember(row coredb.Row) (serviceactor.Installation, serviceactor.Member, error) {
	var installation serviceactor.Installation
	var serviceMember serviceactor.Member
	var disabledAt sql.NullTime
	if err := row.Scan(
		&installation.ID,
		&installation.WorkspaceID,
		&installation.ServiceDefinitionID,
		&installation.ServiceKind,
		&installation.Status,
		&installation.InstalledVersion,
		&installation.Config,
		&installation.Metadata,
		&installation.CreatedByMemberID,
		&installation.CreatedAt,
		&installation.UpdatedAt,
		&disabledAt,
		&serviceMember.ID,
		&serviceMember.WorkspaceID,
		&serviceMember.ServiceDefinitionID,
		&serviceMember.ServiceInstallationID,
		&serviceMember.MemberID,
		&serviceMember.ServiceKind,
		&serviceMember.Handle,
		&serviceMember.DisplayName,
		&serviceMember.AvatarURL,
		&serviceMember.Status,
		&serviceMember.CreatedAt,
		&serviceMember.UpdatedAt,
	); err != nil {
		return serviceactor.Installation{}, serviceactor.Member{}, err
	}
	installation.ID = strings.TrimSpace(installation.ID)
	installation.WorkspaceID = strings.TrimSpace(installation.WorkspaceID)
	installation.ServiceDefinitionID = strings.TrimSpace(installation.ServiceDefinitionID)
	installation.ServiceKind = strings.TrimSpace(installation.ServiceKind)
	installation.Status = strings.TrimSpace(installation.Status)
	installation.DisabledAt = ptrFromSQLTime(disabledAt)
	serviceMember.ID = strings.TrimSpace(serviceMember.ID)
	serviceMember.WorkspaceID = strings.TrimSpace(serviceMember.WorkspaceID)
	serviceMember.ServiceDefinitionID = strings.TrimSpace(serviceMember.ServiceDefinitionID)
	serviceMember.ServiceInstallationID = strings.TrimSpace(serviceMember.ServiceInstallationID)
	serviceMember.MemberID = strings.TrimSpace(serviceMember.MemberID)
	serviceMember.ServiceKind = strings.TrimSpace(serviceMember.ServiceKind)
	serviceMember.Status = strings.TrimSpace(serviceMember.Status)
	return installation, serviceMember, nil
}
