package app

import (
	"context"
	"crypto/sha256"
	"database/sql"
	"encoding/hex"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	installationstore "github.com/tidbcloud/AgentTeam/internal/store/app/installation"
	"github.com/tidbcloud/AgentTeam/internal/store/app/runtime"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
)

type appServiceDefinitionSeed struct {
	ID                    string
	Slug                  string
	Name                  string
	Description           string
	AvatarURL             string
	Kind                  string
	Visibility            string
	LatestVersion         string
	Manifest              []byte
	Config                []byte
	CallbackURL           string
	CallbackSigningSecret string
	AvatarAssetID         string
	CreatedAt             time.Time
	UpdatedAt             time.Time
}

func (store *Store) installationsFromServiceActors(ctx context.Context, workspaceID string) ([]AppInstallationView, error) {
	summaries, err := store.serviceActorStore().ListInstallationSummaries(ctx, workspaceID, serviceactor.KindApp)
	if err != nil {
		return nil, err
	}
	installations := make([]AppInstallationView, 0, len(summaries))
	for _, summary := range summaries {
		if !serviceActorSummaryVisibleInAppManagement(summary) {
			continue
		}
		installation, err := installationViewFromServiceSummary(summary)
		if err != nil {
			return nil, err
		}
		installations = append(installations, installation)
	}
	return store.attachServiceActorInstallationState(ctx, installations)
}

func (store *Store) installationFromServiceActor(ctx context.Context, workspaceID string, installationID string) (AppInstallationView, error) {
	var zero AppInstallationView
	summary, err := store.serviceActorStore().LoadInstallationSummaryByKind(ctx, workspaceID, installationID, serviceactor.KindApp)
	if err != nil {
		if errors.Is(err, serviceactor.ErrNotFound) {
			return zero, ErrNotFound
		}
		return zero, err
	}
	installation, err := installationViewFromServiceSummary(summary)
	if err != nil {
		return zero, err
	}
	installations, err := store.attachServiceActorInstallationState(ctx, []AppInstallationView{installation})
	if err != nil {
		return zero, err
	}
	return installations[0], nil
}

func (store *Store) seedMissingServiceActor(ctx context.Context, workspaceID string, installationID string) error {
	return coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		storedInstallation, err := installationstore.LoadAppInstallationForUpdate(ctx, tx, workspaceID, installationID)
		if err != nil {
			return err
		}
		grantsByInstallation, err := store.serviceActorStore().InTx(tx).ListActiveGrantScopesByInstallationIDs(ctx, []string{storedInstallation.ID})
		if err != nil {
			return err
		}
		createdByMemberID, err := store.serviceActorWriteMemberID(ctx, tx, workspaceID, storedInstallation.CreatedByUserID, storedInstallation.MemberID)
		if err != nil {
			return err
		}
		return store.syncInstallationServiceActor(ctx, tx, storedInstallation, createdByMemberID, grantsByInstallation[storedInstallation.ID], serviceactorStatusFromEnabled(storedInstallation.Enabled), time.Now().UTC())
	})
}

func installationViewFromServiceSummary(summary serviceactor.InstallationSummary) (AppInstallationView, error) {
	runtimeCapability, err := pinnedRuntimeCapabilityView(summary.Installation)
	if err != nil {
		return AppInstallationView{}, fmt.Errorf("decode pinned app installation capability: %w", err)
	}
	return AppInstallationView{
		ID:                summary.Installation.ID,
		AppID:             summary.Definition.ID,
		AppSlug:           summary.Definition.Slug,
		AppName:           summary.Definition.Name,
		AvatarURL:         summary.Definition.AvatarURL,
		InstanceKey:       summary.Installation.InstanceKey,
		OwnershipKind:     summary.Installation.OwnershipKind,
		OwnerMemberID:     summary.Installation.OwnerMemberID,
		MemberID:          summary.Member.MemberID,
		Handle:            summary.Member.Handle,
		InstalledVersion:  summary.Installation.InstalledVersion,
		RuntimeCapability: runtimeCapability,
		Config:            decodeJSONObjectMap(summary.Installation.Config),
		Enabled:           summary.Installation.Status == serviceactor.StatusActive,
		CreatedAt:         summary.Installation.CreatedAt,
		UpdatedAt:         summary.Installation.UpdatedAt,
	}, nil
}

