package services

import (
	"context"
	"database/sql"
	"log"
	"os"
	"sync"
	"time"

	"github.com/nexuscrm/analytics"
	"github.com/nexuscrm/auth"
	"github.com/nexuscrm/automation"
	"github.com/nexuscrm/automation/autonumber"
	"github.com/nexuscrm/events"
	pkgerrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/logging"
	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/metadata"
	"github.com/nexuscrm/outbound/externaldelivery"
	"github.com/nexuscrm/permissions"
	"github.com/nexuscrm/records"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/approvalir"
)

// InfrastructureDeps holds all infrastructure dependencies that must be
// injected from the composition root (cmd/server). This keeps the application
// layer free of infrastructure imports.
type InfrastructureDeps struct {
	// Raw SQL DB handle — needed by FormulaViewService for DDL operations
	DB *sql.DB

	// RollupMaintenanceFence validates the operator's exact serving-state
	// precondition and keeps deployment/apply changes fenced for the complete
	// recalculation. The live server composition root owns this process and pack
	// identity; headless service graphs leave it nil and fail closed.
	RollupMaintenanceFence RollupMaintenanceFence

	FieldProjectionAdmission *permissions.FieldProjectionAdmissionLimits

	// Transaction management
	TxManager ports.TransactionManager
	// Shared transaction-bound Permission State revision authority.
	PermissionStateAuthority records.PermissionStateAuthority

	// Durable parent-lane admission. BuildInfrastructureDeps wires the production
	// SQL-backed controller; records tests can inject fakes through the same seam.
	ParentLaneAdmission records.ParentLaneAdmissionController

	// Repositories (all typed as domain port interfaces)
	UserRepo       ports.UserRepository
	SystemRepo     ports.SystemRepository
	BulkRepo       ports.BulkRepository
	SchemaRepo     ports.SchemaRepository
	MetadataRepo   ports.MetadataRepository
	SessionRepo    ports.SessionRepository
	PermissionRepo ports.PermissionRepository
	// EffectiveSnapshotRepo is the additive aggregate read capability used by
	// production authorization composition. Keeping it separate prevents a
	// legacy PermissionRepository from satisfying the strong path by accident.
	EffectiveSnapshotRepo ports.EffectivePermissionSnapshotRepository
	OAuthRepo             ports.OAuthRepository
	ApiTokenRepo          ports.ApiTokenRepository
	QueryRepo             ports.QueryRepository
	RecordRepo            ports.RecordRepository
	OutboxRepo            ports.OutboxRepository
	// Optional worker-side outbox repository. When set, foreground enqueue
	// remains on OutboxRepo so it can join the caller's transaction, while
	// background drain/claim/bookkeeping uses this isolated repository.
	OutboxWorkerRepo            ports.OutboxRepository
	RecordEventOutboxRepo       ports.OutboxRepository
	RecordEventOutboxWorkerRepo ports.OutboxRepository
	RollupRepo                  ports.RollupRepository

	// EventBus — if set, used instead of creating a real EventBus.
	// Pass events.NoOpEventBus for headless control-plane mode.
	// nil = create real EventBus with metrics (default server behavior).
	EventBus ports.EventPublisher

	// Metrics callbacks (decoupled from middleware layer)
	OnEventDropped           func(eventType string)
	OnQueueUtilizationChange func(current float64)

	// Host/runtime-owned Flow actionCall registrations supplied by a composition
	// root with the matching runtime dependencies wired.
	HostFlowActionCallRegistrations []automation.FlowActionCallRegistration

	// Product-owned decoder supplied by the runtime composition root. nil keeps
	// app-blind and products without an approval grammar valid.
	ApprovalDefinitionDecoder approvalir.DefinitionDecoder
	// Product-owned pack identities whose approval topology is proven before
	// serving. Definitions outside this set retain compatibility repair.
	ApprovalQueryOnlyOwnerPacks []string
}

const (
	validationProjectionRefreshTimeout = 10 * time.Minute
)

