package app

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	"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"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
)

// Personal App installations, the unified installation ACL, and the workspace
// share approval flow. Personal installations live in the reserved
// "personal:<member>" instance-key namespace: one per member per App per
// workspace, owner-managed, and invisible to other members until the owner
// grants access or an admin approves a workspace share.

// requireInstallationManagement loads the installation and requires manage
// authority over it: the personal owner, a manage grant on a personal
// installation, or (for workspace installations) the legacy admin path.
func (store *Store) requireInstallationManagement(ctx context.Context, userID string, workspaceID string, installationID string) (access.HumanWorkspaceMember, serviceactor.InstallationSummary, error) {
	workspaceMember, err := store.requireWorkspaceAccess(ctx, userID, workspaceID)
	if err != nil {
		return access.HumanWorkspaceMember{}, serviceactor.InstallationSummary{}, err
	}
	installationID = strings.TrimSpace(installationID)
	if installationID == "" {
		return access.HumanWorkspaceMember{}, serviceactor.InstallationSummary{}, badRequest("installationId is required")
	}
	summary, err := store.serviceActorStore().LoadInstallationSummaryByKind(ctx, workspaceID, installationID, serviceactor.KindApp)
	if err != nil {
		return access.HumanWorkspaceMember{}, serviceactor.InstallationSummary{}, err
	}
	if summary.Installation.Status != serviceactor.StatusActive {
		return access.HumanWorkspaceMember{}, serviceactor.InstallationSummary{}, ErrNotFound
	}
	if err := store.authorizeInstallationManagementFromSummary(ctx, store.db, workspaceID, workspaceMember, summary); err != nil {
		return access.HumanWorkspaceMember{}, serviceactor.InstallationSummary{}, err
	}
	return workspaceMember, summary, nil
}

// authorizeInstallationManagementFromSummary keeps the legacy admin path for
// workspace installations and routes personal installations through the owner
// or an explicit manage grant. Callers must have validated the summary is an
// active App installation of this workspace.
func (store *Store) authorizeInstallationManagementFromSummary(
	ctx context.Context,
	queryer coredb.QueryRower,
	workspaceID string,
	workspaceMember access.HumanWorkspaceMember,
	summary serviceactor.InstallationSummary,
) error {
	if summary.Installation.OwnershipKind != serviceactor.OwnershipKindPersonal {
		return store.authorizeAppInstallationManagement(ctx, queryer, workspaceID, workspaceMember, summary.Definition.ID)
	}
	memberID := strings.TrimSpace(workspaceMember.Member.ID)
	if summary.Installation.OwnerMemberID == memberID {
		return nil
	}
	accessRole, granted, err := store.serviceActorStore().ResolveHumanInstallationAccess(ctx, workspaceID, memberID, summary.Installation.ID)
	if err != nil {
		return err
	}
	if !granted || accessRole != serviceactor.AccessRoleManage {
		return ErrNotFound
	}
	return nil
}

// revalidateInstallationManagementForUpdate is the transaction-safe form of
// authorizeInstallationManagementFromSummary. Personal installations
// re-verify ownership or an explicit manage grant under the caller's locks;
// workspace installations keep the legacy admin revalidation.
func (store *Store) revalidateInstallationManagementForUpdate(
	ctx context.Context,
	queryer coredb.QueryExecer,
	userID string,
	workspaceID string,
	expectedMemberID string,
	installationID 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.")
	}
	txStore := serviceactor.TxStoreFrom(queryer)
	ownershipKind, err := txStore.RequireInstallationManageInTx(ctx, workspaceID, installationID, currentMember.Member.ID)
	if err != nil {
		if errors.Is(err, serviceactor.ErrNotFound) {
			return access.HumanWorkspaceMember{}, ErrNotFound
		}
		return access.HumanWorkspaceMember{}, err
	}
	if ownershipKind != serviceactor.OwnershipKindPersonal {
		return store.revalidateAppInstallationManagementForUpdate(ctx, queryer, userID, workspaceID, expectedMemberID, serviceDefinitionID)
	}
	return currentMember, nil
}

