package config

import (
	"fmt"
	"math/rand"
	"strconv"
	"strings"
	"time"

	"github.com/matrix-org/gomatrixserverlib"
	"github.com/matrix-org/gomatrixserverlib/fclient"
	"github.com/matrix-org/gomatrixserverlib/spec"
	"golang.org/x/crypto/ed25519"
)

type Global struct {
	// Signing identity contains the server name, private key and key ID of
	// the deployment.
	fclient.SigningIdentity `yaml:",inline"`

	// The secondary server names, used for virtual hosting.
	VirtualHosts []*VirtualHost `yaml:"-"`

	// Path to the private key which will be used to sign requests and events.
	PrivateKeyPath Path `yaml:"private_key"`

	// Information about old private keys that used to be used to sign requests and
	// events on this domain. They will not be used but will be advertised to other
	// servers that ask for them to help verify old events.
	OldVerifyKeys []*OldVerifyKeys `yaml:"old_private_keys"`

	// How long a remote server can cache our server key for before requesting it again.
	// Increasing this number will reduce the number of requests made by remote servers
	// for our key, but increases the period a compromised key will be considered valid
	// by remote servers.
	// Defaults to 24 hours.
	KeyValidityPeriod time.Duration `yaml:"key_validity_period"`

	// Global pool of database connections, which is used only in monolith mode. If a
	// component does not specify any database options of its own, then this pool of
	// connections will be used instead. This way we don't have to manage connection
	// counts on a per-component basis, but can instead do it for the entire monolith.
	DatabaseOptions DatabaseOptions `yaml:"database,omitempty"`

	// The server name to delegate server-server communications to, with optional port
	WellKnownServerName string `yaml:"well_known_server_name"`

	// The server name to delegate client-server communications to, with optional port
	WellKnownClientName string `yaml:"well_known_client_name"`

	// The server name to delegate sliding sync communications to, with optional port.
	// Requires `well_known_client_name` to also be configured.
	WellKnownSlidingSyncProxy string `yaml:"well_known_sliding_sync_proxy"`

	// Disables federation. Dendrite will not be able to make any outbound HTTP requests
	// to other servers and the federation API will not be exposed.
	DisableFederation bool `yaml:"disable_federation"`

	// Configures the handling of presence events.
	Presence PresenceOptions `yaml:"presence"`

	// List of domains that the server will trust as identity servers to
	// verify third-party identifiers.
	// Defaults to an empty array.
	TrustedIDServers []string `yaml:"trusted_third_party_id_servers"`

	// JetStream configuration
	JetStream JetStream `yaml:"jetstream"`

	// Metrics configuration
	Metrics Metrics `yaml:"metrics"`

	// Sentry configuration
	Sentry Sentry `yaml:"sentry"`

	// DNS caching options for all outbound HTTP requests
	DNSCache DNSCacheOptions `yaml:"dns_cache"`

	// ServerNotices configuration used for sending server notices
	ServerNotices ServerNotices `yaml:"server_notices"`

	// ReportStats configures opt-in phone-home statistics reporting.
	ReportStats ReportStats `yaml:"report_stats"`

	// Configuration for the caches.
	Cache Cache `yaml:"cache"`
}

func (c *Global) Defaults(opts DefaultOpts) {
	if opts.Generate {
		c.ServerName = "localhost"
		c.PrivateKeyPath = "matrix_key.pem"
		_, c.PrivateKey, _ = ed25519.GenerateKey(rand.New(rand.NewSource(0)))
		c.KeyID = "ed25519:auto"
		c.TrustedIDServers = []string{
			"matrix.org",
			"vector.im",
		}
	}
	c.KeyValidityPeriod = time.Hour * 24 * 7
	if opts.SingleDatabase {
		c.DatabaseOptions.Defaults(90)
	}
	c.JetStream.Defaults(opts)
	c.Metrics.Defaults(opts)
	c.DNSCache.Defaults()
	c.Sentry.Defaults()
	c.ServerNotices.Defaults(opts)
	c.ReportStats.Defaults()
	c.Cache.Defaults()
}

