package config

import (
	"fmt"
	"os"

	"git.mstar.dev/mstar/goutils/other"
	"github.com/BurntSushi/toml"
	"github.com/rs/zerolog/log"
)

type ConfigSSL struct {
	HandleSSL bool `toml:"handle_ssl"` // Whether Linstrom should handle SSL encryption itself
	// If Linstrom is to handle SSL, whether it should use LetsEncrypt for certificates
	UseLetsEncrypt *bool `toml:"use_lets_encrypt"`
	// Path to the certificate if Linstrom is to handle SSL while not using LetsEncrypt
	CertificateFile *string `toml:"certificate_file"`
	// Mail adress to use in case of using LetsEncrypt
	AdminMail *string `toml:"admin_mail"`
}

type ConfigGeneral struct {
	Protocol string `toml:"protocol"` // The protocol with which to access the server publicly (http/https)
	// The subdomain under which the server lives (example: "linstrom" if the full domain is linstrom.example.com)
	Subdomain *string `toml:"subdomain"`
	// The root domain under which the server lives (example: "example.com" if the full domain is linstrom.example.com)
	Domain string `toml:"domain"`
	// The port on which the server runs on
	PrivatePort int `toml:"private_port"`
	// The port under which the public can reach the server (useful if running behind a reverse proxy)
	PublicPort *int `toml:"public_port"`
	// File to write structured logs to (structured being formatted as json)
	// If not set, Linstrom won't write structured logs
	StructuredLogFile *string `toml:"structured_log_file"`
}

type ConfigWebAuthn struct {
	DisplayName string `toml:"display_name"`
	// HashingSecret string `toml:"hashing_secret"`
}

type ConfigAdmin struct {
	// Name of the server's root admin account
	Username string `toml:"username"`
	// A one time password used to verify account access to the root admin
	// after a server has been created and before the account could be linked to a passkey
	FirstTimeSetupOTP string `toml:"first_time_setup_otp"`
	// Password for protecting profiling data from unauthorised access
	// An empty string equals to no password
	// The password has to be supplied in the `password` GET form value for all requests
	// to /profiling/*
	ProfilingPassword string `toml:"profiling_password"`
	// Allow registration on the server
	// If disabled, user must be manually created (currently via the debug server)
	AllowRegistration bool `toml:"allow_registration"`
}

type ConfigStorage struct {
	Host     string  `toml:"host"`
	Username string  `toml:"username"`
	Password string  `toml:"password"`
	DbName   string  `toml:"db_name"`
	Port     int     `toml:"port"`
	SslMode  *string `toml:"ssl_mode"`
	TimeZone *string `toml:"time_zone"`
	// Url to redis server. If empty, no redis is used
	RedisUrl *string `toml:"redis_url,omitempty"`
	// The maximum size of the in-memory cache in bytes
	MaxInMemoryCacheSize int64 `toml:"max_in_memory_cache_size"`
	// The time to live for in app in memory cache items, in seconds
	MaxInMemoryCacheTTL int `toml:"max_in_memory_cache_ttl"`
	// The time to live for items in redis, in seconds
	MaxRedisCacheTTL *int `toml:"max_redis_cache_ttl"`
	// Key used for encrypting sensitive information in the db
	// DO NOT CHANGE THIS AFTER SETUP
	EncryptionKey string `toml:"encryption_key"`
	// Maximum number of reconnection attempts if the connection to the db
	// breaks for some reason. Server will exit if the last attempt fails
	MaxReconnectAttempts int `toml:"max_reconnect_attempts"`
}

type ConfigS3 struct {
	KeyId      string `toml:"key_id"`
	Secret     string `toml:"secret"`
	Region     string `toml:"region"`
	Endpoint   string `toml:"endpoint"`
	UseSSL     bool   `toml:"use_ssl"`
	BucketName string `toml:"bucket_name"`
}