// ServiceManager assembles headless setup/control-plane services. Runtime
// processes own dispatch worker lifecycles; this graph retains metadata refresh.
type ServiceManager struct {
	deps   InfrastructureDeps
	Logger *logging.Logger

	// Core Services
	TxManager          ports.TransactionManager
	EventBus           ports.EventPublisher
	Schema             *metadata.SchemaManager
	Metadata           *metadata.MetadataService
	ListViews          *metadata.ListViewService
	Permissions        *permissions.PermissionService
	PermissionAdmin    *permissions.AdminService
	Auth               *auth.AuthService
	OAuth              *auth.OAuthService
	ApiToken           *auth.ApiTokenService
	QuerySvc           ports.QueryService
	ReportSvc          *analytics.ReportService
	Persistence        *records.PersistenceService
	ActionSvc          *automation.ActionService
	ExternalDelivery   *externaldelivery.Store
	FlowExecutor       *automation.FlowExecutor
	FlowInterviewSvc   *automation.FlowInterviewService
	Approval           *automation.ApprovalService
	System             *SystemManager
	Validation         *metadata.ValidationService
	Outbox             *events.OutboxService
	FlowTriggerSvc     *automation.FlowTriggerListener
	AutoNumberEngine   autonumber.Engine
	AutoNumberMetadata *autonumber.MetadataSource
	Rollup             *analytics.RollupService
	SecurityValidator  *analytics.SecurityValidator
	FormulaViewSvc     *analytics.FormulaViewService
	BulkSvc            *records.BulkService

	// Neutral API contracts exposed to dialect handlers. The historical engine
	// services above may still use legacy carrier types internally; these fields
	// are the sealed surface new dialect work should consume.
	NeutralQuery                     contract.Query
	NeutralRecords                   contract.Records
	NeutralRecordMutationCoordinator contract.RecordMutationCoordinator
	NeutralRecycleBin                contract.RecycleBin
	NeutralAuditLog                  contract.AuditLog
	NeutralMetadata                  contract.Metadata
	NeutralListViewAuthoring         contract.ListViewAuthoring
	NeutralMetadataAuthoring         contract.MetadataAuthoring
	NeutralMetadataDeploy            contract.MetadataDeploy
	NeutralApplicationNavigation     contract.ApplicationNavigation
	NeutralMetadataMaintenance       contract.MetadataMaintenance
	NeutralRollupMaintenance         contract.RollupMaintenance
	NeutralBulkJobs                  contract.BulkJobs
	NeutralBulkIngest                contract.BulkIngest
	NeutralReports                   contract.Reports
	NeutralFolderAccess              contract.FolderAccess
	NeutralResourceAccess            contract.ResourceAccess
	NeutralUserSessions              contract.UserSessions
	NeutralPolicy                    contract.Policy
	NeutralNotifications             contract.Notifications
	NeutralPermissionAdmin           contract.PermissionAdmin
	NeutralConnectorSyncStatus       contract.ConnectorSyncStatus
	NeutralConnectorWebhookSecrets   contract.ConnectorWebhookSecrets
	NeutralFlowExecutor              contract.FlowExecutor
	NeutralFlowCatalog               contract.FlowCatalog
	NeutralFlowInterviews            contract.FlowInterviews
	NeutralFlowAuthoring             contract.FlowAuthoring
	NeutralApprovals                 contract.Approvals
	NeutralStandardActions           contract.StandardActionService

	// metadataDeployRecordDeleter is a private composition capability consumed
	// only by ApplyMetadataDeployRecord. It must never be included in ContractDeps
	// or implemented by NeutralRecords.
	metadataDeployRecordDeleter contract.MetadataDeployRecordDeleter

	// Infrastructure
	EmailSender ports.EmailSender

	// Repositories
	UserRepo   ports.UserRepository
	SystemRepo ports.SystemRepository
	BulkRepo   ports.BulkRepository

	// Audit
	SetupAuditor *auth.SetupAuditor

	// Folder Access
	FolderAccess *permissions.FolderAccessService

	// Coordinates async validation-projection rehydration kicked off from
	// RefreshMetadataCache. running guards against overlapping rebuilds; pending
	// records that another refresh arrived mid-rebuild so the worker re-runs once
	// more (the generation guard inside RecomputeAllValidationProjections drops a
	// rebuild that read stale schema, so a trailing refresh needs a follow-up pass).
	validationProjectionRefreshMu      sync.Mutex
	validationProjectionRefreshRunning bool
	validationProjectionRefreshPending bool
}

