package app

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

	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	extensionconnection "github.com/tidbcloud/AgentTeam/internal/extension/connection"
	"github.com/tidbcloud/AgentTeam/internal/store/app/installation"
	"github.com/tidbcloud/AgentTeam/internal/store/coordinator"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
)

// extensionProviderConnectionExternalSubjectRef is the authoritative single
// connection identity for token-authenticated provider Apps.
const extensionProviderConnectionExternalSubjectRef = "default"

// ExtensionProviderTarget pins the product App a provider connection may be
// stored under. The API layer resolves it from the trusted provider registry
// so the store never depends on provider packages.
type ExtensionProviderTarget struct {
	AppID   string
	AppSlug string
	Label   string
}

// ExtensionProviderConnectionView is the secret-free connection state for one
// installed provider App.
type ExtensionProviderConnectionView struct {
	Active        bool
	Slug          string
	CredentialSet bool
	UpdatedAt     *time.Time
}

// GetExtensionProviderConnectionForUser returns the connection state of one
// installed provider App. Only a Workspace owner/admin can read it.
func (store *Store) GetExtensionProviderConnectionForUser(
	ctx context.Context,
	userID string,
	workspaceID string,
	installationID string,
	target ExtensionProviderTarget,
) (ExtensionProviderConnectionView, error) {
	summary, _, err := store.authorizeExtensionProviderInstallationAdmin(
		ctx,
		userID,
		workspaceID,
		installationID,
		target,
	)
	if err != nil {
		return ExtensionProviderConnectionView{}, err
	}
	connections, err := store.serviceActorStore().ListActiveConnections(
		ctx,
		strings.TrimSpace(installationID),
	)
	if err != nil {
		return ExtensionProviderConnectionView{}, err
	}
	existing, err := requireAuthoritativeExtensionProviderConnection(
		summary.Installation,
		target,
		connections,
	)
	if err != nil {
		return ExtensionProviderConnectionView{}, err
	}
	if existing == nil {
		return ExtensionProviderConnectionView{
			Active: false,
			Slug:   target.AppSlug,
		}, nil
	}
	credentialSet := false
	if credentials, err := store.extensionProviderCredentials(*existing); err == nil {
		credentialSet = len(credentials) != 0
	}
	updatedAt := existing.UpdatedAt
	return ExtensionProviderConnectionView{
		Active:        existing.Status == serviceactor.StatusActive,
		Slug:          target.AppSlug,
		CredentialSet: credentialSet,
		UpdatedAt:     &updatedAt,
	}, nil
}

// UpsertExtensionProviderConnectionForUser creates or replaces the authoritative
// provider connection. Credentials are validated by the API layer against the
// provider contract before this call; the store seals them under AES-GCM and
// advances the workspace App catalog within the same transaction.
//
// expectedUpdatedAt is the compare-and-swap version of the connection as the
// caller last saw it: it must be nil when creating and must equal the stored
// UpdatedAt when replacing. A mismatch means a concurrent write landed first
// and resolves to ErrConflict instead of silently restoring rotated secrets
// over newer values through the API layer's "empty keeps stored" merge.
func (store *Store) UpsertExtensionProviderConnectionForUser(
	ctx context.Context,
	userID string,
	workspaceID string,
	installationID string,
	target ExtensionProviderTarget,
	credentials map[string]string,
	config extensionconnection.Config,
	expectedUpdatedAt *time.Time,
) (ExtensionProviderConnectionView, error) {
	summary, expectedMemberID, err := store.authorizeExtensionProviderInstallationAdmin(
		ctx,
		userID,
		workspaceID,
		installationID,
		target,
	)
	if err != nil {
		return ExtensionProviderConnectionView{}, err
	}
	configJSON, err := config.MarshalJSONDocument()
	if err != nil {
		return ExtensionProviderConnectionView{}, err
	}
	sealed, err := store.marshalAppCredentialsObject(credentialsMapAny(credentials))
	if err != nil {
		return ExtensionProviderConnectionView{}, err
	}

	var connection serviceactor.Connection
	err = coordinator.New(store.db).Run(ctx, "upsert extension provider connection", func(tx coredb.Tx) error {
		var txErr error
		connection, txErr = store.upsertExtensionProviderConnectionInTx(
			ctx,
			tx,
			summary,
			target,
			sealed,
			configJSON,
			expectedUpdatedAt,
			userID,
			workspaceID,
			installationID,
			expectedMemberID,
		)
		return txErr
	})
	if err != nil {
		return ExtensionProviderConnectionView{}, err
	}
	updatedAt := connection.UpdatedAt
	return ExtensionProviderConnectionView{
		Active:        connection.Status == serviceactor.StatusActive,
		Slug:          target.AppSlug,
		CredentialSet: len(credentials) != 0,
		UpdatedAt:     &updatedAt,
	}, nil
}

