package config

import "os"
import "io"
import "bufio"
import "errors"
import "unicode"
import "strconv"
import "strings"
import "crypto/tls"
import "golang.org/x/crypto/bcrypt"

type matchMode int; const (
	// exactly match
	matchModeExact matchMode = iota

	// match all starting with/ending with (depending on context)
	matchModeGlob

	// match exactly, as well as anything underneath (hierarchically)
	matchModeExtend
)

type pattern struct {
	matchAll bool
	
	scheme string
	host string
	path string
	hostMode matchMode
	pathMode matchMode
}

func (pattern pattern) String () (output string) {
	if pattern.matchAll {
		return "*"
	}
	output += pattern.scheme + "://"
	
	switch pattern.hostMode {
	case matchModeExtend: output += "."
	case matchModeGlob:   output += "*"
	}
	output += pattern.host

	output += pattern.path
	switch pattern.pathMode {
	case matchModeExtend: output += "/"
	case matchModeGlob:   output += "*"
	}

	return
}

func (pattern pattern) Match (scheme, host, path string) bool {
	if pattern.matchAll { return true }
	if scheme != pattern.scheme && pattern.scheme != "*" { return false }
	
	switch pattern.hostMode {
	case matchModeExact:
		if host != pattern.host { return false }
	case matchModeGlob:
		if !strings.HasSuffix(host, pattern.host) { return false }
	case matchModeExtend:
		match :=
			host == pattern.host ||
			strings.HasSuffix(host, "." + pattern.host)
		if !match { return false }
	default: return false
	}
	
	switch pattern.pathMode {
	case matchModeExact:
		if path != pattern.path { return false }
	case matchModeGlob:
		if !strings.HasPrefix(path, pattern.path) { return false }
	case matchModeExtend:
		match :=
			path == pattern.path ||
			strings.HasSuffix(path, "." + pattern.path)
		if !match { return false }
	default: return false
	}
	
	return false
}

// user is a real user parsed from a configuration file.
type user struct {
	name string
	rconAllow bool
	noSecurity bool
	hash []byte
	patterns []pattern
}

func (user user) String () (output string) {
	output += user.name + " ->"
	first := true
	if user.rconAllow {
		output += " (rcon)"
		first = false
	}
	for _, pattern := range user.patterns {
		if !first {
			output += ","
		}
		first = false
		output += " "
		output += pattern.String()
	}
	return
}

func (user user) RconAllow () bool {
	return user.rconAllow
}

func (user user) OverPatterns (callback func (pattern string) bool) {
	for _, pattern := range user.patterns {
		if !callback(pattern.String()) { break }
	}
}

func (user user) Validate (key []byte) bool {
	if user.noSecurity == true { return true }
	return bcrypt.CompareHashAndPassword(user.hash, key) == nil
}

func (user user) CanMountOn (scheme, host, path string) bool {
	if user.noSecurity == true { return true }
	for _, pattern := range user.patterns {
		if pattern.Match(scheme, host, path) { return true }
	}
	return false
}

// dummy is a dummy user that validates everything to true.
type dummy struct { }

func (dummy) Validate ([]byte) bool {
	return true
}

func (dummy) CanMountOn (string, string, string) bool {
	return true
}

// config is a config read from a file
type config struct {
	noSecurity bool
	users      map[string] user

	rconEnable   bool
	routerPort   int
	httpsEnable  bool
	httpsPort    int
	httpEnable   bool
	httpPort     int
	geminiEnable bool
	geminiPort   int
	
	fallback string
	aliases  map[string]string

	keyPath  string
	certPath string
	cert tls.Certificate
}

// File reads configuration values in order of precedence from:
//   - The file specified by path
//   - Environment variables
//   - Default values
// If the given file is inaccessible, this function will not return an error.
// However, if the file is accessible but it could not be parsed, it will return
// an error.
func File (path string) (Config, error) {
	conf := new(config)
	conf.populateDefault()
	file, err := os.Open(path)
	if err == nil {
		err = conf.readFrom(file)
		if err != nil { return nil, err }
	}
	return conf, nil
}

