// Package runtimeassembly owns the L0-only composition root for independent
// Runtime roles. It intentionally does not import the monolith's services,
// serverruntime, controlplane, connector, distribution, or worker roots.
package runtimeassembly

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"log"
	"os"

	"github.com/nexuscrm/analytics"
	"github.com/nexuscrm/audit"
	database "github.com/nexuscrm/db"
	"github.com/nexuscrm/events"
	"github.com/nexuscrm/files"
	"github.com/nexuscrm/kernel/pkg/formula"
	"github.com/nexuscrm/kernel/pkg/logging"
	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/metadata"
	"github.com/nexuscrm/permissions"
	"github.com/nexuscrm/persistence"
	"github.com/nexuscrm/persistence/mru"
	"github.com/nexuscrm/records"
	"github.com/nexuscrm/storage"
)

// RecordsReadDependencies is the finite read-only dependency closure of the
// current Runtime RecordsService capabilities. Construction starts no EventBus
// listener or background worker.
type RecordsReadDependencies struct {
	DB                       *sql.DB
	MetadataRepo             ports.MetadataRepository
	PermissionRepo           ports.PermissionRepository
	UserRepo                 ports.UserRepository
	QueryRepo                ports.QueryRepository
	RecordRepo               ports.RecordRepository
	FieldProjectionAdmission *permissions.FieldProjectionAdmissionLimits
}

func BuildRecordsReadDependencies(connection *database.TiDBConnection) RecordsReadDependencies {
	db := connection.DB()
	limits, configErr := permissions.FieldProjectionAdmissionLimitsFromEnv(os.Getenv)
	if configErr != nil {
		log.Printf("warning: %v", configErr)
	}
	recordRepo := persistence.NewRecordRepository(db, persistence.WithIndependentObjectIDProbeDB(func() (*sql.DB, error) {
		return connection.OpenIndependentDB(database.PoolOptions{MaxOpenConns: 1, MaxIdleConns: 1})
	}))
	return RecordsReadDependencies{
		DB: db, MetadataRepo: persistence.NewMetadataRepository(db),
		PermissionRepo:           persistence.NewPermissionRepository(db),
		UserRepo:                 persistence.NewUserRepository(db),
		QueryRepo:                persistence.NewQueryRepository(db),
		RecordRepo:               recordRepo,
		FieldProjectionAdmission: &limits,
	}
}

// RecordsReadRuntime is the one L0 read-only engine graph behind both coarse
// RecordsService operations. Sharing metadata/permission owners avoids a
// selected-root graph and record-access graph becoming competing read paths.
type RecordsReadRuntime struct {
	DB                *sql.DB
	Metadata          *metadata.MetadataService
	Permissions       *permissions.PermissionService
	Query             *analytics.QueryService
	Reports           *analytics.ReportService
	Persistence       *records.PersistenceService
	Records           *records.ContractService
	RecordView        *records.RuntimeRecordViewService
	AttachmentSources *records.RuntimeAttachmentSourceService
	recordRepo        ports.RecordRepository
	logger            *logging.Logger
}

type runtimeHistoryQueryFactory struct{}

func (runtimeHistoryQueryFactory) Build(
	db *sql.DB,
	metadataSource analytics.MetadataSource,
	permissionChecker analytics.PermissionChecker,
	views analytics.HistoryViewResolver,
) analytics.HistoryQuery {
	return audit.NewHistoryQueryService(db, metadataSource, permissionChecker, views)
}

func NewRecordsReadRuntime(deps RecordsReadDependencies) (*RecordsReadRuntime, error) {
	if deps.MetadataRepo == nil || deps.PermissionRepo == nil || deps.QueryRepo == nil || deps.RecordRepo == nil {
		return nil, errors.New("records-read runtime: metadata, permission, query, and record repositories are required")
	}
	metadataService := metadata.NewMetadataService(deps.MetadataRepo, nil, &events.NoOpEventBus{})
	return newRecordsReadRuntime(deps, metadataService, true)
}

// NewServingRecordsReadRuntime builds the independent Runtime distribution's
// read graph. Metadata shape is available only through the immutable view
// installed in request context; the graph has no metadata repository fallback.
func NewServingRecordsReadRuntime(deps RecordsReadDependencies, resolver metadata.RuntimeMetadataViewResolver) (*RecordsReadRuntime, error) {
	if deps.PermissionRepo == nil || deps.QueryRepo == nil || deps.RecordRepo == nil {
		return nil, errors.New("serving records-read runtime: permission, query, and record repositories are required")
	}
	metadataService, err := metadata.NewRuntimeMetadataService(resolver)
	if err != nil {
		return nil, err
	}
	return newRecordsReadRuntime(deps, metadataService, false)
}

