package server

import (
	"bytes"
	"context"
	"fmt"
	"log/slog"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/archive"
	"github.com/tidbcloud/AgentTeam/internal/config"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/mailer"
	"github.com/tidbcloud/AgentTeam/internal/objectstorage"
	"github.com/tidbcloud/AgentTeam/internal/router"
	"github.com/tidbcloud/AgentTeam/internal/sms"
	"github.com/tidbcloud/AgentTeam/internal/store"
	"github.com/tidbcloud/AgentTeam/internal/store/app"
)

const (
	githubBaseURL    = "https://github.com"
	githubAPIBaseURL = "https://api.github.com"
)

// infra holds foundational infrastructure built before HTTP-layer wiring.
type infra struct {
	coreDBConn               coredb.AdminDB
	coreDBAdmin              coredb.Admin
	coreDataDB               coredb.DB
	coreDBPool               *coredb.SQLDBPoolController
	stores                   *store.Graph
	feishuCredentialKey      []byte
	appCredentialKey         []byte
	legacyAppCredentialKeys  [][]byte
	mailService              mailer.Mailer
	mobileVerificationSender sms.Sender
	registrationMode         string
}

// closeDBOnError closes the core database connection if it is still owned
// (i.e. ownership has not been transferred to the Server).
func (inf *infra) closeDBOnError() {
	if inf.coreDBConn != nil {
		inf.coreDBConn.Close()
	}
}

// buildInfra constructs foundational infrastructure: database connections,
// external service clients, and the store graph.
func buildInfra(ctx context.Context, cfg config.Config, logger *slog.Logger, sqlOpener coredb.SQLOpener, httpWrapper httpClientWrapper) (*infra, error) {
	coreDBAdmin := coredb.NewAdminWithOptions(coredb.WithSQLOpener(sqlOpener))

	coreDBConn, err := coreDBAdmin.Open(ctx, cfg.Database.URL, cfg.Database.ConnectTimeout)
	if err != nil {
		return nil, fmt.Errorf("connect core main-path database: %w", err)
	}

	coreDataDB, ok := coreDBConn.(coredb.DB)
	if !ok {
		coreDBConn.Close()
		return nil, fmt.Errorf("core database connection does not implement data path")
	}
	rawSQLDB := coredb.RawSQLDB(coreDataDB)
	if rawSQLDB == nil {
		coreDBConn.Close()
		return nil, fmt.Errorf("core database connection does not expose sql db")
	}
	coreDBPool, err := coredb.NewSQLDBPoolController(rawSQLDB, coredb.DBPoolConfig{
		MaxOpenConns:    cfg.Database.Pool.MaxOpenConns,
		MaxIdleConns:    cfg.Database.Pool.MaxIdleConns,
		ConnMaxLifetime: cfg.Database.Pool.ConnMaxLifetime,
		ConnMaxIdleTime: cfg.Database.Pool.ConnMaxIdleTime,
	})
	if err != nil {
		coreDBConn.Close()
		return nil, fmt.Errorf("configure core main-path database pool: %w", err)
	}

	if err := coreDBAdmin.RunStartupPreflight(ctx, coreDBConn); err != nil {
		coreDBConn.Close()
		return nil, fmt.Errorf("run core main-path database startup preflight: %w", err)
	}
	if err := coreDBAdmin.RequireSchemaCurrent(ctx, coreDBConn); err != nil {
		coreDBConn.Close()
		return nil, fmt.Errorf("validate core main-path database schema: %w", err)
	}

	mailService, err := buildMailer(ctx, cfg, logger, httpWrapper)
	if err != nil {
		coreDBConn.Close()
		return nil, err
	}
	mobileVerificationSender, err := buildMobileVerificationSender(cfg, logger, httpWrapper)
	if err != nil {
		coreDBConn.Close()
		return nil, err
	}

	archiveStorage, err := buildArchiveStorage(ctx, cfg, httpWrapper)
	if err != nil {
		coreDBConn.Close()
		return nil, err
	}

	mediaStorage, err := buildMediaObjectStorage(ctx, cfg, httpWrapper)
	if err != nil {
		coreDBConn.Close()
		return nil, err
	}

	feishuCredentialKeyResolution, err := cfg.Feishu.ResolveCredentialEncryptionKey()
	if err != nil {
		coreDBConn.Close()
		return nil, err
	}
	if logger != nil {
		switch feishuCredentialKeyResolution.Source {
		case "generated":
			logger.Info("generated org credential master key", slog.String("path", feishuCredentialKeyResolution.Path))
		case "file":
			logger.Info("loaded org credential master key", slog.String("path", feishuCredentialKeyResolution.Path))
		}
	}

	appCredentialKeyResolution, err := cfg.AppCredentials.ResolveCredentialEncryptionKey()
	if err != nil {
		coreDBConn.Close()
		return nil, err
	}
	if logger != nil {
		switch appCredentialKeyResolution.Source {
		case "generated":
			logger.Info("generated app credential master key", slog.String("path", appCredentialKeyResolution.Path))
		case "file":
			logger.Info("loaded app credential master key", slog.String("path", appCredentialKeyResolution.Path))
		}
	}

	legacyAppCredentialStorageKeys := [][]byte{}
	if len(feishuCredentialKeyResolution.Key) == 32 &&
		len(appCredentialKeyResolution.Key) == 32 &&
		!bytes.Equal(feishuCredentialKeyResolution.Key, appCredentialKeyResolution.Key) {
		legacyAppCredentialStorageKeys = append(legacyAppCredentialStorageKeys, feishuCredentialKeyResolution.Key)
	}

	stores := store.NewWithOptions(coreDataDB, archiveStorage, mediaStorage, cfg.Messages.HotRetentionDays, store.Options{
		AppCredentialStorageKey:         appCredentialKeyResolution.Key,
		LegacyAppCredentialStorageKeys:  legacyAppCredentialStorageKeys,
		HTTPClient:                      newOutboundHTTPClient(httpWrapper, "store.app", 10*time.Second, nil),
		PublicBaseURL:                   cfg.PublicBaseURL,
		GitHubAPIBaseURL:                githubAPIBaseURL,
		Logger:                          logger,
		MediaPublicBaseURL:              cfg.MediaStorage.PublicBaseURL,
		MediaObjectPrefix:               cfg.MediaStorage.Prefix,
		AppNotificationInternalServices: appNotificationInternalServiceRegistrations(cfg.Server.AppNotificationInternalServices),
	})

	if err := reconcileMachineConnectionsOnStartup(ctx, cfg, stores.Agent, stores.Workspace, logger); err != nil {
		coreDBConn.Close()
		return nil, err
	}

	return &infra{
		coreDBConn:               coreDBConn,
		coreDBAdmin:              coreDBAdmin,
		coreDataDB:               coreDataDB,
		coreDBPool:               coreDBPool,
		stores:                   stores,
		feishuCredentialKey:      feishuCredentialKeyResolution.Key,
		appCredentialKey:         append([]byte(nil), appCredentialKeyResolution.Key...),
		legacyAppCredentialKeys:  cloneCredentialKeysForInfra(legacyAppCredentialStorageKeys),
		mailService:              mailService,
		mobileVerificationSender: mobileVerificationSender,
		registrationMode:         cfg.Auth.RegistrationModeOrDefault(),
	}, nil
}

