package commands

import (
	"context"
	"errors"
	"fmt"
	"io"

	cmds "github.com/ipfs/go-ipfs-cmds"
	"github.com/ipfs/kubo/core/commands/cmdenv"
	peer "github.com/libp2p/go-libp2p/core/peer"
	routing "github.com/libp2p/go-libp2p/core/routing"
)

var ErrNotDHT = errors.New("routing service is not a DHT")

var DhtCmd = &cmds.Command{
	Helptext: cmds.HelpText{
		Tagline:          "Issue commands directly through the DHT.",
		ShortDescription: ``,
	},

	Subcommands: map[string]*cmds.Command{
		"query":     queryDhtCmd,
		"findprovs": findProvidersDhtCmd,
		"findpeer":  findPeerDhtCmd,
		"get":       getValueDhtCmd,
		"put":       putValueDhtCmd,
		"provide":   provideRefDhtCmd,
	},
}

var findProvidersDhtCmd = &cmds.Command{
	Helptext:  findProvidersRoutingCmd.Helptext,
	Arguments: findProvidersRoutingCmd.Arguments,
	Options:   findProvidersRoutingCmd.Options,
	Run:       findProvidersRoutingCmd.Run,
	Encoders:  findProvidersRoutingCmd.Encoders,
	Type:      findProvidersRoutingCmd.Type,
	Status:    cmds.Deprecated,
}

var findPeerDhtCmd = &cmds.Command{
	Helptext:  findPeerRoutingCmd.Helptext,
	Arguments: findPeerRoutingCmd.Arguments,
	Options:   findPeerRoutingCmd.Options,
	Run:       findPeerRoutingCmd.Run,
	Encoders:  findPeerRoutingCmd.Encoders,
	Type:      findPeerRoutingCmd.Type,
	Status:    cmds.Deprecated,
}

var getValueDhtCmd = &cmds.Command{
	Helptext:  getValueRoutingCmd.Helptext,
	Arguments: getValueRoutingCmd.Arguments,
	Options:   getValueRoutingCmd.Options,
	Run:       getValueRoutingCmd.Run,
	Encoders:  getValueRoutingCmd.Encoders,
	Type:      getValueRoutingCmd.Type,
	Status:    cmds.Deprecated,
}

var putValueDhtCmd = &cmds.Command{
	Helptext:  putValueRoutingCmd.Helptext,
	Arguments: putValueRoutingCmd.Arguments,
	Options:   putValueRoutingCmd.Options,
	Run:       putValueRoutingCmd.Run,
	Encoders:  putValueRoutingCmd.Encoders,
	Type:      putValueRoutingCmd.Type,
	Status:    cmds.Deprecated,
}

var provideRefDhtCmd = &cmds.Command{
	Helptext:  provideRefRoutingCmd.Helptext,
	Arguments: provideRefRoutingCmd.Arguments,
	Options:   provideRefRoutingCmd.Options,
	Run:       provideRefRoutingCmd.Run,
	Encoders:  provideRefRoutingCmd.Encoders,
	Type:      provideRefRoutingCmd.Type,
	Status:    cmds.Deprecated,
}

// kademlia extends the routing interface with a command to get the peers closest to the target
type kademlia interface {
	routing.Routing
	GetClosestPeers(ctx context.Context, key string) ([]peer.ID, error)
}

var queryDhtCmd = &cmds.Command{
	Helptext: cmds.HelpText{
		Tagline:          "Find the closest Peer IDs to a given Peer ID by querying the DHT.",
		ShortDescription: "Outputs a list of newline-delimited Peer IDs.",
	},

	Arguments: []cmds.Argument{
		cmds.StringArg("peerID", true, true, "The peerID to run the query against."),
	},
	Options: []cmds.Option{
		cmds.BoolOption(dhtVerboseOptionName, "v", "Print extra information."),
	},
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
		nd, err := cmdenv.GetNode(env)
		if err != nil {
			return err
		}

		if nd.DHTClient == nil {
			return ErrNotDHT
		}

		id, err := peer.Decode(req.Arguments[0])
		if err != nil {
			return cmds.ClientError("invalid peer ID")
		}

		ctx, cancel := context.WithCancel(req.Context)
		defer cancel()
		ctx, events := routing.RegisterForQueryEvents(ctx)

		client := nd.DHTClient
		if client == nd.DHT {
			client = nd.DHT.WAN
			if !nd.DHT.WANActive() {
				client = nd.DHT.LAN
			}
		}

		if d, ok := client.(kademlia); !ok {
			return fmt.Errorf("dht client does not support GetClosestPeers")
		} else {
			errCh := make(chan error, 1)
			go func() {
				defer close(errCh)
				defer cancel()
				closestPeers, err := d.GetClosestPeers(ctx, string(id))
				for _, p := range closestPeers {
					routing.PublishQueryEvent(ctx, &routing.QueryEvent{
						ID:   p,
						Type: routing.FinalPeer,
					})
				}

				if err != nil {
					errCh <- err
					return
				}
			}()

			for e := range events {
				if err := res.Emit(e); err != nil {
					return err
				}
			}

			return <-errCh
		}
	},
	Encoders: cmds.EncoderMap{
		cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *routing.QueryEvent) error {
			pfm := pfuncMap{
				routing.FinalPeer: func(obj *routing.QueryEvent, out io.Writer, verbose bool) error {
					fmt.Fprintf(out, "%s\n", obj.ID)
					return nil
				},
			}
			verbose, _ := req.Options[dhtVerboseOptionName].(bool)
			return printEvent(out, w, verbose, pfm)
		}),
	},
	Type: routing.QueryEvent{},
}