func (store *Store) attachServiceActorInstallationState(ctx context.Context, installations []AppInstallationView) ([]AppInstallationView, error) {
	if len(installations) == 0 {
		return installations, nil
	}
	installationIDs := make([]string, 0, len(installations))
	for _, installation := range installations {
		installationIDs = append(installationIDs, installation.ID)
	}

	grantsByInstallation, err := store.serviceActorStore().ListActiveGrantScopesByInstallationIDs(ctx, installationIDs)
	if err != nil {
		return nil, err
	}
	connectionsByInstallation, err := store.serviceActorStore().ListActiveConnectionsByInstallationIDs(ctx, installationIDs)
	if err != nil {
		return nil, err
	}
	for idx := range installations {
		grants := grantsByInstallation[installations[idx].ID]
		if grants == nil {
			grants = []string{}
		}
		installations[idx].Grants = grants

		connections := connectionsByInstallation[installations[idx].ID]
		installations[idx].Connections = serviceConnectionsToAppViews(connections)
	}
	return installations, nil
}

func serviceConnectionToAppView(connection serviceactor.Connection) runtime.ConnectionView {
	return runtime.ConnectionView{
		ID:                 connection.ID,
		ExternalSubjectRef: connection.ExternalSubjectRef,
		DisplayLabel:       connection.DisplayLabel,
		Enabled:            connection.Status == serviceactor.StatusActive,
		CreatedAt:          connection.CreatedAt,
		UpdatedAt:          connection.UpdatedAt,
	}
}

func serviceConnectionsToAppViews(connections []serviceactor.Connection) []runtime.ConnectionView {
	if len(connections) == 0 {
		return []runtime.ConnectionView{}
	}
	views := make([]runtime.ConnectionView, 0, len(connections))
	for _, connection := range connections {
		views = append(views, serviceConnectionToAppView(connection))
	}
	return views
}

func (store *Store) syncInstallationServiceActor(ctx context.Context, tx coredb.Tx, storedInstallation model.StoredAppInstallation, createdByMemberID string, grantScopes []string, status string, now time.Time) error {
	if strings.TrimSpace(status) == "" {
		if storedInstallation.Enabled {
			status = serviceactor.StatusActive
		} else {
			status = serviceactor.StatusDisabled
		}
	}
	definition, err := store.syncAppServiceDefinition(ctx, tx, storedInstallation.AppID, now)
	if err != nil {
		return err
	}
	installationMetadata, err := installationstore.LoadServiceInstallationMetadata(ctx, tx, storedInstallation.AppID, storedInstallation.InstalledVersion)
	if err != nil {
		return err
	}
	txStore := store.serviceActorStore().InTx(tx)
	installation, err := txStore.UpsertInstallation(ctx, serviceactor.InstallationUpsert{
		ID:                  storedInstallation.ID,
		WorkspaceID:         storedInstallation.WorkspaceID,
		ServiceDefinitionID: definition.ID,
		ServiceKind:         serviceactor.KindApp,
		InstanceKey:         storedAppInstallationInstanceKey(storedInstallation),
		Status:              status,
		InstalledVersion:    storedInstallation.InstalledVersion,
		Config:              storedInstallation.Config,
		Metadata:            installationMetadata,
		CreatedByMemberID:   createdByMemberID,
		Now:                 now,
	})
	if err != nil {
		return err
	}
	memberRecordID, err := identifiers.NewEntityID("svcmem")
	if err != nil {
		return fmt.Errorf("generate service member record id: %w", err)
	}
	if _, err := txStore.UpsertMember(ctx, serviceactor.MemberUpsert{
		ID:                    memberRecordID,
		WorkspaceID:           storedInstallation.WorkspaceID,
		ServiceDefinitionID:   definition.ID,
		ServiceInstallationID: installation.ID,
		MemberID:              storedInstallation.MemberID,
		ServiceKind:           serviceactor.KindApp,
		Handle:                storedInstallation.Handle,
		DisplayName: serviceDisplayName(
			storedInstallation.MemberDisplayName,
			storedInstallation.AppName,
			definition.Name,
		),
		AvatarURL: definition.AvatarURL,
		Status:    status,
		Now:       now,
	}); err != nil {
		return err
	}
	if err := txStore.SyncGrantScopes(ctx, installation, createdByMemberID, grantScopes, now); err != nil {
		return err
	}
	personalHandle := func(defaultName string) string { return defaultName }
	if installation.OwnershipKind == serviceactor.OwnershipKindPersonal {
		ownerMemberID := strings.TrimPrefix(installation.InstanceKey, serviceactor.PersonalInstanceKeyPrefix)
		personalHandle = func(defaultName string) string {
			return installationstore.PersonalHandle(defaultName, ownerMemberID)
		}
	}
	if err := installationstore.SyncDefaultExtensions(
		ctx,
		tx,
		txStore,
		installation,
		createdByMemberID,
		personalHandle,
		func(installationID string, domain string) string {
			return installationExtensionID(installationID, domain)
		},
		func() error {
			return store.RunWorkspaceAppCatalogReconcileHook(ctx, tx, installation.WorkspaceID, now)
		},
		now,
	); err != nil {
		return err
	}
	if status == serviceactor.StatusDisabled {
		if err := txStore.DisableConnectionsForInstallation(ctx, installation.ID, now); err != nil {
			return err
		}
		if err := txStore.DisableWebhookEndpointsForInstallation(ctx, installation.ID, now); err != nil {
			return err
		}
		if err := store.RunWorkspaceAppCatalogReconcileHook(ctx, tx, installation.WorkspaceID, now); err != nil {
			return err
		}
	}
	return nil
}

