package config

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"time"
)

const (
	defaultClientSignatureKey   = "ymhut-box-feedback-client-v1"
	defaultPackageEncryptionKey = "ymhut-box-feedback-package-v1"
)

type Config struct {
	BaseDir                string            `json:"-"`
	Listen                 string            `json:"listen"`
	AdminPasswordHash      string            `json:"admin_password_hash"`
	AdminPassword          string            `json:"admin_password"`
	ClientSignatureKey     string            `json:"client_signature_key"`
	PackageEncryptionKey   string            `json:"package_encryption_key"`
	TimestampWindowSeconds int64             `json:"timestamp_window_seconds"`
	MaxRequestBytes        int64             `json:"max_request_bytes"`
	MaxPackageBytes        int64             `json:"max_package_bytes"`
	StorageDir             string            `json:"storage_dir"`
	DatabasePath           string            `json:"database_path"`
	Mail                   MailConfig        `json:"mail"`
	RateLimit              RateLimitConfig   `json:"rate_limit"`
	UploadGuard            UploadGuardConfig `json:"upload_guard"`
	Backup                 BackupConfig      `json:"backup"`
	Database               DatabaseConfig    `json:"database"`
	Webhooks               []WebhookConfig   `json:"webhooks"`
}

type MailConfig struct {
	Host             string `json:"host"`
	Port             int    `json:"port"`
	Secure           string `json:"secure"`
	Username         string `json:"username"`
	Password         string `json:"password"`
	FromAddress      string `json:"from_address"`
	FromName         string `json:"from_name"`
	DeveloperAddress string `json:"developer_address"`
	TimeoutSeconds   int    `json:"timeout_seconds"`
}

type RateLimitConfig struct {
	SubmissionPerMinute int `json:"submission_per_minute"`
	SubmissionBurst     int `json:"submission_burst"`
	StatusPerMinute     int `json:"status_per_minute"`
	StatusBurst         int `json:"status_burst"`
	CaptchaPerMinute    int `json:"captcha_per_minute"`
	CaptchaBurst        int `json:"captcha_burst"`
	LoginPerMinute      int `json:"login_per_minute"`
	LoginBurst          int `json:"login_burst"`
	AdminReadPerMinute  int `json:"admin_read_per_minute"`
	AdminReadBurst      int `json:"admin_read_burst"`
	AdminWritePerMinute int `json:"admin_write_per_minute"`
	AdminWriteBurst     int `json:"admin_write_burst"`
}

type UploadGuardConfig struct {
	MaxZipFiles             int     `json:"max_zip_files"`
	MaxDecompressedBytes    int64   `json:"max_decompressed_bytes"`
	MaxSingleFileBytes      int64   `json:"max_single_file_bytes"`
	MaxCompressionRatio     float64 `json:"max_compression_ratio"`
	MaxReadableTextBytes    int64   `json:"max_readable_text_bytes"`
	AllowUnexpectedZipFiles bool    `json:"allow_unexpected_zip_files"`
}

type BackupConfig struct {
	Dir string `json:"dir"`
}

type DatabaseConfig struct {
	Provider               string             `json:"provider"`
	SQLitePath             string             `json:"sqlite_path"`
	Host                   string             `json:"host"`
	Port                   int                `json:"port"`
	Name                   string             `json:"name"`
	User                   string             `json:"user"`
	Password               string             `json:"password"`
	DSN                    string             `json:"dsn"`
	SSLMode                string             `json:"ssl_mode"`
	MaxOpenConns           int                `json:"max_open_conns"`
	MaxIdleConns           int                `json:"max_idle_conns"`
	ConnMaxLifetimeSeconds int                `json:"conn_max_lifetime_seconds"`
	FailoverEnabled        bool               `json:"failover_enabled"`
	HealthIntervalSeconds  int                `json:"health_interval_seconds"`
	Sync                   DatabaseSyncConfig `json:"sync"`
}

type DatabaseSyncConfig struct {
	Enabled         bool `json:"enabled"`
	IntervalSeconds int  `json:"interval_seconds"`
	BatchSize       int  `json:"batch_size"`
}

type WebhookConfig struct {
	Name           string   `json:"name"`
	URL            string   `json:"url"`
	Secret         string   `json:"secret"`
	Enabled        bool     `json:"enabled"`
	Events         []string `json:"events"`
	TimeoutSeconds int      `json:"timeout_seconds"`
	MaxRetries     int      `json:"max_retries"`
}

