package opt

import (
	"errors"
	"fmt"
	"reflect"
	"regexp"
	"strconv"
	"strings"
)

// Command line options specifier
type CmdSpec struct {
	// first argument, for Usage() generation
	name string
	// if true, all arguments will be ignored
	passthrough bool
	// if true, positional arguments may start with "-"
	minusPositionals bool
	// list of option specs extracted from struct tags
	opts []optSpec
	// list of option specs in the order in which they were seen
	seen []*seenArg
	// indexes of short options in the list for quick access
	shortOpts map[string]int
	// indexes of long options in the list for quick access
	longOpts map[string]int
	// indexes of positional arguments
	positionals []int
}

type seenArg struct {
	spec    *optSpec
	indexes []int
}

type optKind int

const (
	unset optKind = iota
	// dummy value to cause the whole command to be passthrough
	passthrough
	// flag without a value: "-f" or "--foo-baz"
	flag
	// flag with a value: "-f bar", "--foo-baz bar" or "--foo-baz=bar"
	option
	// positional argument after interpreting shell quotes
	positional
	// remaining positional arguments after interpreting shell quotes
	remainderSplit
	// remaining positional arguments without interpreting shell quotes
	remainder
	// nested subcommand
	subcommand
)

// Option or argument specifier
type optSpec struct {
	// kind of option/argument
	kind optKind
	// argument is required
	required bool
	// if true, positional may start with "-"
	minusPositional bool
	// option/argument description
	description string
	// argument was seen on the command line
	seen bool
	// argument value was seen on the command line (only applies to options)
	seenValue bool
	// "f", "foo-baz" (only when kind is flag or option)
	short, long string
	// name of option/argument value in usage help
	metavar string
	// default string value before interpretation
	defval string
	// only applies to the specified command aliases
	aliases []string
	// custom action method
	action reflect.Value
	// custom complete method
	complete reflect.Value
	// destination struct field
	dest reflect.Value
	// raw argument value
	arg string
}

var (
	shortOptRe   = regexp.MustCompile(`^(-[a-zA-Z0-9])$`)
	longOptRe    = regexp.MustCompile(`^(--[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9])$`)
	positionalRe = regexp.MustCompile(`^([a-zA-Z][\w-]*)$`)
	subcommandRe = regexp.MustCompile(`^:([a-zA-Z][\w-]*):$`)
)

// Interpret all struct fields to a list of option specs
func NewCmdSpec(name string, v any) *CmdSpec {
	typ := reflect.TypeOf(v)
	val := reflect.ValueOf(v)
	if typ.Kind() == reflect.Pointer {
		typ = typ.Elem()
		val = val.Elem()
	} else {
		panic("NewCmdSpec requires a pointer")
	}
	if typ.Kind() != reflect.Struct {
		panic("NewCmdSpec requires a pointer to a struct")
	}

	cmd := &CmdSpec{
		name:      name,
		opts:      make([]optSpec, 0, typ.NumField()),
		shortOpts: make(map[string]int),
		longOpts:  make(map[string]int),
	}

	allPositionals := false
	for i := 0; i < typ.NumField(); i++ {
		var spec optSpec
		spec.parseField(reflect.ValueOf(v), typ.Field(i))
		switch spec.kind {
		case unset:
			// ignored field
			continue
		case passthrough:
			cmd.passthrough = true
			continue
		}
		spec.dest = val.Field(i)

		if allPositionals {
			panic(`opt:"..." or opt:":cmd:" must be last`)
		}

		switch spec.kind {
		case flag, option:
			if spec.short != "" {
				cmd.shortOpts[spec.short] = len(cmd.opts)
			}
			if spec.long != "" {
				cmd.longOpts[spec.long] = len(cmd.opts)
			}
		case remainder, remainderSplit:
			allPositionals = true
			fallthrough
		default:
			cmd.positionals = append(cmd.positionals, len(cmd.opts))
			if spec.minusPositional {
				cmd.minusPositionals = true
			}
		}
		cmd.opts = append(cmd.opts, spec)
	}

	return cmd
}

