package bootstrap

import (
	"context"
	"encoding/json"
	"log/slog"
	"strings"

	"gorm.io/gorm"

	"qycw/go-server/internal/config"
	"qycw/go-server/internal/db"
	licenseclient "qycw/go-server/internal/license"
	"qycw/go-server/internal/plugins"
	redisstore "qycw/go-server/internal/redis"
	"qycw/go-server/internal/settings"
)

type App struct {
	Config         config.Bootstrap
	DB             *gorm.DB
	Redis          *redisstore.Client
	Settings       settings.Store
	Plugins        *plugins.Service
	License        licenseclient.Remote
	LicenseState   *licenseclient.Store
	LicenseManager *licenseclient.Manager
	LicenseEval    *licenseclient.Evaluator
}

func New(ctx context.Context, cfg config.Bootstrap, logger *slog.Logger) (*App, error) {
	app := &App{
		Config:   cfg,
		Settings: settings.NewMemoryStore(),
		Plugins:  plugins.NewService(cfg.PluginDirs, cfg.PluginCallTimeout, logger),
		License:  licenseRemote(cfg, logger),
	}

	if cfg.DBEnabled {
		conn, err := db.Open(cfg.DBDSN)
		if err != nil {
			return nil, err
		}
		app.DB = conn

		keyProvider := licenseKeyProvider(cfg, logger)
		app.LicenseState = licenseclient.NewStore(conn, keyProvider)
		app.LicenseEval = licenseclient.NewEvaluator(nil)
		app.LicenseManager = licenseclient.NewManager(licenseRemote(cfg, logger), app.LicenseState, cfg.AppVersion, cfg.BackendURL).
			WithEvaluator(app.LicenseEval).
			WithHighWaterClock(&licenseclient.HighWaterClock{}).
			WithConnectivityProber(licenseclient.NewHTTPConnectivityProber(cfg.LicenseProbeURLs...))
		logger.Info("mysql connected")
		if err := db.EnsureCompatibility(conn); err != nil {
			return nil, err
		}
		if err := migrateLegacyPluginProviders(ctx, conn, app.Plugins, logger); err != nil {
			return nil, err
		}
	}

	if app.LicenseEval != nil {
		app.Plugins.WithLicense(app.LicenseEval)
	}

	if cfg.RedisEnabled {
		client, err := redisstore.Open(ctx, redisstore.Options{
			Addr:     cfg.RedisAddr,
			Password: cfg.RedisPassword,
			DB:       cfg.RedisDB,
		})
		if err != nil {
			return nil, err
		}
		app.Redis = client
		logger.Info("redis connected", "addr", cfg.RedisAddr, "db", cfg.RedisDB)
	}

	return app, nil
}

type providerSetting struct {
	Key   string          `gorm:"column:key"`
	Value json.RawMessage `gorm:"column:value"`
}

func (providerSetting) TableName() string {
	return "system_settings"
}

func migrateLegacyPluginProviders(ctx context.Context, conn *gorm.DB, pluginService *plugins.Service, logger *slog.Logger) error {
	if conn == nil || pluginService == nil {
		return nil
	}
	keys := map[string]string{
		"notification.email.provider": plugins.CapabilityEmail,
		"notification.sms.provider":   plugins.CapabilitySMS,
		"verification.provider":       plugins.CapabilityVerification,
	}
	settingKeys := make([]string, 0, len(keys))
	for key := range keys {
		settingKeys = append(settingKeys, key)
	}
	var settings []providerSetting
	if err := conn.WithContext(ctx).Where("`key` IN ?", settingKeys).Find(&settings).Error; err != nil {
		return err
	}
	for _, item := range settings {
		var provider string
		if err := json.Unmarshal(item.Value, &provider); err != nil {
			provider = strings.Trim(strings.TrimSpace(string(item.Value)), `"`)
		}
		pluginID := legacyProviderPluginID(provider, keys[item.Key])
		if pluginID == "" {
			continue
		}
		manifest, err := pluginService.Manifest(ctx, pluginID)
		if err != nil || !manifest.HasCapability(keys[item.Key]) {
			continue
		}
		if !manifest.Enabled {
			manifest, err = pluginService.SetEnabled(ctx, pluginID, manifest.Version, true)
			if err != nil {
				return err
			}
		}
		raw, _ := json.Marshal("plugin:" + pluginID)
		if err := conn.WithContext(ctx).Table("system_settings").Where("`key` = ?", item.Key).Update("value", raw).Error; err != nil {
			return err
		}
		if logger != nil {
			logger.Info("migrated legacy provider to Go plugin", "setting", item.Key, "plugin", pluginID)
		}
	}
	return nil
}

func legacyProviderPluginID(provider string, capability string) string {
	prefix := "mode." + capability + "."
	provider = strings.TrimSpace(provider)
	if !strings.HasPrefix(provider, prefix) {
		return ""
	}
	id := strings.SplitN(strings.TrimPrefix(provider, prefix), ".", 2)[0]
	if plugins.SafeID(id) {
		return id
	}
	return ""
}

// licenseKeyProvider selects between an environment-injected static key
// (required for correct multi-replica deployments — see
// docs/superpowers/specs/2026-07-12-commercial-license-hardening-design.md
// section 5.1) and the single-file key used in single-replica deployments.
func licenseKeyProvider(cfg config.Bootstrap, logger *slog.Logger) licenseclient.KeyProvider {
	hexKey := strings.TrimSpace(cfg.LicenseMasterKeyHex)
	if hexKey == "" {
		return licenseclient.NewFileKeyProvider(cfg.LicenseKeyFile)
	}
	provider, err := licenseclient.NewStaticKeyProvider(hexKey)
	if err != nil {
		logger.Warn("GO_LICENSE_MASTER_KEY_HEX is invalid, falling back to file-based key", "error", err)
		return licenseclient.NewFileKeyProvider(cfg.LicenseKeyFile)
	}
	return provider
}

// licenseRemote builds a Remote that tries every configured authorization
// center in sequence, falling back to a single-node client built from
// LicenseServerURL when LicenseAuthURLs is unset.
func licenseRemote(cfg config.Bootstrap, logger *slog.Logger) licenseclient.Remote {
	urls := cfg.LicenseAuthURLs
	if len(urls) == 0 {
		urls = []string{cfg.LicenseServerURL}
	}
	remotes := make([]licenseclient.Remote, 0, len(urls))
	for _, url := range urls {
		url = strings.TrimSpace(url)
		if url == "" {
			continue
		}
		client := licenseclient.NewClient(url, cfg.LicenseRequestTimeout, logger).
			RequirePinnedKey(cfg.LicenseRequirePinnedKey)
		remotes = append(remotes, client)
	}
	return licenseclient.NewMultiRemote(remotes...)
}

func (a *App) Close() {
	if a == nil {
		return
	}
	if a.Redis != nil {
		_ = a.Redis.Close()
	}
	if a.DB != nil {
		if sqlDB, err := a.DB.DB(); err == nil {
			_ = sqlDB.Close()
		}
	}
}