func (store *Store) upsertExtensionProviderConnectionInTx(
	ctx context.Context,
	tx coredb.Tx,
	summary serviceactor.InstallationSummary,
	target ExtensionProviderTarget,
	sealedCredentials []byte,
	configJSON []byte,
	expectedUpdatedAt *time.Time,
	userID string,
	workspaceID string,
	installationID string,
	expectedMemberID string,
) (serviceactor.Connection, error) {
	if err := installation.LockAppInstallationMutationScope(ctx, tx, summary.Definition.ID); err != nil {
		return serviceactor.Connection{}, err
	}
	currentAdmin, err := store.revalidateInstallationManagementForUpdate(
		ctx,
		tx,
		userID,
		workspaceID,
		expectedMemberID,
		installationID,
		summary.Definition.ID,
	)
	if err != nil {
		return serviceactor.Connection{}, err
	}
	connection, err := store.upsertExtensionProviderConnectionLocked(
		ctx,
		tx,
		summary,
		target,
		sealedCredentials,
		configJSON,
		expectedUpdatedAt,
		userID,
		currentAdmin.Member.ID,
		workspaceID,
		installationID,
	)
	if err != nil {
		return serviceactor.Connection{}, err
	}
	return connection, nil
}

// DeleteExtensionProviderConnectionForUser disconnects the provider App and
// removes its Extension from effective Agent catalogs.
// upsertExtensionProviderConnectionLocked performs the current-read
// installation/connection verification and the canonical connection write
// after the caller's authority revalidation.
func (store *Store) upsertExtensionProviderConnectionLocked(
	ctx context.Context,
	tx coredb.Tx,
	summary serviceactor.InstallationSummary,
	target ExtensionProviderTarget,
	sealedCredentials []byte,
	configJSON []byte,
	expectedUpdatedAt *time.Time,
	actedByUserID string,
	actedByMemberID string,
	workspaceID string,
	installationID string,
) (serviceactor.Connection, error) {
	txStore := store.serviceActorStore().InTx(tx)
	installation, err := txStore.LoadInstallationForUpdate(
		ctx,
		strings.TrimSpace(installationID),
	)
	if err != nil {
		return serviceactor.Connection{}, err
	}
	if !extensionProviderInstallationMatches(installation, workspaceID, target) {
		return serviceactor.Connection{}, conflict("provider App installation changed. Reload and retry.")
	}
	active, err := txStore.ListActiveConnections(ctx, installation.ID)
	if err != nil {
		return serviceactor.Connection{}, err
	}
	existing, err := requireAuthoritativeExtensionProviderConnection(installation, target, active)
	if err != nil {
		return serviceactor.Connection{}, err
	}
	// Lock the authoritative connection row before the version compare: the
	// list read above is not a current read, so two same-version writers
	// could otherwise both observe the old row, both pass the compare, and
	// both commit. The row lock serializes them; the loser re-reads the
	// winner's version and resolves to a conflict.
	locked, found, lockErr := store.serviceActorStore().InTx(tx).
		LoadActiveConnectionForUpdate(
			ctx,
			installation.ID,
			extensionProviderConnectionExternalSubjectRef,
		)
	if lockErr != nil {
		return serviceactor.Connection{}, lockErr
	}
	if found {
		existing = &locked
	} else {
		existing = nil
	}
	if existing != nil {
		if expectedUpdatedAt == nil || !expectedUpdatedAt.Equal(existing.UpdatedAt) {
			return serviceactor.Connection{}, conflict(
				"provider App connection changed. Reload and retry.")
		}
	} else if expectedUpdatedAt != nil {
		return serviceactor.Connection{}, conflict(
			"provider App connection changed. Reload and retry.")
	}
	connectionID, err := identifiers.NewEntityID("appconn")
	if err != nil {
		return serviceactor.Connection{}, fmt.Errorf("generate provider App connection id: %w", err)
	}
	if existing != nil {
		connectionID = existing.ID
	}
	// The connections table stores updated_at at whole-second precision, so
	// a plain clock read can collide for two rotations inside one second
	// and reopen the compare-and-swap hole this contract closes. Pin the
	// write to the stored precision and strictly advance past the loaded
	// row so every accepted write moves the version forward.
	now := time.Now().UTC().Truncate(time.Second)
	if existing != nil && !now.After(existing.UpdatedAt) {
		now = existing.UpdatedAt.Add(time.Second)
	}
	connection, err := store.upsertAppServiceConnection(
		ctx,
		tx,
		installation.ID,
		connectionID,
		extensionProviderConnectionExternalSubjectRef,
		target.Label,
		sealedCredentials,
		configJSON,
		actedByUserID,
		now,
	)
	if err != nil {
		return serviceactor.Connection{}, err
	}
	if connection.CreatedByMemberID != actedByMemberID {
		return serviceactor.Connection{}, conflict("provider App connection administrator changed.")
	}
	if err := store.RunWorkspaceAppCatalogReconcileHook(
		ctx,
		tx,
		installation.WorkspaceID,
		now,
	); err != nil {
		return serviceactor.Connection{}, err
	}
	return connection, nil

}