// requireInstallationManageIfPersonal enforces installation manage authority
// only when the installation is personal. Workspace installations keep their
// existing authorization contract at the caller's surface.
func (store *Store) requireInstallationManageIfPersonal(ctx context.Context, userID string, workspaceID string, installationID string) error {
	installationID = strings.TrimSpace(installationID)
	if installationID == "" {
		return badRequest("installationId is required")
	}
	summary, err := store.serviceActorStore().LoadInstallationSummaryByKind(ctx, strings.TrimSpace(workspaceID), installationID, serviceactor.KindApp)
	if err != nil {
		return err
	}
	if summary.Installation.OwnershipKind != serviceactor.OwnershipKindPersonal {
		return nil
	}
	workspaceMember, err := store.requireWorkspaceAccess(ctx, userID, workspaceID)
	if err != nil {
		return err
	}
	if summary.Installation.OwnerMemberID == strings.TrimSpace(workspaceMember.Member.ID) {
		return nil
	}
	accessRole, granted, err := store.serviceActorStore().ResolveHumanInstallationAccess(
		ctx, strings.TrimSpace(workspaceID), strings.TrimSpace(workspaceMember.Member.ID), installationID,
	)
	if err != nil {
		return err
	}
	if !granted || accessRole != serviceactor.AccessRoleManage {
		return ErrNotFound
	}
	return nil
}

// filterPersonalInstallationsForViewer hides other members' personal
// installations from workspace-wide listings: an installation stays visible
// only to its owner or to holders of an active human/workspace grant.
func (store *Store) filterPersonalInstallationsForViewer(
	ctx context.Context,
	workspaceID string,
	viewerMemberID string,
	installations []AppInstallationView,
) ([]AppInstallationView, error) {
	personalIDs := make([]string, 0, len(installations))
	for _, installation := range installations {
		if installation.OwnershipKind == serviceactor.OwnershipKindPersonal &&
			installation.OwnerMemberID != viewerMemberID {
			personalIDs = append(personalIDs, installation.ID)
		}
	}
	if len(personalIDs) == 0 {
		return installations, nil
	}
	accessible, err := store.serviceActorStore().ListAccessiblePersonalInstallations(ctx, workspaceID, viewerMemberID, personalIDs)
	if err != nil {
		return nil, err
	}
	filtered := make([]AppInstallationView, 0, len(installations))
	for _, installation := range installations {
		if installation.OwnershipKind == serviceactor.OwnershipKindPersonal &&
			installation.OwnerMemberID != viewerMemberID {
			if _, ok := accessible[installation.ID]; !ok {
				continue
			}
		}
		filtered = append(filtered, installation)
	}
	return filtered, nil
}