func storedAppInstallationInstanceKey(
	installation model.StoredAppInstallation,
) string {
	if instanceKey := strings.TrimSpace(installation.InstanceKey); instanceKey != "" {
		return instanceKey
	}
	return serviceactor.DefaultInstanceKey
}

func (store *Store) syncAppServiceDefinition(ctx context.Context, tx coredb.Tx, appID string, now time.Time) (serviceactor.Definition, error) {
	seed, err := loadAppServiceDefinitionSeed(ctx, tx, appID)
	if err != nil {
		return serviceactor.Definition{}, err
	}
	txStore := store.serviceActorStore().InTx(tx)
	definition, err := txStore.UpsertDefinition(ctx, serviceactor.DefinitionUpsert{
		ID:            seed.ID,
		ServiceKind:   serviceactor.KindApp,
		Slug:          seed.Slug,
		Name:          seed.Name,
		Description:   seed.Description,
		AvatarURL:     seed.AvatarURL,
		Visibility:    seed.Visibility,
		LatestVersion: seed.LatestVersion,
		Manifest:      seed.Manifest,
		Config:        seed.Config,
		Status:        serviceactor.StatusActive,
		Now:           now,
	})
	if err != nil {
		return serviceactor.Definition{}, err
	}
	if err := installationstore.SyncDefinitionCapabilities(ctx, txStore, definition, seed.Manifest, now); err != nil {
		return serviceactor.Definition{}, err
	}
	return definition, nil
}

func (store *Store) upsertAppServiceConnection(ctx context.Context, tx coredb.Tx, installationID string, connectionID string, externalSubjectRef string, displayLabel string, credentialsJSON []byte, configJSON []byte, createdByUserID string, now time.Time) (serviceactor.Connection, error) {
	txStore := store.serviceActorStore().InTx(tx)
	installation, err := txStore.LoadInstallationForUpdate(ctx, installationID)
	if err != nil {
		return serviceactor.Connection{}, err
	}
	createdByMemberID, err := store.serviceActorWriteMemberID(ctx, tx, installation.WorkspaceID, createdByUserID, installation.CreatedByMemberID)
	if err != nil {
		return serviceactor.Connection{}, err
	}
	connection, found, err := txStore.LoadActiveConnectionForUpdate(ctx, installationID, externalSubjectRef)
	if err != nil {
		return serviceactor.Connection{}, err
	}
	if found {
		connectionID = connection.ID
	}
	return txStore.UpsertConnection(ctx, serviceactor.ConnectionUpsert{
		ID:                    connectionID,
		WorkspaceID:           installation.WorkspaceID,
		ServiceDefinitionID:   installation.ServiceDefinitionID,
		ServiceInstallationID: installation.ID,
		ServiceKind:           installation.ServiceKind,
		ExternalSubjectRef:    externalSubjectRef,
		DisplayLabel:          displayLabel,
		Credentials:           credentialsJSON,
		Config:                configJSON,
		Status:                serviceactor.StatusActive,
		CreatedByMemberID:     createdByMemberID,
		Now:                   now,
	})
}

