package container

import (
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"maps"
	"os"
	"slices"
	"strings"

	internalclient "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
	commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
	"github.com/chzyer/readline"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

type policyPlaygroundREPL struct {
	cmd     *cobra.Command
	nodes   map[string]netmap.NodeInfo
	console *readline.Instance
}

func newPolicyPlaygroundREPL(cmd *cobra.Command) *policyPlaygroundREPL {
	return &policyPlaygroundREPL{
		cmd:   cmd,
		nodes: map[string]netmap.NodeInfo{},
	}
}

func (repl *policyPlaygroundREPL) handleLs(args []string) error {
	if len(args) > 0 {
		return fmt.Errorf("too many arguments for command 'ls': got %d, want 0", len(args))
	}
	i := 1
	for id, node := range repl.nodes {
		var attrs []string
		node.IterateAttributes(func(k, v string) {
			attrs = append(attrs, fmt.Sprintf("%s:%q", k, v))
		})
		fmt.Fprintf(repl.console, "\t%2d: id=%s attrs={%v}\n", i, id, strings.Join(attrs, " "))
		i++
	}
	return nil
}

func (repl *policyPlaygroundREPL) handleAdd(args []string) error {
	if len(args) == 0 {
		return fmt.Errorf("too few arguments for command 'add': got %d, want >0", len(args))
	}
	id := args[0]
	key, err := hex.DecodeString(id)
	if err != nil {
		return fmt.Errorf("node id must be a hex string: got %q: %v", id, err)
	}
	node := repl.nodes[id]
	node.SetPublicKey(key)
	for _, attr := range args[1:] {
		kv := strings.Split(attr, ":")
		if len(kv) != 2 {
			return fmt.Errorf("node attributes must be in the format 'KEY:VALUE': got %q", attr)
		}
		node.SetAttribute(kv[0], kv[1])
	}
	repl.nodes[id] = node
	return nil
}

func (repl *policyPlaygroundREPL) handleLoad(args []string) error {
	if len(args) != 1 {
		return fmt.Errorf("too few arguments for command 'add': got %d, want 1", len(args))
	}

	jsonNetmap := map[string]map[string]string{}

	b, err := os.ReadFile(args[0])
	if err != nil {
		return fmt.Errorf("reading netmap file %q: %v", args[0], err)
	}

	if err := json.Unmarshal(b, &jsonNetmap); err != nil {
		return fmt.Errorf("decoding json netmap: %v", err)
	}

	repl.nodes = make(map[string]netmap.NodeInfo)
	for id, attrs := range jsonNetmap {
		key, err := hex.DecodeString(id)
		if err != nil {
			return fmt.Errorf("node id must be a hex string: got %q: %v", id, err)
		}

		node := repl.nodes[id]
		node.SetPublicKey(key)
		for k, v := range attrs {
			node.SetAttribute(k, v)
		}
		repl.nodes[id] = node
	}

	return nil
}

func (repl *policyPlaygroundREPL) handleRemove(args []string) error {
	if len(args) == 0 {
		return fmt.Errorf("too few arguments for command 'remove': got %d, want >0", len(args))
	}
	id := args[0]
	if _, exists := repl.nodes[id]; exists {
		delete(repl.nodes, id)
		return nil
	}
	return fmt.Errorf("node not found: id=%q", id)
}

func (repl *policyPlaygroundREPL) handleEval(args []string) error {
	policyStr := strings.TrimSpace(strings.Join(args, " "))
	var nodes [][]netmap.NodeInfo
	nm := repl.netMap()

	if strings.HasPrefix(policyStr, "CBF") || strings.HasPrefix(policyStr, "SELECT") || strings.HasPrefix(policyStr, "FILTER") {
		// Assume that the input is a partial SELECT-FILTER expression.
		// Full inline policies always start with UNIQUE or REP keywords,
		// or different prefixes when it's the case of an external file.
		sfExpr, err := netmap.DecodeSelectFilterString(policyStr)
		if err != nil {
			return fmt.Errorf("parsing select-filter expression: %v", err)
		}
		nodes, err = nm.SelectFilterNodes(sfExpr)
		if err != nil {
			return fmt.Errorf("building select-filter nodes: %v", err)
		}
	} else {
		// Assume that the input is a full policy or input file otherwise.
		placementPolicy, err := parseContainerPolicy(repl.cmd, policyStr)
		if err != nil {
			return fmt.Errorf("parsing placement policy: %v", err)
		}
		nodes, err = nm.ContainerNodes(*placementPolicy, nil)
		if err != nil {
			return fmt.Errorf("building container nodes: %v", err)
		}
	}
	for i, ns := range nodes {
		var ids []string
		for _, node := range ns {
			ids = append(ids, hex.EncodeToString(node.PublicKey()))
		}
		fmt.Fprintf(repl.console, "\t%2d: %v\n", i+1, ids)
	}

	return nil
}

func (repl *policyPlaygroundREPL) handleHelp(args []string) error {
	if len(args) != 0 {
		if _, ok := commands[args[0]]; !ok {
			return fmt.Errorf("unknown command: %q", args[0])
		}
		fmt.Fprintln(repl.console, commands[args[0]].usage)
		return nil
	}

	commandList := slices.Collect(maps.Keys(commands))
	slices.Sort(commandList)
	for _, command := range commandList {
		fmt.Fprintf(repl.console, "%s: %s\n", command, commands[command].descriprion)
	}
	return nil
}

func (repl *policyPlaygroundREPL) netMap() netmap.NetMap {
	var nm netmap.NetMap
	var nodes []netmap.NodeInfo
	for _, node := range repl.nodes {
		nodes = append(nodes, node)
	}
	nm.SetNodes(nodes)
	return nm
}

type commandDescription struct {
	descriprion string
	usage       string
}

var commands = map[string]commandDescription{
	"list": {
		descriprion: "Display all nodes in the netmap",
		usage: `Display all nodes in the netmap
Example of usage:
  list
    1: id=03ff65b6ae79134a4dce9d0d39d3851e9bab4ee97abf86e81e1c5bbc50cd2826ae attrs={Continent:"Europe" Country:"Poland"}
    2: id=02ac920cd7df0b61b289072e6b946e2da4e1a31b9ab1c621bb475e30fa4ab102c3 attrs={Continent:"Antarctica" Country:"Heard Island"}
`,
	},

	"ls": {
		descriprion: "Display all nodes in the netmap",
		usage: `Display all nodes in the netmap
Example of usage:
  ls
    1: id=03ff65b6ae79134a4dce9d0d39d3851e9bab4ee97abf86e81e1c5bbc50cd2826ae attrs={Continent:"Europe" Country:"Poland"}
    2: id=02ac920cd7df0b61b289072e6b946e2da4e1a31b9ab1c621bb475e30fa4ab102c3 attrs={Continent:"Antarctica" Country:"Heard Island"}
`,
	},

	"add": {
		descriprion: "Add a new node: add <node-hash> attr=value",
		usage: `Add a new node
Example of usage:
  add 03ff65b6ae79134a4dce9d0d39d3851e9bab4ee97abf86e81e1c5bbc50cd2826ae continent:Europe country:Poland`,
	},

	"load": {
		descriprion: "Load netmap from file: load <path>",
		usage: `Load netmap from file
Example of usage:
  load "netmap.json"
File format (netmap.json):
{
  "03ff65b6ae79134a4dce9d0d39d3851e9bab4ee97abf86e81e1c5bbc50cd2826ae": {
    "continent": "Europe",
    "country": "Poland"
  },
  "02ac920cd7df0b61b289072e6b946e2da4e1a31b9ab1c621bb475e30fa4ab102c3": {
    "continent": "Antarctica",
    "country": "Heard Island"
  }
}`,
	},

	"remove": {
		descriprion: "Remove a node: remove <node-hash>",
		usage: `Remove a node
Example of usage:
  remove 03ff65b6ae79134a4dce9d0d39d3851e9bab4ee97abf86e81e1c5bbc50cd2826ae`,
	},

	"rm": {
		descriprion: "Remove a node: rm <node-hash>",
		usage: `Remove a node
Example of usage:
  rm 03ff65b6ae79134a4dce9d0d39d3851e9bab4ee97abf86e81e1c5bbc50cd2826ae`,
	},

	"eval": {
		descriprion: "Evaluate a policy: eval <policy>",
		usage: `Evaluate a policy
Example of usage:
  eval REP 2`,
	},

	"help": {
		descriprion: "Show available commands",
	},
}

func (repl *policyPlaygroundREPL) run() error {
	if len(viper.GetString(commonflags.RPC)) > 0 {
		key := key.GetOrGenerate(repl.cmd)
		cli := internalclient.GetSDKClientByFlag(repl.cmd, key, commonflags.RPC)

		var prm internalclient.NetMapSnapshotPrm
		prm.SetClient(cli)

		resp, err := internalclient.NetMapSnapshot(repl.cmd.Context(), prm)
		commonCmd.ExitOnErr(repl.cmd, "unable to get netmap snapshot to populate initial netmap: %w", err)

		for _, node := range resp.NetMap().Nodes() {
			id := hex.EncodeToString(node.PublicKey())
			repl.nodes[id] = node
		}
	}

	cmdHandlers := map[string]func([]string) error{
		"list":   repl.handleLs,
		"ls":     repl.handleLs,
		"add":    repl.handleAdd,
		"load":   repl.handleLoad,
		"remove": repl.handleRemove,
		"rm":     repl.handleRemove,
		"eval":   repl.handleEval,
		"help":   repl.handleHelp,
	}

	var cfgCompleter []readline.PrefixCompleterInterface
	var helpSubItems []readline.PrefixCompleterInterface

	for name := range commands {
		if name != "help" {
			cfgCompleter = append(cfgCompleter, readline.PcItem(name))
			helpSubItems = append(helpSubItems, readline.PcItem(name))
		}
	}

	cfgCompleter = append(cfgCompleter, readline.PcItem("help", helpSubItems...))
	completer := readline.NewPrefixCompleter(cfgCompleter...)
	rl, err := readline.NewEx(&readline.Config{
		Prompt:          "> ",
		InterruptPrompt: "^C",
		AutoComplete:    completer,
	})
	if err != nil {
		return fmt.Errorf("error initializing readline: %w", err)
	}
	repl.console = rl
	defer rl.Close()

	var exit bool
	for {
		line, err := rl.Readline()
		if err != nil {
			if errors.Is(err, readline.ErrInterrupt) {
				if exit {
					return nil
				}
				exit = true
				continue
			}
			return fmt.Errorf("reading line: %w", err)
		}
		exit = false

		parts := strings.Fields(line)
		if len(parts) == 0 {
			continue
		}
		cmd := parts[0]
		if handler, exists := cmdHandlers[cmd]; exists {
			if err := handler(parts[1:]); err != nil {
				fmt.Fprintf(repl.console, "error: %v\n", err)
			}
		} else {
			fmt.Fprintf(repl.console, "error: unknown command %q\n", cmd)
		}
	}
}

var policyPlaygroundCmd = &cobra.Command{
	Use:   "policy-playground",
	Short: "A REPL for testing placement policies",
	Long: `A REPL for testing placement policies.
If a wallet and endpoint is provided, the initial netmap data will be loaded from the snapshot of the node. Otherwise, an empty playground is created.`,
	Run: func(cmd *cobra.Command, _ []string) {
		repl := newPolicyPlaygroundREPL(cmd)
		commonCmd.ExitOnErr(cmd, "policy playground failed: %w", repl.run())
	},
}

func initContainerPolicyPlaygroundCmd() {
	commonflags.Init(policyPlaygroundCmd)
}