func (store *Store) DeleteExtensionProviderConnectionForUser(
	ctx context.Context,
	userID string,
	workspaceID string,
	installationID string,
	target ExtensionProviderTarget,
) error {
	summary, expectedMemberID, err := store.authorizeExtensionProviderInstallationAdmin(
		ctx,
		userID,
		workspaceID,
		installationID,
		target,
	)
	if err != nil {
		return err
	}
	return coordinator.New(store.db).Run(ctx, "delete extension provider connection", func(tx coredb.Tx) error {
		if err := installation.LockAppInstallationMutationScope(ctx, tx, summary.Definition.ID); err != nil {
			return err
		}
		if _, err := store.revalidateInstallationManagementForUpdate(
			ctx,
			tx,
			userID,
			workspaceID,
			expectedMemberID,
			installationID,
			summary.Definition.ID,
		); err != nil {
			return err
		}
		txStore := store.serviceActorStore().InTx(tx)
		installation, err := txStore.LoadInstallationForUpdate(
			ctx,
			strings.TrimSpace(installationID),
		)
		if err != nil {
			return err
		}
		if !extensionProviderInstallationMatches(installation, workspaceID, target) {
			return conflict("provider App installation changed. Reload and retry.")
		}
		active, err := txStore.ListActiveConnections(ctx, installation.ID)
		if err != nil {
			return err
		}
		existing, err := requireAuthoritativeExtensionProviderConnection(installation, target, active)
		if err != nil {
			return err
		}
		if existing == nil {
			return nil
		}
		now := time.Now().UTC()
		if err := store.disableAppConnectionInTx(ctx, tx, installation.ID, existing.ID, now); err != nil {
			return err
		}
		return store.RunWorkspaceAppCatalogReconcileHook(ctx, tx, installation.WorkspaceID, now)
	})
}

// ResolveExtensionProviderRuntimeConnection resolves the complete runtime
// material while also pinning the expected product App target. Credentials
// and validated non-secret config remain separate return values so the store
// does not export a cross-layer runtime DTO.
func (store *Store) ResolveExtensionProviderRuntimeConnection(
	ctx context.Context,
	workspaceID string,
	installationID string,
	target ExtensionProviderTarget,
	configContract extensionconnection.Contract,
) (map[string]string, extensionconnection.Config, error) {
	workspaceID = strings.TrimSpace(workspaceID)
	installationID = strings.TrimSpace(installationID)
	summary, err := store.serviceActorStore().LoadInstallationSummaryByKind(
		ctx,
		workspaceID,
		installationID,
		serviceactor.KindApp,
	)
	if err != nil {
		return nil, nil, err
	}
	if summary.Installation.Status != serviceactor.StatusActive ||
		summary.Definition.Status != serviceactor.StatusActive ||
		((target.AppID != "" || target.AppSlug != "") &&
			!extensionProviderDefinitionMatches(summary.Definition, target)) {
		return nil, nil,
			notFound("provider App connection not found")
	}
	return store.resolveExtensionProviderRuntimeConnection(
		ctx,
		summary,
		target,
		configContract,
	)
}

func (store *Store) resolveExtensionProviderRuntimeConnection(
	ctx context.Context,
	summary serviceactor.InstallationSummary,
	target ExtensionProviderTarget,
	configContract extensionconnection.Contract,
) (map[string]string, extensionconnection.Config, error) {
	connections, err := store.serviceActorStore().ListActiveConnections(
		ctx,
		summary.Installation.ID,
	)
	if err != nil {
		return nil, nil, err
	}
	connection, err := requireAuthoritativeExtensionProviderConnection(
		summary.Installation,
		target,
		connections,
	)
	if err != nil {
		return nil, nil, err
	}
	if connection == nil {
		return nil, nil,
			notFound("provider App connection not found")
	}
	credentials, err := store.extensionProviderCredentials(*connection)
	if err != nil {
		return nil, nil, err
	}
	config, err := extensionconnection.ParseJSONDocument(connection.Config)
	if err != nil {
		return nil, nil, err
	}
	if err := configContract.Validate(config); err != nil {
		return nil, nil, fmt.Errorf(
			"stored provider connection config violates provider contract: %w",
			err,
		)
	}
	return credentials, config, nil
}