func (spec *optSpec) parseField(struc reflect.Value, t reflect.StructField) {
	opt := t.Tag.Get("opt")

	switch opt {
	case "-":
		// ignore all arguments (passthrough) for this command
		spec.kind = passthrough
		fallthrough
	case "":
		// ignored field
		return
	}

	abort := func(msg string, args ...any) {
		msg = fmt.Sprintf(msg, args...)
		panic(fmt.Sprintf("%s.%s: %s", struc.Type(), t.Name, msg))
	}

	if methodName, found := t.Tag.Lookup("action"); found {
		method := struc.MethodByName(methodName)
		if !method.IsValid() {
			abort("action method not found: (*%s).%s", struc, methodName)
		}

		ok := method.Type().NumIn() == 1
		ok = ok && method.Type().In(0).Kind() == reflect.String
		ok = ok && method.Type().NumOut() == 1
		ok = ok && method.Type().Out(0).Kind() == reflect.Interface
		ok = ok && method.Type().Out(0).Name() == "error"
		if !ok {
			abort("(*%s).%s: invalid signature, expected func(string) error",
				struc.Elem().Type().Name(), methodName,
				t.Type.Kind())
		}
		spec.action = method
	}

	if !spec.action.IsValid() {
		switch t.Type.Kind() {
		case reflect.String:
			fallthrough
		case reflect.Bool:
			fallthrough
		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
			fallthrough
		case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
			fallthrough
		case reflect.Float32, reflect.Float64:
			fallthrough
		case reflect.Slice:
			break
		default:
			abort("unsupported field type: %s", t.Type.Kind())
		}
	}

	// check what kind of argument this field maps to
	switch {
	case opt == "...":
		// remainder
		switch t.Type.Kind() {
		case reflect.Slice:
			if !spec.action.IsValid() {
				if t.Type.Elem().Kind() != reflect.String {
					abort("'...' requires []string if action is unset")
				}
			}
			spec.kind = remainderSplit
		case reflect.String:
			spec.kind = remainder
		default:
			if !spec.action.IsValid() {
				abort("'...' requires string or []string if action is unset")
			}
		}
		spec.metavar = "<" + strings.ToLower(t.Name) + ">..."
		spec.required = true

	case strings.HasPrefix(opt, "-"):
		// flag or option
		for _, flag := range strings.Split(opt, ",") {
			m := longOptRe.FindStringSubmatch(flag)
			if m != nil {
				spec.long = m[1]
				continue
			}
			m = shortOptRe.FindStringSubmatch(flag)
			if m != nil {
				spec.short = m[1]
				continue
			}
			abort("invalid opt tag: %q", opt)
		}
		if t.Type.Kind() == reflect.Bool {
			spec.kind = flag
		} else {
			spec.kind = option
			spec.metavar = "<" + strings.ToLower(t.Name) + ">"
		}
		if spec.short == "" && spec.long == "" {
			abort("invalid opt tag: %q", opt)
		}

	case positionalRe.MatchString(opt):
		// named positional
		spec.kind = positional
		spec.metavar = "<" + strings.ToLower(opt) + ">"
		spec.required = true

	case subcommandRe.MatchString(opt):
		// subcommand
		match := subcommandRe.FindStringSubmatch(opt)
		if t.Type.Kind() != reflect.Interface {
			abort("':" + match[1] + ":' requires an interface")
		}
		if !spec.action.IsValid() {
			abort("':" + match[1] + ":' requires action to be set")
		}
		spec.kind = subcommand
		spec.metavar = "<" + match[1] + ">"
		spec.required = true

	default:
		abort("invalid opt tag: %q", opt)
	}

	if metavar, hasMetavar := t.Tag.Lookup("metavar"); hasMetavar {
		// explicit metavar for the generated usage
		spec.metavar = metavar
	}

	spec.description = t.Tag.Get("description")
	if spec.description == "" {
		spec.description = t.Tag.Get("desc")
	}

	spec.defval = t.Tag.Get("default")

	switch t.Tag.Get("required") {
	case "true":
		spec.required = true
	case "false":
		spec.required = false
	case "":
		if spec.defval != "" {
			spec.required = false
		}
	default:
		abort("invalid required value")
	}

	switch t.Tag.Get("minus") {
	case "true":
		spec.minusPositional = true
	case "false", "":
		spec.minusPositional = false
	default:
		abort("invalid minus value")
	}

	if aliases := t.Tag.Get("aliases"); aliases != "" {
		spec.aliases = strings.Split(aliases, ",")
	}

	if methodName, found := t.Tag.Lookup("complete"); found {
		method := struc.MethodByName(methodName)
		if !method.IsValid() {
			abort("complete method not found: (*%s).%s", struc, methodName)
		}
		if !(method.Type().NumIn() == 1 &&
			method.Type().In(0).Kind() == reflect.String &&
			method.Type().NumOut() == 1 &&
			method.Type().Out(0).Kind() == reflect.Slice &&
			method.Type().Out(0).Elem().Kind() == reflect.String) {
			abort("(*%s).%s: invalid signature, expected func(string) []string",
				struc.Elem().Type().Name(), methodName,
				t.Type.Kind())
		}
		spec.complete = method
	}
}

