package initialize

import (
	"context"
	"encoding/json"

	"kra/internal/conf"
	"kra/pkg/protoutil"

	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/durationpb"
)

type configurationEnvelope struct {
	Data  json.RawMessage `json:"data"`
	Admin json.RawMessage `json:"admin"`
	Email *struct {
		To          string `json:"to"`
		From        string `json:"from"`
		Host        string `json:"host"`
		Secret      string `json:"secret"`
		Nickname    string `json:"nickname"`
		Port        int32  `json:"port"`
		IsSSL       bool   `json:"is-ssl"`
		IsLoginAuth bool   `json:"is-loginauth"`
	} `json:"email"`
}

func (r *Repo) ConfigurationJSON() (json.RawMessage, error) {
	dataConfig, adminConfig := r.backend.RuntimeValues()
	admin := map[string]any{"routerPrefix": ""}
	email := map[string]any{}
	if adminConfig != nil {
		admin["routerPrefix"] = adminConfig.RouterPrefix
		if adminConfig.System != nil {
			admin["system"] = map[string]any{
				"useRedis":           adminConfig.System.UseRedis,
				"useMultipoint":      adminConfig.System.UseMultipoint,
				"useStrictAuth":      adminConfig.System.UseStrictAuth,
				"disableAutoMigrate": adminConfig.System.DisableAutoMigrate,
				"useMongo":           adminConfig.System.UseMongo,
				"addr":               adminConfig.System.Addr,
				"iplimitCount":       adminConfig.System.IplimitCount,
				"iplimitTime":        adminConfig.System.IplimitTime,
			}
		}
		if adminConfig.Jwt != nil {
			admin["jwt"] = map[string]any{"signingKey": "******", "expiresTime": durationString(adminConfig.Jwt.ExpiresTime), "bufferTime": durationString(adminConfig.Jwt.BufferTime), "issuer": adminConfig.Jwt.Issuer}
		}
		if adminConfig.Captcha != nil {
			admin["captcha"] = map[string]any{"keyLong": adminConfig.Captcha.KeyLong, "imgWidth": adminConfig.Captcha.ImgWidth, "imgHeight": adminConfig.Captcha.ImgHeight, "storeExpiration": durationString(adminConfig.Captcha.StoreExpiration)}
		}
		if adminConfig.Local != nil {
			admin["local"] = map[string]any{"storePath": adminConfig.Local.StorePath, "pathPrefix": adminConfig.Local.PathPrefix}
		}
		if adminConfig.Media != nil {
			chunkDir := adminConfig.Media.ChunkDir
			if chunkDir == "" {
				chunkDir = "uploads/chunks"
			}
			admin["media"] = map[string]any{"sessionTtl": adminConfig.Media.SessionTtl, "maxFileSize": adminConfig.Media.MaxFileSize, "chunkDir": chunkDir}
		}
		if adminConfig.Email != nil {
			secret := ""
			if adminConfig.Email.Secret != "" {
				secret = "******"
			}
			email = map[string]any{"to": adminConfig.Email.To, "from": adminConfig.Email.From, "host": adminConfig.Email.Host, "secret": secret, "nickname": adminConfig.Email.Nickname, "port": adminConfig.Email.Port, "is-ssl": adminConfig.Email.IsSsl, "is-loginauth": adminConfig.Email.IsLoginAuth}
		}
		if adminConfig.Storage != nil {
			storage := proto.Clone(adminConfig.Storage).(*conf.AdminBackend_Storage)
			maskStorageSecrets(storage)
			admin["storage"] = storage
		}
		disks := make([]map[string]any, 0, len(adminConfig.DiskList))
		for _, disk := range adminConfig.DiskList {
			if disk != nil {
				disks = append(disks, map[string]any{"mount_point": disk.MountPoint})
			}
		}
		admin["disk_list"] = disks
		if adminConfig.Zap != nil {
			admin["zap"] = adminConfig.Zap
		}
		if adminConfig.Cors != nil {
			admin["cors"] = adminConfig.Cors
		}
		if adminConfig.App != nil {
			admin["app"] = adminConfig.App
		}
	}
	// Never mask secrets on the live runtime object. ConfigurationJSON is a
	// read-only operation; mutating dataConfig here would replace the actual
	// database/Redis/Mongo credentials with "******" and make the next reload
	// fail. Work on a clone, just as we already do for adminConfig.
	safeData := cloneDataConfig(dataConfig)
	maskDataSecrets(safeData)
	safeAdmin := cloneAdminConfig(adminConfig)
	if safeAdmin != nil {
		if safeAdmin.Jwt != nil {
			safeAdmin.Jwt.SigningKey = "******"
		}
		if safeAdmin.Email != nil && safeAdmin.Email.Secret != "" {
			safeAdmin.Email.Secret = "******"
		}
		maskStorageSecrets(safeAdmin.Storage)
	}
	dataMap := map[string]any{}
	if safeData != nil {
		raw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(safeData)
		if err != nil {
			return nil, err
		}
		if err = json.Unmarshal(raw, &dataMap); err != nil {
			return nil, err
		}
	}
	config := compatibleConfiguration(safeData, safeAdmin)
	config["admin"], config["email"], config["data"] = admin, email, dataMap
	return json.Marshal(map[string]any{"config": config})
}