func appNotificationInternalServiceRegistrations(configs []config.AppNotificationInternalServiceConfig) []app.AppNotificationInternalServiceRegistration {
	if len(configs) == 0 {
		return nil
	}
	registrations := make([]app.AppNotificationInternalServiceRegistration, 0, len(configs))
	for _, service := range configs {
		registrations = append(registrations, app.AppNotificationInternalServiceRegistration{
			IdentityID: service.IdentityID, Secret: service.Secret,
			ServiceDefinitionSlug:     service.ServiceDefinitionSlug,
			AllowedBusinessNamespaces: append([]string(nil), service.AllowedBusinessNamespaces...),
			AllowedTargetKinds:        append([]string(nil), service.AllowedTargetKinds...),
			AllowedContentKinds:       append([]string(nil), service.AllowedContentKinds...),
		})
	}
	return registrations
}

func cloneCredentialKeysForInfra(keys [][]byte) [][]byte {
	cloned := make([][]byte, 0, len(keys))
	for _, key := range keys {
		cloned = append(cloned, append([]byte(nil), key...))
	}
	return cloned
}

type startupMachineReconciler interface {
	MarkMachinesDisconnectedOnStartup(ctx context.Context) (int64, error)
	MarkMachinesDisconnectedOnStartupForWorkspaces(ctx context.Context, workspaceIDs []string) (int64, error)
}

type startupWorkspacePlacementLister interface {
	ListRoutableWorkspaceIDsForGroup(ctx context.Context, workspaceGroupID string) ([]string, error)
}