func Load(baseDir string) (*Config, error) {
	absBase, err := filepath.Abs(baseDir)
	if err != nil {
		return nil, err
	}

	cfg := Default(absBase)
	for _, name := range []string{"config.txt", "config.json"} {
		path := filepath.Join(absBase, name)
		if isFile(path) {
			if strings.EqualFold(filepath.Ext(path), ".json") {
				if err := loadJSON(path, cfg); err != nil {
					return nil, err
				}
			} else {
				if err := loadLegacy(path, cfg); err != nil {
					return nil, err
				}
			}
			normalize(cfg)
			return cfg, nil
		}
	}

	normalize(cfg)
	return cfg, nil
}

func Default(baseDir string) *Config {
	return &Config{
		BaseDir:                baseDir,
		Listen:                 ":8080",
		AdminPasswordHash:      "",
		AdminPassword:          "CHANGE_ME_ADMIN_PASSWORD",
		ClientSignatureKey:     defaultClientSignatureKey,
		PackageEncryptionKey:   defaultPackageEncryptionKey,
		TimestampWindowSeconds: 600,
		MaxRequestBytes:        12 * 1024 * 1024,
		MaxPackageBytes:        10 * 1024 * 1024,
		StorageDir:             filepath.Join(baseDir, "storage"),
		DatabasePath:           filepath.Join(baseDir, "storage", "feedback.sqlite"),
		Mail: MailConfig{
			Host:             "mail.example.com",
			Port:             465,
			Secure:           "ssl",
			Username:         "sender@example.com",
			Password:         "CHANGE_ME_MAIL_PASSWORD",
			FromAddress:      "sender@example.com",
			FromName:         "YMhut Box Feedback",
			DeveloperAddress: "developer@example.com",
			TimeoutSeconds:   20,
		},
		RateLimit: RateLimitConfig{
			SubmissionPerMinute: 20,
			SubmissionBurst:     5,
			StatusPerMinute:     120,
			StatusBurst:         30,
			CaptchaPerMinute:    60,
			CaptchaBurst:        10,
			LoginPerMinute:      12,
			LoginBurst:          3,
			AdminReadPerMinute:  300,
			AdminReadBurst:      60,
			AdminWritePerMinute: 90,
			AdminWriteBurst:     20,
		},
		UploadGuard: UploadGuardConfig{
			MaxZipFiles:             80,
			MaxDecompressedBytes:    30 * 1024 * 1024,
			MaxSingleFileBytes:      8 * 1024 * 1024,
			MaxCompressionRatio:     120,
			MaxReadableTextBytes:    256 * 1024,
			AllowUnexpectedZipFiles: true,
		},
		Backup: BackupConfig{
			Dir: filepath.Join(baseDir, "storage", "backups"),
		},
		Database: DatabaseConfig{
			Provider:               "sqlite",
			SQLitePath:             filepath.Join(baseDir, "storage", "feedback.sqlite"),
			Port:                   0,
			SSLMode:                "disable",
			MaxOpenConns:           10,
			MaxIdleConns:           4,
			ConnMaxLifetimeSeconds: 300,
			FailoverEnabled:        true,
			HealthIntervalSeconds:  30,
			Sync: DatabaseSyncConfig{
				Enabled:         true,
				IntervalSeconds: 24 * 60 * 60,
				BatchSize:       500,
			},
		},
		Webhooks: []WebhookConfig{},
	}
}

func loadJSON(path string, cfg *Config) error {
	data, err := os.ReadFile(path)
	if err != nil {
		return err
	}

	if err := json.Unmarshal(data, cfg); err != nil {
		return fmt.Errorf("parse %s: %w", filepath.Base(path), err)
	}
	return nil
}

