package app

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

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

// NamedAppConnectionParams is the generic durable Connection input used by
// feature-owned App setup flows after they have validated their provider facts.
type NamedAppConnectionParams struct {
	ExternalSubjectRef string
	DisplayLabel       string
	Credentials        map[string]any
	Config             map[string]any
}

// NamedAppInstallationParams contains only generic named Installation,
// Connection, and Extension-selection identity. Provider-specific facts and
// validation remain in the owning Extension package.
type NamedAppInstallationParams struct {
	AppID           string
	AppSlug         string
	InstanceKey     string
	Handle          string
	DisplayName     string
	ExtensionDomain string
	ExtensionName   string
	Connection      NamedAppConnectionParams
}

// NamedAppReconnectParams creates a new Connection generation for one exact
// active named Installation.
type NamedAppReconnectParams struct {
	AppID                     string
	InstallationID            string
	Connection                NamedAppConnectionParams
	RequireNoActiveConnection bool
}

// NamedAppInstallationView is the generic result of one atomic named App
// materialization.
type NamedAppInstallationView struct {
	Installation AppInstallationView
	Connection   runtime.ConnectionView
	Extension    serviceactor.InstallationExtension
}

// NamedAppMutationResult exposes only generic rows to a feature-owned
// transaction hook.
type NamedAppMutationResult struct {
	Installation serviceactor.Installation
	Connection   runtime.ConnectionView
	Extension    serviceactor.InstallationExtension
}

// NamedAppMutationHooks let a feature validate its own invariants and publish
// feature-owned rows in the same transaction as the generic App mutation.
// Hooks must perform database work only; external I/O is forbidden.
type NamedAppMutationHooks struct {
	BeforeConnection func(
		context.Context,
		coredb.Tx,
		serviceactor.Installation,
	) error
	BeforeCommit func(
		context.Context,
		coredb.Tx,
		NamedAppMutationResult,
		time.Time,
	) error
}

type namedAppInstallationWriter struct {
	store *Store
}

// FinalizeNamedAppInstallationForWorkspaceAdmin atomically materializes one
// trusted, feature-validated named App Installation, Connection, and Extension
// selection. App catalog identity and current administrator authority are
// revalidated under the shared mutation fence.
func (store *Store) FinalizeNamedAppInstallationForWorkspaceAdmin(
	ctx context.Context,
	userID string,
	workspaceID string,
	params NamedAppInstallationParams,
	hooks NamedAppMutationHooks,
) (NamedAppInstallationView, error) {
	admin, err := store.requireWorkspaceAdmin(ctx, userID, workspaceID)
	if err != nil {
		return NamedAppInstallationView{}, err
	}
	params = normalizeNamedAppInstallationParams(params)
	if err := validateNamedAppInstallationParams(params); err != nil {
		return NamedAppInstallationView{}, err
	}
	prepared, err := prepareWorkspaceAppInstallationRequest(
		ctx,
		store.db,
		workspaceID,
		InstallAppParams{AppID: params.AppID},
	)
	if err != nil {
		return NamedAppInstallationView{}, err
	}
	if prepared.app.ID != params.AppID ||
		prepared.app.Slug != params.AppSlug {
		return NamedAppInstallationView{},
			conflict("App identity changed. Reload and connect again.")
	}

	var result NamedAppMutationResult
	writer := namedAppInstallationWriter{store: store}
	err = coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		currentPrepared, currentAdminMemberID, err :=
			writer.revalidateForUpdate(
				ctx,
				tx,
				userID,
				workspaceID,
				admin.Member.ID,
				params,
				prepared,
			)
		if err != nil {
			return err
		}
		result, err = writer.materialize(
			ctx,
			tx,
			userID,
			workspaceID,
			currentAdminMemberID,
			currentPrepared,
			params,
			hooks,
		)
		return err
	})
	if err != nil {
		return NamedAppInstallationView{}, err
	}
	installation, err := store.appInstallationViewByID(
		ctx,
		workspaceID,
		result.Installation.ID,
	)
	if err != nil {
		return NamedAppInstallationView{}, err
	}
	return NamedAppInstallationView{
		Installation: installation,
		Connection:   result.Connection,
		Extension:    result.Extension,
	}, nil
}