func (sm *ServiceManager) PolicyContract() contract.Policy {
	if sm == nil {
		return nil
	}
	if sm.NeutralPolicy != nil {
		return sm.NeutralPolicy
	}
	if sm.Permissions == nil {
		return nil
	}
	return permissions.NewContractPolicy(sm.Permissions)
}

func (sm *ServiceManager) NotificationsContract() contract.Notifications {
	if sm == nil {
		return nil
	}
	if sm.NeutralNotifications != nil {
		return sm.NeutralNotifications
	}
	frontendURL := os.Getenv("FRONTEND_URL")
	if sm.EmailSender == nil && frontendURL == "" {
		return nil
	}
	return NewNotificationContract(sm.EmailSender, frontendURL)
}

func (sm *ServiceManager) PermissionAdminContract() contract.PermissionAdmin {
	if sm == nil {
		return nil
	}
	if sm.NeutralPermissionAdmin != nil {
		return sm.NeutralPermissionAdmin
	}
	if sm.Permissions == nil || sm.PermissionAdmin == nil {
		return nil
	}
	return permissions.NewContractPermissionAdmin(sm.Permissions, sm.PermissionAdmin)
}

func (sm *ServiceManager) QueryContract() contract.Query {
	if sm == nil {
		return nil
	}
	if sm.NeutralQuery != nil {
		return sm.NeutralQuery
	}
	if query, ok := sm.QuerySvc.(contract.Query); ok {
		return query
	}
	return nil
}

func (sm *ServiceManager) ReportsContract() contract.Reports {
	if sm == nil {
		return nil
	}
	if sm.NeutralReports != nil {
		return sm.NeutralReports
	}
	if sm.ReportSvc == nil {
		return nil
	}
	return sm.ReportSvc
}

func (sm *ServiceManager) BulkJobsContract() contract.BulkJobs {
	if sm == nil {
		return nil
	}
	if sm.NeutralBulkJobs != nil {
		return sm.NeutralBulkJobs
	}
	if sm.BulkSvc == nil {
		return nil
	}
	return sm.BulkSvc
}

func (sm *ServiceManager) BulkIngestContract() contract.BulkIngest {
	if sm == nil {
		return nil
	}
	if sm.NeutralBulkIngest != nil {
		return sm.NeutralBulkIngest
	}
	if sm.BulkSvc == nil || sm.Metadata == nil || sm.Permissions == nil {
		return nil
	}
	return records.NewBulkIngestContract(sm.BulkSvc, sm.Metadata, sm.Permissions)
}

func (sm *ServiceManager) FolderAccessContract() contract.FolderAccess {
	if sm == nil {
		return nil
	}
	if sm.NeutralFolderAccess != nil {
		return sm.NeutralFolderAccess
	}
	if sm.FolderAccess == nil {
		return nil
	}
	return permissions.NewContractFolderAccess(sm.FolderAccess)
}

func (sm *ServiceManager) ResourceAccessContract() contract.ResourceAccess {
	if sm == nil {
		return nil
	}
	if sm.NeutralResourceAccess != nil {
		return sm.NeutralResourceAccess
	}
	if sm.FolderAccess == nil {
		return nil
	}
	return permissions.NewContractResourceAccess(sm.FolderAccess)
}

func (sm *ServiceManager) UserSessionsContract() contract.UserSessions {
	if sm == nil {
		return nil
	}
	if sm.NeutralUserSessions != nil {
		return sm.NeutralUserSessions
	}
	if sm.Persistence == nil {
		return nil
	}
	return sm.Persistence
}

func (sm *ServiceManager) MetadataContract() contract.Metadata {
	if sm == nil {
		return nil
	}
	if sm.NeutralMetadata != nil {
		return sm.NeutralMetadata
	}
	if sm.Metadata == nil {
		return nil
	}
	return metadata.NewContractService(sm.Metadata, sm.roleAncestorsSource())
}