func reconcileMachineConnectionsOnStartup(ctx context.Context, cfg config.Config, agentStore startupMachineReconciler, workspaceStore startupWorkspacePlacementLister, logger *slog.Logger) error {
	if agentStore == nil {
		return fmt.Errorf("reconcile machine connections: agent store is not configured")
	}

	profile := router.Profile(cfg.Server.ProfileOrDefault())
	var (
		disconnectedCount int64
		err               error
		workspaceGroupID  string
		workspaceCount    int
	)
	switch profile {
	case router.ProfileCentral:
		if logger != nil {
			logger.Info("skipped machine connection reconcile on startup", slog.String("profile", string(profile)))
		}
		return nil
	case router.ProfileAll:
		disconnectedCount, err = agentStore.MarkMachinesDisconnectedOnStartup(ctx)
	case router.ProfileWorkspace:
		if workspaceStore == nil {
			return fmt.Errorf("reconcile machine connections: workspace store is not configured")
		}
		workspaceGroupID = strings.TrimSpace(cfg.Server.WorkspaceGroupID)
		if workspaceGroupID == "" {
			return fmt.Errorf("reconcile machine connections: AGENTTEAM_WORKSPACE_GROUP_ID is required for workspace profile")
		}
		workspaceIDs, listErr := workspaceStore.ListRoutableWorkspaceIDsForGroup(ctx, workspaceGroupID)
		if listErr != nil {
			return fmt.Errorf("reconcile machine connections: list routable workspaces for group %s: %w", workspaceGroupID, listErr)
		}
		workspaceCount = len(workspaceIDs)
		disconnectedCount, err = agentStore.MarkMachinesDisconnectedOnStartupForWorkspaces(ctx, workspaceIDs)
	default:
		return fmt.Errorf("reconcile machine connections: unsupported server profile %q", profile)
	}
	if err != nil {
		return fmt.Errorf("reconcile machine connections: %w", err)
	}
	if logger != nil && (disconnectedCount > 0 || profile == router.ProfileWorkspace) {
		attrs := []slog.Attr{
			slog.String("profile", string(profile)),
			slog.Int64("machines", disconnectedCount),
		}
		if profile == router.ProfileWorkspace {
			attrs = append(attrs, slog.String("workspace_group_id", workspaceGroupID), slog.Int("workspaces", workspaceCount))
		}
		logger.LogAttrs(ctx, slog.LevelInfo, "reconciled machine connections on startup", attrs...)
	}
	return nil
}

func buildArchiveStorage(ctx context.Context, cfg config.Config, httpWrapper httpClientWrapper) (archive.Storage, error) {
	switch cfg.Archive.Driver {
	case "filesystem":
		return archive.NewFilesystem(cfg.Archive.FilesystemRoot)
	case "s3":
		return archive.NewS3(ctx, cfg.Archive.S3Region, cfg.Archive.S3Bucket, cfg.Archive.S3Prefix, archive.WithS3HTTPClient(newOutboundHTTPClient(httpWrapper, "aws.s3", 0, nil)))
	default:
		return nil, fmt.Errorf("unsupported archive driver %q in current build", cfg.Archive.Driver)
	}
}

func buildMailer(ctx context.Context, cfg config.Config, logger *slog.Logger, httpWrapper httpClientWrapper) (mailer.Mailer, error) {
	switch cfg.Mail.Driver {
	case "noop":
		return mailer.NewNoop(logger, cfg.PublicBaseURL), nil
	case "ses":
		return mailer.NewSES(ctx, cfg.PublicBaseURL, cfg.Mail.FromAddress, cfg.Mail.SESRegion, mailer.WithSESHTTPClient(newOutboundHTTPClient(httpWrapper, "aws.ses", 30*time.Second, nil)))
	case "smtp":
		return mailer.NewSMTP(
			cfg.PublicBaseURL,
			cfg.Mail.FromAddress,
			cfg.Mail.SMTPHost,
			cfg.Mail.SMTPPort,
			cfg.Mail.SMTPUsername,
			cfg.Mail.SMTPPassword,
			cfg.Mail.SMTPSecurity,
		)
	default:
		return nil, fmt.Errorf("unsupported mail driver %q in current build", cfg.Mail.Driver)
	}
}

func buildMobileVerificationSender(cfg config.Config, logger *slog.Logger, httpWrapper httpClientWrapper) (sms.Sender, error) {
	switch cfg.MobileVerify.Driver {
	case "", "noop":
		return sms.NewNoop(logger), nil
	case "twilio":
		return sms.NewTwilio(sms.TwilioOptions{
			AccountSID:          cfg.MobileVerify.TwilioAccountSID,
			AuthToken:           cfg.MobileVerify.TwilioAuthToken,
			FromNumber:          cfg.MobileVerify.TwilioFromNumber,
			MessagingServiceSID: cfg.MobileVerify.TwilioMessagingServiceSID,
			HTTPClient:          newOutboundHTTPClient(httpWrapper, "twilio", 10*time.Second, nil),
		})
	default:
		return nil, fmt.Errorf("unsupported mobile verify driver %q in current build", cfg.MobileVerify.Driver)
	}
}

func buildMediaObjectStorage(ctx context.Context, cfg config.Config, httpWrapper httpClientWrapper) (objectstorage.Storage, error) {
	switch cfg.MediaStorage.Driver {
	case "", "disabled":
		return nil, nil
	case "oss", "s3":
		storage, err := objectstorage.NewOSS(ctx, objectstorage.OSSConfig{
			Region:          cfg.MediaStorage.Region,
			Bucket:          cfg.MediaStorage.Bucket,
			Prefix:          cfg.MediaStorage.Prefix,
			Endpoint:        cfg.MediaStorage.Endpoint,
			AccessKeyID:     cfg.MediaStorage.AccessKeyID,
			AccessKeySecret: cfg.MediaStorage.AccessKeySecret,
		}, objectstorage.WithOSSHTTPClient(newOutboundHTTPClient(httpWrapper, "aliyun.oss", 0, nil)))
		if err != nil {
			return nil, fmt.Errorf("build media object storage: %w", err)
		}
		return storage, nil
	default:
		return nil, fmt.Errorf("unsupported media object storage driver %q in current build", cfg.MediaStorage.Driver)
	}
}
