package app

import (
	"context"
	"fmt"
	"log/slog"
	"net/http"
	"strings"
	"sync"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/archive"
	"github.com/tidbcloud/AgentTeam/internal/core/app/capabilities"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/objectstorage"
	"github.com/tidbcloud/AgentTeam/internal/store/app/installation"
	"github.com/tidbcloud/AgentTeam/internal/store/app/registry"
	"github.com/tidbcloud/AgentTeam/internal/store/app/webhooks"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/plan"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
	"github.com/tidbcloud/AgentTeam/internal/store/taskboard"
)

// Store owns app-ecosystem persistence and business logic.
type Store struct {
	db                             coredb.DB
	logger                         *slog.Logger
	appCapabilitySigner            *capabilities.Signer
	appCredentialStorageKey        []byte
	legacyAppCredentialStorageKeys [][]byte
	appHTTPClient                  *http.Client
	publicBaseURL                  string
	githubAPIBaseURL               string
	archiveStorage                 archive.Storage
	mediaObjectStorage             objectstorage.Storage

	mediaPublicBaseURL               string
	mediaObjectPrefix                string
	hotMessageRetentionDays          int
	options                          Options
	registry                         *registry.Store
	installation                     *installation.Store
	webhooks                         *webhooks.Store
	serviceActor                     *serviceactor.Store
	messages                         *message.Store
	moderationKeywords               moderationKeywordCache
	appNotificationRegistryMu        sync.RWMutex
	appNotificationRecipients        map[string]AppNotificationRecipientResolver
	appNotificationInternalServices  map[string]appNotificationInternalService
	builtinDeliveryMutationFaultMu   sync.RWMutex
	builtinDeliveryMutationFaultHook func(context.Context, AppNotificationMutationCommand) error
	committedMessageFanoutMu         sync.RWMutex
	committedMessageFanout           CommittedMessageFanout
	missingFanoutLogOnce             sync.Once
	missingProjectionFanoutLogOnce   sync.Once
	taskChangeSinkMu                 sync.RWMutex
	taskChangeSink                   taskboard.TaskChangeSink
	agentAppCatalogChangeHook        AgentAppCatalogChangeHook
	workspaceAppCatalogReconcileHook WorkspaceAppCatalogReconcileHook
}

// AgentAppCatalogChangeHook runs INSIDE an Agent app Enable/Disable transaction,
// after the authorization write, so the Extension Catalog version bump + update
// can be applied atomically (stage-6 final-review blocker 2). It receives the
// transaction so the Extension store can participate in the caller's transaction
// (ApplyExtensionCatalogChange). A nil hook disables catalog coordination.
type AgentAppCatalogChangeHook func(ctx context.Context, tx coredb.Tx, workspaceID, agentID string, now time.Time) error

// WorkspaceAppCatalogReconcileHook runs INSIDE a Workspace-scoped App lifecycle
// mutation transaction and records that every Agent Catalog in the Workspace may
// need asynchronous reconciliation. It must be coalescing/idempotent: callers use
// it for App install/uninstall, connection enable/disable, and Extension
// selection changes where enumerating every Agent inside the source transaction
// would make the write path proportional to Workspace Agent count.
type WorkspaceAppCatalogReconcileHook func(ctx context.Context, tx coredb.Tx, workspaceID string, now time.Time) error

// RunAgentAppCatalogChangeHook invokes the same-transaction Extension Catalog
// coordination hook after an Agent app authorization write (stage-6 final-review
// blocker 2). Exported so wire-up can call it from a service-layer orchestrator
// if needed; the app Enable/Disable paths call it internally. No-op when unset.
func (store *Store) RunAgentAppCatalogChangeHook(ctx context.Context, tx coredb.Tx, workspaceID, agentID string, now time.Time) error {
	if store == nil || store.agentAppCatalogChangeHook == nil {
		return nil
	}
	return store.agentAppCatalogChangeHook(ctx, tx, workspaceID, agentID, now)
}