func (sm *ServiceManager) ApplicationNavigationContract() contract.ApplicationNavigation {
	if sm == nil {
		return nil
	}
	return sm.NeutralApplicationNavigation
}

// roleAncestorsSource returns the role-ancestor resolver the neutral metadata
// adapter folds behind ResolveLayout. It resolves sm.Permissions lazily (the
// permission service is wired after the neutral metadata adapter during init)
// and stays nil-safe so a pack with no role hierarchy resolves
// profile/RecordType-only layout assignments.
func (sm *ServiceManager) roleAncestorsSource() func(string) []string {
	return func(roleID string) []string {
		if sm == nil || sm.Permissions == nil {
			return nil
		}
		return sm.Permissions.GetRoleAncestors(roleID)
	}
}

func (sm *ServiceManager) StandardActionContract() contract.StandardActionService {
	if sm == nil {
		return nil
	}
	return sm.NeutralStandardActions
}

func (sm *ServiceManager) FlowExecutorContract() contract.FlowExecutor {
	if sm == nil {
		return nil
	}
	if sm.NeutralFlowExecutor != nil {
		return sm.NeutralFlowExecutor
	}
	if sm.FlowExecutor == nil {
		return nil
	}
	return automation.NewContractFlowExecutor(sm.FlowExecutor)
}

func (sm *ServiceManager) FlowCatalogContract() contract.FlowCatalog {
	if sm == nil {
		return nil
	}
	if sm.NeutralFlowCatalog != nil {
		return sm.NeutralFlowCatalog
	}
	if sm.deps.DB == nil || sm.Metadata == nil {
		return nil
	}
	return NewFlowCatalogContract(sm.Metadata, automation.NewProtectedFlowCatalog(sm.deps.DB))
}

func (sm *ServiceManager) RecordsContract() contract.Records {
	if sm == nil {
		return nil
	}
	if sm.NeutralRecords != nil {
		return sm.NeutralRecords
	}
	if sm.Persistence == nil {
		return nil
	}
	return records.NewContractService(sm.Persistence)
}

func (sm *ServiceManager) RecordMutationCoordinatorContract() contract.RecordMutationCoordinator {
	if sm == nil {
		return nil
	}
	if sm.NeutralRecordMutationCoordinator != nil {
		return sm.NeutralRecordMutationCoordinator
	}
	if sm.Persistence == nil {
		return nil
	}
	return records.NewContractService(sm.Persistence)
}

func (sm *ServiceManager) RecycleBinContract() contract.RecycleBin {
	if sm == nil {
		return nil
	}
	if sm.NeutralRecycleBin != nil {
		return sm.NeutralRecycleBin
	}
	if sm.Persistence == nil {
		return nil
	}
	return records.NewContractService(sm.Persistence)
}

func (sm *ServiceManager) ListViewAuthoringContract() contract.ListViewAuthoring {
	if sm == nil {
		return nil
	}
	if sm.NeutralListViewAuthoring != nil {
		return sm.NeutralListViewAuthoring
	}
	if sm.ListViews == nil || sm.Persistence == nil {
		return nil
	}
	return metadata.NewListViewAuthoringContract(sm.ListViews, sm.Persistence)
}

// RecordLifecycleContract exposes the startup-only lifecycle registration seam.
// It is deliberately not bundled into ContractDeps: route handlers receive
// ContractDeps during mount/request handling, while lifecycle hook registration
// mutates the records engine and must happen once during composition.
func (sm *ServiceManager) RecordLifecycleContract() contract.RecordLifecycleRegistrar {
	if sm == nil {
		return nil
	}
	if sm.Persistence == nil {
		return nil
	}
	return sm.Persistence
}

// RecordWriteValidationContract exposes the startup-only write-validator
// registration seam. Like RecordLifecycleContract it is deliberately not
// bundled into ContractDeps: pack-supplied write validators mutate the
// records engine and must be registered once during composition, not on
// every request that receives ContractDeps.
func (sm *ServiceManager) RecordWriteValidationContract() contract.RecordWriteValidatorRegistrar {
	if sm == nil {
		return nil
	}
	if sm.Persistence == nil {
		return nil
	}
	return sm.Persistence
}

