package config

import (
	"errors"
	"fmt"
	"net/url"
	"path/filepath"
	"strconv"
	"strings"
)

type MySQLInput struct {
	Host      string `json:"host"`
	Port      int    `json:"port"`
	Database  string `json:"database"`
	Username  string `json:"username"`
	Password  string `json:"password"`
	Charset   string `json:"charset"`
	ParseTime bool   `json:"parseTime"`
	TLS       string `json:"tls"`
}

type SafeDatabaseConfig struct {
	Provider      string `json:"provider"`
	SQLitePath    string `json:"sqlitePath"`
	MySQLDSN      string `json:"mysqlDsn"`
	MySQLHost     string `json:"mysqlHost"`
	MySQLPort     int    `json:"mysqlPort"`
	MySQLDatabase string `json:"mysqlDatabase"`
	MySQLUser     string `json:"mysqlUser"`
	HasPassword   bool   `json:"hasPassword"`
}

func BuildMySQLDSN(input MySQLInput) (string, error) {
	host := strings.TrimSpace(input.Host)
	if host == "" {
		host = "127.0.0.1"
	}
	port := input.Port
	if port <= 0 {
		port = 3306
	}
	database := strings.TrimSpace(input.Database)
	username := strings.TrimSpace(input.Username)
	if database == "" {
		return "", errors.New("mysql database is required")
	}
	if username == "" {
		return "", errors.New("mysql username is required")
	}
	params := url.Values{}
	params.Set("charset", firstNonEmpty(strings.TrimSpace(input.Charset), "utf8mb4"))
	params.Set("parseTime", strconv.FormatBool(input.ParseTime))
	if tls := strings.TrimSpace(input.TLS); tls != "" {
		params.Set("tls", tls)
	}
	return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?%s", username, input.Password, host, port, database, params.Encode()), nil
}

func NormalizeDatabase(baseDir string, current DatabaseConfig, incoming DatabaseConfig, keepPassword bool) (DatabaseConfig, error) {
	next := current
	structuredChanged := false
	if incoming.Provider != "" {
		next.Provider = strings.ToLower(strings.TrimSpace(incoming.Provider))
	}
	if next.Provider == "" {
		next.Provider = "sqlite"
	}
	if incoming.SQLitePath != "" {
		next.SQLitePath = incoming.SQLitePath
	}
	if next.SQLitePath != "" && !filepath.IsAbs(next.SQLitePath) && !strings.HasPrefix(strings.ToLower(next.SQLitePath), "file:") {
		next.SQLitePath = filepath.Join(baseDir, next.SQLitePath)
	}
	if incoming.MySQLHost != "" {
		next.MySQLHost = strings.TrimSpace(incoming.MySQLHost)
		structuredChanged = true
	}
	if incoming.MySQLPort > 0 {
		next.MySQLPort = incoming.MySQLPort
		structuredChanged = true
	}
	if incoming.MySQLDatabase != "" {
		next.MySQLDatabase = strings.TrimSpace(incoming.MySQLDatabase)
		structuredChanged = true
	}
	if incoming.MySQLUser != "" {
		next.MySQLUser = strings.TrimSpace(incoming.MySQLUser)
		structuredChanged = true
	}
	if incoming.MySQLPassword != "" || !keepPassword {
		next.MySQLPassword = incoming.MySQLPassword
		structuredChanged = true
	}
	if incoming.MySQLDSN != "" {
		next.MySQLDSN = strings.TrimSpace(incoming.MySQLDSN)
	}
	if next.MySQLHost == "" {
		next.MySQLHost = "127.0.0.1"
	}
	if next.MySQLPort <= 0 {
		next.MySQLPort = 3306
	}
	if next.Provider == "sqlite" {
		next.MySQLDSN = ""
	} else if next.Provider == "mysql" {
		if structuredChanged || next.MySQLDSN == "" {
			dsn, err := BuildMySQLDSN(MySQLInput{
				Host:      next.MySQLHost,
				Port:      next.MySQLPort,
				Database:  next.MySQLDatabase,
				Username:  next.MySQLUser,
				Password:  next.MySQLPassword,
				Charset:   "utf8mb4",
				ParseTime: true,
			})
			if err != nil {
				return DatabaseConfig{}, err
			}
			next.MySQLDSN = dsn
		}
		if strings.TrimSpace(next.MySQLDSN) == "" {
			return DatabaseConfig{}, errors.New("mysql connection is required")
		}
	} else {
		return DatabaseConfig{}, errors.New("provider must be sqlite or mysql")
	}
	if strings.TrimSpace(next.SQLitePath) == "" {
		return DatabaseConfig{}, errors.New("sqlite path is required")
	}
	if next.MaxOpenConns <= 0 {
		next.MaxOpenConns = 10
	}
	if next.MaxIdleConns <= 0 {
		next.MaxIdleConns = 4
	}
	if next.ConnMaxLifetimeSeconds <= 0 {
		next.ConnMaxLifetimeSeconds = 300
	}
	if next.HealthIntervalSec <= 0 {
		next.HealthIntervalSec = 30
	}
	return next, nil
}

func SafeDatabase(baseDir string, cfg DatabaseConfig) SafeDatabaseConfig {
	return SafeDatabaseConfig{
		Provider:      firstNonEmpty(cfg.Provider, "sqlite"),
		SQLitePath:    relativeToBase(baseDir, cfg.SQLitePath),
		MySQLDSN:      MaskDSN(cfg.MySQLDSN),
		MySQLHost:     cfg.MySQLHost,
		MySQLPort:     cfg.MySQLPort,
		MySQLDatabase: cfg.MySQLDatabase,
		MySQLUser:     cfg.MySQLUser,
		HasPassword:   strings.TrimSpace(cfg.MySQLPassword) != "" || dsnHasPassword(cfg.MySQLDSN),
	}
}

func MaskDSN(value string) string {
	value = strings.TrimSpace(value)
	if value == "" {
		return ""
	}
	at := strings.Index(value, "@")
	colon := strings.Index(value, ":")
	if at > -1 && colon > -1 && colon < at {
		return value[:colon+1] + "******" + value[at:]
	}
	return value
}

func relativeToBase(base, value string) string {
	if strings.TrimSpace(value) == "" {
		return ""
	}
	if base != "" {
		if rel, err := filepath.Rel(base, value); err == nil && !strings.HasPrefix(rel, "..") && rel != "." {
			return filepath.ToSlash(rel)
		}
	}
	return filepath.ToSlash(value)
}

func dsnHasPassword(value string) bool {
	value = strings.TrimSpace(value)
	at := strings.Index(value, "@")
	colon := strings.Index(value, ":")
	return at > -1 && colon > -1 && colon < at && colon+1 < at
}