func (r *Repo) SaveConfigurationJSON(ctx context.Context, raw json.RawMessage) error {
	currentData, currentAdmin := r.backend.RuntimeValues()
	if currentAdmin == nil {
		return nil
	}
	nextData := cloneDataConfig(currentData)
	nextAdmin := proto.Clone(currentAdmin).(*conf.AdminBackend)
	if err := applyCompatibleConfiguration(raw, nextData, nextAdmin); err != nil {
		return err
	}
	var value configurationEnvelope
	if err := json.Unmarshal(raw, &value); err != nil {
		return err
	}
	options := protojson.UnmarshalOptions{DiscardUnknown: true}
	if len(value.Data) > 0 && string(value.Data) != "null" {
		if err := protoutil.MergeProtoJSON(nextData, value.Data, options); err != nil {
			return err
		}
	}
	if len(value.Admin) > 0 && string(value.Admin) != "null" {
		if err := protoutil.MergeProtoJSON(nextAdmin, value.Admin, options); err != nil {
			return err
		}
	}
	preserveDataSecrets(nextData, currentData)
	preserveAdminSecrets(nextAdmin, currentAdmin)
	if value.Email != nil {
		if nextAdmin.Email == nil {
			nextAdmin.Email = &conf.AdminBackend_Email{}
		}
		nextAdmin.Email.To, nextAdmin.Email.From, nextAdmin.Email.Host = value.Email.To, value.Email.From, value.Email.Host
		nextAdmin.Email.Nickname, nextAdmin.Email.Port = value.Email.Nickname, value.Email.Port
		nextAdmin.Email.IsSsl, nextAdmin.Email.IsLoginAuth = value.Email.IsSSL, value.Email.IsLoginAuth
		if value.Email.Secret != "" && value.Email.Secret != "******" {
			nextAdmin.Email.Secret = value.Email.Secret
		}
	}
	if err := r.backend.RefreshDatabaseSources(nextData); err != nil {
		return err
	}
	nextAdmin.ConfigPath = currentAdmin.ConfigPath
	dataRaw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(nextData)
	if err != nil {
		return err
	}
	adminRaw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(nextAdmin)
	if err != nil {
		return err
	}
	return r.PersistRuntimeConfig(ctx, dataRaw, adminRaw)
}

func (r *Repo) DiskMountPoints() []string {
	config := r.backend.RuntimeAdmin()
	if config == nil {
		return nil
	}
	points := make([]string, 0, len(config.DiskList))
	for _, item := range config.DiskList {
		if item != nil && item.MountPoint != "" {
			points = append(points, item.MountPoint)
		}
	}
	return points
}

func cloneDataConfig(value *conf.Data) *conf.Data {
	if value == nil {
		return &conf.Data{}
	}
	return proto.Clone(value).(*conf.Data)
}

func cloneAdminConfig(value *conf.AdminBackend) *conf.AdminBackend {
	if value == nil {
		return &conf.AdminBackend{}
	}
	return proto.Clone(value).(*conf.AdminBackend)
}

func durationString(value *durationpb.Duration) string {
	if value == nil {
		return "0s"
	}
	return value.AsDuration().String()
}

