// Package ini implements an opinionated ini parser that only implements what we exactly need for WireGuard configs
// - key/values MUST live under a section
// - empty section names are NOT allowed
// - comments are indicated with a #
package ini

import (
	"errors"
	"fmt"
	"strings"
)

// shouldSkip returns whether or not a line should be skipped, empty line (after whitespace omitting) or a comment
func shouldSkip(f string) bool {
	return f == "" || strings.HasPrefix(f, "#")
}

// isSection returns whether a line is a section
// this happens when it begins with [ and ends with ]
func isSection(f string) bool {
	return strings.HasPrefix(f, "[") && strings.HasSuffix(f, "]")
}

// sectionName extracts the section name from a line by removing the [ and ] prefix and suffix
func sectionName(f string) string {
	name := strings.TrimSuffix(strings.TrimPrefix(f, "["), "]")
	return strings.TrimSpace(name)
}

// keyValue extracts a key and a value from a line
// if no 2 components are found (separated by =), we will return an error
// the key and value have their spaces trimmed
func keyValue(f string) (string, string, error) {
	sl := strings.SplitN(f, "=", 2)
	if len(sl) < 2 {
		return "", "", errors.New("no key/value found")
	}
	k := strings.TrimSpace(sl[0])
	if k == "" {
		return "", "", errors.New("key cannot be empty")
	}
	v := strings.TrimSpace(sl[1])
	return k, v, nil
}

// OrderedKeys is a slice of strings that is used for an ordered map
type OrderedKeys []string

// Section represents a single section within an ini file
// It consists of multiple key and values
type Section struct {
	keyValues map[string]string
	keys      OrderedKeys
}

// keyValue gets a value for key `key`
// It returns an error if the key does not exist
func (sec *Section) keyValue(key string) (string, error) {
	if v, ok := sec.keyValues[key]; ok {
		return v, nil
	}
	return "", fmt.Errorf("key: '%s' does not exist", key)
}

func (sec *Section) newKeyValue(key string, value string) {
	if sec.keyValues == nil {
		sec.keyValues = make(map[string]string)
	}
	sec.keyValues[key] = value
	sec.keys = append(sec.keys, key)
}

// AddOrReplaceKeyValue adds a key `key` with value `value`
// If the key already exists it modifies the value
func (sec *Section) AddOrReplaceKeyValue(key string, value string) {
	_, err := sec.keyValue(key)
	if err == nil {
		sec.keyValues[key] = value
		return
	}
	sec.newKeyValue(key, value)
}

// AddKeyValue adds a new key `key` with value `value`
// It returns an error if the key already exists
func (sec *Section) AddKeyValue(key string, value string) error {
	// get an existing key
	_, err := sec.keyValue(key)
	if err == nil {
		return fmt.Errorf("key: '%s' already exists", key)
	}
	sec.newKeyValue(key, value)
	return nil
}

// INI is the struct for a ini file
type INI struct {
	sections map[string]*Section
	keys     OrderedKeys
}

// Empty returns true if there are no sections defined in the INI
func (i *INI) Empty() bool {
	return len(i.keys) == 0
}

// Section gets a section from the ini file
func (i *INI) Section(name string) (*Section, error) {
	if _, ok := i.sections[name]; ok {
		return i.sections[name], nil
	}
	return nil, fmt.Errorf("section: '%s' does not exist", name)
}

// AddSection adds a section with name `name` and returns an error if the section already exists
func (i *INI) AddSection(name string) error {
	// get an existing section
	_, err := i.Section(name)
	if err == nil {
		return errors.New("section: '%s' already exists")
	}
	if i.sections == nil {
		i.sections = make(map[string]*Section)
	}
	i.sections[name] = &Section{}
	i.keys = append(i.keys, name)
	return nil
}

// String returns the representation of the ini as a string
func (i *INI) String() string {
	var out strings.Builder
	for _, s := range i.keys {
		sec, err := i.Section(s)
		if err != nil {
			continue
		}
		fmt.Fprintf(&out, "[%s]\n", s)

		for _, k := range sec.keys {
			v, err := sec.keyValue(k)
			if err != nil {
				continue
			}
			delim := ""
			if v != "" {
				delim = " "
			}
			fmt.Fprintf(&out, "%s =%s%s\n", k, delim, v)
		}
	}
	return out.String()
}

// Parse returns the INI struct from a string `f`
func Parse(f string) INI {
	lines := strings.Split(f, "\n")

	var secs INI
	sec := ""
	for _, line := range lines {
		// clean the line
		line = strings.TrimSpace(line)

		if shouldSkip(line) {
			continue
		}

		if isSection(line) {
			name := sectionName(line)
			// we do not allow sections with empty names
			if name == "" {
				continue
			}
			_ = secs.AddSection(name)
			sec = name
			continue
		}

		// no section has been parsed yet
		// we will ignore the rest of the values
		if sec == "" {
			continue
		}

		// split key and value
		key, value, err := keyValue(line)
		if err != nil {
			continue
		}

		csec := secs.sections[sec]
		// This adds a new key and value to the section
		// If it already exists it ignores it as this function would return an error
		_ = csec.AddKeyValue(key, value)
	}
	return secs
}
