package config

import (
	"fmt"
	"net"
	"os"
	"sort"
	"strings"

	"github.com/robfig/config"
)

// SMTPConfig contains the SMTP server configuration - not using pointers so that we can pass around
// copies of the object safely.
type SMTPConfig struct {
	IP4address      net.IP
	IP4port         int
	Domain          string
	DomainNoStore   string
	MaxRecipients   int
	MaxIdleSeconds  int
	MaxMessageBytes int
	StoreMessages   bool
}

// POP3Config contains the POP3 server configuration
type POP3Config struct {
	IP4address     net.IP
	IP4port        int
	Domain         string
	MaxIdleSeconds int
}

// WebConfig contains the HTTP server configuration
type WebConfig struct {
	IP4address     net.IP
	IP4port        int
	TemplateDir    string
	TemplateCache  bool
	PublicDir      string
	GreetingFile   string
	MailboxPrompt  string
	CookieAuthKey  string
	MonitorVisible bool
	MonitorHistory int
}

// DataStoreConfig contains the mail store configuration
type DataStoreConfig struct {
	Path             string
	RetentionMinutes int
	RetentionSleep   int
	MailboxMsgCap    int
}

const (
	missingErrorFmt = "[%v] missing required option %q"
	parseErrorFmt   = "[%v] option %q error: %v"
)

var (
	// Version of this build, set by main
	Version = ""

	// BuildDate for this build, set by main
	BuildDate = ""

	// Config is our global robfig/config object
	Config   *config.Config
	logLevel string

	// Parsed specific configs
	smtpConfig      = &SMTPConfig{}
	pop3Config      = &POP3Config{}
	webConfig       = &WebConfig{}
	dataStoreConfig = &DataStoreConfig{}
)

// GetSMTPConfig returns a copy of the SmtpConfig object
func GetSMTPConfig() SMTPConfig {
	return *smtpConfig
}

// GetPOP3Config returns a copy of the Pop3Config object
func GetPOP3Config() POP3Config {
	return *pop3Config
}

// GetWebConfig returns a copy of the WebConfig object
func GetWebConfig() WebConfig {
	return *webConfig
}

// GetDataStoreConfig returns a copy of the DataStoreConfig object
func GetDataStoreConfig() DataStoreConfig {
	return *dataStoreConfig
}

// GetLogLevel returns the configured log level
func GetLogLevel() string {
	return logLevel
}