func (writer namedAppInstallationWriter) revalidateForUpdate(
	ctx context.Context,
	tx coredb.Tx,
	userID string,
	workspaceID string,
	adminMemberID string,
	params NamedAppInstallationParams,
	prepared preparedAppInstallation,
) (preparedAppInstallation, string, error) {
	if err := installation.LockAppInstallationMutationScope(
		ctx,
		tx,
		params.AppID,
	); err != nil {
		return preparedAppInstallation{}, "", err
	}
	currentPrepared, err := prepareWorkspaceAppInstallationRequest(
		ctx,
		tx,
		workspaceID,
		InstallAppParams{AppID: params.AppID},
	)
	if err != nil {
		return preparedAppInstallation{}, "", err
	}
	if currentPrepared.app.ID != params.AppID ||
		currentPrepared.app.Slug != params.AppSlug {
		return preparedAppInstallation{}, "", conflict(
			"App identity changed. Reload and connect again.",
		)
	}
	if currentPrepared.version.Version != prepared.version.Version {
		return preparedAppInstallation{}, "", conflict(
			"App version changed. Reload and connect again.",
		)
	}
	if err := authorizeAppInstallationRequest(
		ctx,
		tx,
		workspaceID,
		currentPrepared,
	); err != nil {
		return preparedAppInstallation{}, "", err
	}
	currentAdmin, err :=
		writer.store.revalidateServiceInstallationManagementForUpdate(
			ctx,
			tx,
			userID,
			workspaceID,
			adminMemberID,
		)
	if err != nil {
		return preparedAppInstallation{}, "", err
	}
	return currentPrepared, currentAdmin.Member.ID, nil
}

func (writer namedAppInstallationWriter) materialize(
	ctx context.Context,
	tx coredb.Tx,
	userID string,
	workspaceID string,
	adminMemberID string,
	prepared preparedAppInstallation,
	params NamedAppInstallationParams,
	hooks NamedAppMutationHooks,
) (NamedAppMutationResult, error) {
	now := time.Now().UTC()
	installer := preparedAppInstaller{store: writer.store}
	installationID, err := installer.installInstance(
		ctx,
		tx,
		userID,
		workspaceID,
		adminMemberID,
		prepared,
		appInstallationInstanceIdentity{
			InstanceKey: params.InstanceKey,
			Handle:      params.Handle,
			DisplayName: params.DisplayName,
			// Named Exec-only Apps do not implicitly join #all or create a
			// message-delivery surface.
			JoinAllChannelMembership: false,
		},
		now,
	)
	if err != nil {
		return NamedAppMutationResult{}, err
	}
	txActors := writer.store.serviceActorStore().InTx(tx)
	installation, err := txActors.LoadInstallationForUpdate(
		ctx,
		installationID,
	)
	if err != nil {
		return NamedAppMutationResult{}, err
	}
	if hooks.BeforeConnection != nil {
		if err := hooks.BeforeConnection(ctx, tx, installation); err != nil {
			return NamedAppMutationResult{}, err
		}
	}
	connection, err := writer.store.upsertAppConnectionInTx(
		ctx,
		tx,
		UpsertAppConnectionParams{
			InstallationID: installation.ID,
			ExternalSubjectRef: params.Connection.
				ExternalSubjectRef,
			DisplayLabel:    params.Connection.DisplayLabel,
			Credentials:     params.Connection.Credentials,
			Config:          params.Connection.Config,
			CreatedByUserID: userID,
		},
		"",
		now,
	)
	if err != nil {
		return NamedAppMutationResult{}, err
	}
	selection, err := writer.store.addInstallationExtensionInTx(
		ctx,
		tx,
		txActors,
		installation,
		adminMemberID,
		params.ExtensionDomain,
		params.ExtensionName,
		serviceactor.InstallationExtensionSourceUser,
		now,
	)
	if err != nil {
		return NamedAppMutationResult{}, err
	}
	result := NamedAppMutationResult{
		Installation: installation,
		Connection:   connection,
		Extension:    selection,
	}
	if hooks.BeforeCommit != nil {
		if err := hooks.BeforeCommit(ctx, tx, result, now); err != nil {
			return NamedAppMutationResult{}, err
		}
	}
	return result, nil
}

