package seamtestwiring

import (
	"context"
	"database/sql"
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/nexuscrm/analytics"
	"github.com/nexuscrm/auth"
	"github.com/nexuscrm/automation/autonumber"
	"github.com/nexuscrm/db"
	"github.com/nexuscrm/events"
	"github.com/nexuscrm/kernel/pkg/flowactions"
	"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/records"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/pingcap-inc/nexuscrm-controlplane"
	"github.com/pingcap-inc/nexuscrm-controlplane/internal/persistencewiring"
	"github.com/pingcap-inc/nexuscrm-controlplane/packs"
	"golang.org/x/crypto/bcrypt"
)

func wireServiceGraph() {
	controlplane.BuildServiceGraph = buildServiceGraph
}

func buildServiceGraph(conn *database.TiDBConnection) (controlplane.ServiceGraph, controlplane.RunnerInfra, error) {
	sqlDB := conn.DB()
	logger := logging.New(context.Background())

	txm := persistence.NewTransactionManager(conn)
	userRepo := persistence.NewUserRepository(sqlDB)
	systemRepo := persistence.NewSystemRepository(sqlDB)
	schemaRepo := persistence.NewSchemaRepository(sqlDB, logging.Default)
	metadataRepo := persistence.NewMetadataRepository(sqlDB)
	sessionRepo := persistence.NewSessionRepository(sqlDB)
	permissionRepo := persistence.NewPermissionRepository(sqlDB)
	queryRepo := persistence.NewQueryRepository(sqlDB)
	recordRepo := persistence.NewRecordRepository(sqlDB)
	outboxRepo := persistence.NewOutboxRepository(sqlDB)
	rollupRepo := persistence.NewRollupRepository(sqlDB)
	directWriters, err := persistencewiring.NewPermissionStateDirectWritersForTransactionManager(txm)
	if err != nil {
		return nil, controlplane.RunnerInfra{}, err
	}
	if err := directWriters.BindUser(userRepo); err != nil {
		return nil, controlplane.RunnerInfra{}, err
	}
	if err := directWriters.BindSchema(schemaRepo); err != nil {
		return nil, controlplane.RunnerInfra{}, err
	}
	if err := directWriters.BindMetadata(metadataRepo); err != nil {
		return nil, controlplane.RunnerInfra{}, err
	}
	if err := directWriters.BindPermission(permissionRepo); err != nil {
		return nil, controlplane.RunnerInfra{}, err
	}

	eventBus := &events.NoOpEventBus{}
	validation := metadata.NewValidationService(formula.NewEngine())
	schema := metadata.NewSchemaManager(schemaRepo)
	metadataSvc := metadata.NewMetadataService(metadataRepo, schema, eventBus)
	metadataSvc.SetTransactionManager(txm)
	metadataSvc.SetValidationService(validation)
	validation.SetFormulaRelationResolver(metadata.NewValidationFormulaRelationResolver(metadataSvc, recordRepo))
	validation.SetActiveProjectionResolver(metadata.NewValidationActiveProjectionResolver(metadataSvc, queryRepo))
	recordRepo.SetSchemaProvider(metadataSvc)

	permissionSvc := permissions.NewPermissionService(permissionRepo, metadataSvc, userRepo)
	if listViewRepo, ok := any(metadataRepo).(metadata.ListViewRepository); ok {
		listViews := metadata.NewListViewService(listViewRepo, permissionSvc)
		metadataSvc.SetListViewRLSProvider(permissionSvc)
		_ = listViews
	}
	metadataSvc.SetListViewRLSProvider(permissionSvc)
	metadataSvc.RegisterSchemaDecorator(permissions.NewSchemaDecorator(permissionSvc))
	metadataSvc.RegisterSchemaDecorator(metadata.NewCompoundVirtualFieldDecorator(metadata.PlatformCompoundVirtualFieldDeclarations()...))
	metadataSvc.RegisterSchemaDecorator(metadata.NewServerDefaultedFieldsDecorator(metadata.PlatformServerDefaultedFieldDeclarations()...))

	authSvc := auth.NewAuthService(userRepo, sessionRepo, permissionRepo)
	authSvc.SetPermissionService(permissionSvc)
	authSvc.SetLoginHistoryRepo(persistence.NewLoginHistoryRepository(sqlDB))
	setupAuditor := auth.NewSetupAuditor(persistence.NewSetupAuditRepository(sqlDB))
	authSvc.SetSetupAuditor(setupAuditor)
	permissionSvc.SetSetupAuditor(setupAuditor)
	folderAccess := permissions.NewFolderAccessService(permissionSvc, sqlDB)
	permissionSvc.SetFolderAccess(folderAccess)

	formulaView := analytics.NewFormulaViewService(sqlDB, metadataSvc, logger)
	metadataSvc.SetFormulaViewRebuilder(formulaView)
	metadataSvc.SetValidationProjectionInvalidator(formulaView)
	metadataSvc.RegisterCacheInvalidationHook(formulaView.InvalidateAllValidationProjections)
	validation.SetActiveFormulaResolver(analytics.NewValidationActiveFormulaResolver(formulaView, queryRepo))
	validation.SetValidationProjectionProvider(formulaView)
	metadata.SetAggregateFormulaDetector(func(expr string) (bool, error) {
		aggregates, err := analytics.FindAggregateAuthoringExprs(expr)
		return len(aggregates) > 0, err
	})
	queryRepo.SetViewResolver(formulaView)
	recordRepo.SetViewResolver(formulaView)
	rollupRepo.SetSchemaProvider(metadataSvc)
	rollupRepo.SetViewResolver(formulaView)

	querySvc := analytics.NewQueryService(queryRepo, metadataSvc, permissionSvc, logger)
	validation.SetRecordSetBindingResolver(metadata.NewValidationRecordSetBindingResolver(
		metadataSvc,
		querySvc,
		metadata.NewValidationFormulaRelationResolver(metadataSvc, recordRepo),
	))
	rollupSvc := analytics.NewRollupService(rollupRepo, metadataSvc, txm, logger)
	outboxSvc := events.NewOutboxService(outboxRepo, eventBus, txm).WithMetadataLookup(metadataSvc)

	autoNumberMetadata := autonumber.NewMetadataSource(sqlDB, nil)
	if err := autoNumberMetadata.RefreshWithBackoff(context.Background(), 5, 500*time.Millisecond); err != nil {
		return nil, controlplane.RunnerInfra{}, fmt.Errorf("autonumber metadata init failed: %w", err)
	}
	metadataSvc.SetAutoNumberInvalidator(autoNumberMetadata)
	autoNumberFieldResolver := func(objectName string) map[string]struct{} {
		fields := 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
	}
	validation.SetAutoNumberResolver(autoNumberFieldResolver)
	metadataSvc.SetAutoNumberFieldResolver(autoNumberFieldResolver)
	metadataSvc.RegisterSchemaDecorator(metadata.NewAutoNumberDefaultedFieldsDecorator(autoNumberFieldResolver))
	autoNumberEngine, err := autonumber.NewEngine(
		autonumber.WithReservationDB(sqlDB),
		autonumber.WithMetadataProvider(autoNumberMetadata),
		autonumber.WithCrossRecordResolver(autonumber.NewRepoCrossRecordResolver(recordRepo)),
	)
	if err != nil {
		return nil, controlplane.RunnerInfra{}, fmt.Errorf("autonumber engine init: %w", err)
	}

	persistenceSvc := records.NewPersistenceService(
		recordRepo,
		rollupSvc,
		metadataSvc,
		permissionSvc,
		eventBus,
		validation,
		txm,
		outboxSvc,
		logger,
		autoNumberEngine,
		autoNumberMetadata,
		sessionRepo,
		authSvc,
	)
	persistenceSvc.SetRawDB(sqlDB)
	persistenceSvc.SetParentLaneAdmissionController(
		records.NewParentLaneAdmissionController(persistence.NewParentLaneStore(conn)),
	)
	persistenceSvc.SetCurrentUserPasswordLookup(userRepo)
	persistenceSvc.SetQueryService(querySvc)
	persistenceSvc.SetActiveFormulaProjector(analytics.NewActiveFormulaProjectionAdapter(querySvc))
	metadataSvc.RegisterCacheInvalidationHook(persistenceSvc.InvalidateCreateFromSourceGuardCache)
	metadataSvc.RegisterCacheInvalidationHook(persistenceSvc.InvalidateInsertInheritanceCache)
	rollupSvc.SetDerivedWriter(persistenceSvc)
	flowactions.Seal()

	graph := serviceGraph{
		db:                 sqlDB,
		txManager:          txm,
		metadataRepo:       metadataRepo,
		sessionRepo:        sessionRepo,
		schema:             schema,
		metadata:           metadataSvc,
		permissions:        permissionSvc,
		formulaView:        formulaView,
		autoNumberMetadata: autoNumberMetadata,
		auth:               authSvc,
		persistence:        persistenceSvc,
		querySvc:           querySvc,
		system:             systemAdmin{persistence: persistenceSvc, repo: systemRepo},
	}
	return graph, controlplane.RunnerInfra{
		TxManager:    txm,
		MetadataRepo: metadataRepo,
		SessionRepo:  sessionRepo,
	}, nil
}