// LoadConfig loads the specified configuration file into inbucket.Config and performs validations
// on it.
func LoadConfig(filename string) error {
	var err error
	Config, err = config.ReadDefault(filename)
	if err != nil {
		return err
	}
	// Validation error messages
	messages := make([]string, 0)
	// Validate sections
	for _, s := range []string{"logging", "smtp", "pop3", "web", "datastore"} {
		if !Config.HasSection(s) {
			messages = append(messages,
				fmt.Sprintf("Config section [%v] is required", s))
		}
	}
	// Return immediately if config is missing entire sections
	if len(messages) > 0 {
		fmt.Fprintln(os.Stderr, "Error(s) validating configuration:")
		for _, m := range messages {
			fmt.Fprintln(os.Stderr, " -", m)
		}
		return fmt.Errorf("Failed to validate configuration")
	}
	// Load string config options
	stringOptions := []struct {
		section  string
		name     string
		target   *string
		required bool
	}{
		{"logging", "level", &logLevel, true},
		{"smtp", "domain", &smtpConfig.Domain, true},
		{"smtp", "domain.nostore", &smtpConfig.DomainNoStore, false},
		{"pop3", "domain", &pop3Config.Domain, true},
		{"web", "template.dir", &webConfig.TemplateDir, true},
		{"web", "public.dir", &webConfig.PublicDir, true},
		{"web", "greeting.file", &webConfig.GreetingFile, true},
		{"web", "mailbox.prompt", &webConfig.MailboxPrompt, false},
		{"web", "cookie.auth.key", &webConfig.CookieAuthKey, false},
		{"datastore", "path", &dataStoreConfig.Path, true},
	}
	for _, opt := range stringOptions {
		str, err := Config.String(opt.section, opt.name)
		if Config.HasOption(opt.section, opt.name) && err != nil {
			messages = append(messages, fmt.Sprintf(parseErrorFmt, opt.section, opt.name, err))
			continue
		}
		if str == "" && opt.required {
			messages = append(messages, fmt.Sprintf(missingErrorFmt, opt.section, opt.name))
		}
		*opt.target = str
	}
	// Load boolean config options
	boolOptions := []struct {
		section  string
		name     string
		target   *bool
		required bool
	}{
		{"smtp", "store.messages", &smtpConfig.StoreMessages, true},
		{"web", "template.cache", &webConfig.TemplateCache, true},
		{"web", "monitor.visible", &webConfig.MonitorVisible, true},
	}
	for _, opt := range boolOptions {
		if Config.HasOption(opt.section, opt.name) {
			flag, err := Config.Bool(opt.section, opt.name)
			if err != nil {
				messages = append(messages, fmt.Sprintf(parseErrorFmt, opt.section, opt.name, err))
			}
			*opt.target = flag
		} else {
			if opt.required {
				messages = append(messages, fmt.Sprintf(missingErrorFmt, opt.section, opt.name))
			}
		}
	}
	// Load integer config options
	intOptions := []struct {
		section  string
		name     string
		target   *int
		required bool
	}{
		{"smtp", "ip4.port", &smtpConfig.IP4port, true},
		{"smtp", "max.recipients", &smtpConfig.MaxRecipients, true},
		{"smtp", "max.idle.seconds", &smtpConfig.MaxIdleSeconds, true},
		{"smtp", "max.message.bytes", &smtpConfig.MaxMessageBytes, true},
		{"pop3", "ip4.port", &pop3Config.IP4port, true},
		{"pop3", "max.idle.seconds", &pop3Config.MaxIdleSeconds, true},
		{"web", "ip4.port", &webConfig.IP4port, true},
		{"web", "monitor.history", &webConfig.MonitorHistory, true},
		{"datastore", "retention.minutes", &dataStoreConfig.RetentionMinutes, true},
		{"datastore", "retention.sleep.millis", &dataStoreConfig.RetentionSleep, true},
		{"datastore", "mailbox.message.cap", &dataStoreConfig.MailboxMsgCap, true},
	}
	for _, opt := range intOptions {
		if Config.HasOption(opt.section, opt.name) {
			num, err := Config.Int(opt.section, opt.name)
			if err != nil {
				messages = append(messages, fmt.Sprintf(parseErrorFmt, opt.section, opt.name, err))
			}
			*opt.target = num
		} else {
			if opt.required {
				messages = append(messages, fmt.Sprintf(missingErrorFmt, opt.section, opt.name))
			}
		}
	}
	// Load IP address config options
	ipOptions := []struct {
		section  string
		name     string
		target   *net.IP
		required bool
	}{
		{"smtp", "ip4.address", &smtpConfig.IP4address, true},
		{"pop3", "ip4.address", &pop3Config.IP4address, true},
		{"web", "ip4.address", &webConfig.IP4address, true},
	}
	for _, opt := range ipOptions {
		if Config.HasOption(opt.section, opt.name) {
			str, err := Config.String(opt.section, opt.name)
			if err != nil {
				messages = append(messages, fmt.Sprintf(parseErrorFmt, opt.section, opt.name, err))
				continue
			}
			addr := net.ParseIP(str)
			if addr == nil {
				messages = append(messages,
					fmt.Sprintf("Failed to parse IP [%v]%v: %q", opt.section, opt.name, str))
				continue
			}
			addr = addr.To4()
			if addr == nil {
				messages = append(messages,
					fmt.Sprintf("Failed to parse IP [%v]%v: %q not IPv4!",
						opt.section, opt.name, str))
			}
			*opt.target = addr
		} else {
			if opt.required {
				messages = append(messages, fmt.Sprintf(missingErrorFmt, opt.section, opt.name))
			}
		}
	}
	// Validate log level
	switch strings.ToUpper(logLevel) {
	case "":
		// Missing was already reported
	case "TRACE", "INFO", "WARN", "ERROR":
	default:
		messages = append(messages,
			fmt.Sprintf("Invalid value provided for [logging]level: %q", logLevel))
	}
	// Print messages and return error if any validations failed
	if len(messages) > 0 {
		fmt.Fprintln(os.Stderr, "Error(s) validating configuration:")
		sort.Strings(messages)
		for _, m := range messages {
			fmt.Fprintln(os.Stderr, " -", m)
		}
		return fmt.Errorf("Failed to validate configuration")
	}
	return nil
}
