package services

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

	"github.com/nexuscrm/backend/internal/domain/ports"
	"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"
	pkgerrors "github.com/nexuscrm/backend/pkg/errors"
	"github.com/nexuscrm/backend/pkg/logging"
	"github.com/nexuscrm/shared/pkg/models"
)

// 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

	// Transaction management
	TxManager ports.TransactionManager

	// 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
	OAuthRepo      ports.OAuthRepository
	QueryRepo      ports.QueryRepository
	RecordRepo     ports.RecordRepository
	OutboxRepo     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)
}

// ServiceManager orchestrates all services with dependency injection
type ServiceManager struct {
	deps   InfrastructureDeps
	Logger *logging.Logger

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

	// HrefFieldResolver rewrites legacy SF ContentDistribution hrefs in
	// Contract/Order columns to point at the matching `_System_File` row on
	// the same record, so the detail-page renderer can serve them as
	// downloadable files instead of dead pingcap.my.salesforce.com links.
	HrefFieldResolver *files.HrefFieldResolver

	// Infrastructure
	EmailSender ports.EmailSender

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

	// Features
	UIAPI *uiapi.Service

	// Audit
	SetupAuditor *auth.SetupAuditor

	// Folder Access
	FolderAccess *permissions.FolderAccessService

	// Background worker lifecycle for processed outbox cleanup.
	outboxCleanupMu        sync.Mutex
	outboxCleanupWG        sync.WaitGroup
	outboxCleanupStopCh    chan struct{}
	outboxCleanupRunning   bool
	outboxCleanupInterval  time.Duration
	outboxCleanupRetention time.Duration
	outboxCleanupDisabled  bool // When true, StartOutboxCleanupWorker is a no-op (preserves full history)
}

// 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 {
	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)
		}
	}

	if sm.UIAPI != nil {
		sm.UIAPI.InvalidateCaches()
	}

	return nil
}

