package wand

import (
	"io"
	"reflect"
)

func bindKeyVals(receiver reflect.Value, keyVals map[string][]string) bool {
	v := make(map[string][]any)
	for name, values := range keyVals {
		for _, vi := range values {
			v[name] = append(v[name], vi)
		}
	}

	u := bindFields(receiver, v)
	return len(v) > 0 && len(u) < len(v)
}

func bindOptions(receiver reflect.Value, shortForms []string, o []option) bool {
	ms := make(map[string]string)
	for i := 0; i < len(shortForms); i += 2 {
		ms[shortForms[i]] = shortForms[i+1]
	}

	v := make(map[string][]any)
	for _, oi := range o {
		n := oi.name
		if oi.shortForm {
			n = ms[n]
		}

		var val any
		if oi.value.isBool {
			val = oi.value.boolean
		} else {
			val = oi.value.str
		}

		v[n] = append(v[n], val)
	}

	u := bindFields(receiver, v)
	return len(v) > 0 && len(u) < len(v)
}

func createStructArg(t reflect.Type, shortForms []string, c config, e env, o []option) (reflect.Value, bool) {
	r := allocate(reflect.PointerTo(t))
	hasConfigMatches := bindKeyVals(r, c.values)
	hasEnvMatches := bindKeyVals(r, e.values)
	hasOptionMatches := bindOptions(r, shortForms, o)
	return r.Elem(), hasConfigMatches || hasEnvMatches || hasOptionMatches
}

func createPositional(t reflect.Type, v string) reflect.Value {
	r := allocate(reflect.PointerTo(t))
	bindScalar(r, v)
	return r.Elem()
}

func createArgs(stdin io.Reader, stdout io.Writer, t reflect.Type, shortForms []string, c config, e env, cl commandLine) []reflect.Value {
	var args []reflect.Value
	positional := cl.positional
	for i := 0; i < t.NumIn(); i++ {
		ti := t.In(i)
		structure := isStruct(ti)
		variadic := t.IsVariadic() && i == t.NumIn()-1
		ior := isReader(ti)
		iow := isWriter(ti)
		switch {
		case ior:
			args = append(args, reflect.ValueOf(stdin))
		case iow:
			args = append(args, reflect.ValueOf(stdout))
		case structure && variadic:
			if arg, ok := createStructArg(ti.Elem(), shortForms, c, e, cl.options); ok {
				args = append(args, arg)
			}
		case structure:
			arg, _ := createStructArg(ti, shortForms, c, e, cl.options)
			args = append(args, arg)
		case variadic:
			for _, p := range positional {
				args = append(args, createPositional(ti.Elem(), p))
			}
		default:
			var p string
			p, positional = positional[0], positional[1:]
			args = append(args, createPositional(ti, p))
		}
	}

	return args
}

func processResults(t reflect.Type, out []reflect.Value) ([]any, error) {
	if len(out) == 0 {
		return nil, nil
	}

	var err error
	last := len(out) - 1
	isErrorType := t.Out(last) == reflect.TypeFor[error]()
	if isErrorType && !out[last].IsZero() {
		err = out[last].Interface().(error)
	}

	if isErrorType {
		out = out[:last]
	}

	var values []any
	for _, o := range out {
		values = append(values, o.Interface())
	}

	return values, err
}

func apply(stdin io.Reader, stdout io.Writer, cmd Cmd, c config, e env, cl commandLine) ([]any, error) {
	v := reflect.ValueOf(cmd.impl)
	v = unpackValue(v)
	t := v.Type()
	args := createArgs(stdin, stdout, t, cmd.shortForms, c, e, cl)
	out := v.Call(args)
	return processResults(t, out)
}
