package app

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

	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/core/secrets"
	"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/app/registry"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
)

// RequestCustomAppInstallationParams is the workspace-scoped input for registering
// one external App definition and submitting its first installation request.
// The creating member is recorded both as immutable attribution provenance and
// as the App's first active manager.
type RequestCustomAppInstallationParams struct {
	Slug              string
	Name              string
	Description       string
	AvatarURL         string
	Manifest          map[string]any
	Config            map[string]any
	Reason            string
	CatalogVisibility string
}

// CustomAppRequestResult deliberately contains only the durable request.
// Initial runtime credentials do not exist while an ordinary member's request
// is pending; the requester claims them from the installed App after approval.
type CustomAppRequestResult struct {
	Request AppInstallationRequestView
}

// CustomAppInstallationResult is returned only by the explicit workspace-admin
// direct onboarding command. It contains the materialized installation and
// one-time secrets without fabricating an approval request lifecycle.
type CustomAppInstallationResult struct {
	Installation          AppInstallationView
	CallbackSigningSecret string
	RunnerToken           string
}

// RequestCustomAppInstallationForUser registers an immutable external App manifest
// and feeds installation through the durable approval ledger. This method is
// member-only; workspace owners/admins use InstallCustomAppForWorkspaceAdmin so a
// privileged caller can never accidentally create a self-approval request.
func (store *Store) RequestCustomAppInstallationForUser(
	ctx context.Context,
	userID string,
	workspaceID string,
	params RequestCustomAppInstallationParams,
) (CustomAppRequestResult, error) {
	workspaceMember, err := store.requireWorkspaceAccess(ctx, userID, workspaceID)
	if err != nil {
		return CustomAppRequestResult{}, err
	}
	if err := requireAppApprovalRequester(workspaceMember); err != nil {
		return CustomAppRequestResult{}, err
	}
	preparedDefinition, err := prepareWorkspaceAppDefinition(params)
	if err != nil {
		return CustomAppRequestResult{}, err
	}
	return store.requestPreparedAppInstallationForWorkspaceMember(
		ctx,
		userID,
		workspaceID,
		workspaceMember,
		params,
		preparedDefinition,
	)
}