type ConfigMail struct {
	Host                           string  `toml:"host"`
	Port                           int     `toml:"port"`
	Username                       string  `toml:"username"`
	Password                       string  `toml:"password"`
	EncryptionOverwrite            *string `toml:"encryption_overwrite,omitempty"`
	KeepAliveOverwrite             *bool   `toml:"keep_alive_overwrite,omitempty"`
	ConnectTimeoutSecondsOverwrite *int    `toml:"connect_timeout_seconds_overwrite,omitempty"`
	SendTimeoutSecondsOverwrite    *int    `toml:"send_timeout_seconds_overwrite,omitempty"`
	TemplateOverwriteDirectory     *string `toml:"template_overwrite_directory,omitempty"`
}

type ConfigSelf struct {
	ServerActorDisplayName string `toml:"server_actor_display_name"`
	ServerDisplayName      string `toml:"server_display_name"`
}

// Contains experimental features that could be good to have
// but are either in an unstable implementation state
// or maybe not widely supported by other implementations
//
// All features controlled by this config section
// **MUST BE**
// disabled by default
type ConfigExperimental struct {
	// Use ED25519 key pairs instead of RSA 2048
	// ED25519 keys are shorter and safer, but might not be supported by other
	// implementations
	// Both are created and stored for each local user. If this flag is enabled,
	// Linstrom shares the ED25519 key on request, otherwise the RSA key
	UseEd25519Keys bool `toml:"use_ed25519_keys"`
}

type Config struct {
	General      ConfigGeneral      `toml:"general"`
	SSL          ConfigSSL          `toml:"ssl"`
	Admin        ConfigAdmin        `toml:"admin"`
	Webauthn     ConfigWebAuthn     `toml:"webauthn"`
	Storage      ConfigStorage      `toml:"storage"`
	Mail         ConfigMail         `toml:"mail"`
	Self         ConfigSelf         `toml:"self"`
	S3           ConfigS3           `toml:"s3"`
	Experimental ConfigExperimental `toml:"experimental"`
}

var GlobalConfig Config

var defaultConfig Config = Config{
	General: ConfigGeneral{
		Protocol:          "http",
		Subdomain:         nil,
		Domain:            "localhost",
		PrivatePort:       8080,
		PublicPort:        nil,
		StructuredLogFile: nil,
	},
	SSL: ConfigSSL{
		HandleSSL:       false,
		UseLetsEncrypt:  nil,
		CertificateFile: nil,
		AdminMail:       nil,
	},
	Admin: ConfigAdmin{
		Username:          "server-admin",
		FirstTimeSetupOTP: "Example otp password",
		ProfilingPassword: "Example profiling password",
		AllowRegistration: true,
	},
	Webauthn: ConfigWebAuthn{
		DisplayName: "Linstrom",
		// HashingSecret: "some super secure secret that should never be changed or else password storage breaks",
	},
	Storage: ConfigStorage{
		Host:                 "localhost",
		Username:             "linstrom",
		Password:             "linstrom",
		DbName:               "linstrom",
		Port:                 5432,
		SslMode:              other.IntoPointer("disable"),
		TimeZone:             other.IntoPointer("Europe/Berlin"),
		RedisUrl:             nil,
		MaxInMemoryCacheSize: 1e6, // 1 Megabyte
		MaxInMemoryCacheTTL:  5,
		MaxRedisCacheTTL:     nil,
		EncryptionKey:        "Encryption key for sensitive information. DO NOT CHANGE THIS AFTER SETUP",
		MaxReconnectAttempts: 3,
	},
	Mail: ConfigMail{
		Host:                           "localhost",
		Port:                           587,
		Username:                       "linstrom",
		Password:                       "linstrom",
		KeepAliveOverwrite:             nil,
		EncryptionOverwrite:            nil,
		ConnectTimeoutSecondsOverwrite: nil,
		SendTimeoutSecondsOverwrite:    nil,
		TemplateOverwriteDirectory:     nil,
	},
	Self: ConfigSelf{
		ServerActorDisplayName: "Server actor",
		ServerDisplayName:      "Linstrom",
	},
	S3: ConfigS3{
		KeyId:    "Example key ID",
		Secret:   "Example key secret",
		Region:   "Example region",
		Endpoint: "http://localhost:3900",
		UseSSL:   false,
	},
	Experimental: ConfigExperimental{
		UseEd25519Keys: false,
	},
}