// RunWorkspaceAppCatalogReconcileHook records a coalesced Workspace dirty job
// after an App/Connection/Extension-selection lifecycle mutation. No-op when
// unset so tests and deployments without the Extension catalog service keep
// working.
func (store *Store) RunWorkspaceAppCatalogReconcileHook(ctx context.Context, tx coredb.Tx, workspaceID string, now time.Time) error {
	if store == nil || store.workspaceAppCatalogReconcileHook == nil {
		return nil
	}
	return store.workspaceAppCatalogReconcileHook(ctx, tx, strings.TrimSpace(workspaceID), now)
}

// SetAgentAppCatalogChangeHook configures the same-tx Extension Catalog change
// hook (stage-6 final-review blocker 2a). It is set by the composition root
// after the Extension service is built (the app Store is constructed before the
// Extension registry/service, so the hook is wired post-construction), so an
// Agent app Enable/Disable advances the Extension catalog atomically.
func (store *Store) SetAgentAppCatalogChangeHook(hook AgentAppCatalogChangeHook) {
	if store == nil {
		return
	}
	store.agentAppCatalogChangeHook = hook
}

// SetWorkspaceAppCatalogReconcileHook configures the coalesced Workspace dirty
// job hook. It complements SetAgentAppCatalogChangeHook: single-Agent explicit
// authorization mutations still reconcile synchronously in their transaction,
// while Workspace-scoped App lifecycle mutations only enqueue one async job.
func (store *Store) SetWorkspaceAppCatalogReconcileHook(hook WorkspaceAppCatalogReconcileHook) {
	if store == nil {
		return
	}
	store.workspaceAppCatalogReconcileHook = hook
}

// SetBuiltinDeliveryMutationFaultHook installs a default-off, narrowly scoped
// fault seam for exercising durable internal App action recovery. The hook is
// consulted only by MutateAppNotificationForBuiltinDelivery before a terminal
// mutation transaction begins; it must not mutate state or claim success.
func (store *Store) SetBuiltinDeliveryMutationFaultHook(hook func(context.Context, AppNotificationMutationCommand) error) {
	if store == nil {
		return
	}
	store.builtinDeliveryMutationFaultMu.Lock()
	store.builtinDeliveryMutationFaultHook = hook
	store.builtinDeliveryMutationFaultMu.Unlock()
}

func (store *Store) runBuiltinDeliveryMutationFaultHook(ctx context.Context, command AppNotificationMutationCommand) error {
	if store == nil {
		return nil
	}
	store.builtinDeliveryMutationFaultMu.RLock()
	hook := store.builtinDeliveryMutationFaultHook
	store.builtinDeliveryMutationFaultMu.RUnlock()
	if hook == nil {
		return nil
	}
	return hook(ctx, command)
}

type moderationKeywordCache struct {
	mu        sync.Mutex
	expiresAt time.Time
	keywords  []string
}

// Options contains cross-domain dependencies needed by app-ecosystem storage.
type Options struct {
	Logger                          *slog.Logger
	AppCapabilitySigner             *capabilities.Signer
	AppCredentialStorageKey         []byte
	LegacyAppCredentialStorageKeys  [][]byte
	HTTPClient                      *http.Client
	PublicBaseURL                   string
	GitHubAPIBaseURL                string
	ArchiveStorage                  archive.Storage
	MediaObjectStorage              objectstorage.Storage
	MediaPublicBaseURL              string
	MediaObjectPrefix               string
	HotMessageRetentionDays         int
	AppNotificationInternalServices []AppNotificationInternalServiceRegistration
	TaskChangeSink                  taskboard.TaskChangeSink

	RequireWorkspaceAccess            func(context.Context, string, string) error
	CheckWorkspaceQuotaInTx           func(context.Context, coredb.Tx, string, plan.MetricKey, int64) error
	CheckWorkspaceAgentOwnerQuotaInTx func(context.Context, coredb.Tx, string, string, int64) error

	// DeleteAttachmentDerivatives overrides the default derivative cleanup
	// callback for the message store. When nil, the app Store wires it to
	// store.deleteAttachmentDerivative (which calls attachmentStore().DeleteAttachmentThumbnail).
	// Tests inject a recording callback to verify post-commit cleanup behavior.
	DeleteAttachmentDerivatives func(ctx context.Context, workspaceID string, attachmentID string)

	// AgentAppCatalogChangeHook runs inside an Agent app Enable/Disable
	// transaction after the authorization write, so the Extension Catalog change
	// is applied atomically (stage-6 final-review blocker 2). nil = disabled.
	AgentAppCatalogChangeHook AgentAppCatalogChangeHook
	// WorkspaceAppCatalogReconcileHook runs inside Workspace App lifecycle
	// transactions and enqueues one coalesced async reconcile job. nil = disabled.
	WorkspaceAppCatalogReconcileHook WorkspaceAppCatalogReconcileHook
}

