package repl

import (
	"context"
	"errors"
	"fmt"
	"github.com/lmika/gopkgs/fp/maps"
	"slices"
	"sort"
	"strings"
	"text/tabwriter"
	"ucl.lmika.dev/ucl"
	"unicode"
)

type ArgDoc struct {
	Name  string
	Brief string
}

type Doc struct {
	Brief    string
	Usage    string
	Args     []ArgDoc
	Detailed string
}

func (d Doc) config(cmdName string, r *REPL) {
	r.commandDocs[cmdName] = d
}

func (r *REPL) helpBuiltin(ctx context.Context, args ucl.CallArgs) (any, error) {
	if args.NArgs() == 0 {
		return NoResults{}, r.listHelpTopics(maps.Keys(r.commandDocs))
	}

	var cmdName string
	if err := args.Bind(&cmdName); err != nil {
		return nil, err
	}

	found := make([]string, 0)
	for name := range r.commandDocs {
		if name == cmdName {
			return NoResults{}, r.showHelpTopic(name)
		}

		if strings.Contains(name, cmdName) {
			found = append(found, name)
		}
	}

	if len(found) == 0 {
		return nil, errors.New("no help found for topic")
	}

	return NoResults{}, r.listHelpTopics(found)
}

func (r *REPL) listHelpTopics(topics []string) error {
	sort.Strings(topics)

	tabWriter := tabwriter.NewWriter(r.inst.Out(), 0, 0, 1, ' ', 0)

	for _, name := range topics {
		cdoc := r.commandDocs[name]
		if cdoc.Brief != "" {
			fmt.Fprintf(tabWriter, "%v\t  %v\n", name, r.commandDocs[name].Brief)
		} else {
			fmt.Fprintf(tabWriter, "%v\n", name)
		}
	}

	tabWriter.Flush()
	return nil
}

func (r *REPL) showHelpTopic(topic string) error {

	docs, ok := r.commandDocs[topic]
	if !ok {
		return errors.New("no help docs found for command")
	}

	fmt.Fprintf(r.inst.Out(), "%v\n", topic)
	fmt.Fprintf(r.inst.Out(), "  %v\n", docs.Brief)

	if docs.Usage != "" {
		fmt.Fprintln(r.inst.Out(), "\nUsage:")
		fmt.Fprintf(r.inst.Out(), "  %v %v\n", topic, docs.Usage)
	}

	if len(docs.Args) > 0 {
		fmt.Fprintln(r.inst.Out(), "\nArguments:")

		docArgs := slices.Clone(docs.Args)
		sort.Slice(docArgs, func(i, j int) bool {
			return strings.TrimPrefix(docs.Args[i].Name, "-") < strings.TrimPrefix(docs.Args[j].Name, "-")
		})

		tw := tabwriter.NewWriter(r.inst.Out(), 0, 0, 1, ' ', 0)
		for _, arg := range docArgs {
			fmt.Fprintf(tw, "  %v\t  %v\n", arg.Name, arg.Brief)
		}
		tw.Flush()
	}

	if docs.Detailed != "" {
		fmt.Fprintln(r.inst.Out(), "\nDetails:")

		lines := strings.Split(docs.Detailed, "\n")

		trimLeft := 0
		if len(lines) == 0 {
			return nil
		} else if len(strings.TrimSpace(lines[0])) == 0 && len(lines) > 1 {
			// indicates that the next line should indicate the indentation
			trimLeft = len(lines[1]) - len(strings.TrimLeftFunc(lines[1], unicode.IsSpace))
			lines = lines[1:]
		}

		for _, line := range lines {
			fmt.Fprintf(r.inst.Out(), "  %v\n", trimSpaceLeftUpto(line, trimLeft))
		}
	}
	return nil
}

func trimSpaceLeftUpto(s string, n int) string {
	if n == 0 {
		return s
	}

	for i, c := range s {
		if i >= n {
			return s[i:]
		} else if !unicode.IsSpace(c) {
			return s[i:]
		}
	}
	return s
}