package wand

import (
	"code.squareroundforest.org/arpio/bind"
	"code.squareroundforest.org/arpio/docreflect"
	"fmt"
	"io"
	"reflect"
	"sort"
	"strings"
	"time"
)

type (
	argumentSet struct {
		count         int
		names         []string
		types         []string
		variadic      bool
		usesStdin     bool
		usesStdout    bool
		minPositional int
		maxPositional int
	}

	synopsis struct {
		command        string
		hasOptions     bool
		arguments      argumentSet
		hasSubcommands bool
	}

	docOption struct {
		name            string
		typ             string
		description     string
		shortNames      []string
		isBool          bool
		acceptsMultiple bool
	}

	docConfig struct {
		fromOption bool
		optional   bool
		fn         string
	}

	doc struct {
		name                string
		appName             string
		fullCommand         string
		synopsis            synopsis
		description         string
		hasImplementation   bool
		isHelp              bool
		isVersion           bool
		isDefault           bool
		hasHelpSubcommand   bool
		hasHelpOption       bool
		hasConfigFromOption bool
		options             []docOption
		hasBoolOptions      bool
		hasListOptions      bool
		arguments           argumentSet
		subcommands         []doc
		configFiles         []docConfig
		date                time.Time
	}
)

func help(cmd Cmd) Cmd {
	return Cmd{
		name:       "help",
		helpFor:    &cmd,
		shortForms: cmd.shortForms,
	}
}

func insertHelp(cmd Cmd) Cmd {
	var hasHelpCmd bool
	for i, sc := range cmd.subcommands {
		if sc.name == "help" {
			hasHelpCmd = true
			continue
		}

		cmd.subcommands[i] = insertHelp(sc)
	}

	if !hasHelpCmd && cmd.version == "" {
		cmd.subcommands = append(cmd.subcommands, help(cmd))
	}

	return cmd
}

func hasHelpSubcommand(cmd Cmd) bool {
	for _, sc := range cmd.subcommands {
		if sc.helpFor != nil {
			return true
		}
	}

	return false
}

func hasCustomHelpOption(cmd Cmd) bool {
	if cmd.impl == nil {
		return false
	}

	mf := mapFields(cmd.impl)
	_, has := mf["help"]
	return has
}

func suggestHelp(out io.Writer, cmd Cmd, fullCommand []string) {
	if hasHelpSubcommand(cmd) {
		fmt.Fprintf(out, "Show help:\n%s help\n", strings.Join(fullCommand, " "))
		return
	}

	if !hasCustomHelpOption(cmd) {
		fmt.Fprintf(out, "Show help:\n%s --help\n", strings.Join(fullCommand, " "))
		return
	}
}

func hasOptions(cmd Cmd) bool {
	if cmd.impl == nil {
		return false
	}

	return len(fields(cmd.impl)) > 0
}

func allCommands(cmd doc) []doc {
	commands := []doc{cmd}
	for _, sc := range cmd.subcommands {
		commands = append(commands, allCommands(sc)...)
	}

	sort.Slice(commands, func(i, j int) bool {
		return commands[i].fullCommand < commands[j].fullCommand
	})

	return commands
}

func functionParams(v any, indices []int) ([]string, []string) {
	var names []string
	r := reflect.ValueOf(v)
	allNames := docreflect.FunctionParams(r)
	for _, i := range indices {
		names = append(names, allNames[i])
	}

	var types []reflect.Kind
	for _, i := range indices {
		types = append(types, r.Type().In(i).Kind())
	}

	var stypes []string
	for _, t := range types {
		stypes = append(stypes, strings.ToLower(fmt.Sprint(t)))
	}

	return names, stypes
}

func constructArguments(cmd Cmd) argumentSet {
	if cmd.impl == nil {
		return argumentSet{}
	}

	p, variadic := positional(cmd.impl)
	pi := positionalIndices(cmd.impl)
	ior, iow := ioParameters(cmd.impl)
	names, types := functionParams(cmd.impl, pi)
	if len(names) < len(p) {
		names = nil
		for i := 0; i < len(p); i++ {
			names = append(names, fmt.Sprintf("arg%d", i))
		}
	}

	return argumentSet{
		count:         len(p),
		names:         names,
		types:         types,
		variadic:      variadic,
		usesStdin:     len(ior) > 0,
		usesStdout:    len(iow) > 0,
		minPositional: cmd.minPositional,
		maxPositional: cmd.maxPositional,
	}
}