// uninstallPersonalInstallationsForOffboardedMemberInTx removes the personal
// App installations owned by a member being offboarded. Locking follows the
// canonical definition-first order: for each affected App definition the
// mutation-scope lock is taken before any installation row is locked, and the
// owner's installations are re-verified under that lock so a concurrently
// created personal install cannot survive the removal.
func (store *Store) uninstallPersonalInstallationsForOffboardedMemberInTx(
	ctx context.Context,
	tx coredb.Tx,
	workspaceID string,
	offboardedUserID string,
	actedByUserID string,
) error {
	workspaceID = strings.TrimSpace(workspaceID)

	// Definition discovery without locks: the definition lock below fences
	// concurrent install creation, and each installation is re-verified
	// under its lock before removal.
	definitionRows, err := tx.Query(ctx, `
SELECT DISTINCT si.service_definition_id
FROM service_installations si
JOIN members owner ON owner.id = si.owner_member_id
WHERE si.workspace_id = $1
  AND si.service_kind = 'app'
  AND si.ownership_kind = 'personal'
  AND si.status = 'active'
  AND owner.workspace_id = si.workspace_id
  AND owner.user_id = $2
ORDER BY si.service_definition_id ASC
`, workspaceID, strings.TrimSpace(offboardedUserID))
	if err != nil {
		return fmt.Errorf("load offboarded member personal App definitions: %w", err)
	}
	definitionIDs := make([]string, 0)
	for definitionRows.Next() {
		var definitionID string
		if err := definitionRows.Scan(&definitionID); err != nil {
			definitionRows.Close()
			return fmt.Errorf("scan offboarded member personal App definition: %w", err)
		}
		definitionIDs = append(definitionIDs, strings.TrimSpace(definitionID))
	}
	if err := definitionRows.Err(); err != nil {
		definitionRows.Close()
		return fmt.Errorf("iterate offboarded member personal App definitions: %w", err)
	}
	definitionRows.Close()

	if len(definitionIDs) == 0 {
		return nil
	}
	actorMemberID, err := store.serviceActorWriteMemberID(ctx, tx, workspaceID, strings.TrimSpace(actedByUserID), "")
	if err != nil {
		return err
	}
	// Lock every affected definition in one sorted batch, then load the
	// owner's personal installations for all of them in one query.
	if err := installation.LockAppDefinitionScopeBatch(ctx, tx, definitionIDs); err != nil {
		return err
	}
	definitionPlaceholders := make([]string, 0, len(definitionIDs))
	queryArgs := make([]any, 0, len(definitionIDs)+2)
	queryArgs = append(queryArgs, workspaceID, strings.TrimSpace(offboardedUserID))
	for _, definitionID := range definitionIDs {
		queryArgs = append(queryArgs, definitionID)
		definitionPlaceholders = append(definitionPlaceholders, fmt.Sprintf("$%d", len(queryArgs)))
	}
	allRows, err := tx.Query(ctx, fmt.Sprintf(`
SELECT si.service_definition_id, si.id
FROM service_installations si
JOIN members owner ON owner.id = si.owner_member_id
WHERE si.workspace_id = $1
  AND si.service_kind = 'app'
  AND si.ownership_kind = 'personal'
  AND si.status = 'active'
  AND owner.workspace_id = si.workspace_id
  AND owner.user_id = $2
  AND si.service_definition_id IN (%s)
ORDER BY si.service_definition_id ASC, si.id ASC
FOR UPDATE
`, strings.Join(definitionPlaceholders, ", ")), queryArgs...)
	if err != nil {
		return fmt.Errorf("lock offboarded member personal installations: %w", err)
	}
	installationsByDefinition := make(map[string][]string)
	for allRows.Next() {
		var definitionID, installationID string
		if err := allRows.Scan(&definitionID, &installationID); err != nil {
			allRows.Close()
			return fmt.Errorf("scan offboarded member personal installation: %w", err)
		}
		installationsByDefinition[strings.TrimSpace(definitionID)] = append(installationsByDefinition[strings.TrimSpace(definitionID)], strings.TrimSpace(installationID))
	}
	if err := allRows.Err(); err != nil {
		allRows.Close()
		return fmt.Errorf("iterate offboarded member personal installations: %w", err)
	}
	allRows.Close()

	for _, definitionID := range definitionIDs {
		for _, installationID := range installationsByDefinition[definitionID] {
			if _, err := store.uninstallPersonalInstallationInTx(
				ctx,
				tx,
				workspaceID,
				installationID,
				definitionID,
				strings.TrimSpace(actedByUserID),
				actorMemberID,
			); err != nil {
				return fmt.Errorf("uninstall offboarded member personal installation %s: %w", installationID, err)
			}
		}
	}
	return nil
}

// uninstallPersonalInstallationInTx removes exactly one personal
// installation. Unlike the workspace uninstall flow it never settles the
// definition-wide privileged request ledger and never deletes the App
// Definition: sibling installations of the same App keep running. The caller
// holds the App-definition mutation-scope lock.
func (store *Store) uninstallPersonalInstallationInTx(
	ctx context.Context,
	tx coredb.Tx,
	workspaceID string,
	installationID string,
	definitionID string,
	actedByUserID string,
	actedByMemberID string,
) (AppInstallationView, error) {
	txStore := store.serviceActorStore().InTx(tx)
	installation, err := txStore.LoadInstallationForUpdate(ctx, strings.TrimSpace(installationID))
	if err != nil {
		return AppInstallationView{}, err
	}
	if strings.TrimSpace(installation.WorkspaceID) != strings.TrimSpace(workspaceID) ||
		strings.TrimSpace(installation.ServiceKind) != serviceactor.KindApp ||
		strings.TrimSpace(installation.ServiceDefinitionID) != strings.TrimSpace(definitionID) ||
		strings.TrimSpace(installation.Status) != serviceactor.StatusActive ||
		strings.TrimSpace(installation.OwnershipKind) != serviceactor.OwnershipKindPersonal {
		return AppInstallationView{}, ErrNotFound
	}
	now := time.Now().UTC()
	if err := store.serviceActorStore().InTx(tx).RejectPendingShareRequestsForInstallation(ctx, workspaceID, installation.ID, "installation_removed", now); err != nil {
		return AppInstallationView{}, err
	}
	member, err := txStore.LoadMemberByInstallation(ctx, installation.ID)
	if err != nil {
		return AppInstallationView{}, err
	}
	seed, err := loadAppServiceDefinitionSeed(ctx, tx, installation.ServiceDefinitionID)
	if err != nil {
		return AppInstallationView{}, err
	}
	storedInstallation := model.StoredAppInstallation{
		ID:                installation.ID,
		AppID:             installation.ServiceDefinitionID,
		AppSlug:           seed.Slug,
		AppName:           seed.Name,
		WorkspaceID:       installation.WorkspaceID,
		InstanceKey:       installation.InstanceKey,
		MemberID:          member.MemberID,
		Handle:            member.Handle,
		MemberDisplayName: member.DisplayName,
		InstalledVersion:  installation.InstalledVersion,
		Config:            installation.Config,
		Enabled:           false,
		CreatedByUserID:   strings.TrimSpace(actedByUserID),
		CreatedAt:         installation.CreatedAt,
		UpdatedAt:         now,
	}
	// The destructive tail is the single shared core; personal installs
	// never touch workspace-owned provider state.
	if err := store.deleteInstallationDestructiveStateInTx(ctx, tx, storedInstallation, false, actedByMemberID, now); err != nil {
		return AppInstallationView{}, err
	}
	return AppInstallationView{
		ID:               storedInstallation.ID,
		AppID:            storedInstallation.AppID,
		AppSlug:          storedInstallation.AppSlug,
		AppName:          storedInstallation.AppName,
		InstanceKey:      storedInstallation.InstanceKey,
		OwnershipKind:    installation.OwnershipKind,
		OwnerMemberID:    installation.OwnerMemberID,
		MemberID:         storedInstallation.MemberID,
		Handle:           storedInstallation.Handle,
		InstalledVersion: storedInstallation.InstalledVersion,
		Config:           decodeJSONObjectMap(storedInstallation.Config),
		Enabled:          false,
		CreatedAt:        storedInstallation.CreatedAt,
		UpdatedAt:        storedInstallation.UpdatedAt,
	}, nil
}