func loadLegacy(path string, cfg *Config) error {
	data, err := os.ReadFile(path)
	if err != nil {
		return err
	}

	text := string(data)
	vars := parseLegacyVars(text)
	entries := parseLegacyEntries(text, vars, filepath.Dir(path))
	if len(entries) == 0 {
		return errors.New("legacy config did not contain any parseable entries")
	}

	assignString(entries, "admin_password_hash", &cfg.AdminPasswordHash)
	assignString(entries, "admin_password", &cfg.AdminPassword)
	assignString(entries, "listen", &cfg.Listen)
	assignString(entries, "client_signature_key", &cfg.ClientSignatureKey)
	assignString(entries, "package_encryption_key", &cfg.PackageEncryptionKey)
	assignInt64(entries, "timestamp_window_seconds", &cfg.TimestampWindowSeconds)
	assignInt64(entries, "max_request_bytes", &cfg.MaxRequestBytes)
	assignInt64(entries, "max_package_bytes", &cfg.MaxPackageBytes)
	assignString(entries, "storage_dir", &cfg.StorageDir)
	assignString(entries, "database_path", &cfg.DatabasePath)
	assignString(entries, "database_provider", &cfg.Database.Provider)
	assignString(entries, "database_sqlite_path", &cfg.Database.SQLitePath)
	assignString(entries, "database_host", &cfg.Database.Host)
	assignInt(entries, "database_port", &cfg.Database.Port)
	assignString(entries, "database_name", &cfg.Database.Name)
	assignString(entries, "database_user", &cfg.Database.User)
	assignString(entries, "database_password", &cfg.Database.Password)
	assignString(entries, "database_dsn", &cfg.Database.DSN)
	assignString(entries, "database_ssl_mode", &cfg.Database.SSLMode)
	assignInt(entries, "database_max_open_conns", &cfg.Database.MaxOpenConns)
	assignInt(entries, "database_max_idle_conns", &cfg.Database.MaxIdleConns)
	assignInt(entries, "database_conn_max_lifetime_seconds", &cfg.Database.ConnMaxLifetimeSeconds)
	assignBool(entries, "database_failover_enabled", &cfg.Database.FailoverEnabled)
	assignInt(entries, "database_health_interval_seconds", &cfg.Database.HealthIntervalSeconds)
	assignBool(entries, "database_sync_enabled", &cfg.Database.Sync.Enabled)
	assignInt(entries, "database_sync_interval_seconds", &cfg.Database.Sync.IntervalSeconds)
	assignInt(entries, "database_sync_batch_size", &cfg.Database.Sync.BatchSize)

	assignString(entries, "host", &cfg.Mail.Host)
	assignInt(entries, "port", &cfg.Mail.Port)
	assignString(entries, "secure", &cfg.Mail.Secure)
	assignString(entries, "username", &cfg.Mail.Username)
	assignString(entries, "password", &cfg.Mail.Password)
	assignString(entries, "from_address", &cfg.Mail.FromAddress)
	assignString(entries, "from_name", &cfg.Mail.FromName)
	assignString(entries, "developer_address", &cfg.Mail.DeveloperAddress)
	assignInt(entries, "timeout_seconds", &cfg.Mail.TimeoutSeconds)

	assignIntAliases(entries, &cfg.RateLimit.SubmissionPerMinute, "submission_per_minute", "rate_limit_submission_per_minute")
	assignIntAliases(entries, &cfg.RateLimit.SubmissionBurst, "submission_burst", "rate_limit_submission_burst")
	assignIntAliases(entries, &cfg.RateLimit.StatusPerMinute, "status_per_minute", "rate_limit_status_per_minute")
	assignIntAliases(entries, &cfg.RateLimit.StatusBurst, "status_burst", "rate_limit_status_burst")
	assignIntAliases(entries, &cfg.RateLimit.CaptchaPerMinute, "captcha_per_minute", "rate_limit_captcha_per_minute")
	assignIntAliases(entries, &cfg.RateLimit.CaptchaBurst, "captcha_burst", "rate_limit_captcha_burst")
	assignIntAliases(entries, &cfg.RateLimit.LoginPerMinute, "login_per_minute", "rate_limit_login_per_minute")
	assignIntAliases(entries, &cfg.RateLimit.LoginBurst, "login_burst", "rate_limit_login_burst")
	assignIntAliases(entries, &cfg.RateLimit.AdminReadPerMinute, "admin_read_per_minute", "rate_limit_admin_read_per_minute")
	assignIntAliases(entries, &cfg.RateLimit.AdminReadBurst, "admin_read_burst", "rate_limit_admin_read_burst")
	assignIntAliases(entries, &cfg.RateLimit.AdminWritePerMinute, "admin_write_per_minute", "rate_limit_admin_write_per_minute")
	assignIntAliases(entries, &cfg.RateLimit.AdminWriteBurst, "admin_write_burst", "rate_limit_admin_write_burst")

	assignInt(entries, "max_zip_files", &cfg.UploadGuard.MaxZipFiles)
	assignInt64(entries, "max_decompressed_bytes", &cfg.UploadGuard.MaxDecompressedBytes)
	assignInt64(entries, "max_single_file_bytes", &cfg.UploadGuard.MaxSingleFileBytes)
	assignFloat(entries, "max_compression_ratio", &cfg.UploadGuard.MaxCompressionRatio)
	assignInt64(entries, "max_readable_text_bytes", &cfg.UploadGuard.MaxReadableTextBytes)
	assignBool(entries, "allow_unexpected_zip_files", &cfg.UploadGuard.AllowUnexpectedZipFiles)
	assignString(entries, "backup_dir", &cfg.Backup.Dir)

	if hooks := parseLegacyWebhooks(text, vars, filepath.Dir(path)); len(hooks) > 0 {
		cfg.Webhooks = hooks
	}
	return nil
}