func (sm *ServiceManager) GetEffectiveSchema(ctx context.Context, apiName string, user *models.UserSession) *models.ObjectMetadata {
	schema := sm.Metadata.GetSchema(ctx, apiName)
	if schema == nil {
		return 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

// StartOutboxWorker starts the background outbox event processing worker.
// Call this during server startup. The worker processes pending events every 500ms.
func (sm *ServiceManager) StartOutboxWorker() {
	if sm.Outbox != nil {
		sm.Outbox.StartWorker(500 * time.Millisecond)
	}
}

// StopOutboxWorker stops the background outbox event processing worker gracefully.
// Call this during server shutdown.
func (sm *ServiceManager) StopOutboxWorker() {
	if sm.Outbox != nil {
		sm.Outbox.StopWorker()
	}
}

// StartOutboxCleanupWorker starts a background goroutine that periodically
// removes processed outbox events older than the configured retention window.
// Without this, the _System_OutboxEvent table grows unboundedly.
//
// Configuration (env var OUTBOX_RETENTION_DAYS):
//   - unset / empty   → default 14 days
//   - "0"             → cleanup DISABLED; every processed event is kept (for
//                       downstream replay consumers that need full history)
//   - positive int N  → retain for N days
//   - invalid value   → falls back to default with a warning log
func (sm *ServiceManager) StartOutboxCleanupWorker() {
	if sm.Outbox == nil {
		return
	}

	// All reads/writes of outboxCleanup* fields must happen under the mutex.
	// applyOutboxRetentionEnv mutates outboxCleanupDisabled / outboxCleanupRetention,
	// so invoke it inside the lock to keep concurrent Start() calls race-free.
	sm.outboxCleanupMu.Lock()
	if sm.outboxCleanupRunning {
		sm.outboxCleanupMu.Unlock()
		return
	}

	sm.applyOutboxRetentionEnvLocked()

	if sm.outboxCleanupDisabled {
		sm.outboxCleanupMu.Unlock()
		log.Printf("🧹 [OutboxCleanup] cleanup disabled (OUTBOX_RETENTION_DAYS=0); processed events will be retained indefinitely")
		return
	}

	interval := sm.outboxCleanupInterval
	if interval <= 0 {
		interval = 1 * time.Hour
	}

	retention := sm.outboxCleanupRetention
	if retention <= 0 {
		retention = 14 * 24 * time.Hour
	}

	stopCh := make(chan struct{})
	sm.outboxCleanupStopCh = stopCh
	sm.outboxCleanupRunning = true
	sm.outboxCleanupWG.Add(1)
	sm.outboxCleanupMu.Unlock()

	go func() {
		defer sm.outboxCleanupWG.Done()
		ticker := time.NewTicker(interval)
		defer ticker.Stop()

		for {
			select {
			case <-stopCh:
				return
			case <-ticker.C:
				deleted, err := sm.Outbox.CleanupProcessed(context.Background(), retention)
				if err != nil {
					log.Printf("⚠️ [OutboxCleanup] Failed to clean up processed events: %v", err)
				} else if deleted > 0 {
					log.Printf("🧹 [OutboxCleanup] Removed %d processed outbox events older than %s", deleted, retention)
				}
			}
		}
	}()
}

// maxOutboxRetentionDays caps retention at ~100 years. `time.Duration` is an
// int64 nanosecond count; days × 24h would overflow around ~106752 days, and
// the signed wrap-around would silently cap retention back to the default 14
// days instead of "keep forever". Any operator value above the cap is a
// misconfiguration; log and fall back to default rather than wrap.
const maxOutboxRetentionDays = 36524

// applyOutboxRetentionEnvLocked reads OUTBOX_RETENTION_DAYS and maps it onto
// the cleanup-worker fields. Caller MUST hold outboxCleanupMu — both reads and
// writes of outboxCleanupDisabled / outboxCleanupRetention must be serialized.
func (sm *ServiceManager) applyOutboxRetentionEnvLocked() {
	// Reset the sticky-disable flag so Start/Stop cycles that change the env
	// don't silently keep the worker off after a previous disabled pass.
	sm.outboxCleanupDisabled = false

	raw := strings.TrimSpace(os.Getenv("OUTBOX_RETENTION_DAYS"))
	if raw == "" {
		return
	}
	days, err := strconv.Atoi(raw)
	if err != nil {
		log.Printf("⚠️ [OutboxCleanup] invalid OUTBOX_RETENTION_DAYS=%q (falling back to default): %v", raw, err)
		return
	}
	if days < 0 {
		log.Printf("⚠️ [OutboxCleanup] negative OUTBOX_RETENTION_DAYS=%d (falling back to default)", days)
		return
	}
	if days == 0 {
		sm.outboxCleanupDisabled = true
		return
	}
	if days > maxOutboxRetentionDays {
		log.Printf("⚠️ [OutboxCleanup] OUTBOX_RETENTION_DAYS=%d exceeds max %d (falling back to default). Use 0 to disable cleanup entirely.", days, maxOutboxRetentionDays)
		return
	}
	sm.outboxCleanupRetention = time.Duration(days) * 24 * time.Hour
}

// StopOutboxCleanupWorker stops the processed outbox cleanup worker gracefully.
func (sm *ServiceManager) StopOutboxCleanupWorker() {
	sm.outboxCleanupMu.Lock()
	if !sm.outboxCleanupRunning {
		sm.outboxCleanupMu.Unlock()
		return
	}
	stopCh := sm.outboxCleanupStopCh
	sm.outboxCleanupStopCh = nil
	sm.outboxCleanupRunning = false
	sm.outboxCleanupMu.Unlock()

	close(stopCh)
	sm.outboxCleanupWG.Wait()
}

// StartScheduler starts the scheduled job executor.
// Call this during server startup.
func (sm *ServiceManager) StartScheduler() {
	if sm.Scheduler != nil {
		go sm.Scheduler.StartScheduler()
	}
}

// StopScheduler stops the scheduled job executor gracefully.
// Call this during server shutdown.
func (sm *ServiceManager) StopScheduler() {
	if sm.Scheduler != nil {
		sm.Scheduler.StopScheduler()
	}
}