// FinalizeNamedAppReconnectForWorkspaceAdmin creates a new exact Connection
// generation and lets the feature publish its own generation-bound facts in
// the same short transaction.
func (store *Store) FinalizeNamedAppReconnectForWorkspaceAdmin(
	ctx context.Context,
	userID string,
	workspaceID string,
	params NamedAppReconnectParams,
	hooks NamedAppMutationHooks,
) (runtime.ConnectionView, error) {
	admin, err := store.requireWorkspaceAdmin(ctx, userID, workspaceID)
	if err != nil {
		return runtime.ConnectionView{}, err
	}
	params = normalizeNamedAppReconnectParams(params)
	if err := validateNamedAppReconnectParams(params); err != nil {
		return runtime.ConnectionView{}, err
	}
	var connection runtime.ConnectionView
	err = coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		if err := installation.LockAppInstallationMutationScope(
			ctx,
			tx,
			params.AppID,
		); err != nil {
			return err
		}
		_, err =
			store.revalidateServiceInstallationManagementForUpdate(
				ctx,
				tx,
				userID,
				workspaceID,
				admin.Member.ID,
			)
		if err != nil {
			return err
		}
		txActors := store.serviceActorStore().InTx(tx)
		installation, err := txActors.LoadInstallationForUpdate(
			ctx,
			params.InstallationID,
		)
		if err != nil {
			return err
		}
		if installation.WorkspaceID != workspaceID ||
			installation.ServiceDefinitionID != params.AppID ||
			installation.ServiceKind != serviceactor.KindApp ||
			installation.Status != serviceactor.StatusActive {
			return conflict(
				"App Installation changed during reconnect.",
			)
		}
		if hooks.BeforeConnection != nil {
			if err := hooks.BeforeConnection(
				ctx,
				tx,
				installation,
			); err != nil {
				return err
			}
		}
		if params.RequireNoActiveConnection {
			if err := requireNoActiveConnectionInTx(
				ctx,
				tx,
				installation.ID,
			); err != nil {
				return err
			}
		}
		now := time.Now().UTC()
		connection, err = store.upsertAppConnectionInTx(
			ctx,
			tx,
			UpsertAppConnectionParams{
				InstallationID: installation.ID,
				ExternalSubjectRef: params.Connection.
					ExternalSubjectRef,
				DisplayLabel:    params.Connection.DisplayLabel,
				Credentials:     params.Connection.Credentials,
				Config:          params.Connection.Config,
				CreatedByUserID: userID,
			},
			"",
			now,
		)
		if err != nil {
			return err
		}
		result := NamedAppMutationResult{
			Installation: installation,
			Connection:   connection,
		}
		if hooks.BeforeCommit != nil {
			if err := hooks.BeforeCommit(
				ctx,
				tx,
				result,
				now,
			); err != nil {
				return err
			}
		}
		return store.RunWorkspaceAppCatalogReconcileHook(
			ctx,
			tx,
			workspaceID,
			now,
		)
	})
	return connection, err
}

// DisconnectNamedAppConnectionForWorkspaceAdmin disables one exact active
// Connection and lets the feature remove its private projections atomically.
func (store *Store) DisconnectNamedAppConnectionForWorkspaceAdmin(
	ctx context.Context,
	userID string,
	workspaceID string,
	appID string,
	installationID string,
	connectionID string,
	beforeCommit func(context.Context, coredb.Tx, time.Time) error,
) error {
	admin, err := store.requireWorkspaceAdmin(ctx, userID, workspaceID)
	if err != nil {
		return err
	}
	appID = strings.TrimSpace(appID)
	installationID = strings.TrimSpace(installationID)
	connectionID = strings.TrimSpace(connectionID)
	if appID == "" || installationID == "" || connectionID == "" {
		return badRequest(
			"App, Installation, and Connection identities are required.",
		)
	}
	return coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		if err := installation.LockAppInstallationMutationScope(ctx, tx, appID); err != nil {
			return err
		}
		if _, err := store.
			revalidateServiceInstallationManagementForUpdate(
				ctx,
				tx,
				userID,
				workspaceID,
				admin.Member.ID,
			); err != nil {
			return err
		}
		installation, err := store.serviceActorStore().InTx(tx).
			LoadInstallationForUpdate(ctx, installationID)
		if err != nil {
			return err
		}
		if installation.WorkspaceID != strings.TrimSpace(workspaceID) ||
			installation.ServiceDefinitionID != appID ||
			installation.ServiceKind != serviceactor.KindApp ||
			installation.Status != serviceactor.StatusActive {
			return notFound("App Installation is unavailable.")
		}
		var currentID string
		err = tx.QueryRow(ctx, `
SELECT id
FROM service_connections
WHERE id = $1
  AND workspace_id = $2
  AND service_installation_id = $3
  AND service_definition_id = $4
  AND status = 'active'
FOR UPDATE
`, connectionID, workspaceID, installation.ID, appID).Scan(&currentID)
		if coredb.IsNotFound(err) {
			return notFound("App Connection is unavailable.")
		}
		if err != nil {
			return fmt.Errorf("lock App Connection: %w", err)
		}
		now := time.Now().UTC()
		if err := store.disableAppConnectionInTx(
			ctx,
			tx,
			installation.ID,
			currentID,
			now,
		); err != nil {
			return err
		}
		if beforeCommit != nil {
			if err := beforeCommit(ctx, tx, now); err != nil {
				return err
			}
		}
		return store.RunWorkspaceAppCatalogReconcileHook(
			ctx,
			tx,
			workspaceID,
			now,
		)
	})
}