func Save(cfg *Config) error {
	if cfg == nil {
		return errors.New("config is nil")
	}
	normalize(cfg)
	path := filepath.Join(cfg.BaseDir, "config.txt")
	if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
		return err
	}
	if isFile(path) {
		backup := filepath.Join(cfg.BaseDir, "config-"+time.Now().UTC().Format("20060102-150405")+".bak")
		data, err := os.ReadFile(path)
		if err != nil {
			return err
		}
		if err := os.WriteFile(backup, data, 0o600); err != nil {
			return err
		}
	}
	return os.WriteFile(path, []byte(RenderLegacy(cfg)), 0o600)
}

func RenderLegacy(cfg *Config) string {
	var b strings.Builder
	b.WriteString("<?php\nreturn [\n")
	writeKV := func(key, value string) {
		b.WriteString("    '")
		b.WriteString(key)
		b.WriteString("' => ")
		b.WriteString(quotePHP(value))
		b.WriteString(",\n")
	}
	writeInt := func(key string, value int64) {
		b.WriteString("    '")
		b.WriteString(key)
		b.WriteString("' => ")
		b.WriteString(strconv.FormatInt(value, 10))
		b.WriteString(",\n")
	}
	writeBool := func(key string, value bool) {
		b.WriteString("    '")
		b.WriteString(key)
		b.WriteString("' => ")
		if value {
			b.WriteString("true")
		} else {
			b.WriteString("false")
		}
		b.WriteString(",\n")
	}

	writeKV("listen", cfg.Listen)
	writeKV("admin_password_hash", cfg.AdminPasswordHash)
	writeKV("admin_password", cfg.AdminPassword)
	writeKV("client_signature_key", cfg.ClientSignatureKey)
	writeKV("package_encryption_key", cfg.PackageEncryptionKey)
	writeInt("timestamp_window_seconds", cfg.TimestampWindowSeconds)
	writeInt("max_request_bytes", cfg.MaxRequestBytes)
	writeInt("max_package_bytes", cfg.MaxPackageBytes)
	writeKV("storage_dir", portablePath(cfg, cfg.StorageDir))
	writeKV("database_path", portablePath(cfg, cfg.Database.SQLitePath))
	writeKV("database_provider", cfg.Database.Provider)
	writeKV("database_sqlite_path", portablePath(cfg, cfg.Database.SQLitePath))
	writeKV("database_host", cfg.Database.Host)
	writeInt("database_port", int64(cfg.Database.Port))
	writeKV("database_name", cfg.Database.Name)
	writeKV("database_user", cfg.Database.User)
	writeKV("database_password", cfg.Database.Password)
	writeKV("database_dsn", cfg.Database.DSN)
	writeKV("database_ssl_mode", cfg.Database.SSLMode)
	writeInt("database_max_open_conns", int64(cfg.Database.MaxOpenConns))
	writeInt("database_max_idle_conns", int64(cfg.Database.MaxIdleConns))
	writeInt("database_conn_max_lifetime_seconds", int64(cfg.Database.ConnMaxLifetimeSeconds))
	writeBool("database_failover_enabled", cfg.Database.FailoverEnabled)
	writeInt("database_health_interval_seconds", int64(cfg.Database.HealthIntervalSeconds))
	writeBool("database_sync_enabled", cfg.Database.Sync.Enabled)
	writeInt("database_sync_interval_seconds", int64(cfg.Database.Sync.IntervalSeconds))
	writeInt("database_sync_batch_size", int64(cfg.Database.Sync.BatchSize))

	writeKV("host", cfg.Mail.Host)
	writeInt("port", int64(cfg.Mail.Port))
	writeKV("secure", cfg.Mail.Secure)
	writeKV("username", cfg.Mail.Username)
	writeKV("password", cfg.Mail.Password)
	writeKV("from_address", cfg.Mail.FromAddress)
	writeKV("from_name", cfg.Mail.FromName)
	writeKV("developer_address", cfg.Mail.DeveloperAddress)
	writeInt("timeout_seconds", int64(cfg.Mail.TimeoutSeconds))
	writeKV("backup_dir", portablePath(cfg, cfg.Backup.Dir))

	writeInt("submission_per_minute", int64(cfg.RateLimit.SubmissionPerMinute))
	writeInt("submission_burst", int64(cfg.RateLimit.SubmissionBurst))
	writeInt("status_per_minute", int64(cfg.RateLimit.StatusPerMinute))
	writeInt("status_burst", int64(cfg.RateLimit.StatusBurst))
	writeInt("captcha_per_minute", int64(cfg.RateLimit.CaptchaPerMinute))
	writeInt("captcha_burst", int64(cfg.RateLimit.CaptchaBurst))
	writeInt("login_per_minute", int64(cfg.RateLimit.LoginPerMinute))
	writeInt("login_burst", int64(cfg.RateLimit.LoginBurst))
	writeInt("admin_read_per_minute", int64(cfg.RateLimit.AdminReadPerMinute))
	writeInt("admin_read_burst", int64(cfg.RateLimit.AdminReadBurst))
	writeInt("admin_write_per_minute", int64(cfg.RateLimit.AdminWritePerMinute))
	writeInt("admin_write_burst", int64(cfg.RateLimit.AdminWriteBurst))

	writeInt("max_zip_files", int64(cfg.UploadGuard.MaxZipFiles))
	writeInt("max_decompressed_bytes", cfg.UploadGuard.MaxDecompressedBytes)
	writeInt("max_single_file_bytes", cfg.UploadGuard.MaxSingleFileBytes)
	b.WriteString("    'max_compression_ratio' => ")
	b.WriteString(strconv.FormatFloat(cfg.UploadGuard.MaxCompressionRatio, 'f', -1, 64))
	b.WriteString(",\n")
	writeInt("max_readable_text_bytes", cfg.UploadGuard.MaxReadableTextBytes)
	writeBool("allow_unexpected_zip_files", cfg.UploadGuard.AllowUnexpectedZipFiles)

	b.WriteString("    'webhooks' => [\n")
	for _, hook := range cfg.Webhooks {
		b.WriteString("        [\n")
		b.WriteString("            'name' => " + quotePHP(hook.Name) + ",\n")
		b.WriteString("            'url' => " + quotePHP(hook.URL) + ",\n")
		b.WriteString("            'secret' => " + quotePHP(hook.Secret) + ",\n")
		if hook.Enabled {
			b.WriteString("            'enabled' => true,\n")
		} else {
			b.WriteString("            'enabled' => false,\n")
		}
		b.WriteString("            'events' => [")
		for index, event := range hook.Events {
			if index > 0 {
				b.WriteString(", ")
			}
			b.WriteString(quotePHP(event))
		}
		b.WriteString("],\n")
		b.WriteString("            'timeout_seconds' => " + strconv.Itoa(hook.TimeoutSeconds) + ",\n")
		b.WriteString("            'max_retries' => " + strconv.Itoa(hook.MaxRetries) + ",\n")
		b.WriteString("        ],\n")
	}
	b.WriteString("    ],\n")
	b.WriteString("];\n")
	return b.String()
}