// extensionProviderCredentials decrypts and shape-checks one connection's
// credential object into the flat string map executors consume.
func (store *Store) extensionProviderCredentials(
	connection serviceactor.Connection,
) (map[string]string, error) {
	plaintext, err := store.decryptStoredAppCredentialsJSON(connection.Credentials)
	if err != nil {
		return nil, err
	}
	var decoded map[string]any
	if err := model.UnmarshalJSONObject(
		"provider connection credentials",
		plaintext,
		&decoded,
	); err != nil {
		return nil, err
	}
	credentials := make(map[string]string, len(decoded))
	for name, value := range decoded {
		text, ok := value.(string)
		if !ok {
			return nil, fmt.Errorf(
				"stored provider credential field %q is invalid", name)
		}
		credentials[name] = text
	}
	return credentials, nil
}

// authorizeExtensionProviderInstallationAdmin checks the caller is a
// Workspace owner/admin and the installation is the expected active product
// App.
func (store *Store) authorizeExtensionProviderInstallationAdmin(
	ctx context.Context,
	userID string,
	workspaceID string,
	installationID string,
	target ExtensionProviderTarget,
) (serviceactor.InstallationSummary, string, error) {
	if strings.TrimSpace(installationID) == "" {
		return serviceactor.InstallationSummary{}, "", badRequest("installationId is required")
	}
	admin, summary, err := store.requireInstallationManagement(ctx, userID, workspaceID, installationID)
	if err != nil {
		return serviceactor.InstallationSummary{}, "", err
	}
	if summary.Installation.Status != serviceactor.StatusActive ||
		summary.Definition.Status != serviceactor.StatusActive ||
		!extensionProviderDefinitionMatches(summary.Definition, target) {
		return serviceactor.InstallationSummary{}, "", ErrNotFound
	}
	return summary, admin.Member.ID, nil
}

func extensionProviderDefinitionMatches(
	definition serviceactor.Definition,
	target ExtensionProviderTarget,
) bool {
	return definition.ID == target.AppID && definition.Slug == target.AppSlug
}

func extensionProviderInstallationMatches(
	installation serviceactor.Installation,
	workspaceID string,
	target ExtensionProviderTarget,
) bool {
	return installation.WorkspaceID == strings.TrimSpace(workspaceID) &&
		installation.ServiceDefinitionID == target.AppID &&
		installation.ServiceKind == serviceactor.KindApp &&
		installation.Status == serviceactor.StatusActive
}

// requireAuthoritativeExtensionProviderConnection returns the single active
// authoritative connection or nil when none exists. Ambiguous or foreign
// connection state is a conflict, not a silent override.
func requireAuthoritativeExtensionProviderConnection(
	installation serviceactor.Installation,
	target ExtensionProviderTarget,
	connections []serviceactor.Connection,
) (*serviceactor.Connection, error) {
	var matched []serviceactor.Connection
	for _, connection := range connections {
		if connection.WorkspaceID != installation.WorkspaceID ||
			(target.AppID != "" && connection.ServiceDefinitionID != target.AppID) ||
			connection.ServiceInstallationID != installation.ID ||
			connection.ServiceKind != serviceactor.KindApp ||
			connection.ExternalSubjectRef != extensionProviderConnectionExternalSubjectRef ||
			connection.Status != serviceactor.StatusActive {
			continue
		}
		matched = append(matched, connection)
	}
	if len(matched) == 0 {
		return nil, nil
	}
	if len(matched) > 1 {
		return nil, conflict("provider App connection state is ambiguous")
	}
	return &matched[0], nil
}

func credentialsMapAny(credentials map[string]string) map[string]any {
	if len(credentials) == 0 {
		return map[string]any{}
	}
	converted := make(map[string]any, len(credentials))
	for name, value := range credentials {
		converted[name] = value
	}
	return converted
}

// AuthorizeExtensionProviderConnectionAdmin pre-flights the owner/admin
// authorization for flows that must not reach a provider before the caller is
// authorized (credential live-check, connection test).
func (store *Store) AuthorizeExtensionProviderConnectionAdmin(
	ctx context.Context,
	userID string,
	workspaceID string,
	installationID string,
	target ExtensionProviderTarget,
) error {
	_, _, err := store.authorizeExtensionProviderInstallationAdmin(
		ctx,
		userID,
		workspaceID,
		installationID,
		target,
	)
	return err
}

// ExtensionProviderAppIDForInstallation resolves the App definition ID behind
// one active app-kind installation for the connection API provider lookup.
func (store *Store) ExtensionProviderAppIDForInstallation(
	ctx context.Context,
	workspaceID string,
	installationID string,
) (string, error) {
	summary, err := store.serviceActorStore().LoadInstallationSummaryByKind(
		ctx,
		strings.TrimSpace(workspaceID),
		strings.TrimSpace(installationID),
		serviceactor.KindApp,
	)
	if err != nil {
		return "", err
	}
	if summary.Installation.Status != serviceactor.StatusActive ||
		summary.Definition.Status != serviceactor.StatusActive {
		return "", notFound("provider App installation not found")
	}
	return summary.Definition.ID, nil
}