// New constructs an app-ecosystem Store.
func New(db coredb.DB, options Options) *Store {
	internalServiceRegistrations := append([]AppNotificationInternalServiceRegistration(nil), options.AppNotificationInternalServices...)
	// Keep only the hashed registration contract on Store. Options remains on
	// the Store for quota callbacks, so retaining this slice would also retain
	// every trusted-ingress plaintext secret for the process lifetime.
	options.AppNotificationInternalServices = nil
	httpClient := options.HTTPClient
	if httpClient == nil {
		httpClient = &http.Client{Timeout: 10 * time.Second}
	}
	githubAPIBaseURL := strings.TrimRight(strings.TrimSpace(options.GitHubAPIBaseURL), "/")
	if githubAPIBaseURL == "" {
		githubAPIBaseURL = "https://api.github.com"
	}
	if options.CheckWorkspaceQuotaInTx == nil {
		options.CheckWorkspaceQuotaInTx = plan.New(db).CheckWorkspaceQuotaInTx
	}
	if options.CheckWorkspaceAgentOwnerQuotaInTx == nil {
		options.CheckWorkspaceAgentOwnerQuotaInTx = plan.New(db).CheckWorkspaceAgentOwnerQuotaInTx
	}
	store := &Store{
		db:                               db,
		logger:                           options.Logger,
		appCapabilitySigner:              options.AppCapabilitySigner,
		appCredentialStorageKey:          append([]byte(nil), options.AppCredentialStorageKey...),
		legacyAppCredentialStorageKeys:   cloneCredentialStorageKeys(options.LegacyAppCredentialStorageKeys),
		appHTTPClient:                    httpClient,
		publicBaseURL:                    strings.TrimRight(strings.TrimSpace(options.PublicBaseURL), "/"),
		githubAPIBaseURL:                 githubAPIBaseURL,
		archiveStorage:                   options.ArchiveStorage,
		mediaObjectStorage:               options.MediaObjectStorage,
		mediaPublicBaseURL:               options.MediaPublicBaseURL,
		mediaObjectPrefix:                options.MediaObjectPrefix,
		hotMessageRetentionDays:          options.HotMessageRetentionDays,
		options:                          options,
		appNotificationRecipients:        map[string]AppNotificationRecipientResolver{},
		appNotificationInternalServices:  map[string]appNotificationInternalService{},
		taskChangeSink:                   options.TaskChangeSink,
		agentAppCatalogChangeHook:        options.AgentAppCatalogChangeHook,
		workspaceAppCatalogReconcileHook: options.WorkspaceAppCatalogReconcileHook,
	}
	for _, registration := range internalServiceRegistrations {
		normalized, err := normalizeAppNotificationInternalService(registration)
		if err != nil {
			panic(fmt.Errorf("configure internal App notification service %q: %w", registration.IdentityID, err))
		}
		if err := store.registerAppNotificationService(normalized); err != nil {
			panic(fmt.Errorf("configure internal App notification service %q: %w", registration.IdentityID, err))
		}
	}
	store.registry = registry.New(store.db)
	store.installation = installation.New(store.db)
	store.webhooks = webhooks.New(store.db)
	store.serviceActor = serviceactor.New(store.db)
	store.messages = store.newMessageStore()
	return store
}
