package services

import (
	"context"
	"fmt"
	"time"

	"github.com/nexuscrm/backend/internal/features/analytics"
	"github.com/nexuscrm/backend/internal/features/auth"
	"github.com/nexuscrm/backend/internal/features/automation"
	"github.com/nexuscrm/backend/internal/features/automation/autonumber"
	"github.com/nexuscrm/backend/internal/features/events"
	"github.com/nexuscrm/backend/internal/features/files"
	"github.com/nexuscrm/backend/internal/features/metadata"
	"github.com/nexuscrm/backend/internal/features/permissions"
	"github.com/nexuscrm/backend/internal/features/records"
	"github.com/nexuscrm/backend/internal/features/uiapi"
	"github.com/nexuscrm/backend/internal/infrastructure/email"
	"github.com/nexuscrm/backend/internal/infrastructure/persistence"
	"github.com/nexuscrm/backend/pkg/formula"
)

// Initialization Helpers

func (sm *ServiceManager) initInfrastructure() {
	sm.TxManager = sm.deps.TxManager

	// Use injected EventBus if provided (headless control-plane mode),
	// otherwise create a real EventBus with metrics (runtime server mode).
	if sm.deps.EventBus != nil {
		sm.EventBus = sm.deps.EventBus
	} else {
		realBus := events.NewEventBus()
		realBus.SetMetricsCallbacks(events.MetricsCallbacks{
			OnEventDropped:           sm.deps.OnEventDropped,
			OnQueueUtilizationChange: sm.deps.OnQueueUtilizationChange,
		})
		sm.EventBus = realBus
	}

	formulaEngine := formula.NewEngine()
	sm.Validation = metadata.NewValidationService(formulaEngine)

	sm.EmailSender = email.NewEmailSenderFromEnv(sm.Logger)
}

func (sm *ServiceManager) initRepositories() {
	sm.UserRepo = sm.deps.UserRepo
	sm.SystemRepo = sm.deps.SystemRepo
	sm.BulkRepo = sm.deps.BulkRepo
}

func (sm *ServiceManager) initCoreDomain() {
	sm.Schema = metadata.NewSchemaManager(sm.deps.SchemaRepo)

	sm.Metadata = metadata.NewMetadataService(sm.deps.MetadataRepo, sm.Schema, sm.EventBus)
	sm.Metadata.SetTransactionManager(sm.TxManager)

	// Circular dependency resolution
	sm.Metadata.SetValidationService(sm.Validation)
	sm.Validation.SetFormulaRelationResolver(metadata.NewValidationFormulaRelationResolver(sm.Metadata, sm.deps.RecordRepo))

	// Wire MetadataService as the single schema cache authority for RecordRepository,
	// eliminating the duplicate sync.Map cache that RecordRepository previously maintained.
	if rr, ok := sm.deps.RecordRepo.(*persistence.RecordRepository); ok {
		rr.SetSchemaProvider(sm.Metadata)
	}
}

func (sm *ServiceManager) initAuthStack() {
	sm.Permissions = permissions.NewPermissionService(sm.deps.PermissionRepo, sm.Metadata, sm.UserRepo)

	// Register the schema decorator that synthesizes PermissionSet's
	// Permissions<X> virtual fields from the _System_Permission catalog.
	// Must run after PermissionService is constructed; registration
	// invalidates the metadata cache so the next GetSchema rebuilds
	// through this decorator.
	sm.Metadata.RegisterSchemaDecorator(permissions.NewSchemaDecorator(sm.Permissions))

	sm.Auth = auth.NewAuthService(sm.UserRepo, sm.deps.SessionRepo, sm.deps.PermissionRepo)
	sm.Auth.SetPermissionService(sm.Permissions)
	sm.Auth.SetLoginHistoryRepo(persistence.NewLoginHistoryRepository(sm.deps.DB))

	setupAuditRepo := persistence.NewSetupAuditRepository(sm.deps.DB)
	sm.SetupAuditor = auth.NewSetupAuditor(setupAuditRepo)
	sm.Auth.SetSetupAuditor(sm.SetupAuditor)
	sm.Permissions.SetSetupAuditor(sm.SetupAuditor)

	sm.OAuth = auth.NewOAuthService(sm.UserRepo, sm.SystemRepo, sm.deps.SessionRepo, sm.Auth, sm.deps.OAuthRepo)

	// FolderAccessService: dedicated folder permission enforcement (Phase 1, #697)
	sm.FolderAccess = permissions.NewFolderAccessService(sm.Permissions, sm.deps.DB)
	sm.Permissions.SetFolderAccess(sm.FolderAccess)
}

