package app

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

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/app/installation"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
)

// UpdateAppVisibilityForUser changes one active external App's workspace-local
// catalog audience. Management authorization remains independent: publishing
// a App lets ordinary workspace members discover and add it, but never grants
// configuration, credential, manager, or uninstall authority.
func (store *Store) UpdateAppVisibilityForUser(
	ctx context.Context,
	userID string,
	workspaceID string,
	installationID string,
	catalogVisibility string,
	now time.Time,
) (AppVisibilityUpdateResult, error) {
	targetVisibility, err := registry.NormalizeCatalogVisibility(catalogVisibility, false)
	if err != nil {
		return AppVisibilityUpdateResult{}, err
	}
	workspaceMember, summary, err := store.loadManagedWorkspaceApp(
		ctx, userID, workspaceID, installationID,
	)
	if err != nil {
		return AppVisibilityUpdateResult{}, err
	}
	if err := store.authorizeAppInstallationManagement(
		ctx, store.db, workspaceID, workspaceMember, summary.Definition.ID,
	); err != nil {
		return AppVisibilityUpdateResult{}, err
	}
	if now.IsZero() {
		now = time.Now().UTC()
	} else {
		now = now.UTC()
	}

	result := AppVisibilityUpdateResult{
		CatalogVisibility: targetVisibility,
		RemovedChannelIDs: []string{},
	}
	if err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		return applyAppVisibilityUpdateInTx(ctx, store, tx, appVisibilityMutation{
			UserID:                  userID,
			WorkspaceID:             workspaceID,
			InstallationID:          installationID,
			ExpectedActorMemberID:   workspaceMember.Member.ID,
			ExpectedSummary:         summary,
			TargetCatalogVisibility: targetVisibility,
			Now:                     now,
			Result:                  &result,
		})
	}); err != nil {
		return AppVisibilityUpdateResult{}, err
	}
	return result, nil
}

type appVisibilityMutation struct {
	UserID                  string
	WorkspaceID             string
	InstallationID          string
	ExpectedActorMemberID   string
	ExpectedSummary         serviceactor.InstallationSummary
	TargetCatalogVisibility string
	Now                     time.Time
	Result                  *AppVisibilityUpdateResult
}

func applyAppVisibilityUpdateInTx(
	ctx context.Context,
	store *Store,
	tx coredb.Tx,
	mutation appVisibilityMutation,
) error {
	summary := mutation.ExpectedSummary
	if err := installation.LockAppInstallationMutationScope(ctx, tx, summary.Definition.ID); err != nil {
		return err
	}
	currentActor, err := store.revalidateAppInstallationManagementForUpdate(
		ctx,
		tx,
		mutation.UserID,
		mutation.WorkspaceID,
		mutation.ExpectedActorMemberID,
		summary.Definition.ID,
	)
	if err != nil {
		return err
	}
	attribution, attributed, err := loadAppManagementAttribution(
		ctx, tx, mutation.WorkspaceID, summary.Definition.ID, false,
	)
	if err != nil {
		return err
	}
	if !attributed {
		return ErrNotFound
	}

	txStore := store.serviceActorStore().InTx(tx)
	currentInstallation, err := txStore.LoadInstallationForUpdate(ctx, strings.TrimSpace(mutation.InstallationID))
	if err != nil {
		return err
	}
	if !currentAppRuntimeInstallationMatchesSummary(currentInstallation, summary, mutation.WorkspaceID) {
		return conflict("App installation changed. Retry the visibility update.")
	}
	if _, err := requirePinnedRuntimeCapability(currentInstallation); err != nil {
		return err
	}
	currentMember, err := txStore.LoadMemberByInstallation(ctx, currentInstallation.ID)
	if err != nil {
		return err
	}
	if strings.TrimSpace(currentMember.WorkspaceID) != strings.TrimSpace(mutation.WorkspaceID) ||
		strings.TrimSpace(currentMember.ServiceDefinitionID) != strings.TrimSpace(summary.Definition.ID) ||
		strings.TrimSpace(currentMember.ServiceInstallationID) != strings.TrimSpace(currentInstallation.ID) ||
		strings.TrimSpace(currentMember.MemberID) != strings.TrimSpace(summary.Member.MemberID) ||
		strings.TrimSpace(currentMember.ServiceKind) != serviceactor.KindApp ||
		strings.TrimSpace(currentMember.Status) != serviceactor.StatusActive {
		return conflict("App service member changed. Retry the visibility update.")
	}

	// Run private cleanup even for private -> private retries. This repairs
	// historical residue and makes the command idempotent at the desired-state
	// boundary rather than merely at the attribution-row boundary.
	if mutation.TargetCatalogVisibility == registry.CatalogVisibilityPrivate {
		removedChannelIDs, err := removeAppFromAllChannelsForVisibilityChangeInTx(
			ctx,
			tx,
			mutation.WorkspaceID,
			currentActor.Member.ID,
			currentInstallation,
			currentMember.MemberID,
			mutation.Now,
		)
		if err != nil {
			return err
		}
		mutation.Result.RemovedChannelIDs = append([]string(nil), removedChannelIDs...)
		if mutation.Result.RemovedChannelIDs == nil {
			mutation.Result.RemovedChannelIDs = []string{}
		}
	}

	// The revalidation above already locked this exact attribution. Pass its
	// current value only as a corruption guard; both same-value and changed
	// updates converge through the same durable helper.
	if attribution.CatalogVisibility != registry.CatalogVisibilityPrivate &&
		attribution.CatalogVisibility != registry.CatalogVisibilityPublic {
		return fmt.Errorf("update App catalog visibility: invalid current visibility %q", attribution.CatalogVisibility)
	}
	return updateWorkspaceAppCatalogVisibilityInTx(
		ctx,
		tx,
		mutation.WorkspaceID,
		summary.Definition.ID,
		mutation.TargetCatalogVisibility,
		mutation.Now,
	)
}

func updateWorkspaceAppCatalogVisibilityInTx(
	ctx context.Context,
	tx coredb.Tx,
	workspaceID string,
	appID string,
	catalogVisibility string,
	now time.Time,
) error {
	catalogVisibility, err := registry.NormalizeCatalogVisibility(catalogVisibility, false)
	if err != nil {
		return err
	}
	result, err := tx.Exec(ctx, `
UPDATE extension_catalog_attributions
SET catalog_visibility = $1,
    updated_at = $2
WHERE workspace_id = $3
  AND service_definition_id = $4
`, catalogVisibility, now.UTC(), strings.TrimSpace(workspaceID), strings.TrimSpace(appID))
	if err != nil {
		return fmt.Errorf("update App catalog visibility: %w", err)
	}
	if result.RowsAffected() > 1 {
		return conflict("App catalog attribution changed. Retry the visibility update.")
	}
	if result.RowsAffected() == 1 {
		return nil
	}
	// MySQL-compatible drivers may report zero affected rows for an idempotent
	// same-value update. Distinguish that success from a missing attribution.
	var current string
	if err := tx.QueryRow(ctx, `
SELECT catalog_visibility
FROM extension_catalog_attributions
WHERE workspace_id = $1
  AND service_definition_id = $2
LIMIT 1
`, strings.TrimSpace(workspaceID), strings.TrimSpace(appID)).Scan(&current); err != nil {
		if coredb.IsNotFound(err) {
			return ErrNotFound
		}
		return fmt.Errorf("verify App catalog visibility update: %w", err)
	}
	if strings.TrimSpace(current) != catalogVisibility {
		return conflict("App catalog visibility changed. Retry the visibility update.")
	}
	return nil
}