func quotePHP(value string) string {
	replacer := strings.NewReplacer(`\`, `\\`, `'`, `\'`, "\r", `\r`, "\n", `\n`)
	return "'" + replacer.Replace(value) + "'"
}

func portablePath(cfg *Config, value string) string {
	value = strings.TrimSpace(value)
	if value == "" || cfg == nil || strings.TrimSpace(cfg.BaseDir) == "" {
		return value
	}
	absValue, err := filepath.Abs(value)
	if err != nil {
		return value
	}
	absBase, err := filepath.Abs(cfg.BaseDir)
	if err != nil {
		return value
	}
	rel, err := filepath.Rel(absBase, absValue)
	if err != nil || rel == "." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) || rel == ".." || filepath.IsAbs(rel) {
		return value
	}
	return filepath.ToSlash(rel)
}

var (
	legacyVarPattern   = regexp.MustCompile(`(?s)\$([A-Za-z_][A-Za-z0-9_]*)\s*=\s*('(?:\\.|[^'])*'|"(?:\\.|[^"])*")\s*;`)
	legacyEntryPattern = regexp.MustCompile(`(?s)['"]([A-Za-z0-9_]+)['"]\s*=>\s*([^,\]\r\n]+(?:\s*\*\s*\d+)*)`)
)

func parseLegacyVars(text string) map[string]string {
	vars := map[string]string{}
	for _, match := range legacyVarPattern.FindAllStringSubmatch(text, -1) {
		if len(match) == 3 {
			vars[match[1]] = stripQuoted(match[2])
		}
	}
	return vars
}