func (sm *ServiceManager) ContractDeps() contract.Deps {
	if sm == nil {
		return contract.Deps{}
	}
	return contract.Deps{
		Records:                   sm.RecordsContract(),
		RecordMutationCoordinator: sm.RecordMutationCoordinatorContract(),
		RecycleBin:                sm.RecycleBinContract(),
		AuditLog:                  sm.AuditLogContract(),
		Query:                     sm.QueryContract(),
		BulkJobs:                  sm.BulkJobsContract(),
		BulkIngest:                sm.BulkIngestContract(),
		Reports:                   sm.ReportsContract(),
		FolderAccess:              sm.FolderAccessContract(),
		ResourceAccess:            sm.ResourceAccessContract(),
		UserSessions:              sm.UserSessionsContract(),
		Metadata:                  sm.MetadataContract(),
		ListViewAuthoring:         sm.ListViewAuthoringContract(),
		MetadataAuthoring:         sm.MetadataAuthoringContract(),
		MetadataDeploy:            sm.MetadataDeployContract(),
		ApplicationNavigation:     sm.ApplicationNavigationContract(),
		MetadataMaintenance:       sm.MetadataMaintenanceContract(),
		RollupMaintenance:         sm.RollupMaintenanceContract(),
		Policy:                    sm.PolicyContract(),
		Notifications:             sm.NotificationsContract(),
		PermissionAdmin:           sm.PermissionAdminContract(),
		ConnectorSyncStatus:       sm.ConnectorSyncStatusContract(),
		ConnectorWebhookSecrets:   sm.NeutralConnectorWebhookSecrets,
		FlowExecutor:              sm.FlowExecutorContract(),
		FlowCatalog:               sm.FlowCatalogContract(),
		FlowInterviews:            sm.FlowInterviewsContract(),
		FlowAuthoring:             sm.FlowAuthoringContract(),
		Approvals:                 sm.ApprovalContract(),
		StandardActions:           sm.StandardActionContract(),
	}
}

// NewServiceManager creates a new service manager with all dependencies wired.
// Infrastructure dependencies are injected from the composition root (cmd/server).
//
// Round-5 Codex V2-C2: signature now returns error. initAutomationStack
// refuses to continue when the autonumber metadata snapshot cannot be
// populated (fail-closed). Callers must propagate the error — typically
// via log.Fatal so the pod fails its init/readiness probe rather than
// serving traffic with a half-wired engine.
func NewServiceManager(deps InfrastructureDeps) (*ServiceManager, error) {
	// Initialize global logger
	logging.InitLogger()
	logger := logging.New(context.Background())

	sm := &ServiceManager{
		deps:   deps,
		Logger: logger,
	}

	// 1. Initialize Infrastructure & Repositories
	sm.initInfrastructure()
	sm.initRepositories()

	// 2. Initialize Core Domain (Metadata, Schema)
	sm.initCoreDomain()

	// 3. Initialize Stacks
	sm.initAuthStack()
	sm.initAnalyticsStack()
	if err := sm.initAutomationStack(); err != nil {
		return nil, err
	}

	// UIMetadata depends on Permissions, Persistence, and QuerySvc
	sm.initBusinessServices()

	return sm, nil
}

// DB returns the raw *sql.DB handle injected at composition time.
// Used by handlers that need to run ad-hoc read queries against
// system tables (e.g. the audit admin API at C11 §5.2).
func (sm *ServiceManager) DB() *sql.DB {
	return sm.deps.DB
}