func (s *optSpec) Usage() string {
	var usage string

	switch s.kind {
	case flag:
		if s.short != "" {
			usage = s.short
		} else {
			usage = s.long
		}

	case option:
		if s.short != "" {
			usage = s.short
		} else {
			usage = s.long
		}
		usage += " " + s.metavar

	case subcommand:
		usage = s.metavar + " ..."

	default:
		usage = s.metavar
	}

	if s.required {
		return usage
	}
	return "[" + usage + "]"
}

func (s *optSpec) Flag() string {
	var usage string

	switch s.kind {
	case flag, option:
		if s.short != "" {
			usage = s.short
		} else {
			usage = s.long
		}
	default:
		usage = s.metavar
	}

	return usage
}

func (c *CmdSpec) Usage() string {
	if c.passthrough {
		return c.name + " ..."
	}
	args := []string{c.name}
	for _, spec := range c.opts {
		if spec.appliesToAlias(c.name) {
			if spec.kind == subcommand && !spec.dest.IsNil() {
				cc := NewCmdSpec(spec.arg, spec.dest.Elem().Interface())
				args = append(args, cc.Usage())
			} else {
				args = append(args, spec.Usage())
			}
		}
	}
	return strings.Join(args, " ")
}

type errKind int

const (
	unknownErr errKind = iota
	missingValue
	takesNoValue
	unknownFlag
	unexpectedArg
	badValue
	requiredArg
)

type ArgError struct {
	kind errKind
	spec *optSpec
	err  error
}

func (e *ArgError) Error() string {
	switch e.kind {
	case missingValue:
		return fmt.Sprintf("%s takes a value", e.spec.Flag())
	case takesNoValue:
		return fmt.Sprintf("%s does not take a value", e.spec.Flag())
	case unknownFlag:
		return fmt.Sprintf("%s unknown flag", e.err)
	case unexpectedArg:
		return fmt.Sprintf("%q unexpected argument", e.err.Error())
	case badValue:
		return fmt.Sprintf("%s: %s", e.spec.Flag(), e.err)
	case requiredArg:
		return fmt.Sprintf("%s is required", e.spec.Flag())
	default:
		return fmt.Sprintf("unknown error: %#v", e)
	}
}

func (c *CmdSpec) ParseArgs(args *Args) error {
	if c.passthrough {
		return nil
	}
	if _, errors := c.parseArgs(args); len(errors) > 0 {
		return errors[0]
	}
	return nil
}

func (c *CmdSpec) markSeen(spec *optSpec, index int) *seenArg {
	spec.seen = true
	arg := &seenArg{spec: spec, indexes: []int{index}}
	c.seen = append(c.seen, arg)
	return arg
}

