package cmds

import (
	"context"
	"fmt"
	"maps"
	"reflect"

	"github.com/ipfs/boxo/files"
)

// Request represents a call to a command from a consumer
type Request struct {
	Context       context.Context
	Root, Command *Command

	Path      []string
	Arguments []string
	Options   OptMap

	Files files.Directory

	bodyArgs *arguments
}

// NewRequest returns a request initialized with given arguments
// An non-nil error will be returned if the provided option values are invalid
func NewRequest(ctx context.Context,
	path []string,
	opts OptMap,
	args []string,
	file files.Directory,
	root *Command,
) (*Request, error) {

	cmd, err := root.Get(path)
	if err != nil {
		return nil, err
	}

	options, err := checkAndConvertOptions(root, opts, path)

	req := &Request{
		Path:      path,
		Options:   options,
		Arguments: args,
		Files:     file,
		Root:      root,
		Command:   cmd,
		Context:   ctx,
	}

	return req, err
}

// BodyArgs returns a scanner that returns arguments passed in the body as tokens.
//
// Returns nil if there are no arguments to be consumed via stdin.
func (req *Request) BodyArgs() StdinArguments {
	// dance to make sure we return an *untyped* nil.
	// DO NOT just return `req.bodyArgs`.
	// If you'd like to complain, go to
	// https://github.com/golang/go/issues/.
	if req.bodyArgs != nil {
		return req.bodyArgs
	}
	return nil
}

// ParseBodyArgs parses arguments in the request body.
func (req *Request) ParseBodyArgs() error {
	s := req.BodyArgs()
	if s == nil {
		return nil
	}

	for s.Scan() {
		req.Arguments = append(req.Arguments, s.Argument())
	}
	return s.Err()
}

// SetOption sets a request option.
func (req *Request) SetOption(name string, value any) {
	optDefs, err := req.Root.GetOptions(req.Path)
	optDef, found := optDefs[name]

	if req.Options == nil {
		req.Options = map[string]any{}
	}

	// unknown option, simply set the value and return
	// TODO we might error out here instead
	if err != nil || !found {
		req.Options[name] = value
		return
	}

	name = optDef.Name()
	req.Options[name] = value
}

func checkAndConvertOptions(root *Command, opts OptMap, path []string) (OptMap, error) {
	optDefs, err := root.GetOptions(path)
	options := make(OptMap)

	if err != nil {
		return options, err
	}
	maps.Copy(options, opts)

	for k, v := range opts {
		opt, ok := optDefs[k]
		if !ok {
			continue
		}

		kind := reflect.TypeOf(v).Kind()
		if kind != opt.Type() {
			if opt.Type() == Strings {
				if _, ok := v.([]string); !ok {
					return options, fmt.Errorf("option %q should be type %q, but got type %q",
						k, opt.Type().String(), kind.String())
				}
			} else {
				str, ok := v.(string)
				if !ok {
					return options, fmt.Errorf("option %q should be type %q, but got type %q",
						k, opt.Type().String(), kind.String())
				}

				val, err := opt.Parse(str)
				if err != nil {
					value := fmt.Sprintf("value %q", v)
					if len(str) == 0 {
						value = "empty value"
					}
					return options, fmt.Errorf("could not convert %s to type %q (for option %q)",
						value, opt.Type().String(), "-"+k)
				}
				options[k] = val
			}
		}

		for _, name := range opt.Names() {
			if _, ok := options[name]; name != k && ok {
				return options, fmt.Errorf("duplicate command options were provided (%q and %q)",
					k, name)
			}
		}
	}

	return options, nil
}

// GetEncoding returns the EncodingType set in a request, falling back to JSON
func GetEncoding(req *Request, def EncodingType) EncodingType {
	switch enc := req.Options[EncLong].(type) {
	case string:
		return EncodingType(enc)
	case EncodingType:
		return enc
	default:
		if def == "" {
			return DefaultOutputEncoding
		}
		return def
	}
}

// FillDefaults fills in default values if option has not been set.
func (req *Request) FillDefaults() error {
	optDefMap, err := req.Root.GetOptions(req.Path)
	if err != nil {
		return err
	}

	optDefs := map[Option]struct{}{}

	for _, optDef := range optDefMap {
		optDefs[optDef] = struct{}{}
	}

Outer:
	for optDef := range optDefs {
		dflt := optDef.Default()
		if dflt == nil {
			// option has no dflt, continue
			continue
		}

		names := optDef.Names()
		for _, name := range names {
			if _, ok := req.Options[name]; ok {
				// option has been set, continue with next option
				continue Outer
			}
		}

		req.Options[optDef.Name()] = dflt
	}

	return nil
}