func normalizeNamedAppInstallationParams(
	params NamedAppInstallationParams,
) NamedAppInstallationParams {
	params.AppID = strings.TrimSpace(params.AppID)
	params.AppSlug = strings.TrimSpace(params.AppSlug)
	params.InstanceKey = strings.TrimSpace(params.InstanceKey)
	params.Handle = strings.TrimSpace(params.Handle)
	params.DisplayName = strings.TrimSpace(params.DisplayName)
	params.ExtensionDomain = strings.TrimSpace(params.ExtensionDomain)
	params.ExtensionName = strings.TrimSpace(params.ExtensionName)
	params.Connection = normalizeNamedAppConnectionParams(params.Connection)
	return params
}

func normalizeNamedAppReconnectParams(
	params NamedAppReconnectParams,
) NamedAppReconnectParams {
	params.AppID = strings.TrimSpace(params.AppID)
	params.InstallationID = strings.TrimSpace(params.InstallationID)
	params.Connection = normalizeNamedAppConnectionParams(params.Connection)
	return params
}

func normalizeNamedAppConnectionParams(
	params NamedAppConnectionParams,
) NamedAppConnectionParams {
	params.ExternalSubjectRef = strings.TrimSpace(params.ExternalSubjectRef)
	params.DisplayLabel = strings.TrimSpace(params.DisplayLabel)
	return params
}

func validateNamedAppInstallationParams(
	params NamedAppInstallationParams,
) error {
	if params.AppID == "" || params.AppSlug == "" ||
		params.InstanceKey == "" || params.Handle == "" ||
		params.DisplayName == "" || params.ExtensionDomain == "" ||
		params.ExtensionName == "" {
		return badRequest(
			"Named App Installation identity is incomplete.",
		)
	}
	return validateNamedAppConnectionParams(params.Connection)
}

func validateNamedAppReconnectParams(params NamedAppReconnectParams) error {
	if params.AppID == "" || params.InstallationID == "" {
		return badRequest("Named App reconnect identity is incomplete.")
	}
	return validateNamedAppConnectionParams(params.Connection)
}

func validateNamedAppConnectionParams(
	params NamedAppConnectionParams,
) error {
	if params.ExternalSubjectRef == "" || params.DisplayLabel == "" {
		return badRequest("Named App Connection identity is incomplete.")
	}
	if len(params.Credentials) == 0 {
		return badRequest("Named App Connection credentials are required.")
	}
	return nil
}

func requireNoActiveConnectionInTx(
	ctx context.Context,
	tx coredb.Tx,
	installationID string,
) error {
	var activeConnectionID string
	err := tx.QueryRow(ctx, `
SELECT id
FROM service_connections
WHERE service_installation_id = $1
  AND status = 'active'
ORDER BY id ASC
LIMIT 1
FOR UPDATE
`, strings.TrimSpace(installationID)).Scan(&activeConnectionID)
	if coredb.IsNotFound(err) {
		return nil
	}
	if err != nil {
		return fmt.Errorf("lock active App Connection: %w", err)
	}
	return conflict("App Installation already has an active Connection.")
}