type serviceGraph struct {
	db                 *sql.DB
	txManager          ports.TransactionManager
	metadataRepo       ports.MetadataRepository
	sessionRepo        ports.SessionRepository
	schema             *metadata.SchemaManager
	metadata           *metadata.MetadataService
	permissions        *permissions.PermissionService
	formulaView        *analytics.FormulaViewService
	autoNumberMetadata *autonumber.MetadataSource
	auth               *auth.AuthService
	persistence        *records.PersistenceService
	querySvc           ports.QueryService
	system             packs.SystemAdmin
}

func (g serviceGraph) Metadata() *metadata.MetadataService         { return g.metadata }
func (g serviceGraph) Schema() *metadata.SchemaManager             { return g.schema }
func (g serviceGraph) Permissions() *permissions.PermissionService { return g.permissions }
func (g serviceGraph) FormulaView() *analytics.FormulaViewService  { return g.formulaView }
func (g serviceGraph) AutoNumberMetadata() *autonumber.MetadataSource {
	return g.autoNumberMetadata
}
func (g serviceGraph) Auth() *auth.AuthService                  { return g.auth }
func (g serviceGraph) System() packs.SystemAdmin                { return g.system }
func (g serviceGraph) Persistence() *records.PersistenceService { return g.persistence }
func (g serviceGraph) QuerySvc() ports.QueryService             { return g.querySvc }
func (g serviceGraph) DB() *sql.DB                              { return g.db }