// RefreshMetadataCache refreshes all metadata caches
func (sm *ServiceManager) RefreshMetadataCache(ctx context.Context) error {
	// AutoNumberMetadata feeds Metadata's schema decorators. Refresh it first so
	// the rebuilt schema cache projects current auto_number_format-backed
	// DefaultedOnCreate facts for describe and create-required validation.
	if sm.AutoNumberMetadata != nil {
		if err := sm.AutoNumberMetadata.Refresh(ctx); err != nil {
			return pkgerrors.NewInternalError("failed to refresh autonumber metadata cache", err)
		}
	}
	if sm.Metadata != nil {
		if err := sm.Metadata.RefreshCache(ctx); err != nil {
			return pkgerrors.NewInternalError("failed to refresh metadata cache", err)
		}
	}

	if sm.Permissions != nil {
		if err := sm.Permissions.RefreshPermissions(ctx); err != nil {
			return pkgerrors.NewInternalError("failed to refresh permissions", err)
		}
	}

	// Metadata.RefreshCache fires the cache-invalidation hooks, which clear the
	// process-local validation-projection closure cache. Unlike control-plane
	// RebuildDerived (recomputes after its own RefreshCache) and process startup,
	// this runtime refresh path had no rehydration: after any refresh-cache call
	// (for example the deploy finalize step) every validated save fell back to
	// projecting all active computed fields per object instead of the narrowed
	// rule-referenced closure, ballooning validateUpdate latency.
	//
	// Rehydrate off the request path. RefreshMetadataCache is on latency-sensitive
	// paths, and the fallback path stays correct until the background rebuild
	// finishes.
	sm.kickValidationProjectionRefresh()

	return nil
}

// kickValidationProjectionRefresh rehydrates the validation-projection closure
// cache in the background after a metadata refresh. It is best-effort and never
// blocks the caller: RefreshMetadataCache is on latency-sensitive paths, and a
// recompute failure only loses the optimization (the fallback path is correct).
//
// At most one rebuild runs at a time; if a refresh arrives while a rebuild is in
// flight, a single follow-up pass is scheduled so the final cache reflects the
// latest metadata (the generation guard in RecomputeAllValidationProjections
// drops a rebuild that read now-stale schema, so the trailing pass is required).
func (sm *ServiceManager) kickValidationProjectionRefresh() {
	if sm.FormulaViewSvc == nil {
		return
	}

	sm.validationProjectionRefreshMu.Lock()
	if sm.validationProjectionRefreshRunning {
		sm.validationProjectionRefreshPending = true
		sm.validationProjectionRefreshMu.Unlock()
		return
	}
	sm.validationProjectionRefreshRunning = true
	sm.validationProjectionRefreshMu.Unlock()

	go func() {
		for {
			sm.runValidationProjectionRefreshPass()

			sm.validationProjectionRefreshMu.Lock()
			if sm.validationProjectionRefreshPending {
				sm.validationProjectionRefreshPending = false
				sm.validationProjectionRefreshMu.Unlock()
				continue
			}
			sm.validationProjectionRefreshRunning = false
			sm.validationProjectionRefreshMu.Unlock()
			return
		}
	}()
}

func (sm *ServiceManager) runValidationProjectionRefreshPass() {
	defer recoverServiceManagerBackgroundPanic("validation_projection_refresh")
	refreshCtx, cancel := validationProjectionRefreshContext()
	defer cancel()
	err := sm.FormulaViewSvc.RecomputeAllValidationProjections(refreshCtx)
	if err != nil {
		log.Printf("⚠️  Failed to recompute validation projections after metadata refresh (fallback path remains correct): %v", err)
	}
}

func validationProjectionRefreshContext() (context.Context, context.CancelFunc) {
	return context.WithTimeout(context.Background(), validationProjectionRefreshTimeout)
}

func (sm *ServiceManager) GetEffectiveSchema(ctx context.Context, apiName string, user *models.UserSession) (*models.ObjectMetadata, error) {
	schema := sm.Metadata.GetSchema(ctx, apiName)
	if schema == nil {
		return nil, nil
	}
	return sm.Permissions.GetEffectiveSchema(ctx, schema, user)
}

func (sm *ServiceManager) GetSchemas(ctx context.Context) []*models.ObjectMetadata {
	return sm.Metadata.GetSchemas(ctx)
}

// App and Tab management
// Delegated directly to UIMetadata in handlers

func recoverServiceManagerBackgroundPanic(worker string) {
	if recovered := recover(); recovered != nil {
		log.Printf("service manager background worker recovered panic (worker=%s): %v", worker, recovered)
	}
}