// Default returns configuration values from environment variables. Unset values
// are populated with default values.
func Default () Config {
	conf := new(config)
	conf.populateDefault()
	return conf
}

// ReadFrom reads a config file from a reader. It always closes the reader when
// it is done.
func ReadFrom (reader io.ReadCloser) (Config, error) {
	config := new(config)
	err := config.readFrom(reader)
	if err != nil { return nil, err }
	return config, nil
}

func (config *config) readFrom (reader io.ReadCloser) error {
	buferred := bufio.NewReader(reader)
	defer reader.Close()

	config.aliases = map[string]string {
		"localhost":        "@",
		"127.0.0.1":        "@",
		"::ffff:127.0.0.1": "@",
		"::1":              "@",
	}
	config.users = make(map[string] user)

	var key   string
	var val   string
	var state int
	for {
		ch, _, err := buferred.ReadRune()
		if err != nil {
			if err == io.EOF { break }
			return err
		}

		switch state {
		case 0:
			// wait for key or comment
			if ch == '#' {
				state = 3
			} else if !unicode.IsSpace(ch) {
				state = 1
				buferred.UnreadRune()
			}
			break

		case 1:
			// ignore whitespace until value (or EOL)
			if ch == '\n' {
				err = config.handleKeyVal(key, "")
				if err != nil { return err }
				key = ""
				val = ""
				state = 0
			} else if unicode.IsSpace(ch) {
				state = 2
			} else {
				key += string(ch)
			}
			break

		case 2:
			// get key until EOL
			if ch == '\n' {
				err = config.handleKeyVal(key, strings.TrimSpace(val))
				if err != nil { return err }
				key = ""
				val = ""
				state = 0
			} else {
				val += string(ch)
			}
			break

		case 3:
			// ignore comment until EOL
			if ch == '\n' {
				state = 0
			}
			break
		}
	}
	
	cert, err := tls.LoadX509KeyPair(config.certPath, config.keyPath)
	if err != nil { return err }
	config.cert = cert

	return nil
}

func (config *config) populateDefault () {
	defaultRouterPortStr := os.Getenv("HNAKRA_ROUTER_PORT")
	defaultHTTPSPortStr  := os.Getenv("HNAKRA_HTTPS_PORT")
	defaultHTTPPortStr   := os.Getenv("HNAKRA_HTTP_PORT")
	defaultGeminiPortStr := os.Getenv("HNAKRA_GEMINI_PORT")
	defaultRouterPort, _ := strconv.Atoi(defaultRouterPortStr)
	defaultHTTPSPort,  _ := strconv.Atoi(defaultHTTPSPortStr)
	defaultHTTPPort,   _ := strconv.Atoi(defaultHTTPPortStr)
	defaultGeminiPort, _ := strconv.Atoi(defaultGeminiPortStr)
	if defaultRouterPort == 0 { defaultRouterPort = 2048 }
	if defaultHTTPSPort  == 0 { defaultHTTPSPort  = 443  }
	if defaultHTTPPort   == 0 { defaultHTTPPort   = 80   }
	if defaultGeminiPort == 0 { defaultGeminiPort = 1965 }

	config.routerPort = defaultRouterPort
	config.httpsPort  = defaultHTTPSPort
	config.httpPort   = defaultHTTPPort
	config.geminiPort = defaultGeminiPort
}

func (config *config) handleKeyVal (key, val string) error {
	valn, _ := strconv.Atoi(val)

	switch key {
	case "alias":
		return config.parseAlias(key, val)
	case "unalias":
		delete(config.aliases, val)
	case "keyPath":
		config.keyPath = val
	case "certPath":
		config.certPath = val
	case "rcon":
		config.rconEnable = val == "true"
	case "router":
		config.routerPort = valn
	case "https":
		config.httpsEnable = true
		if valn > 0 { config.httpsPort = valn }
	case "http":
		config.httpEnable = true
		if valn > 0 { config.httpPort = valn }
	case "gemini":
		config.geminiEnable = true
		if valn > 0 { config.geminiPort = valn }
	case "noSecurity":
		config.noSecurity = val == "I AM NOT USING THIS IN PRODUCTION."
	case "user":
		return config.parseUser(key, val)
	}
	return nil
}