func (c *Global) Verify(configErrs *ConfigErrors) {
	checkNotEmpty(configErrs, "global.server_name", string(c.ServerName))
	checkNotEmpty(configErrs, "global.private_key", string(c.PrivateKeyPath))

	// Check that client well-known has a proper format
	if c.WellKnownClientName != "" && !strings.HasPrefix(c.WellKnownClientName, "http://") && !strings.HasPrefix(c.WellKnownClientName, "https://") {
		configErrs.Add("The configuration for well_known_client_name does not have a proper format, consider adding http:// or https://. Some clients may fail to connect.")
	}

	for _, v := range c.VirtualHosts {
		v.Verify(configErrs)
	}

	c.JetStream.Verify(configErrs)
	c.Metrics.Verify(configErrs)
	c.Sentry.Verify(configErrs)
	c.DNSCache.Verify(configErrs)
	c.ServerNotices.Verify(configErrs)
	c.ReportStats.Verify(configErrs)
	c.Cache.Verify(configErrs)
}

func (c *Global) IsLocalServerName(serverName spec.ServerName) bool {
	if c.ServerName == serverName {
		return true
	}
	for _, v := range c.VirtualHosts {
		if v.ServerName == serverName {
			return true
		}
	}
	return false
}

func (c *Global) SplitLocalID(sigil byte, id string) (string, spec.ServerName, error) {
	u, s, err := gomatrixserverlib.SplitID(sigil, id)
	if err != nil {
		return u, s, err
	}
	if !c.IsLocalServerName(s) {
		return u, s, fmt.Errorf("server name %q not known", s)
	}
	return u, s, nil
}

func (c *Global) VirtualHost(serverName spec.ServerName) *VirtualHost {
	for _, v := range c.VirtualHosts {
		if v.ServerName == serverName {
			return v
		}
	}
	return nil
}

func (c *Global) VirtualHostForHTTPHost(serverName spec.ServerName) *VirtualHost {
	for _, v := range c.VirtualHosts {
		if v.ServerName == serverName {
			return v
		}
		for _, h := range v.MatchHTTPHosts {
			if h == serverName {
				return v
			}
		}
	}
	return nil
}

func (c *Global) SigningIdentityFor(serverName spec.ServerName) (*fclient.SigningIdentity, error) {
	for _, id := range c.SigningIdentities() {
		if id.ServerName == serverName {
			return id, nil
		}
	}
	return nil, fmt.Errorf("no signing identity for %q", serverName)
}

func (c *Global) SigningIdentities() []*fclient.SigningIdentity {
	identities := make([]*fclient.SigningIdentity, 0, len(c.VirtualHosts)+1)
	identities = append(identities, &c.SigningIdentity)
	for _, v := range c.VirtualHosts {
		identities = append(identities, &v.SigningIdentity)
	}
	return identities
}

type VirtualHost struct {
	// Signing identity contains the server name, private key and key ID of
	// the virtual host.
	fclient.SigningIdentity `yaml:",inline"`

	// Path to the private key. If not specified, the default global private key
	// will be used instead.
	PrivateKeyPath Path `yaml:"private_key"`

	// How long a remote server can cache our server key for before requesting it again.
	// Increasing this number will reduce the number of requests made by remote servers
	// for our key, but increases the period a compromised key will be considered valid
	// by remote servers.
	// Defaults to 24 hours.
	KeyValidityPeriod time.Duration `yaml:"key_validity_period"`

	// Match these HTTP Host headers on the `/key/v2/server` endpoint, this needs
	// to match all delegated names, likely including the port number too if
	// the well-known delegation includes that also.
	MatchHTTPHosts []spec.ServerName `yaml:"match_http_hosts"`

	// Is registration enabled on this virtual host?
	AllowRegistration bool `yaml:"allow_registration"`

	// Is guest registration enabled on this virtual host?
	AllowGuests bool `yaml:"allow_guests"`
}