func (sm *ServiceManager) initAnalyticsStack() {
	// Initialize FormulaViewService for TiDB View-based formula computation.
	sm.FormulaViewSvc = analytics.NewFormulaViewService(sm.deps.DB, sm.Metadata, sm.Logger)

	// Wire view resolver into QueryRepository so read queries route to v_{object} views
	if qr, ok := sm.deps.QueryRepo.(*persistence.QueryRepository); ok {
		qr.SetViewResolver(sm.FormulaViewSvc)
	}
	// Also wire the resolver into RecordRepository so single-record reads
	// (FindOne, used by RetrieveWithAccess and GetRecordUI) route through
	// v_{object} when active formulas exist. Without this, /ui-api/record-ui
	// returns NULL for every formula column on the detail page.
	if rr, ok := sm.deps.RecordRepo.(*persistence.RecordRepository); ok {
		rr.SetViewResolver(sm.FormulaViewSvc)
	}

	sm.QuerySvc = analytics.NewQueryService(sm.deps.QueryRepo, sm.Metadata, sm.Permissions, sm.Logger)

	// CRT wiring (design §4.9) — auto-scan standard objects + construct the
	// CRT resolver so Custom Report Types resolve through a real
	// *JoinTemplate instead of returning 404.
	stdObjs := analytics.BuildStandardObjectResolver(context.Background(), sm.Metadata, sm.Logger)
	crtLoader := analytics.NewCRTLoader(sm.deps.DB, sm.Logger)
	crtResolver := analytics.NewCRTResolver(crtLoader, sm.Metadata, stdObjs, sm.Logger)
	sm.ReportSvc = analytics.NewReportService(
		sm.QuerySvc, sm.Metadata, sm.Permissions, sm.Logger,
		analytics.WithCRTResolver(crtResolver),
		analytics.WithStandardObjectResolver(stdObjs),
		analytics.WithRawDB(sm.deps.DB),
	)
	sm.DashboardSvc = analytics.NewDashboardService(sm.QuerySvc, sm.ReportSvc, sm.Logger)

	sm.Rollup = analytics.NewRollupService(sm.deps.RollupRepo, sm.Metadata, sm.TxManager, sm.Logger)

	sm.SecurityValidator = analytics.NewSecurityValidator(sm.Permissions, sm.Metadata)

	// Subscribe to schema change events to auto-rebuild formula views
	formulaViewListener := analytics.NewFormulaViewListener(sm.FormulaViewSvc, sm.EventBus)
	formulaViewListener.Subscribe()
}