func newRecordsReadRuntime(deps RecordsReadDependencies, metadataService *metadata.MetadataService, decorate bool) (*RecordsReadRuntime, error) {
	logging.InitLogger()
	logger := logging.New(context.Background())
	noWorkBus := &events.NoOpEventBus{}

	var permissionOptions []permissions.PermissionServiceOption
	if limits := deps.FieldProjectionAdmission; limits != nil {
		permissionOptions = append(permissionOptions, permissions.WithFieldProjectionAdmissionLimits(limits.Active, limits.Waiters))
	}
	permissionService := permissions.NewPermissionService(deps.PermissionRepo, metadataService, deps.UserRepo, permissionOptions...)
	if deps.DB != nil {
		permissionService.SetFolderAccess(permissions.NewFolderAccessService(permissionService, deps.DB))
	}
	metadataService.SetListViewRLSProvider(permissionService)
	if decorate {
		metadataService.RegisterSchemaDecorator(permissions.NewSchemaDecorator(permissionService))
		metadataService.RegisterSchemaDecorator(metadata.NewCompoundVirtualFieldDecorator(metadata.PlatformCompoundVirtualFieldDeclarations()...))
		metadataService.RegisterSchemaDecorator(metadata.NewServerDefaultedFieldsDecorator(metadata.PlatformServerDefaultedFieldDeclarations()...))
	}

	var formulaViews *analytics.FormulaViewService
	var historyViews analytics.HistoryViewResolver
	if deps.DB != nil {
		formulaViews = analytics.NewFormulaViewService(deps.DB, metadataService, logger)
		historyViews = formulaViews
		if queryRepo, ok := deps.QueryRepo.(*persistence.QueryRepository); ok {
			queryRepo.SetViewResolver(formulaViews)
		}
		if recordRepo, ok := deps.RecordRepo.(*persistence.RecordRepository); ok {
			recordRepo.SetViewResolver(formulaViews)
		}
	}
	if recordRepo, ok := deps.RecordRepo.(*persistence.RecordRepository); ok {
		recordRepo.SetSchemaProvider(metadataService)
	}
	queryService := analytics.NewQueryService(deps.QueryRepo, metadataService, permissionService, logger)
	if deps.DB != nil {
		queryService.SetHistoryBackend(deps.DB, historyViews)
		queryService.SetHistoryFactory(runtimeHistoryQueryFactory{})
	}
	persistenceService := records.NewPersistenceService(
		deps.RecordRepo, nil, metadataService, permissionService, noWorkBus,
		metadata.NewValidationService(formula.NewEngine()), nil, nil, logger, nil, nil, nil, nil,
	)
	persistenceService.SetSelectedRootProjectionReader(queryService)
	persistenceService.SetQueryService(queryService)
	runtime := &RecordsReadRuntime{
		DB: deps.DB, Metadata: metadataService, Permissions: permissionService, Query: queryService,
		Reports:     analytics.NewReportService(queryService, metadataService, permissionService, logger),
		Persistence: persistenceService, Records: records.NewContractService(persistenceService),
		recordRepo: deps.RecordRepo, logger: logger,
	}
	if deps.DB == nil {
		return runtime, nil
	}
	attachmentResolver, err := files.NewRuntimeAttachmentSourceResolver(
		persistence.NewRecordAttachmentSourceRepository(deps.DB), persistenceService, permissionService, metadataService,
	)
	if err != nil {
		return nil, err
	}
	runtime.AttachmentSources, err = records.NewRuntimeAttachmentSourceServiceFromPersistence(persistenceService, metadataService, attachmentResolver)
	if err != nil {
		return nil, err
	}
	runtime.RecordView, err = records.NewRuntimeRecordViewServiceFromPersistence(
		persistenceService, queryService, metadataService,
		mru.NewContractActivity(mru.NewService(persistence.NewMRURepository(deps.DB))),
	)
	if err != nil {
		return nil, err
	}
	return runtime, nil
}

func (r *RecordsReadRuntime) NewFileService() (*files.ContractService, error) {
	if r == nil || r.DB == nil || r.recordRepo == nil || r.Permissions == nil ||
		r.Metadata == nil || r.logger == nil {
		return nil, errors.New("records-read runtime: file graph is incomplete")
	}
	config := storage.LoadConfigFromEnv()
	authorities, err := storage.LoadAuthorityCatalogFromEnv(config)
	if err != nil {
		return nil, fmt.Errorf("records-read runtime: configure file storage authorities: %w", err)
	}
	store, err := storage.NewStore(config)
	if err != nil {
		return nil, err
	}
	// Files is an explicit write capability, but the Records service above must
	// stay a read-only graph. Build the narrow Files records adapter from the
	// same immutable metadata, permission service, repository, and Runtime-owned
	// database while giving only this adapter the transaction/rollup dependencies
	// required by `_System_File` create/delete. The upload journal opens the outer
	// pessimistic transaction; this records engine detects and joins it.
	transactions := persistence.NewTransactionManager(
		database.NewConnectionFromDB(r.DB),
	)
	formulaViews := analytics.NewFormulaViewService(r.DB, r.Metadata, r.logger)
	rollupRepo := persistence.NewRollupRepository(r.DB)
	rollupRepo.SetSchemaProvider(r.Metadata)
	rollupRepo.SetViewResolver(formulaViews)
	rollup := analytics.NewRollupService(
		rollupRepo, r.Metadata, transactions, r.logger,
	)
	filePersistence := records.NewPersistenceService(
		r.recordRepo, rollup, r.Metadata, r.Permissions, &events.NoOpEventBus{},
		metadata.NewValidationService(formula.NewEngine()), transactions, nil,
		r.logger, nil, nil, nil, nil,
	)
	filePersistence.SetQueryService(r.Query)
	return files.NewContractService(
		store, config.Provider, config.Bucket, config.LegacyUploadRoot,
		filePersistence, r.Permissions, r.Metadata,
		files.WithoutLegacyFileFallback(),
		files.WithStorageAuthorityCatalog(authorities),
		files.WithDurableUploadJournal(persistence.NewFileUploadOperationRepository(r.DB)),
	), nil
}

// Prepare loads the same metadata and permission authorities used by the
// in-process engine before the split process may report readiness.
func (r *RecordsReadRuntime) Prepare(ctx context.Context) error {
	if r == nil || r.Metadata == nil || r.Permissions == nil {
		return errors.New("records-read runtime: graph is incomplete")
	}
	if !r.Metadata.UsesRequestBoundRuntimeView() {
		if err := r.Metadata.RefreshCache(ctx); err != nil {
			return err
		}
	}
	return r.Permissions.RefreshPermissions(ctx)
}