func (store *Store) syncAppServiceWebhookEndpoint(ctx context.Context, tx coredb.Tx, connection serviceactor.Connection, credentialsJSON []byte, rawCredentials map[string]any, now time.Time) error {
	if strings.TrimSpace(connection.ID) == "" {
		return nil
	}
	txStore := store.serviceActorStore().InTx(tx)
	featureOwned, err := appUsesFeatureOwnedSetup(
		ctx,
		tx,
		connection.ServiceDefinitionID,
	)
	if err != nil {
		return err
	}
	if featureOwned {
		// Feature-owned setup never opts into the generic App webhook
		// substrate, even if a malformed credential payload contains a
		// webhook_secret field.
		return txStore.DisableWebhookEndpointsForInstallation(
			ctx,
			connection.ServiceInstallationID,
			now,
		)
	}
	signingSecretCiphertext, secretMaterial, hasSecret, err := store.appWebhookEndpointSecret(credentialsJSON, rawCredentials)
	if err != nil {
		return err
	}
	if !hasSecret {
		return txStore.DisableWebhookEndpointsForConnection(ctx, connection.ID, serviceactor.EndpointKindProviderWebhook, now)
	}

	var serviceSlug string
	if err := tx.QueryRow(ctx, `
SELECT slug
FROM service_definitions
WHERE id = $1
  AND service_kind = $2
`, strings.TrimSpace(connection.ServiceDefinitionID), strings.TrimSpace(connection.ServiceKind)).Scan(&serviceSlug); err != nil {
		if coredb.IsNotFound(err) {
			return serviceactor.ErrNotFound
		}
		return fmt.Errorf("load service webhook endpoint definition: %w", err)
	}
	serviceSlug = strings.TrimSpace(serviceSlug)
	config, err := model.MarshalJSONObject("service webhook endpoint config", map[string]any{
		"provider":       serviceSlug,
		"secretField":    "webhook_secret",
		"secretMaterial": secretMaterial,
	})
	if err != nil {
		return err
	}
	_, err = txStore.UpsertWebhookEndpoint(ctx, serviceactor.WebhookEndpointUpsert{
		WorkspaceID:             connection.WorkspaceID,
		ServiceDefinitionID:     connection.ServiceDefinitionID,
		ServiceInstallationID:   connection.ServiceInstallationID,
		ServiceConnectionID:     connection.ID,
		ServiceKind:             connection.ServiceKind,
		EndpointKind:            serviceactor.EndpointKindProviderWebhook,
		ExternalSubjectRef:      connection.ExternalSubjectRef,
		CallbackPath:            "/api/apps/" + serviceSlug + "/webhook",
		SigningSecretCiphertext: signingSecretCiphertext,
		SecretVersion:           1,
		Config:                  config,
		Status:                  serviceactor.StatusActive,
		CreatedByMemberID:       connection.CreatedByMemberID,
		Now:                     now,
		ActiveEndpointKey:       serviceWebhookEndpointActiveKey(connection.ID, serviceactor.EndpointKindProviderWebhook),
	})
	return err
}

func (store *Store) appWebhookEndpointSecret(credentialsJSON []byte, rawCredentials map[string]any) (string, string, bool, error) {
	encryptedEnvelope := storedCredentialsAreEncrypted(credentialsJSON)
	credentials := rawCredentials
	if credentials == nil {
		plaintext, err := store.decryptStoredAppCredentialsJSON(credentialsJSON)
		if err != nil {
			return "", "", false, err
		}
		var decoded map[string]any
		if err := model.UnmarshalJSONObject("app webhook credentials", plaintext, &decoded); err != nil {
			return "", "", false, err
		}
		credentials = decoded
	}
	webhookSecret, _ := credentials["webhook_secret"].(string)
	webhookSecret = strings.TrimSpace(webhookSecret)
	if webhookSecret == "" {
		return "", "", false, nil
	}
	if !encryptedEnvelope {
		return "", "", false, fmt.Errorf("app connection credentials must use the canonical encrypted envelope")
	}
	return strings.TrimSpace(string(credentialsJSON)), "encrypted_connection_credentials", true, nil
}

func storedCredentialsAreEncrypted(credentialsJSON []byte) bool {
	var envelope model.CredentialEnvelope
	if err := model.UnmarshalJSONObject("app credential envelope", credentialsJSON, &envelope); err != nil {
		return false
	}
	return strings.TrimSpace(envelope.Storage) == CredentialStorageScheme && strings.TrimSpace(envelope.Ciphertext) != ""
}

func serviceWebhookEndpointActiveKey(connectionID string, endpointKind string) string {
	keyKind := strings.TrimSpace(endpointKind)
	if keyKind == serviceactor.EndpointKindProviderWebhook {
		keyKind = "provider-webhook"
	}
	digest := sha256.Sum256([]byte("service-webhook-endpoint/" + strings.TrimSpace(connectionID) + "/" + keyKind))
	return hex.EncodeToString(digest[:])
}

