// Wand provides utilities for constructing command line applications from functions, with automatic parameter
// binding from command line arguments, environment variables and configuration files.
package wand

import (
	"fmt"
	"os"
	"path"
)

// the parsing errors might be tricky
type Config struct {
	file       func(Cmd) *file
	merge      []Config
	fromOption bool
	optional   bool
	test       string
}

type Cmd struct {
	name          string
	impl          any
	group         bool
	subcommands   []Cmd
	isDefault     bool
	minPositional int
	maxPositional int
	shortForms    []string
	helpFor       *Cmd
	version       string
}

// name needs to be a valid symbol. The application name should also be a valid symbol,
// though not mandatory. If it is not, the environment variables may not work properly.
func Command(name string, impl any, subcmds ...Cmd) Cmd {
	return Cmd{name: name, impl: impl, subcommands: subcmds}
}

func Group(name string, subcmds ...Cmd) Cmd {
	return Cmd{name: name, group: true, subcommands: subcmds}
}

func Default(cmd Cmd) Cmd {
	cmd.isDefault = true
	return cmd
}

func Args(cmd Cmd, min, max int) Cmd {
	cmd.minPositional = min
	cmd.maxPositional = max
	return cmd
}

func ShortForm(cmd Cmd, f ...string) Cmd {
	if len(f)%2 != 0 {
		f = append(f, "")
	}

	cmd.shortForms = append(cmd.shortForms, f...)
	for i := range cmd.subcommands {
		cmd.subcommands[i] = ShortForm(
			cmd.subcommands[i],
			f...,
		)
	}

	return cmd
}

func Version(cmd Cmd, version string) Cmd {
	cmd.subcommands = append(
		cmd.subcommands,
		Cmd{name: "version", version: version},
	)

	return cmd
}

func MergeConfig(conf ...Config) Config {
	return Config{
		merge: conf,
	}
}

func OptionalConfig(conf Config) Config {
	conf.optional = true
	for i := range conf.merge {
		conf.merge[i] = OptionalConfig(conf.merge[i])
	}

	return conf
}

func ConfigFile(name string) Config {
	return Config{
		file: func(Cmd) *file { return fileReader(name) },
	}
}

// the config will consider the command name as in the arguments
func Etc() Config {
	return OptionalConfig(Config{
		file: func(cmd Cmd) *file {
			return fileReader(path.Join("/etc", cmd.name, "config"))
		},
	})
}

// the config will consider the command name as in the arguments
func UserConfig() Config {
	return OptionalConfig(
		MergeConfig(
			Config{
				file: func(cmd Cmd) *file {
					return fileReader(path.Join(os.Getenv("HOME"), fmt.Sprintf(".%s", cmd.name), "config"))
				},
			},
			Config{
				file: func(cmd Cmd) *file {
					return fileReader(path.Join(os.Getenv("HOME"), ".config", cmd.name, "config"))
				},
			},
		),
	)
}

func ConfigFromOption() Config {
	return Config{fromOption: true}
}

func SystemConfig() Config {
	return MergeConfig(Etc(), UserConfig(), ConfigFromOption())
}

// the env will consider the command name as in the arguments
func Exec(impl any, conf ...Config) {
	exec(os.Stdin, os.Stdout, os.Stderr, os.Exit, wrap(impl), MergeConfig(conf...), os.Environ(), os.Args)
}
