package app

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

	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/extension/providers/searchengine/runtime"
	"github.com/tidbcloud/AgentTeam/internal/store/app/installation"
	"github.com/tidbcloud/AgentTeam/internal/store/coordinator"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
)

// GetSearchEngineConnectionForUser returns the configured state for one
// installed search Product App. Only a Workspace owner/admin can read it.
func (store *Store) GetSearchEngineConnectionForUser(
	ctx context.Context,
	userID string,
	workspaceID string,
	installationID string,
) (SearchEngineConnectionView, error) {
	summary, product, _, err := store.authorizeSearchEngineInstallationAdmin(
		ctx,
		userID,
		workspaceID,
		installationID,
	)
	if err != nil {
		return SearchEngineConnectionView{}, err
	}
	connections, err := store.serviceActorStore().ListActiveConnections(
		ctx,
		strings.TrimSpace(installationID),
	)
	if err != nil {
		return SearchEngineConnectionView{}, err
	}
	return store.searchEngineConnectionView(
		summary.Installation,
		product,
		connections,
	)
}

// UpsertSearchEngineConnectionForUser creates or replaces the stable default
// connection while preserving an omitted existing API key.
func (store *Store) UpsertSearchEngineConnectionForUser(
	ctx context.Context,
	userID string,
	workspaceID string,
	installationID string,
	params UpsertSearchEngineConnectionParams,
) (SearchEngineConnectionView, error) {
	summary, product, expectedMemberID, err := store.authorizeSearchEngineInstallationAdmin(
		ctx,
		userID,
		workspaceID,
		installationID,
	)
	if err != nil {
		return SearchEngineConnectionView{}, err
	}
	prepared, err := prepareSearchEngineConnection(product, params)
	if err != nil {
		return SearchEngineConnectionView{}, err
	}

	var connection serviceactor.Connection
	err = coordinator.New(store.db).Run(ctx, "upsert search engine connection", func(tx coredb.Tx) error {
		var txErr error
		connection, txErr = store.upsertSearchEngineConnectionInTx(
			ctx,
			tx,
			summary,
			product,
			prepared,
			userID,
			workspaceID,
			installationID,
			expectedMemberID,
		)
		return txErr
	})
	if err != nil {
		return SearchEngineConnectionView{}, err
	}
	return store.searchEngineConnectionView(
		summary.Installation,
		product,
		[]serviceactor.Connection{connection},
	)
}

func (store *Store) upsertSearchEngineConnectionInTx(
	ctx context.Context,
	tx coredb.Tx,
	summary serviceactor.InstallationSummary,
	product searchEngineProduct,
	prepared preparedSearchEngineConnection,
	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
	}
	txStore := store.serviceActorStore().InTx(tx)
	installation, err := txStore.LoadInstallationForUpdate(
		ctx,
		strings.TrimSpace(installationID),
	)
	if err != nil {
		return serviceactor.Connection{}, err
	}
	if !searchEngineInstallationMatches(
		installation,
		strings.TrimSpace(workspaceID),
		product,
	) {
		return serviceactor.Connection{}, conflict("Search engine App installation changed. Reload and retry.")
	}
	active, err := txStore.ListActiveConnections(ctx, installation.ID)
	if err != nil {
		return serviceactor.Connection{}, err
	}
	existing, err := requireAuthoritativeSearchEngineConnection(
		installation,
		product,
		active,
	)
	if err != nil {
		return serviceactor.Connection{}, err
	}
	credentialsJSON, configJSON, err := store.marshalSearchEngineConnectionPayloads(
		product,
		prepared,
		existing,
	)
	if err != nil {
		return serviceactor.Connection{}, err
	}
	connectionID, err := newSearchEngineConnectionID(existing)
	if err != nil {
		return serviceactor.Connection{}, err
	}
	now := time.Now().UTC()
	connection, err := store.upsertAppServiceConnection(
		ctx,
		tx,
		installation.ID,
		connectionID,
		searchEngineConnectionExternalSubjectRef,
		product.DisplayName,
		credentialsJSON,
		configJSON,
		userID,
		now,
	)
	if err != nil {
		return serviceactor.Connection{}, err
	}
	if connection.CreatedByMemberID != currentAdmin.Member.ID {
		return serviceactor.Connection{}, conflict("Search engine 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) marshalSearchEngineConnectionPayloads(
	product searchEngineProduct,
	prepared preparedSearchEngineConnection,
	existing *serviceactor.Connection,
) ([]byte, []byte, error) {
	if product.RequiresKey && !prepared.APIKeyProvided {
		if existing == nil {
			return nil, nil, badRequest("credentials.apiKey is required")
		}
		apiKey, err := store.searchEngineAPIKey(*existing)
		if err != nil {
			return nil, nil, err
		}
		if apiKey == "" {
			return nil, nil, badRequest("credentials.apiKey is required")
		}
		prepared.APIKey = apiKey
	}
	configJSON, err := encodeSearchEngineConnectionConfig(product, prepared.Config)
	if err != nil {
		return nil, nil, err
	}
	credentials := map[string]any{}
	if product.RequiresKey {
		credentials["apiKey"] = prepared.APIKey
	}
	credentialsJSON, err := store.marshalAppCredentialsObject(credentials)
	if err != nil {
		return nil, nil, err
	}
	return credentialsJSON, configJSON, nil
}

func newSearchEngineConnectionID(
	existing *serviceactor.Connection,
) (string, error) {
	if existing != nil {
		return existing.ID, nil
	}
	connectionID, err := identifiers.NewEntityID("appconn")
	if err != nil {
		return "", fmt.Errorf("generate search engine App connection id: %w", err)
	}
	return connectionID, nil
}

// DeleteSearchEngineConnectionForUser disconnects only the selected search
// provider and removes its Extension from effective Agent catalogs.
func (store *Store) DeleteSearchEngineConnectionForUser(
	ctx context.Context,
	userID string,
	workspaceID string,
	installationID string,
) (SearchEngineConnectionView, error) {
	summary, product, expectedMemberID, err := store.authorizeSearchEngineInstallationAdmin(
		ctx,
		userID,
		workspaceID,
		installationID,
	)
	if err != nil {
		return SearchEngineConnectionView{}, err
	}
	err = coordinator.New(store.db).Run(ctx, "delete search engine 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 !searchEngineInstallationMatches(
			installation,
			strings.TrimSpace(workspaceID),
			product,
		) {
			return conflict("Search engine App installation changed. Reload and retry.")
		}
		active, err := txStore.ListActiveConnections(ctx, installation.ID)
		if err != nil {
			return err
		}
		existing, err := requireAuthoritativeSearchEngineConnection(
			installation,
			product,
			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)
	})
	if err != nil {
		return SearchEngineConnectionView{}, err
	}
	return SearchEngineConnectionView{
		Provider: product.Provider,
		Active:   false,
		Config:   SearchEngineConnectionConfigView{},
	}, nil
}