// uninstallPersonalInstallationForUser removes the caller's (or a manage
// grantee's) personal installation: definition-scope lock, transaction
// authority revalidation, then the installation-scoped uninstall.
func (store *Store) uninstallPersonalInstallationForUser(
	ctx context.Context,
	userID string,
	workspaceID string,
	workspaceMember access.HumanWorkspaceMember,
	summary serviceactor.InstallationSummary,
) (AppInstallationView, error) {
	cleanupCtx, avatarCleanup := withAppAvatarMediaCleanup(ctx)
	var view AppInstallationView
	err := coredb.WithTx(cleanupCtx, store.db, func(tx coredb.Tx) error {
		// cleanupCtx flows through the whole deletion chain so the avatar
		// collector records every external object the definition drop frees.
		if err := installation.LockAppInstallationMutationScope(cleanupCtx, tx, summary.Definition.ID); err != nil {
			return err
		}
		currentMember, err := store.revalidateInstallationManagementForUpdate(
			cleanupCtx,
			tx,
			userID,
			workspaceID,
			workspaceMember.Member.ID,
			summary.Installation.ID,
			summary.Definition.ID,
		)
		if err != nil {
			return err
		}
		view, err = store.uninstallPersonalInstallationInTx(
			cleanupCtx,
			tx,
			workspaceID,
			summary.Installation.ID,
			summary.Definition.ID,
			userID,
			currentMember.Member.ID,
		)
		return err
	})
	if err != nil {
		return AppInstallationView{}, err
	}
	// Dropping the last installation deletes Definition-owned avatar media:
	// finalize the external object cleanup only after the commit.
	store.finalizeAppAvatarMediaCleanup(ctx, avatarCleanup)
	return view, nil
}

// requireInstallationUseForUser enforces the unified human access boundary on
// one installation: workspace installations keep their legacy visibility,
// personal installations are usable only by the owner or holders of an
// active human/workspace grant.
func (store *Store) requireInstallationUseForUser(ctx context.Context, workspaceMember access.HumanWorkspaceMember, workspaceID string, installationID string) error {
	installationID = strings.TrimSpace(installationID)
	if installationID == "" {
		return badRequest("installationId is required")
	}
	installation, err := store.serviceActorStore().LoadInstallationSummaryByKind(ctx, workspaceID, installationID, serviceactor.KindApp)
	if err != nil {
		return err
	}
	if strings.TrimSpace(installation.Installation.Status) != serviceactor.StatusActive {
		return ErrNotFound
	}
	if strings.TrimSpace(installation.Installation.OwnershipKind) != serviceactor.OwnershipKindPersonal {
		return nil
	}
	memberID := strings.TrimSpace(workspaceMember.Member.ID)
	if strings.TrimSpace(installation.Installation.OwnerMemberID) == memberID {
		return nil
	}
	_, granted, err := store.serviceActorStore().ResolveHumanInstallationAccess(ctx, workspaceID, memberID, installationID)
	if err != nil {
		return err
	}
	if !granted {
		return ErrNotFound
	}
	return nil
}