func (store *Store) requestPreparedAppInstallationForWorkspaceMember(
	ctx context.Context,
	userID string,
	workspaceID string,
	expectedRequester access.HumanWorkspaceMember,
	params RequestCustomAppInstallationParams,
	preparedDefinition preparedWorkspaceAppDefinition,
) (CustomAppRequestResult, error) {
	var requestID string
	var request AppInstallationRequestView
	var registration appDefinitionRegistrationResult
	if err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		// This is a create-only command. Lock the requester membership first,
		// then attempt a fresh apps row insert. We deliberately do not lock an
		// existing apps row: install/approval paths can hold membership before an
		// apps FK check, so apps -> membership would reverse their lock order.
		// Concurrent creators are serialized by the apps.slug unique constraint.
		currentRequester, err := store.revalidateAppApprovalRequesterForUpdate(
			ctx,
			tx,
			userID,
			workspaceID,
			expectedRequester,
		)
		if err != nil {
			return err
		}
		now := time.Now().UTC()
		registration, err = store.registerPreparedWorkspaceAppDefinitionInTx(
			ctx,
			tx,
			workspaceID,
			currentRequester.Member.ID,
			registry.CatalogVisibilityPrivate,
			preparedDefinition,
			now,
		)
		if err != nil {
			return err
		}
		preparedInstallation, err := prepareInstalledAppConfigurationAtVersion(
			ctx,
			tx,
			registration.appID,
			preparedDefinition.manifest.Version,
			params.Config,
		)
		if err != nil {
			return err
		}
		if err := authorizeAppInstallationRequestForMember(
			ctx,
			tx,
			workspaceID,
			preparedInstallation,
			currentRequester.Member.ID,
		); err != nil {
			return err
		}
		definition, err := store.syncAppServiceDefinition(ctx, tx, registration.appID, now)
		if err != nil {
			return err
		}
		if strings.TrimSpace(definition.ID) != strings.TrimSpace(registration.appID) {
			return conflict("App definition changed. Please retry.")
		}
		if err := installation.LockAppInstallationMutationScope(ctx, tx, definition.ID); err != nil {
			return err
		}
		txStore := store.serviceActorStore().InTx(tx)
		if existing, found, err := txStore.LoadInstallationByInstanceForUpdate(
			ctx,
			workspaceID,
			definition.ID,
			serviceactor.DefaultInstanceKey,
		); err != nil {
			return err
		} else if found && strings.TrimSpace(existing.Status) == serviceactor.StatusActive {
			return conflict("This App already exists.")
		}

		baseConfig := appInstallationReviewBaseConfig(&preparedDefinition.reviewSnapshot)
		requestedConfig := decodeJSONObjectMap(preparedInstallation.configJSON)
		requestID, err = identifiers.NewEntityID("svcinstreq")
		if err != nil {
			return fmt.Errorf("generate service installation request id: %w", err)
		}
		requestedScopesJSON, err := model.MarshalJSONObject(
			"service installation request scopes",
			registry.SortedUniqueScopes(preparedInstallation.version.Scopes),
		)
		if err != nil {
			return fmt.Errorf("encode service installation request scopes: %w", err)
		}
		baseConfigJSON, err := model.MarshalJSONObject("service installation request base config", baseConfig)
		if err != nil {
			return fmt.Errorf("encode service installation request base config: %w", err)
		}
		requestedConfigJSON, err := model.MarshalJSONObject("service installation request config", requestedConfig)
		if err != nil {
			return fmt.Errorf("encode service installation request config: %w", err)
		}
		if _, err := tx.Exec(ctx, `
INSERT INTO service_installation_requests (
    id, workspace_id, service_definition_id, service_kind, request_kind,
    requested_version, requested_config, base_config, requested_scopes,
    status, approval_required, request_reason, requested_by_member_id, decision_reason,
    service_installation_id, created_at, updated_at
) VALUES (
    $1, $2, $3, $4, $5,
    $6, $7, $8, $9,
    $10, TRUE, $11, $12, '',
    NULL, $13, $13
)
`, requestID, strings.TrimSpace(workspaceID), definition.ID, serviceactor.KindApp,
			AppInstallationRequestKindInstall, preparedInstallation.version.Version,
			requestedConfigJSON, baseConfigJSON, requestedScopesJSON,
			AppInstallationRequestStatusPending, strings.TrimSpace(params.Reason),
			currentRequester.Member.ID, now); err != nil {
			return fmt.Errorf("create App installation request: %w", err)
		}
		request, err = loadAppInstallationRequest(ctx, tx, workspaceID, requestID)
		return err
	}); err != nil {
		return CustomAppRequestResult{}, err
	}
	return CustomAppRequestResult{Request: request}, nil
}

type preparedWorkspaceAppDefinition struct {
	slug                  string
	name                  string
	description           string
	avatarURL             string
	manifest              registry.ValidatedDeveloperManifest
	reviewSnapshot        AppInstallationReviewSnapshot
	catalogVisibility     string
	appID                 string
	versionID             string
	callbackSigningSecret string
}

type appDefinitionRegistrationResult struct {
	appID                 string
	callbackSigningSecret string
}