func (c *CmdSpec) parseArgs(args *Args) (*CmdSpec, []*ArgError) {
	var cur *seenArg
	var argErrors []*ArgError

	fail := func(kind errKind, s *optSpec, detail error) {
		argErrors = append(argErrors, &ArgError{kind, s, detail})
	}
	positionals := c.positionals
	ignoreFlags := false
	c.seen = nil
	args.Shift(1) // skip command name
	i := 1

	for args.Count() > 0 {
		arg := args.Arg(0)

		switch {
		case !ignoreFlags && arg == "--":
			ignoreFlags = true

		case !ignoreFlags && c.hasLongFlag(arg):
			if cur != nil {
				fail(missingValue, cur.spec, nil)
			}
			f, val, hasValue := strings.Cut(arg, "=")
			spec := c.getLongFlag(f)
			if spec == nil {
				fail(unknownFlag, nil, errors.New(f))
				goto next
			}
			cur = c.markSeen(spec, i)
			if cur.spec.kind == flag {
				if hasValue {
					fail(takesNoValue, cur.spec, nil)
					cur = nil
					goto next
				}
				if err := cur.spec.parseValue("true"); err != nil {
					fail(badValue, cur.spec, err)
				}
				cur = nil
			} else if hasValue {
				if err := cur.spec.parseValue(val); err != nil {
					fail(badValue, cur.spec, err)
				}
				cur = nil
			}

		case !ignoreFlags && c.hasShortFlag(arg):
			if cur != nil {
				fail(missingValue, cur.spec, nil)
			}
			arg = arg[1:]
			for len(arg) > 0 {
				f := arg[:1]
				arg = arg[1:]
				var spec *optSpec

				if o, ok := c.shortOpts["-"+f]; ok {
					spec = &c.opts[o]
				}
				if spec == nil || !spec.appliesToAlias(c.name) {
					fail(unknownFlag, spec, errors.New("-"+f))
					goto next
				}
				cur = c.markSeen(spec, i)
				if spec.kind == flag {
					if err := spec.parseValue("true"); err != nil {
						fail(badValue, spec, err)
					}
					cur = nil
				} else if len(arg) > 0 {
					if err := spec.parseValue(arg); err != nil {
						fail(badValue, spec, err)
					}
					cur = nil
					arg = ""
				}
			}

		default:
			// positional
			if cur != nil {
				// separate value for a short/long flag
				cur.indexes = append(cur.indexes, i)
				if err := cur.spec.parseValue(arg); err != nil {
					fail(badValue, cur.spec, err)
				}
				cur = nil
				goto next
			}

			for len(positionals) > 0 {
				spec := &c.opts[positionals[0]]
				positionals = positionals[1:]
				if spec.appliesToAlias(c.name) {
					cur = c.markSeen(spec, i)
					break
				}
			}

			if cur == nil {
				fail(unexpectedArg, nil, errors.New(arg))
				goto next
			}

			switch cur.spec.kind {
			case remainder:
				if err := cur.spec.parseValue(args.String()); err != nil {
					fail(badValue, cur.spec, err)
				}
				for args.Count() > 0 {
					i += 1
					cur.indexes = append(cur.indexes, i)
					args.Shift(1)
				}

			case remainderSplit:
				if cur.spec.action.IsValid() {
					for _, a := range args.Args() {
						if err := cur.spec.parseValue(a); err != nil {
							fail(badValue, cur.spec, err)
						}
					}
				} else {
					cur.spec.dest.Set(reflect.ValueOf(args.Args()))
					cur.spec.seenValue = true
					cur.spec.arg = args.String()
				}
				for args.Count() > 0 {
					i += 1
					cur.indexes = append(cur.indexes, i)
					args.Shift(1)
				}

			case subcommand:
				if err := cur.spec.parseValue(arg); err != nil {
					fail(badValue, cur.spec, err)
				} else {
					cc := NewCmdSpec(arg, cur.spec.dest.Elem().Interface())
					positionals = cc.positionals
					cc.seen = c.seen
					c = cc
				}

			default:
				if err := cur.spec.parseValue(arg); err != nil {
					fail(badValue, cur.spec, err)
				}
			}

			cur = nil
		}
	next:
		args.Shift(1)
		i += 1
	}

	if cur != nil {
		fail(missingValue, cur.spec, nil)
	}

	for i := 0; i < len(c.opts); i++ {
		spec := &c.opts[i]
		if !spec.appliesToAlias(c.name) {
			continue
		}
		if !spec.seen && spec.defval != "" {
			if err := spec.parseValue(spec.defval); err != nil {
				fail(missingValue, spec, nil)
			}
		} else if spec.required && !spec.seen {
			fail(requiredArg, spec, nil)
		}
	}

	return c, argErrors
}

func (c *CmdSpec) hasShortFlag(arg string) bool {
	if len(arg) <= 1 || !strings.HasPrefix(arg, "-") {
		return false
	}
	if o, ok := c.shortOpts[arg[:2]]; ok {
		spec := &c.opts[o]
		if spec.appliesToAlias(c.name) {
			return true
		}
	}
	return !c.minusPositionals
}

func (c *CmdSpec) hasLongFlag(arg string) bool {
	if len(arg) <= 2 || !strings.HasPrefix(arg, "--") {
		return false
	}
	if c.getLongFlag(arg) != nil {
		return true
	}
	return !c.minusPositionals
}

func (c *CmdSpec) getLongFlag(arg string) *optSpec {
	arg, _, _ = strings.Cut(arg, "=")
	if o, ok := c.longOpts[arg]; ok {
		spec := &c.opts[o]
		if spec.appliesToAlias(c.name) {
			return spec
		}
	}
	return nil
}

func (s *optSpec) parseValue(arg string) error {
	s.seenValue = true
	s.arg = arg
	if s.action.IsValid() {
		in := []reflect.Value{reflect.ValueOf(arg)}
		out := s.action.Call(in)
		err, _ := out[0].Interface().(error)
		return err
	}

	switch s.dest.Type().Kind() {
	case reflect.String:
		s.dest.SetString(arg)
	case reflect.Bool:
		if b, err := strconv.ParseBool(arg); err == nil {
			s.dest.SetBool(b)
		} else {
			return err
		}
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		if i, err := strconv.ParseInt(arg, 10, 64); err == nil {
			s.dest.SetInt(i)
		} else {
			return err
		}
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
		if u, err := strconv.ParseUint(arg, 10, 64); err == nil {
			s.dest.SetUint(u)
		} else {
			return err
		}
	case reflect.Float32, reflect.Float64:
		if f, err := strconv.ParseFloat(arg, 64); err == nil {
			s.dest.SetFloat(f)
		} else {
			return err
		}
	default:
		return fmt.Errorf("unsupported type: %s", s.dest)
	}

	return nil
}

func (s *optSpec) appliesToAlias(alias string) bool {
	if s.aliases == nil {
		return true
	}
	for _, a := range s.aliases {
		if a == alias {
			return true
		}
	}
	return false
}