func parseLegacyEntries(text string, vars map[string]string, dir string) map[string]string {
	entries := map[string]string{}
	for _, match := range legacyEntryPattern.FindAllStringSubmatch(text, -1) {
		if len(match) != 3 {
			continue
		}
		key := match[1]
		value := strings.TrimSpace(match[2])
		entries[key] = parseLegacyValue(value, vars, dir)
	}
	return entries
}

func parseLegacyValue(value string, vars map[string]string, dir string) string {
	value = strings.TrimSpace(value)
	if strings.HasPrefix(value, "__DIR__") {
		re := regexp.MustCompile(`__DIR__\s*\.\s*('(?:\\.|[^'])*'|"(?:\\.|[^"])*")`)
		if match := re.FindStringSubmatch(value); len(match) == 2 {
			return filepath.Clean(dir + filepath.FromSlash(stripQuoted(match[1])))
		}
		return dir
	}

	if strings.HasPrefix(value, "$") {
		name := strings.TrimPrefix(value, "$")
		if parsed, ok := vars[name]; ok {
			return parsed
		}
		return ""
	}

	if strings.HasPrefix(value, "'") || strings.HasPrefix(value, `"`) {
		return stripQuoted(value)
	}

	if result, ok := evalIntExpression(value); ok {
		return strconv.FormatInt(result, 10)
	}

	return strings.Trim(value, " \t;")
}

func parseLegacyWebhooks(text string, vars map[string]string, dir string) []WebhookConfig {
	block, ok := findLegacyArrayBlock(text, "webhooks")
	if !ok {
		return nil
	}
	items := splitNestedArrayItems(block)
	hooks := make([]WebhookConfig, 0, len(items))
	for _, item := range items {
		entries := parseLegacyEntries(item, vars, dir)
		if len(entries) == 0 {
			continue
		}
		hook := WebhookConfig{Enabled: true, TimeoutSeconds: 5, MaxRetries: 2}
		assignString(entries, "name", &hook.Name)
		assignString(entries, "url", &hook.URL)
		assignString(entries, "secret", &hook.Secret)
		assignBool(entries, "enabled", &hook.Enabled)
		assignInt(entries, "timeout_seconds", &hook.TimeoutSeconds)
		assignInt(entries, "max_retries", &hook.MaxRetries)
		hook.Events = parseStringArrayValue(item, "events")
		if hook.Name == "" && hook.URL != "" {
			hook.Name = "webhook"
		}
		if hook.URL != "" {
			hooks = append(hooks, hook)
		}
	}
	return hooks
}

func findLegacyArrayBlock(text, key string) (string, bool) {
	re := regexp.MustCompile(`['"]` + regexp.QuoteMeta(key) + `['"]\s*=>\s*\[`)
	loc := re.FindStringIndex(text)
	if loc == nil {
		return "", false
	}
	open := loc[1] - 1
	depth := 0
	for index := open; index < len(text); index++ {
		switch text[index] {
		case '[':
			depth++
		case ']':
			depth--
			if depth == 0 {
				return text[open+1 : index], true
			}
		}
	}
	return "", false
}

func splitNestedArrayItems(block string) []string {
	items := []string{}
	depth := 0
	start := -1
	for index := 0; index < len(block); index++ {
		switch block[index] {
		case '[':
			if depth == 0 {
				start = index + 1
			}
			depth++
		case ']':
			depth--
			if depth == 0 && start >= 0 {
				items = append(items, block[start:index])
				start = -1
			}
		}
	}
	return items
}

func parseStringArrayValue(block, key string) []string {
	sub, ok := findLegacyArrayBlock(block, key)
	if !ok {
		return nil
	}
	re := regexp.MustCompile(`'(?:\\.|[^'])*'|"(?:\\.|[^"])*"`)
	values := []string{}
	for _, raw := range re.FindAllString(sub, -1) {
		if value := strings.TrimSpace(stripQuoted(raw)); value != "" {
			values = append(values, value)
		}
	}
	return values
}