func constructSynopsis(cmd Cmd, fullCommand []string) synopsis {
	return synopsis{
		command:        strings.Join(fullCommand, " "),
		hasOptions:     hasOptions(cmd),
		arguments:      constructArguments(cmd),
		hasSubcommands: len(cmd.subcommands) > 0,
	}
}

func constructDescription(cmd Cmd) string {
	if cmd.version != "" {
		return versionDocs
	}

	if cmd.impl == nil {
		return ""
	}

	return strings.TrimSpace(docreflect.Function(reflect.ValueOf(cmd.impl)))
}

func constructOptions(cmd Cmd, hasConfigFromOption bool) []docOption {
	if cmd.impl == nil {
		return nil
	}

	sf := make(map[string][]string)
	for i := 0; i < len(cmd.shortForms); i += 2 {
		l, s := cmd.shortForms[i], cmd.shortForms[i+1]
		sf[l] = append(sf[l], s)
	}

	s := structParameters(cmd.impl)
	d := make(map[string]string)
	for _, si := range s {
		f := structFields(si)
		for _, fi := range f {
			d[fi.Name()] = docreflect.Field(si, fi.Path()...)
		}
	}

	var o []docOption
	f := mapFields(cmd.impl)
	for name, fi := range f {
		opt := docOption{
			name:        name,
			typ:         scalarTypeString(fi[0].Type()),
			description: d[name],
			shortNames:  sf[name],
			isBool:      fi[0].Type() == bind.Bool,
		}

		for _, fii := range fi {
			if fii.List() {
				opt.acceptsMultiple = true
			}
		}

		o = append(o, opt)
	}

	if hasConfigFromOption {
		o = append(o, docOption{
			name:            "config",
			description:     configOptionDocs,
			shortNames:      sf["config"],
			acceptsMultiple: true,
		})
	}

	return o
}

func constructConfigDocs(cmd Cmd, conf Config) []docConfig {
	var docs []docConfig
	if conf.file != nil {
		docs = append(docs, docConfig{fn: conf.file(cmd).filename, optional: conf.optional})
		return docs
	}

	if conf.fromOption {
		docs = append(docs, docConfig{fromOption: true})
		return docs
	}

	for _, m := range conf.merge {
		docs = append(docs, constructConfigDocs(cmd, m)...)
	}

	return docs
}

func constructDoc(cmd Cmd, conf Config, fullCommand []string) doc {
	var subcommands []doc
	for _, sc := range cmd.subcommands {
		subcommands = append(subcommands, constructDoc(sc, conf, append(fullCommand, sc.name)))
	}

	var hasBoolOptions, hasListOptions bool
	options := constructOptions(cmd, hasConfigFromOption(conf))
	for _, o := range options {
		if o.isBool {
			hasBoolOptions = true
		}

		if o.acceptsMultiple {
			hasListOptions = true
		}
	}

	return doc{
		name:              cmd.name,
		appName:           fullCommand[0],
		fullCommand:       strings.Join(fullCommand, " "),
		synopsis:          constructSynopsis(cmd, fullCommand),
		description:       constructDescription(cmd),
		hasImplementation: cmd.impl != nil,
		isDefault:         cmd.isDefault,
		isHelp:            cmd.helpFor != nil,
		isVersion:         cmd.version != "",
		hasHelpSubcommand: hasHelpSubcommand(cmd),
		hasHelpOption:     !hasCustomHelpOption(cmd),
		options:           options,
		hasBoolOptions:    hasBoolOptions,
		hasListOptions:    hasListOptions,
		arguments:         constructArguments(cmd),
		subcommands:       subcommands,
		configFiles:       constructConfigDocs(cmd, conf),
		date:              time.Now(),
	}
}

func showHelp(out io.Writer, cmd Cmd, conf Config, fullCommand []string) error {
	if cmd.helpFor != nil {
		cmd = *cmd.helpFor
	}

	doc := constructDoc(cmd, conf, fullCommand)
	return formatHelp(out, doc)
}

func generateMan(out io.Writer, cmd Cmd, conf Config) error {
	doc := constructDoc(cmd, conf, []string{cmd.name})
	return formatMan(out, doc)
}

func generateMarkdown(out io.Writer, cmd Cmd, conf Config, level int) error {
	doc := constructDoc(cmd, conf, []string{cmd.name})
	return formatMarkdown(out, doc, level)
}

func showVersion(out io.Writer, cmd Cmd) error {
	_, err := fmt.Fprintln(out, cmd.version)
	return err
}
