package wand

import (
	"bytes"
	"errors"
	"fmt"
	"github.com/iancoleman/strcase"
	"io"
	"io/ioutil"
	"os"
)

type file struct {
	filename string
	file     io.ReadCloser
}

type config struct {
	values        map[string][]string
	discard       []string
	originalNames map[string]string
}

func fileReader(filename string) *file {
	return &file{
		filename: filename,
	}
}

func (f *file) wrapErr(err error) error {
	return fmt.Errorf("%s: %w", f.filename, err)
}

func (f *file) Read(p []byte) (int, error) {
	if f.file == nil {
		file, err := os.Open(f.filename)
		if err != nil {
			return 0, f.wrapErr(err)
		}

		f.file = file
	}

	n, err := f.file.Read(p)
	if err != nil {
		return n, f.wrapErr(err)
	}

	return n, nil
}

func (f *file) Close() error {
	if f.file == nil {
		return nil
	}

	if err := f.file.Close(); err != nil {
		return f.wrapErr(err)
	}

	return nil
}

func unescapeConfig(s string) string {
	var (
		u       []rune
		escaped bool
	)

	r := []rune(s)
	for _, ri := range r {
		if escaped {
			u = append(u, ri)
			continue
		}

		if ri == '\\' {
			escaped = true
			continue
		}

		u = append(u, ri)
	}

	return string(u)
}

func unquoteConfig(s string) string {
	if len(s) < 2 {
		return s
	}

	s = s[1 : len(s)-1]
	return unescapeConfig(s)
}

func readConfigFile(cmd Cmd, conf Config) (config, error) {
	var f io.ReadCloser
	if conf.test == "" {
		f = conf.file(cmd)
		defer f.Close()
	} else {
		f = ioutil.NopCloser(bytes.NewBufferString(conf.test))
	}

	doc, err := parse(f)
	if err != nil {
		if conf.optional && (errors.Is(err, os.ErrPermission) || errors.Is(err, os.ErrNotExist)) {
			return config{}, nil
		}

		return config{}, fmt.Errorf("failed to read config file: %w", err)
	}

	var c config
	for _, entry := range doc.Nodes {
		if entry.Name != "key-val" {
			continue
		}

		var (
			key      string
			value    string
			hasValue bool
		)

		for _, token := range entry.Nodes {
			if token.Name == "key" {
				key = token.Text()
				continue
			}

			if token.Name != "value" {
				continue
			}

			hasValue = true
			for _, valueToken := range token.Nodes {
				if valueToken.Name == "value-chars" {
					value = unescapeConfig(valueToken.Text())
					break
				}

				if valueToken.Name == "quoted" {
					value = unquoteConfig(valueToken.Text())
					break
				}
			}
		}

		if c.originalNames == nil {
			c.originalNames = make(map[string]string)
		}

		name := strcase.ToKebab(key)
		c.originalNames[name] = key
		if !hasValue {
			delete(c.values, name)
			c.discard = append(c.discard, name)
			continue
		}

		if c.values == nil {
			c.values = make(map[string][]string)
		}

		c.values[name] = append(c.values[name], value)
	}

	return c, nil
}

func readConfigFromOption(cmd Cmd, cl commandLine, conf Config) (config, error) {
	f := mapFields(cmd.impl)
	if _, ok := f["config"]; ok {
		return config{}, nil
	}

	var c []Config
	for _, o := range cl.options {
		if o.name != "config" {
			continue
		}

		c = append(c, ConfigFile(o.value.str))
	}

	return readConfig(cmd, cl, MergeConfig(c...))
}

func readMergeConfig(cmd Cmd, cl commandLine, conf Config) (config, error) {
	var c []config
	for _, ci := range conf.merge {
		cci, err := readConfig(cmd, cl, ci)
		if err != nil {
			return config{}, err
		}

		c = append(c, cci)
	}

	var mc config
	for _, ci := range c {
		for _, d := range ci.discard {
			delete(mc.values, d)
		}

		for name, values := range ci.values {
			if mc.values == nil {
				mc.values = make(map[string][]string)
			}

			mc.values[name] = values
		}
	}

	return mc, nil
}

func readConfig(cmd Cmd, cl commandLine, conf Config) (config, error) {
	if conf.file != nil || conf.test != "" {
		return readConfigFile(cmd, conf)
	}

	if conf.fromOption {
		return readConfigFromOption(cmd, cl, conf)
	}

	return readMergeConfig(cmd, cl, conf)
}

func hasConfigFromOption(conf Config) bool {
	if conf.fromOption {
		return true
	}

	for _, c := range conf.merge {
		if hasConfigFromOption(c) {
			return true
		}
	}

	return false
}