func maskDataSecrets(value *conf.Data) {
	if value == nil {
		return
	}
	if value.Database != nil {
		value.Database.Password = "******"
		// Source may contain an embedded password (for example a MySQL DSN).
		// The management response must never expose it. Structured database
		// fields are retained and refreshDatabaseSources rebuilds Source when
		// the configuration is saved.
		value.Database.Source = ""
	}
	if value.Redis != nil {
		value.Redis.Password = "******"
	}
	if value.Mongo != nil {
		value.Mongo.Password = "******"
	}
	for _, item := range value.DatabaseList {
		if item != nil {
			item.Password = "******"
			item.Source = ""
		}
	}
	for _, item := range value.RedisList {
		if item != nil {
			item.Password = "******"
		}
	}
}

func preserveDataSecrets(next, current *conf.Data) {
	if next == nil || current == nil {
		return
	}
	if next.Database != nil && current.Database != nil && maskedSecret(next.Database.Password) {
		next.Database.Password = current.Database.Password
	}
	if next.Database != nil && current.Database != nil && next.Database.Source == "" {
		next.Database.Source = current.Database.Source
	}
	if next.Redis != nil && current.Redis != nil && maskedSecret(next.Redis.Password) {
		next.Redis.Password = current.Redis.Password
	}
	if next.Mongo != nil && current.Mongo != nil && maskedSecret(next.Mongo.Password) {
		next.Mongo.Password = current.Mongo.Password
	}
	currentDatabases := make(map[string]*conf.Data_Database, len(current.DatabaseList))
	for _, item := range current.DatabaseList {
		if item != nil && item.AliasName != "" {
			currentDatabases[item.AliasName] = item
		}
	}
	for i, item := range next.DatabaseList {
		var previous *conf.Data_Database
		if item != nil && item.AliasName != "" {
			previous = currentDatabases[item.AliasName]
		}
		if previous == nil && i < len(current.DatabaseList) {
			previous = current.DatabaseList[i]
		}
		if item != nil && previous != nil {
			if maskedSecret(item.Password) {
				item.Password = previous.Password
			}
			if item.Source == "" {
				item.Source = previous.Source
			}
		}
	}
	currentRedis := make(map[string]*conf.Data_Redis, len(current.RedisList))
	for _, item := range current.RedisList {
		if item != nil && item.Name != "" {
			currentRedis[item.Name] = item
		}
	}
	for i, item := range next.RedisList {
		var previous *conf.Data_Redis
		if item != nil && item.Name != "" {
			previous = currentRedis[item.Name]
		}
		if previous == nil && i < len(current.RedisList) {
			previous = current.RedisList[i]
		}
		if item != nil && previous != nil && maskedSecret(item.Password) {
			item.Password = previous.Password
		}
	}
}

func preserveAdminSecrets(next, current *conf.AdminBackend) {
	if next.Jwt != nil && current.Jwt != nil && maskedSecret(next.Jwt.SigningKey) {
		next.Jwt.SigningKey = current.Jwt.SigningKey
	}
	if next.Email != nil && current.Email != nil && maskedSecret(next.Email.Secret) {
		next.Email.Secret = current.Email.Secret
	}
	preserveStorageSecrets(next.Storage, current.Storage)
}

func maskedSecret(value string) bool { return value == "" || value == "******" }

func objectStores(storage *conf.AdminBackend_Storage) []*conf.AdminBackend_ObjectStore {
	if storage == nil {
		return nil
	}
	return []*conf.AdminBackend_ObjectStore{storage.AliyunOss, storage.HuaweiObs, storage.TencentCos, storage.AwsS3, storage.CloudflareR2, storage.Minio}
}

func maskStorageSecrets(storage *conf.AdminBackend_Storage) {
	if storage == nil {
		return
	}
	if storage.Qiniu != nil && storage.Qiniu.SecretKey != "" {
		storage.Qiniu.SecretKey = "******"
	}
	for _, item := range objectStores(storage) {
		if item != nil && item.SecretKey != "" {
			item.SecretKey = "******"
		}
	}
}

func preserveStorageSecrets(next, current *conf.AdminBackend_Storage) {
	if next == nil || current == nil {
		return
	}
	if next.Qiniu != nil && current.Qiniu != nil && maskedSecret(next.Qiniu.SecretKey) {
		next.Qiniu.SecretKey = current.Qiniu.SecretKey
	}
	nextItems, currentItems := objectStores(next), objectStores(current)
	for i := range nextItems {
		if nextItems[i] != nil && currentItems[i] != nil && maskedSecret(nextItems[i].SecretKey) {
			nextItems[i].SecretKey = currentItems[i].SecretKey
		}
	}
}
