package config

import (
	"fmt"
	"os"

	"github.com/BurntSushi/toml"
	"github.com/rs/zerolog/log"
	"gitlab.com/mstarongitlab/goutils/other"
)

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
}

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"`
}

type ConfigStorage struct {
	// Url to the database to use
	// If DbIsPostgres is either not set or false, the url is expected to be a path to a sqlite file
	// Otherwise, it's expected to be an url to a postgres server
	DatabaseUrl string `toml:"database_url"`
	// Whether the target of the database url is a postgres server
	DbIsPostgres *bool `toml:"db_is_postgres,omitempty"`
	// 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"`
}

type Config struct {
	General  ConfigGeneral  `toml:"general"`
	SSL      ConfigSSL      `toml:"ssl"`
	Admin    ConfigAdmin    `toml:"admin"`
	Webauthn ConfigWebAuthn `toml:"webauthn"`
	Storage  ConfigStorage  `toml:"storage"`
}

var GlobalConfig Config

var defaultConfig Config = Config{
	General: ConfigGeneral{
		Protocol:    "http",
		Subdomain:   nil,
		Domain:      "localhost",
		PrivatePort: 8080,
		PublicPort:  nil,
	},
	SSL: ConfigSSL{
		HandleSSL:       false,
		UseLetsEncrypt:  nil,
		CertificateFile: nil,
		AdminMail:       nil,
	},
	Admin: ConfigAdmin{
		Username:          "server-admin",
		FirstTimeSetupOTP: "Example otp password",
	},
	Webauthn: ConfigWebAuthn{
		DisplayName:   "Linstrom",
		HashingSecret: "some super secure secret that should never be changed or else password storage breaks",
	},
	Storage: ConfigStorage{
		DatabaseUrl:          "db.sqlite",
		DbIsPostgres:         other.IntoPointer(false),
		RedisUrl:             nil,
		MaxInMemoryCacheSize: 1e6, // 1 Megabyte
	},
}

func (gc *ConfigGeneral) GetFullDomain() string {
	if gc.Subdomain != nil {
		return *gc.Subdomain + gc.Domain
	}
	return gc.Domain
}

func (gc *ConfigGeneral) GetFullPublicUrl() string {
	str := gc.Protocol + gc.GetFullDomain()
	if gc.PublicPort != nil {
		str += fmt.Sprint(*gc.PublicPort)
	} else {
		str += fmt.Sprint(gc.PrivatePort)
	}
	return str
}

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
}