func (store *Store) serviceActorWriteMemberID(ctx context.Context, queryer coredb.QueryRower, workspaceID string, userID string, fallbackMemberID string) (string, error) {
	trimmedUserID := strings.TrimSpace(userID)
	if trimmedUserID == "" {
		return strings.TrimSpace(fallbackMemberID), nil
	}
	// The creator attribution may already be a workspace human member id (the
	// canonical created_by_member_id column). Resolve it directly before
	// treating the value as a user id, so a member-id attribution does not
	// silently fall back to the service member.
	var memberID string
	err := queryer.QueryRow(ctx, `
SELECT id
FROM members
WHERE workspace_id = $1
  AND id = $2
  AND type = 'human'
  AND status = 'active'
LIMIT 1
`, strings.TrimSpace(workspaceID), trimmedUserID).Scan(&memberID)
	if err == nil {
		return strings.TrimSpace(memberID), nil
	}
	if err != nil && !coredb.IsNotFound(err) {
		return "", fmt.Errorf("load service actor writer member: %w", err)
	}
	err = queryer.QueryRow(ctx, `
SELECT id
FROM members
WHERE workspace_id = $1
  AND user_id = $2
  AND type = 'human'
  AND status = 'active'
LIMIT 1
`, strings.TrimSpace(workspaceID), trimmedUserID).Scan(&memberID)
	if err != nil {
		if coredb.IsNotFound(err) && strings.TrimSpace(fallbackMemberID) != "" {
			return strings.TrimSpace(fallbackMemberID), nil
		}
		return "", fmt.Errorf("load service actor writer member: %w", err)
	}
	return strings.TrimSpace(memberID), nil
}

func loadAppServiceDefinitionSeed(ctx context.Context, queryer coredb.QueryRower, appID string) (appServiceDefinitionSeed, error) {
	row := queryer.QueryRow(ctx, `
SELECT
    app.id,
    app.slug,
    app.name,
    app.description,
    app.avatar_url,
    COALESCE(JSON_UNQUOTE(JSON_EXTRACT(COALESCE(app.config, JSON_OBJECT()), '$.appKind')), ''),
    app.visibility,
    app.latest_version,
    version.manifest,
    version.callback_url,
    COALESCE(JSON_UNQUOTE(JSON_EXTRACT(COALESCE(app.config, JSON_OBJECT()), '$.callbackSigningSecret')), ''),
    COALESCE(JSON_UNQUOTE(JSON_EXTRACT(COALESCE(app.config, JSON_OBJECT()), '$.avatarAssetId')), ''),
    app.created_at,
    app.updated_at
FROM service_definitions app
LEFT JOIN extension_definition_versions version
  ON version.service_definition_id = app.id
 AND version.version = app.latest_version
WHERE app.id = $1
`, strings.TrimSpace(appID))

	var seed appServiceDefinitionSeed
	var manifest sql.NullString
	var callbackURL sql.NullString
	if err := row.Scan(
		&seed.ID,
		&seed.Slug,
		&seed.Name,
		&seed.Description,
		&seed.AvatarURL,
		&seed.Kind,
		&seed.Visibility,
		&seed.LatestVersion,
		&manifest,
		&callbackURL,
		&seed.CallbackSigningSecret,
		&seed.AvatarAssetID,
		&seed.CreatedAt,
		&seed.UpdatedAt,
	); err != nil {
		if coredb.IsNotFound(err) {
			return appServiceDefinitionSeed{}, ErrNotFound
		}
		return appServiceDefinitionSeed{}, fmt.Errorf("load service definition seed: %w", err)
	}
	if manifest.Valid {
		seed.Manifest = []byte(manifest.String)
	}
	seed.Kind = strings.TrimSpace(seed.Kind)
	if callbackURL.Valid {
		seed.CallbackURL = strings.TrimSpace(callbackURL.String)
	}
	seed.CallbackSigningSecret = strings.TrimSpace(seed.CallbackSigningSecret)
	config, err := model.MarshalJSONObject("app service definition config", installationstore.DefinitionSeedConfig{
		CallbackURL:           seed.CallbackURL,
		AppKind:               seed.Kind,
		CallbackSigningSecret: seed.CallbackSigningSecret,
		AvatarAssetID:         seed.AvatarAssetID,
	})
	if err != nil {
		return appServiceDefinitionSeed{}, err
	}
	seed.Config = config
	return seed, nil
}

func serviceactorStatusFromEnabled(enabled bool) string {
	if enabled {
		return serviceactor.StatusActive
	}
	return serviceactor.StatusDisabled
}

func serviceDisplayName(values ...string) string {
	for _, value := range values {
		if trimmed := strings.TrimSpace(value); trimmed != "" {
			return trimmed
		}
	}
	return "Service"
}