func prepareWorkspaceAppDefinition(params RequestCustomAppInstallationParams) (preparedWorkspaceAppDefinition, error) {
	catalogVisibility, err := registry.NormalizeCatalogVisibility(params.CatalogVisibility, true)
	if err != nil {
		return preparedWorkspaceAppDefinition{}, err
	}
	slug, err := registry.NormalizeDeveloperSlug(params.Slug)
	if err != nil {
		return preparedWorkspaceAppDefinition{}, err
	}
	if registry.IsProductAppSlug(slug) {
		return preparedWorkspaceAppDefinition{}, badRequest(
			"slug is reserved for a first-party App",
		)
	}
	name := strings.TrimSpace(params.Name)
	if name == "" {
		return preparedWorkspaceAppDefinition{}, badRequest("name is required")
	}
	manifestJSON, err := model.MarshalJSONObject("App manifest", params.Manifest)
	if err != nil {
		return preparedWorkspaceAppDefinition{}, badRequest("manifest must be a JSON object")
	}
	manifest, err := registry.ValidateDeveloperManifest(slug, "third_party", manifestJSON)
	if err != nil {
		return preparedWorkspaceAppDefinition{}, err
	}
	capability, err := registry.ParseStoredAppRuntimeCapability(manifest.Payload)
	if err != nil {
		return preparedWorkspaceAppDefinition{}, badRequest("manifest capabilities.runtime is invalid")
	}
	if !capability.Supported || !containsTrimmedString(capability.Events, registry.AppEventMessageMention) {
		return preparedWorkspaceAppDefinition{}, badRequest("manifest must declare the agentteam.message.mention App event")
	}
	reviewSnapshot := AppInstallationReviewSnapshot{
		RuntimeTransports: normalizedAppReviewTransports(capability.Transports),
		CallbackURL:       strings.TrimSpace(manifest.CallbackURL),
		CatalogVisibility: catalogVisibility,
	}
	appID, err := identifiers.NewEntityID("app")
	if err != nil {
		return preparedWorkspaceAppDefinition{}, fmt.Errorf("generate App app id: %w", err)
	}
	versionID, err := identifiers.NewEntityID("appver")
	if err != nil {
		return preparedWorkspaceAppDefinition{}, fmt.Errorf("generate App app version id: %w", err)
	}
	return preparedWorkspaceAppDefinition{
		slug:              slug,
		name:              name,
		description:       strings.TrimSpace(params.Description),
		avatarURL:         strings.TrimSpace(params.AvatarURL),
		manifest:          manifest,
		reviewSnapshot:    reviewSnapshot,
		catalogVisibility: catalogVisibility,
		appID:             appID,
		versionID:         versionID,
	}, nil
}

// withGeneratedAppCallbackSigningSecret prepares the one-time callback secret
// used by the privileged direct-install path. Member requests intentionally do
// not call this helper: rejected or withdrawn requests must never have runtime
// credentials to reveal.
func withGeneratedAppCallbackSigningSecret(prepared preparedWorkspaceAppDefinition) (preparedWorkspaceAppDefinition, error) {
	if !containsTrimmedString(prepared.reviewSnapshot.RuntimeTransports, serviceactor.RuntimeTransportKindCallback) {
		return prepared, nil
	}
	secret, err := secrets.NewToken(32)
	if err != nil {
		return preparedWorkspaceAppDefinition{}, fmt.Errorf("generate App callback signing secret: %w", err)
	}
	prepared.callbackSigningSecret = secret
	return prepared, nil
}