// ResolveSearchEngineRuntimeConnection implements the search service's
// connection reader after Extension authorization has selected an App
// installation.
func (store *Store) ResolveSearchEngineRuntimeConnection(
	ctx context.Context,
	workspaceID string,
	installationID string,
	provider string,
) (runtime.RuntimeConnection, error) {
	product, ok := searchEngineProductsByProvider[strings.TrimSpace(provider)]
	if !ok {
		return runtime.RuntimeConnection{}, runtime.RuntimeConnectionUnavailable(
			notFound("search provider connection not found"),
		)
	}
	summary, err := store.serviceActorStore().LoadInstallationSummaryByKind(
		ctx,
		strings.TrimSpace(workspaceID),
		strings.TrimSpace(installationID),
		serviceactor.KindApp,
	)
	if err != nil {
		if errors.Is(err, serviceactor.ErrNotFound) {
			return runtime.RuntimeConnection{}, runtime.RuntimeConnectionUnavailable(err)
		}
		return runtime.RuntimeConnection{}, err
	}
	if summary.Installation.Status != serviceactor.StatusActive ||
		summary.Definition.Status != serviceactor.StatusActive ||
		summary.Definition.ID != product.AppID ||
		summary.Definition.Slug != product.AppSlug {
		return runtime.RuntimeConnection{}, runtime.RuntimeConnectionUnavailable(
			notFound("search provider connection not found"),
		)
	}
	connections, err := store.serviceActorStore().ListActiveConnections(
		ctx,
		summary.Installation.ID,
	)
	if err != nil {
		return runtime.RuntimeConnection{}, err
	}
	connection, err := requireAuthoritativeSearchEngineConnection(
		summary.Installation,
		product,
		connections,
	)
	if err != nil {
		return runtime.RuntimeConnection{}, runtime.RuntimeConnectionUnavailable(err)
	}
	if connection == nil {
		return runtime.RuntimeConnection{}, runtime.RuntimeConnectionUnavailable(
			notFound("search provider connection not found"),
		)
	}
	config, err := decodeSearchEngineConnectionConfig(product, connection.Config)
	if err != nil {
		return runtime.RuntimeConnection{}, runtime.RuntimeConnectionUnavailable(err)
	}
	apiKey, err := store.searchEngineAPIKey(*connection)
	if err != nil {
		return runtime.RuntimeConnection{}, runtime.RuntimeConnectionUnavailable(err)
	}
	if product.RequiresKey && apiKey == "" {
		return runtime.RuntimeConnection{}, runtime.RuntimeConnectionUnavailable(
			notFound("search provider credential not found"),
		)
	}
	return runtime.RuntimeConnection{
		Provider: product.Provider,
		BaseURL:  config.BaseURL,
		Language: config.Language,
		APIKey:   apiKey,
	}, nil
}

func (store *Store) authorizeSearchEngineInstallationAdmin(
	ctx context.Context,
	userID string,
	workspaceID string,
	installationID string,
) (serviceactor.InstallationSummary, searchEngineProduct, string, error) {
	if strings.TrimSpace(installationID) == "" {
		return serviceactor.InstallationSummary{}, searchEngineProduct{}, "", badRequest("installationId is required")
	}
	admin, summary, err := store.requireInstallationManagement(ctx, userID, workspaceID, installationID)
	if err != nil {
		return serviceactor.InstallationSummary{}, searchEngineProduct{}, "", err
	}
	product, ok := searchEngineProductForDefinition(
		summary.Definition.ID,
		summary.Definition.Slug,
	)
	if !ok ||
		summary.Installation.Status != serviceactor.StatusActive ||
		summary.Definition.Status != serviceactor.StatusActive {
		return serviceactor.InstallationSummary{}, searchEngineProduct{}, "", ErrNotFound
	}
	return summary, product, admin.Member.ID, nil
}