// Get the full domain name of the server, as specified via the config.
//
// Example: "git.mstar.dev" (with subdomain = "git" and domain = "mstar.dev")
func (gc *ConfigGeneral) GetFullDomain() string {
	if gc.Subdomain != nil {
		return *gc.Subdomain + gc.Domain
	}
	return gc.Domain
}

// Get the public root url of the server, including port (if needed, public takes precedence if set)
// and set protocol
//
// Example: "http://git.mstar.dev:5722" (with Subdomain = "git", domain = "mstar.dev",
// privatePort = 34546, publicPort = 5722 and protocol = "http")
func (gc *ConfigGeneral) GetFullPublicUrl() string {
	str := gc.Protocol + "://" + gc.GetFullDomain()
	if gc.PublicPort != nil {
		str += generatePortAppender(gc.Protocol, *gc.PublicPort)
	} else {
		str += generatePortAppender(gc.Protocol, gc.PrivatePort)
	}
	return str
}

func (sc *ConfigStorage) BuildPostgresDSN() string {
	dsn := fmt.Sprintf(
		"host=%s user=%s password=%s dbname=%s port=%d",
		sc.Host,
		sc.Username,
		sc.Password,
		sc.DbName,
		sc.Port,
	)
	if sc.SslMode != nil {
		dsn += fmt.Sprintf(" sslmode=%s", *sc.SslMode)
	}
	if sc.TimeZone != nil {
		dsn += fmt.Sprintf(" TimeZone=%s", *sc.TimeZone)
	}

	return dsn
}

func WriteDefaultConfig(toFile string) error {
	log.Trace().Caller().Send()
	log.Info().Str("config-file", toFile).Msg("Writing default config to file")
	file, err := os.Create(toFile)
	if err != nil {
		log.Error().
			Err(err).
			Str("config-file", toFile).
			Msg("Failed to create file for default config")
		return err
	}
	defer file.Close()

	data, err := toml.Marshal(&defaultConfig)
	if err != nil {
		log.Error().Err(err).Msg("Failed to marshal default config to toml")
		return err
	}
	_, err = file.Write(data)
	if err != nil {
		log.Error().Err(err).Str("config-file", toFile).Msg("Failed to write default config")
		return err
	}

	log.Info().Str("config-file", toFile).Msg("Wrote default config")
	return nil
}

func ReadAndWriteToGlobal(fileName string) error {
	log.Trace().Caller().Send()
	log.Debug().Str("config-file", fileName).Msg("Attempting to read config file")
	data, err := os.ReadFile(fileName)
	if err != nil {
		log.Warn().
			Str("config-file", fileName).
			Err(err).
			Msg("Failed to read config file, attempting to write default config")
		err = WriteDefaultConfig(fileName)
		if err != nil {
			log.Error().
				Err(err).
				Str("config-file", fileName).
				Msg("Failed to create default config file")
			return err
		}
		GlobalConfig = defaultConfig
		return nil
	}
	config := Config{}
	log.Debug().Str("config-file", fileName).Msg("Read config file, attempting to unmarshal")
	err = toml.Unmarshal(data, &config)
	if err != nil {
		log.Error().Err(err).Bytes("config-data-raw", data).Msg("Failed to unmarshal config file")
		return err
	}
	GlobalConfig = config
	log.Info().Str("config-file", fileName).Msg("Read and applied config file")
	return nil
}

func generatePortAppender(protocol string, portNum int) string {
	if protocol == "http" && portNum == 80 {
		return ""
	} else if protocol == "https" && portNum == 443 {
		return ""
	} else {
		return fmt.Sprintf(":%d", portNum)
	}
}