func (store *Store) registerPreparedWorkspaceAppDefinitionInTx(
	ctx context.Context,
	tx coredb.Tx,
	workspaceID string,
	creatorMemberID string,
	catalogVisibility string,
	prepared preparedWorkspaceAppDefinition,
	now time.Time,
) (appDefinitionRegistrationResult, error) {
	catalogVisibility, err := registry.NormalizeCatalogVisibility(catalogVisibility, true)
	if err != nil {
		return appDefinitionRegistrationResult{}, err
	}
	configJSON, err := model.MarshalJSONObject("app service definition config", installation.DefinitionSeedConfig{
		CallbackURL:           strings.TrimSpace(prepared.manifest.CallbackURL),
		AppKind:               "third_party",
		CallbackSigningSecret: strings.TrimSpace(prepared.callbackSigningSecret),
	})
	if err != nil {
		return appDefinitionRegistrationResult{}, err
	}
	if _, err := tx.Exec(ctx, `
INSERT INTO service_definitions (
    id, service_kind, installation_mode, slug, name, description, avatar_url, visibility,
    developer_id, latest_version, manifest, config, status, created_at, updated_at
) VALUES ($1, 'app', 'singleton', $2, $3, $4, $5, 'unlisted', '', $6, $7, $8, 'active', $9, $9)
`, prepared.appID, prepared.slug, prepared.name, prepared.description, prepared.avatarURL, prepared.manifest.Version, prepared.manifest.Payload, configJSON, now); err != nil {
		if isUniqueViolation(err) {
			return appDefinitionRegistrationResult{}, conflict("A App with this name already exists. Choose another name.")
		}
		return appDefinitionRegistrationResult{}, fmt.Errorf("create App definition: %w", err)
	}
	if _, err := tx.Exec(ctx, `
INSERT INTO extension_catalog_attributions (
    workspace_id, service_definition_id, created_by_member_id, catalog_visibility, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $5)
`, strings.TrimSpace(workspaceID), prepared.appID, strings.TrimSpace(creatorMemberID), catalogVisibility, now); err != nil {
		return appDefinitionRegistrationResult{}, fmt.Errorf("record App catalog creator: %w", err)
	}
	managerID, err := identifiers.NewEntityID("appmanager")
	if err != nil {
		return appDefinitionRegistrationResult{}, fmt.Errorf("generate initial App manager id: %w", err)
	}
	if _, err := tx.Exec(ctx, `
INSERT INTO extension_definition_managers (
    id, workspace_id, service_definition_id, member_id, status, version,
    granted_by_member_id, created_at, updated_at, ended_at
) VALUES ($1, $2, $3, $4, 'active', 1, $4, $5, $5, NULL)
`, managerID, strings.TrimSpace(workspaceID), prepared.appID, strings.TrimSpace(creatorMemberID), now); err != nil {
		return appDefinitionRegistrationResult{}, fmt.Errorf("record initial App manager: %w", err)
	}
	if _, err := tx.Exec(ctx, `
INSERT INTO extension_definition_versions (
    id, service_definition_id, version, manifest, callback_url, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $6)
`, prepared.versionID, prepared.appID, prepared.manifest.Version, prepared.manifest.Payload, prepared.manifest.CallbackURL, now); err != nil {
		return appDefinitionRegistrationResult{}, fmt.Errorf("create App manifest version: %w", err)
	}
	return appDefinitionRegistrationResult{
		appID:                 prepared.appID,
		callbackSigningSecret: prepared.callbackSigningSecret,
	}, nil
}

func appDefinitionAssociatedWithWorkspace(ctx context.Context, queryer coredb.QueryRower, workspaceID string, appID string) (bool, error) {
	var marker int
	err := queryer.QueryRow(ctx, `
SELECT 1
FROM extension_catalog_attributions
WHERE workspace_id = $1
  AND service_definition_id = $2
LIMIT 1
`, strings.TrimSpace(workspaceID), strings.TrimSpace(appID)).Scan(&marker)
	if coredb.IsNotFound(err) {
		return false, nil
	}
	if err != nil {
		return false, fmt.Errorf("load workspace App association: %w", err)
	}
	return true, nil
}

func appDefinitionAssociatedWorkspaceCount(ctx context.Context, queryer coredb.QueryRower, appID string) (int, error) {
	var count int
	if err := queryer.QueryRow(ctx, `
SELECT COUNT(*)
FROM (
    SELECT workspace_id
    FROM service_installations
    WHERE service_definition_id = $1
      AND service_kind = $2
    UNION
    SELECT workspace_id
    FROM service_installation_requests
    WHERE service_definition_id = $1
      AND service_kind = $2
) association
`, strings.TrimSpace(appID), serviceactor.KindApp).Scan(&count); err != nil {
		return 0, fmt.Errorf("count associated App workspaces: %w", err)
	}
	return count, nil
}

func (store *Store) removeUnreferencedAppDefinition(ctx context.Context, appID string) {
	_ = coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		result, err := tx.Exec(ctx, `
DELETE FROM service_definitions
WHERE id = $1
  AND service_kind = 'app'
  AND JSON_UNQUOTE(JSON_EXTRACT(COALESCE(config, JSON_OBJECT()), '$.appKind')) = 'third_party'
  AND visibility = 'unlisted'
  AND NOT EXISTS (
      SELECT 1
      FROM service_installation_requests request
      WHERE request.service_definition_id = service_definitions.id
  )
  AND NOT EXISTS (
      SELECT 1
      FROM service_installations installation
      WHERE installation.service_definition_id = service_definitions.id
  )
`, strings.TrimSpace(appID))
		if err != nil {
			return err
		}
		_ = result
		return nil
	})
}

func containsTrimmedString(values []string, expected string) bool {
	expected = strings.TrimSpace(expected)
	for _, value := range values {
		if strings.TrimSpace(value) == expected {
			return true
		}
	}
	return false
}