func (sm *ServiceManager) initAutomationStack() error {
	// MetadataLookup arms the dynamic side of the OutboxService's
	// outboxNoSubscriberObjects filter — events for FlowInterview etc.
	// are only dropped when no active record-triggered flow is wired
	// against them. Tests that bypass ServiceManager wire-up may leave
	// the lookup unset, in which case the filter falls through to the
	// safe enqueue path (issue #1658).
	sm.Outbox = events.NewOutboxService(sm.deps.OutboxRepo, sm.EventBus, sm.TxManager).
		WithMetadataLookup(sm.Metadata)

	// Autonumber v2 engine + metadata source (PRD §3.1 / §7.2 step k).
	// Replaces the legacy CompoundAutoNumberListener — the engine is
	// synchronously invoked from PersistenceService.Insert / BulkInsert
	// before RecordBeforeCreate publishes, so flows/triggers still see
	// the rendered identifier.
	//
	// Round-5 Codex V2-C2 fail-closed: the previous implementation
	// logged the error and continued. An empty MetadataSource snapshot
	// causes LookupByObject to return nil for every object, which the
	// persistence layer reads as "no autonumber configured" and
	// silently skips assignment — turning a transient DB blip at init
	// into permanently missing identifiers until the pod restarts on
	// an unrelated trigger. We now retry with exponential backoff (5×
	// starting at 500 ms: 500 ms, 1 s, 2 s, 4 s, 8 s ≈ 15.5 s total)
	// and propagate the terminal error so the process aborts before
	// serving traffic.
	sm.AutoNumberMetadata = autonumber.NewMetadataSource(sm.deps.DB, nil)
	if err := sm.AutoNumberMetadata.RefreshWithBackoff(context.Background(), 5, 500*time.Millisecond); err != nil {
		return fmt.Errorf("autonumber metadata init failed: %w", err)
	}
	// Wire the invalidator into MetadataService so admin field-create
	// paths (field_create.go + field_batch.go) can nudge the engine's
	// snapshot after writing AutoNumberFormat. Round-5 Codex
	// V1-P2 / V2-H3.
	sm.Metadata.SetAutoNumberInvalidator(sm.AutoNumberMetadata)
	// Let ValidationService skip required-field check on fields whose
	// `_System_Field.auto_number_format` is set — engine renders them
	// inside the insert transaction (Quote.Name, Contract.Name, etc.).
	sm.Validation.SetAutoNumberResolver(func(objectName string) map[string]struct{} {
		fields := sm.AutoNumberMetadata.LookupByObject(objectName)
		if len(fields) == 0 {
			return nil
		}
		out := make(map[string]struct{}, len(fields))
		for _, fm := range fields {
			out[fm.FieldAPIName] = struct{}{}
		}
		return out
	})
	resolver := autonumber.NewRepoCrossRecordResolver(sm.deps.RecordRepo)
	engine, err := autonumber.NewEngine(
		autonumber.WithReservationDB(sm.deps.DB),
		autonumber.WithMetadataProvider(sm.AutoNumberMetadata),
		autonumber.WithCrossRecordResolver(resolver),
	)
	if err != nil {
		// Engine construction only fails on missing required option; we
		// wired both required opts above, so an error here is a
		// developer bug — panic rather than mask it.
		panic(fmt.Sprintf("autonumber engine init: %v", err))
	}
	sm.AutoNumberEngine = engine

	sm.Persistence = records.NewPersistenceService(
		sm.deps.RecordRepo,
		sm.Rollup,
		sm.Metadata,
		sm.Permissions,
		sm.EventBus,
		sm.Validation,
		sm.TxManager,
		sm.Outbox,
		sm.Logger,
		sm.AutoNumberEngine,
		sm.AutoNumberMetadata,
		sm.deps.SessionRepo, // session revocation on user/app deactivation
		sm.Auth,             // cache invalidation on user deactivation
	)
	// Wire raw DB for the insert-inheritance applier's registry lookup
	// (reads _System_ObjectFieldMapping rows with ActionType =
	// 'insertInheritFromLookup'). Unwired ps.rawDB makes applyInsertInheritance
	// a silent no-op in production — greenfield is happy, tenant-overlay
	// rules never fire. Must run after NewPersistenceService.
	sm.Persistence.SetRawDB(sm.deps.DB)

	sm.ActionSvc = automation.NewActionService(sm.Metadata, sm.Persistence, sm.Permissions, sm.TxManager, sm.EmailSender, sm.Logger)

	sm.FlowInterviewSvc = automation.NewFlowInterviewService(sm.Persistence, sm.QuerySvc)
	sm.FlowExecutor = automation.NewFlowExecutor(
		sm.Metadata,
		sm.ActionSvc,
		sm.FlowInterviewSvc,
		sm.Persistence,
		sm.QuerySvc,
		automation.NewBootstrapFieldMappingApplier(sm.deps.DB),
		sm.Logger,
	)

	sm.Approval = automation.NewApprovalService(sm.Persistence, sm.QuerySvc, sm.Permissions, sm.FlowExecutor, sm.FlowInterviewSvc, sm.ActionSvc, sm.Metadata, sm.Logger)
	// emailAlert actionCalls in flows resolve via the shared WorkflowAlert
	// pipeline on ApprovalService — wire the back-reference now that both
	// services exist.
	sm.FlowExecutor.SetApprovalService(sm.Approval)

	sm.Scheduler = automation.NewSchedulerService(sm.Persistence, sm.QuerySvc, sm.Metadata, sm.FlowExecutor)

	// Record-triggered flow listener
	sm.FlowTriggerSvc = automation.NewFlowTriggerListener(sm.FlowExecutor, sm.Metadata, sm.EventBus)
	sm.FlowTriggerSvc.Subscribe()
	return nil
}

func (sm *ServiceManager) initBusinessServices() {
	sm.System = NewSystemManager(sm.Persistence, sm.SystemRepo)
	// Create UI API Service (Orchestration Layer)
	sm.UIAPI = uiapi.NewService(sm.Persistence, sm.Metadata, sm.Permissions, sm.Logger)

	sm.BulkSvc = records.NewBulkService(sm.BulkRepo, sm.Logger)
	sm.BulkProcessor = records.NewBulkProcessor(sm.BulkSvc, sm.Persistence, sm.EventBus, sm.Logger)

	sm.HrefFieldResolver = files.NewHrefFieldResolver(sm.deps.DB)
}

// StartBackgroundWorkers starts all background processes
func (sm *ServiceManager) StartBackgroundWorkers() {
	sm.StartOutboxWorker()
	sm.StartOutboxCleanupWorker()
	sm.StartScheduler()
	if sm.BulkProcessor != nil {
		sm.BulkProcessor.Start()
	}
}

// StopBackgroundWorkers stops all background processes
func (sm *ServiceManager) StopBackgroundWorkers() {
	sm.StopOutboxCleanupWorker()
	sm.StopOutboxWorker()
	sm.StopScheduler()
	if sm.BulkProcessor != nil {
		sm.BulkProcessor.Stop()
	}
}