func (v *VirtualHost) Verify(configErrs *ConfigErrors) {
	checkNotEmpty(configErrs, "virtual_host.*.server_name", string(v.ServerName))
}

// RegistrationAllowed returns two bools, the first states whether registration
// is allowed for this virtual host and the second states whether guests are
// allowed for this virtual host.
func (v *VirtualHost) RegistrationAllowed() (bool, bool) {
	if v == nil {
		return false, false
	}
	return v.AllowRegistration, v.AllowGuests
}

type OldVerifyKeys struct {
	// Path to the private key.
	PrivateKeyPath Path `yaml:"private_key"`

	// The private key itself.
	PrivateKey ed25519.PrivateKey `yaml:"-"`

	// The public key, in case only that part is known.
	PublicKey spec.Base64Bytes `yaml:"public_key"`

	// The key ID of the private key.
	KeyID gomatrixserverlib.KeyID `yaml:"key_id"`

	// When the private key was designed as "expired", as a UNIX timestamp
	// in millisecond precision.
	ExpiredAt spec.Timestamp `yaml:"expired_at"`
}

// The configuration to use for Prometheus metrics
type Metrics struct {
	// Whether or not the metrics are enabled
	Enabled bool `yaml:"enabled"`
	// Use BasicAuth for Authorization
	BasicAuth struct {
		// Authorization via Static Username & Password
		// Hardcoded Username and Password
		Username string `yaml:"username"`
		Password string `yaml:"password"`
	} `yaml:"basic_auth"`
}

func (c *Metrics) Defaults(opts DefaultOpts) {
	c.Enabled = false
	if opts.Generate {
		c.BasicAuth.Username = "metrics"
		c.BasicAuth.Password = "metrics"
	}
}

func (c *Metrics) Verify(configErrs *ConfigErrors) {
}

// ServerNotices defines the configuration used for sending server notices
type ServerNotices struct {
	Enabled bool `yaml:"enabled"`
	// The localpart to be used when sending notices
	LocalPart string `yaml:"local_part"`
	// The displayname to be used when sending notices
	DisplayName string `yaml:"display_name"`
	// The avatar of this user
	AvatarURL string `yaml:"avatar_url"`
	// The roomname to be used when creating messages
	RoomName string `yaml:"room_name"`
}

func (c *ServerNotices) Defaults(opts DefaultOpts) {
	if opts.Generate {
		c.Enabled = true
		c.LocalPart = "_server"
		c.DisplayName = "Server Alert"
		c.RoomName = "Server Alert"
		c.AvatarURL = ""
	}
}

func (c *ServerNotices) Verify(errors *ConfigErrors) {}

type Cache struct {
	EstimatedMaxSize DataUnit      `yaml:"max_size_estimated"`
	MaxAge           time.Duration `yaml:"max_age"`
}

func (c *Cache) Defaults() {
	c.EstimatedMaxSize = 1024 * 1024 * 1024 // 1GB
	c.MaxAge = time.Hour
}

func (c *Cache) Verify(errors *ConfigErrors) {
	checkPositive(errors, "max_size_estimated", int64(c.EstimatedMaxSize))
}

// ReportStats configures opt-in phone-home statistics reporting.
type ReportStats struct {
	// Enabled configures phone-home statistics of the server
	Enabled bool `yaml:"enabled"`

	// Endpoint the endpoint to report stats to
	Endpoint string `yaml:"endpoint"`
}

func (c *ReportStats) Defaults() {
	c.Enabled = false
	c.Endpoint = "https://panopticon.matrix.org/push"
}

func (c *ReportStats) Verify(configErrs *ConfigErrors) {
	// We prefer to hit panopticon (https://github.com/matrix-org/panopticon) directly over
	// the "old" matrix.org endpoint.
	if c.Endpoint == "https://matrix.org/report-usage-stats/push" {
		c.Endpoint = "https://panopticon.matrix.org/push"
	}
	if c.Enabled {
		checkNotEmpty(configErrs, "global.report_stats.endpoint", c.Endpoint)
	}
}