func stripQuoted(value string) string {
	value = strings.TrimSpace(value)
	if len(value) < 2 {
		return value
	}

	quote := value[0]
	if (quote != '\'' && quote != '"') || value[len(value)-1] != quote {
		return value
	}

	body := value[1 : len(value)-1]
	body = strings.ReplaceAll(body, `\\`, `\`)
	if quote == '\'' {
		body = strings.ReplaceAll(body, `\'`, `'`)
	} else {
		body = strings.ReplaceAll(body, `\"`, `"`)
	}
	return body
}

func evalIntExpression(value string) (int64, bool) {
	parts := strings.Split(value, "*")
	total := int64(1)
	seen := false
	for _, part := range parts {
		part = strings.TrimSpace(strings.TrimSuffix(part, ";"))
		if part == "" {
			return 0, false
		}
		number, err := strconv.ParseInt(part, 10, 64)
		if err != nil {
			return 0, false
		}
		total *= number
		seen = true
	}
	return total, seen
}

func normalize(cfg *Config) {
	if cfg.BaseDir == "" {
		cfg.BaseDir, _ = os.Getwd()
	}
	if cfg.ClientSignatureKey == "" {
		cfg.ClientSignatureKey = defaultClientSignatureKey
	}
	if cfg.PackageEncryptionKey == "" {
		cfg.PackageEncryptionKey = defaultPackageEncryptionKey
	}
	if cfg.TimestampWindowSeconds <= 0 {
		cfg.TimestampWindowSeconds = 600
	}
	if cfg.MaxRequestBytes <= 0 {
		cfg.MaxRequestBytes = 12 * 1024 * 1024
	}
	if cfg.MaxPackageBytes <= 0 {
		cfg.MaxPackageBytes = 10 * 1024 * 1024
	}
	if cfg.StorageDir == "" {
		cfg.StorageDir = filepath.Join(cfg.BaseDir, "storage")
	}
	if cfg.DatabasePath == "" {
		cfg.DatabasePath = filepath.Join(cfg.StorageDir, "feedback.sqlite")
	}
	if cfg.Database.Provider == "" {
		cfg.Database.Provider = "sqlite"
	}
	cfg.Database.Provider = strings.ToLower(strings.TrimSpace(cfg.Database.Provider))
	if cfg.Database.Provider != "mysql" && cfg.Database.Provider != "postgres" && cfg.Database.Provider != "pgsql" {
		cfg.Database.Provider = "sqlite"
	}
	if cfg.Database.Provider == "pgsql" {
		cfg.Database.Provider = "postgres"
	}
	if cfg.Database.SQLitePath == "" {
		cfg.Database.SQLitePath = cfg.DatabasePath
	}
	if cfg.Backup.Dir == "" {
		cfg.Backup.Dir = filepath.Join(cfg.StorageDir, "backups")
	}

	cfg.StorageDir = normalizePath(cfg.BaseDir, cfg.StorageDir)
	cfg.DatabasePath = normalizePath(cfg.BaseDir, cfg.DatabasePath)
	cfg.Database.SQLitePath = normalizePath(cfg.BaseDir, cfg.Database.SQLitePath)
	cfg.DatabasePath = cfg.Database.SQLitePath
	cfg.Backup.Dir = normalizePath(cfg.BaseDir, cfg.Backup.Dir)
	if cfg.Database.Port <= 0 {
		switch cfg.Database.Provider {
		case "mysql":
			cfg.Database.Port = 3306
		case "postgres":
			cfg.Database.Port = 5432
		}
	}
	if cfg.Database.SSLMode == "" {
		cfg.Database.SSLMode = "disable"
	}
	if cfg.Database.MaxOpenConns <= 0 {
		cfg.Database.MaxOpenConns = 10
	}
	if cfg.Database.MaxIdleConns < 0 {
		cfg.Database.MaxIdleConns = 0
	}
	if cfg.Database.MaxIdleConns == 0 {
		cfg.Database.MaxIdleConns = 4
	}
	if cfg.Database.ConnMaxLifetimeSeconds <= 0 {
		cfg.Database.ConnMaxLifetimeSeconds = 300
	}
	if cfg.Database.HealthIntervalSeconds <= 0 {
		cfg.Database.HealthIntervalSeconds = 30
	}
	if cfg.Database.Sync.IntervalSeconds <= 0 {
		cfg.Database.Sync.IntervalSeconds = 24 * 60 * 60
	}
	if cfg.Database.Sync.BatchSize <= 0 {
		cfg.Database.Sync.BatchSize = 500
	}

	if cfg.Mail.Port <= 0 {
		cfg.Mail.Port = 465
	}
	if cfg.Mail.Secure == "" {
		cfg.Mail.Secure = "ssl"
	}
	cfg.Mail.Secure = strings.ToLower(cfg.Mail.Secure)
	if cfg.Mail.FromAddress == "" {
		cfg.Mail.FromAddress = cfg.Mail.Username
	}
	if cfg.Mail.FromName == "" {
		cfg.Mail.FromName = "YMhut Box Feedback"
	}
	if cfg.Mail.TimeoutSeconds <= 0 {
		cfg.Mail.TimeoutSeconds = 20
	}

	if cfg.RateLimit.SubmissionPerMinute <= 0 {
		cfg.RateLimit.SubmissionPerMinute = 20
	}
	if cfg.RateLimit.SubmissionBurst <= 0 {
		cfg.RateLimit.SubmissionBurst = 5
	}
	if cfg.RateLimit.StatusPerMinute <= 0 {
		cfg.RateLimit.StatusPerMinute = 120
	}
	if cfg.RateLimit.StatusBurst <= 0 {
		cfg.RateLimit.StatusBurst = 30
	}
	if cfg.RateLimit.CaptchaPerMinute <= 0 {
		cfg.RateLimit.CaptchaPerMinute = 60
	}
	if cfg.RateLimit.CaptchaBurst <= 0 {
		cfg.RateLimit.CaptchaBurst = 10
	}
	if cfg.RateLimit.LoginPerMinute <= 0 {
		cfg.RateLimit.LoginPerMinute = 12
	}
	if cfg.RateLimit.LoginBurst <= 0 {
		cfg.RateLimit.LoginBurst = 3
	}
	if cfg.RateLimit.AdminReadPerMinute <= 0 {
		cfg.RateLimit.AdminReadPerMinute = 300
	}
	if cfg.RateLimit.AdminReadBurst <= 0 {
		cfg.RateLimit.AdminReadBurst = 60
	}
	if cfg.RateLimit.AdminWritePerMinute <= 0 {
		cfg.RateLimit.AdminWritePerMinute = 90
	}
	if cfg.RateLimit.AdminWriteBurst <= 0 {
		cfg.RateLimit.AdminWriteBurst = 20
	}

	if cfg.UploadGuard.MaxZipFiles <= 0 {
		cfg.UploadGuard.MaxZipFiles = 80
	}
	if cfg.UploadGuard.MaxDecompressedBytes <= 0 {
		cfg.UploadGuard.MaxDecompressedBytes = 30 * 1024 * 1024
	}
	if cfg.UploadGuard.MaxSingleFileBytes <= 0 {
		cfg.UploadGuard.MaxSingleFileBytes = 8 * 1024 * 1024
	}
	if cfg.UploadGuard.MaxCompressionRatio <= 0 {
		cfg.UploadGuard.MaxCompressionRatio = 120
	}
	if cfg.UploadGuard.MaxReadableTextBytes <= 0 {
		cfg.UploadGuard.MaxReadableTextBytes = 256 * 1024
	}

	for index := range cfg.Webhooks {
		hook := &cfg.Webhooks[index]
		hook.Name = strings.TrimSpace(hook.Name)
		hook.URL = strings.TrimSpace(hook.URL)
		if hook.TimeoutSeconds <= 0 {
			hook.TimeoutSeconds = 5
		}
		if hook.MaxRetries < 0 {
			hook.MaxRetries = 0
		}
		if len(hook.Events) == 0 {
			hook.Events = []string{"feedback.created", "feedback.updated", "feedback.status_changed", "feedback.comment_created", "mail.failed"}
		}
	}
}

func normalizePath(baseDir, value string) string {
	value = strings.TrimSpace(value)
	if value == "" {
		return value
	}
	if filepath.IsAbs(value) {
		return filepath.Clean(value)
	}
	return filepath.Clean(filepath.Join(baseDir, value))
}

func assignString(entries map[string]string, key string, target *string) {
	if value, ok := entries[key]; ok {
		*target = value
	}
}

func assignInt(entries map[string]string, key string, target *int) {
	if value, ok := entries[key]; ok {
		if parsed, err := strconv.Atoi(value); err == nil {
			*target = parsed
		}
	}
}

func assignIntAliases(entries map[string]string, target *int, keys ...string) {
	for _, key := range keys {
		if _, ok := entries[key]; ok {
			assignInt(entries, key, target)
			return
		}
	}
}

func assignInt64(entries map[string]string, key string, target *int64) {
	if value, ok := entries[key]; ok {
		if parsed, err := strconv.ParseInt(value, 10, 64); err == nil {
			*target = parsed
		}
	}
}

func assignFloat(entries map[string]string, key string, target *float64) {
	if value, ok := entries[key]; ok {
		if parsed, err := strconv.ParseFloat(value, 64); err == nil {
			*target = parsed
		}
	}
}

func assignBool(entries map[string]string, key string, target *bool) {
	if value, ok := entries[key]; ok {
		switch strings.ToLower(strings.TrimSpace(value)) {
		case "true", "1", "yes", "on":
			*target = true
		case "false", "0", "no", "off":
			*target = false
		}
	}
}

func isFile(path string) bool {
	info, err := os.Stat(path)
	return err == nil && !info.IsDir()
}