func (g serviceGraph) ConnectorInbound() controlplane.ConnectorInboundLifecycle { return nil }

func (g serviceGraph) RefreshMetadataCache(ctx context.Context) error {
	if g.autoNumberMetadata != nil {
		if err := g.autoNumberMetadata.Refresh(ctx); err != nil {
			return err
		}
	}
	if g.metadata != nil {
		if err := g.metadata.RefreshCache(ctx); err != nil {
			return err
		}
	}
	if g.permissions != nil {
		if err := g.permissions.RefreshPermissions(ctx); err != nil {
			return err
		}
	}
	return nil
}

func (g serviceGraph) ReconcileChangedConnectorInboundWorkers(context.Context) error { return nil }

type systemAdmin struct {
	persistence *records.PersistenceService
	repo        ports.SystemRepository
}

func (s systemAdmin) BatchUpsertProfiles(ctx context.Context, profiles []models.Profile) error {
	return s.repo.BatchUpsertProfiles(ctx, profiles)
}

func (s systemAdmin) BatchUpsertUsers(ctx context.Context, users []models.User) error {
	if len(users) == 0 {
		return nil
	}
	hashes := map[string]string{}
	prepared := make([]models.User, len(users))
	for i, user := range users {
		hashed, ok := hashes[user.Password]
		if !ok {
			raw, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
			if err != nil {
				return err
			}
			hashed = string(raw)
			hashes[user.Password] = hashed
		}
		user.Password = hashed
		prepared[i] = user
	}
	return s.repo.BatchUpsertUsers(ctx, prepared)
}

func (s systemAdmin) ForceSetUserPassword(ctx context.Context, email, password string) error {
	hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return err
	}
	return s.repo.ForceUpdateUserPassword(ctx, email, string(hashed))
}

func (s systemAdmin) UpsertConnectedApp(ctx context.Context, id, name, consumerKey, consumerSecret string, sessionIssuerClass ...string) error {
	ctx = constants.WithSecurityContext(ctx, constants.SystemContext, "systemAdmin.UpsertConnectedApp")
	app := models.SObject{
		constants.FieldID:             id,
		constants.FieldName:           name,
		constants.FieldConsumerKey:    consumerKey,
		constants.FieldConsumerSecret: consumerSecret,
		constants.FieldIsActive:       true,
		constants.FieldCallbackURL:    bootstrapConnectedAppCallbackURL(),
	}
	if len(sessionIssuerClass) > 0 && strings.TrimSpace(sessionIssuerClass[0]) != "" {
		app[constants.ColSysConnectedApp_SessionIssuerClass] = strings.TrimSpace(sessionIssuerClass[0])
	}
	if err := s.persistence.Update(ctx, constants.TableConnectedApp, id, app, nil); err == nil {
		return nil
	}
	_, err := s.persistence.Insert(ctx, constants.TableConnectedApp, app, nil)
	return err
}

func bootstrapConnectedAppCallbackURL() string {
	baseURL := strings.TrimRight(strings.TrimSpace(os.Getenv("API_BASE_URL")), "/")
	if baseURL == "" {
		return "http://127.0.0.1:8080/callback"
	}
	return baseURL + "/callback"
}