// The configuration to use for Sentry error reporting
type Sentry struct {
	Enabled bool `yaml:"enabled"`
	// The DSN to connect to e.g "https://examplePublicKey@o0.ingest.sentry.io/0"
	// See https://docs.sentry.io/platforms/go/configuration/options/
	DSN string `yaml:"dsn"`
	// The environment e.g "production"
	// See https://docs.sentry.io/platforms/go/configuration/environments/
	Environment string `yaml:"environment"`
}

func (c *Sentry) Defaults() {
	c.Enabled = false
}

func (c *Sentry) Verify(configErrs *ConfigErrors) {
}

type DatabaseOptions struct {
	// The connection string, file:filename.db or postgres://server....
	ConnectionString DataSource `yaml:"connection_string"`
	// Maximum open connections to the DB (0 = use default, negative means unlimited)
	MaxOpenConnections int `yaml:"max_open_conns"`
	// Maximum idle connections to the DB (0 = use default, negative means unlimited)
	MaxIdleConnections int `yaml:"max_idle_conns"`
	// maximum amount of time (in seconds) a connection may be reused (<= 0 means unlimited)
	ConnMaxLifetimeSeconds int `yaml:"conn_max_lifetime"`
}

func (c *DatabaseOptions) Defaults(conns int) {
	c.MaxOpenConnections = conns
	c.MaxIdleConnections = 2
	c.ConnMaxLifetimeSeconds = -1
}

func (c *DatabaseOptions) Verify(configErrs *ConfigErrors) {}

// MaxIdleConns returns maximum idle connections to the DB
func (c DatabaseOptions) MaxIdleConns() int {
	return c.MaxIdleConnections
}

// MaxOpenConns returns maximum open connections to the DB
func (c DatabaseOptions) MaxOpenConns() int {
	return c.MaxOpenConnections
}

// ConnMaxLifetime returns maximum amount of time a connection may be reused
func (c DatabaseOptions) ConnMaxLifetime() time.Duration {
	return time.Duration(c.ConnMaxLifetimeSeconds) * time.Second
}

type DNSCacheOptions struct {
	// Whether the DNS cache is enabled or not
	Enabled bool `yaml:"enabled"`
	// How many entries to store in the DNS cache at a given time
	CacheSize int `yaml:"cache_size"`
	// How long a cache entry should be considered valid for
	CacheLifetime time.Duration `yaml:"cache_lifetime"`
}

func (c *DNSCacheOptions) Defaults() {
	c.Enabled = false
	c.CacheSize = 256
	c.CacheLifetime = time.Minute * 5
}

func (c *DNSCacheOptions) Verify(configErrs *ConfigErrors) {
	checkPositive(configErrs, "cache_size", int64(c.CacheSize))
	checkPositive(configErrs, "cache_lifetime", int64(c.CacheLifetime))
}

// PresenceOptions defines possible configurations for presence events.
type PresenceOptions struct {
	// Whether inbound presence events are allowed
	EnableInbound bool `yaml:"enable_inbound"`
	// Whether outbound presence events are allowed
	EnableOutbound bool `yaml:"enable_outbound"`
}

type DataUnit int64

func (d *DataUnit) UnmarshalText(text []byte) error {
	var magnitude float64
	s := strings.ToLower(string(text))
	switch {
	case strings.HasSuffix(s, "tb"):
		s, magnitude = s[:len(s)-2], 1024*1024*1024*1024
	case strings.HasSuffix(s, "gb"):
		s, magnitude = s[:len(s)-2], 1024*1024*1024
	case strings.HasSuffix(s, "mb"):
		s, magnitude = s[:len(s)-2], 1024*1024
	case strings.HasSuffix(s, "kb"):
		s, magnitude = s[:len(s)-2], 1024
	default:
		magnitude = 1
	}
	v, err := strconv.ParseFloat(s, 64)
	if err != nil {
		return err
	}
	*d = DataUnit(v * magnitude)
	return nil
}