func (config *config) parseAlias (key, val string) error {
	alias, target, found := strings.Cut(val, "->")
	alias = strings.TrimSpace(alias)
	target = strings.TrimSpace(target)
	if !found {
		return errors.New("syntax: alias must be of the form: alias <alias> -> <target>")
	}

	if len(alias) < 1 || len(target) < 1 {
		return errors.New("syntax: alias must be of the form: alias <alias> -> <target>")
	}

	if alias == "(fallback)" {
		config.fallback = target
	} else {
		config.aliases[alias] = target
	}
	return nil
}

func (config *config) parseUser (key, val string) error {
	user := user { }
	
	name, keyAndPatterns, _ := strings.Cut(val, ":")
	name = strings.TrimSpace(name)
	user.name = name
	key, patternsStr, _ := strings.Cut(keyAndPatterns, "->")
	key = strings.TrimSpace(key)
	user.hash = []byte(key)
	patternStrs := strings.Split(patternsStr, ",")

	for _, patternStr := range patternStrs {
		patternStr = strings.TrimSpace(patternStr)
		if patternStr == "(rcon)" {
			user.rconAllow = true
		} else if patternStr != "" {
			pattern, err := parsePattern(patternStr)
			if err != nil { return err }
			user.patterns = append(user.patterns, pattern)
		}
	}

	config.users[name] = user
	return nil
}

func parsePattern (val string) (pattern, error) {
	pattern := pattern { }
	if val == "*" {
		pattern.matchAll = true
		return pattern, nil
	}

	scheme, hostAndPath, found := strings.Cut(val, "://")
	if !found { return pattern, errors.New("pattern must include scheme") }
	pattern.scheme = scheme

	host, path, found := strings.Cut(hostAndPath, "/")
	if !found { return pattern, errors.New("pattern must include path") }
	path = "/" + path

	if strings.HasPrefix(host, "*") {
		pattern.hostMode = matchModeGlob
		host = strings.TrimPrefix(host, "*")
	} else if strings.HasPrefix(host, ".") {
		pattern.hostMode = matchModeExtend
		host = strings.TrimPrefix(host, ".")
	}

	if strings.HasSuffix(path, "*") {
		pattern.pathMode = matchModeGlob
		path = strings.TrimSuffix(path, "*")
	} else if strings.HasSuffix(path, "/") {
		pattern.pathMode = matchModeExtend
		path = strings.TrimSuffix(path, "/")
	}

	pattern.host = host
	pattern.path = path
	return pattern, nil
}

func (config *config) OverUsers (callback func (name string, user User) bool) {
	for name, user := range config.users {
		if !callback(name, user) { break }
	}
}

func (config *config) User (name string) User {
	user := config.users[name]
	user.noSecurity = config.noSecurity
	return user
}

func (config *config) RconEnable () bool {
	return config.rconEnable
}

func (config *config) RouterPort () int {
	return config.routerPort
}

func (config *config) HTTPSEnable () bool {
	return config.httpsEnable
}

func (config *config) HTTPSPort () int {
	return config.httpsPort
}

func (config *config) HTTPEnable () bool {
	return config.httpEnable
}

func (config *config) HTTPPort () int {
	return config.httpPort
}

func (config *config) GeminiEnable () bool {
	return config.geminiEnable
}

func (config *config) GeminiPort () int {
	return config.geminiPort
}

func (config *config) ResolveAlias (alias string) string {
	// try to match an alias
	for key, value := range config.aliases {
		if alias == key {
			return value
		}
	}

	// if a fallback is set, and no aliases were found, use fallback
	if config.fallback != "" {
		return config.fallback
	}

	// if we don't have anything to resolve, return input as is
	return alias
}

func (config *config) AliasFallback () string {
	return config.fallback
}

func (config *config) OverAliases (callback func (alias, target string) bool) {
	for alias, target := range config.aliases {
		if !callback(alias, target) { break }
	}
}

func (config *config) Certificate () tls.Certificate {
	return config.cert
}
