// Package config implements the officeHours configuration
//
// It provides a struct type holding all necessary information for the programm.
// The configuration can be read from a JSON file.
package config

import (
	"encoding/json"
	"fmt"
	"html/template"
	"io/ioutil"
	"log"
	"net/mail"
)

// A Config holds all the constants for the programm.
// It is passed to most repositories.
type Config struct {
	Server struct {
		ListenAddress string // Address on which the http server is supposed to listen
		ListenPort    int    // Port on which the http server is supposed to listen
		Protocol      string // Protocol to access the server; either "http" or "https"
		Domain        string // A string indicating the tool's base domain and path, e.g. "example.com/subfolder"
	}
	Date struct {
		MinuteGranularity int // Restricts the minutes on which office hours can start and end to multiples of it.
		MaxDuration       int // Limits the length of office hours to minutes
		EarliestStartTime struct {
			Hour   int
			Minute int
		}
		LatestStartTime struct {
			Hour   int
			Minute int
		}
	}
	Request struct {
		SecretLength int // Length of the secret token for requests
	}
	Mailer struct {
		Type         string        // Send mail to "Stdout" or "Smtp"
		FromAddress  string        // Send mail from this address
		FromName     template.HTML // Send mail from this name, e.g. "Office hours <officeHours@localhost>"
		SmtpHost     string        // Host to use as smarthost
		SmtpPort     int           // Port of the smarthost
		SmtpUseAuth  bool          // Set whether to use authentication on the smarthosthost
		SmtpIdentity string        // Smarthost username
		SmtpPassword string        // Smarthost password
	}
	SQL struct {
		Type          string // Can be "SQLite" or "Mysql"
		SQLiteFile    string // Path to SQLite file
		MysqlUser     string
		MysqlPassword string
		MysqlHost     string
		MysqlPort     int
		MysqlDatabase string
	}
	Tutor struct {
		MailSuffix string // Restrict mailaddresses of tutors to suffixes.
		// e.g. "example.com" allowes "foo@example.com", "foo@sub.example.com", but not "foo@another.org" or "foo@barexample.com".
	}
}

// ReadConfigFile takes a file path as an argument and attempts to
// unmarshal the content of the file into a struct containing a
// configuration.
func ReadConfigFile(filename string, conf *Config) error {
	configData, err := ioutil.ReadFile(filename)
	if err != nil {
		err = fmt.Errorf("error reading config file: %w", err)
		log.Println(err.Error())
		return err
	}
	err = json.Unmarshal(configData, conf)
	if err != nil {
		err = fmt.Errorf("error parsing config file as json: %w", err)
		log.Println(err.Error())
		return err
	}
	return validateConfig(conf)
}

// Checks config for sane values (e.g. correct port range or database type).
func validateConfig(conf *Config) error {
	var err error
	if !(conf.Server.ListenPort >= 1 && conf.Server.ListenPort <= 65535) {
		err = fmt.Errorf("validating config: Server port must be between 1 and 65535, but is %d.", conf.Server.ListenPort)
		log.Println(err.Error())
	}
	if !(conf.Server.Protocol == "http" || conf.Server.Protocol == "https") {
		err = fmt.Errorf("validating config: Server protocol must be 'http' or 'https', but is '%s'.", conf.Server.Protocol)
		log.Println(err.Error())
	}
	if !(conf.Date.MinuteGranularity >= 1 && conf.Date.MinuteGranularity <= 60) {
		err = fmt.Errorf("validating config: Minute granularity must be between 1 and 60, but is %d.", conf.Date.MinuteGranularity)
		log.Println(err.Error())
	}
	if !(conf.Date.EarliestStartTime.Hour >= 0 && conf.Date.EarliestStartTime.Hour <= 23) {
		err = fmt.Errorf("validating config: Earliest start time hour must be between 0 and 23, but is %d.", conf.Date.EarliestStartTime.Hour)
		log.Println(err.Error())
	}
	if !(conf.Date.EarliestStartTime.Minute >= 0 && conf.Date.EarliestStartTime.Minute <= 60) {
		err = fmt.Errorf("validating config: Earliest start time minute must be between 0 and 60, but is %d.", conf.Date.EarliestStartTime.Minute)
		log.Println(err.Error())
	}
	if !(conf.Date.MaxDuration >= conf.Date.MinuteGranularity && conf.Date.MaxDuration <= 4*60) {
		err = fmt.Errorf("validating config: Maximum duration must be between %d minute and 4 hours, but is %d.", conf.Date.MinuteGranularity, conf.Date.MaxDuration)
		log.Println(err.Error())
	}
	if !(conf.Date.LatestStartTime.Hour >= 0 && conf.Date.LatestStartTime.Hour <= 23) {
		err = fmt.Errorf("validating config: Latest start time hour must be between 0 and 23, but is %d.", conf.Date.LatestStartTime.Hour)
		log.Println(err.Error())
	}
	if !(conf.Date.LatestStartTime.Minute >= 0 && conf.Date.LatestStartTime.Minute <= 60) {
		err = fmt.Errorf("validating config: Latest start time minute must be between 0 and 60, but is %d.", conf.Date.LatestStartTime.Minute)
		log.Println(err.Error())
	}
	if !(conf.Date.EarliestStartTime.Hour < conf.Date.LatestStartTime.Hour || (conf.Date.EarliestStartTime.Hour == conf.Date.LatestStartTime.Hour && conf.Date.EarliestStartTime.Minute < conf.Date.LatestStartTime.Minute)) {
		err = fmt.Errorf("validating config: Latest start time minute must be after earliest start time.")
		log.Println(err.Error())
	}
	if !(conf.Request.SecretLength >= 5 && conf.Request.SecretLength <= 50) {
		err = fmt.Errorf("validating config: Requests' secret length must be between 5 and 50, but is %d.", conf.Request.SecretLength)
		log.Println(err.Error())
	}
	if !(conf.Mailer.Type == "Stdout" || conf.Mailer.Type == "Smtp") {
		err = fmt.Errorf("validating config: Mailer type must be 'Stdout' or 'Smtp', but is '%s'.", conf.Mailer.Type)
		log.Println(err.Error())
	}
	mailFromAddress, mailFromAddressErr := mail.ParseAddress(conf.Mailer.FromAddress)
	if !(mailFromAddressErr == nil) {
		err = fmt.Errorf("validating config: Mail FromAddress could not be parsed (%w)", mailFromAddressErr)
		log.Println(err)
	} else {
		if !(mailFromAddress.Name == "") {
			err = fmt.Errorf("validating config: Mail FromAddress must not contain a name value, but has '%s'", mailFromAddress.Name)
			log.Println(err)
		}
	}
	_, mailFromNameErr := mail.ParseAddress(string(conf.Mailer.FromName))
	if !(mailFromNameErr == nil) {
		err = fmt.Errorf("validating config: Mail FromName could not be parsed (%w)", mailFromNameErr)
		log.Println(err)
	}
	if !(conf.SQL.Type == "SQLite" || conf.SQL.Type == "Mysql") {
		err = fmt.Errorf("validating config: SQL type must be 'SQLite' or 'Mysql', but is '%s'.", conf.SQL.Type)
		log.Println(err.Error())
	}
	return err
}
