/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package ui

import (
	"context"
	"fmt"
	"os"
	"path"
	"reflect"
	"runtime/debug"
	"strings"
	"time"

	plc4xconfig "github.com/apache/plc4x/plc4go/pkg/api/config"
	"github.com/apache/plc4x/plc4go/spi"
	"github.com/pkg/errors"
	"github.com/rivo/tview"
	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"

	cliConfig "github.com/apache/plc4x-extras/plc4go/tools/plc4xpcapanalyzer/config"
	"github.com/apache/plc4x-extras/plc4go/tools/plc4xpcapanalyzer/internal/analyzer"
	"github.com/apache/plc4x-extras/plc4go/tools/plc4xpcapanalyzer/internal/extractor"
)

const rootCommandIndicator = "rootCommand"

var rootCommand = Command{
	Name: rootCommandIndicator,
	subCommands: []Command{
		{
			Name:        "ls",
			Description: "list directories",
			action: func(_ context.Context, _ Command, dir string) error {
				if dir == "" {
					dir = currentDir
				}
				_, _ = fmt.Fprintf(commandOutput, "dir cotents of %s\n", dir)
				readDir, err := os.ReadDir(dir)
				if err != nil {
					return err
				}
				for _, dirEntry := range readDir {
					isDir := dirEntry.IsDir()
					name := dirEntry.Name()
					name = strings.TrimPrefix(name, dir)
					if isDir {
						name = fmt.Sprintf("[#0000ff]%s[white]", name)
					} else if strings.HasSuffix(name, ".pcap") || strings.HasSuffix(name, ".pcapng") {
						name = fmt.Sprintf("[#00ff00]%s[white]", name)
					}
					_, _ = fmt.Fprintf(commandOutput, "%s\n", name)
				}
				return nil
			},
			// TODO: add parameter suggestions
		},
		{
			Name:        "cd",
			Description: "changes directory",
			action: func(_ context.Context, _ Command, newDir string) error {
				var proposedCurrentDir string
				if newDir == "" {
					var err error
					proposedCurrentDir, err = os.UserHomeDir()
					if err != nil {
						return err
					}
				} else if strings.HasPrefix(newDir, "."+string(os.PathSeparator)) {
					proposedCurrentDir = currentDir + strings.TrimPrefix(newDir, ".")
				} else if strings.HasPrefix(newDir, ""+string(os.PathSeparator)) {
					proposedCurrentDir = newDir
				} else {
					proposedCurrentDir = currentDir + string(os.PathSeparator) + newDir
				}
				stat, err := os.Stat(proposedCurrentDir)
				if err != nil {
					return err
				}
				if !stat.IsDir() {
					return errors.Errorf("%s is not a dir", newDir)
				}
				currentDir = proposedCurrentDir
				_, _ = fmt.Fprintf(commandOutput, "current directory: %s\n", currentDir)
				return nil
			},
			parameterSuggestions: func(currentText string) (entries []string) {
				if strings.HasPrefix(currentText, string(os.PathSeparator)) {
					dirEntries, err := os.ReadDir(currentText)
					if err != nil {
						plc4xpcapanalyzerLog.Warn().Err(err).Msg("Error suggesting directories")
						return
					}
					for _, dirEntry := range dirEntries {
						entry := path.Join(currentText, dirEntry.Name())
						entries = append(entries, entry)
					}
				} else {
					dirEntries, err := os.ReadDir(currentDir)
					if err != nil {
						plc4xpcapanalyzerLog.Warn().Err(err).Msg("Error suggesting directories")
						return
					}
					for _, dirEntry := range dirEntries {
						entry := path.Join(".", dirEntry.Name())
						entries = append(entries, entry)
					}
				}
				return
			},
		},
		{
			Name:        "pwd",
			Description: "shows current directory",
			action: func(_ context.Context, _ Command, _ string) error {
				_, _ = fmt.Fprintf(commandOutput, "current directory: %s\n", currentDir)
				return nil
			},
		},
		{
			Name:        "open",
			Description: "open file",
			action: func(_ context.Context, _ Command, pcapFile string) error {
				return OpenFile(pcapFile)
			},
			parameterSuggestions: func(currentText string) (entries []string) {
				entries = append(entries, config.History.Last10Files...)
				readDir, err := os.ReadDir(currentDir)
				if err != nil {
					return
				}
				for _, dirEntry := range readDir {
					name := dirEntry.Name()
					name = strings.TrimPrefix(name, currentDir)
					if strings.HasSuffix(dirEntry.Name(), ".cap") || strings.HasSuffix(dirEntry.Name(), ".pcap") || strings.HasSuffix(name, ".pcapng") {
						entries = append(entries, name)
					}
				}
				return
			},
		},
		{
			Name:        "analyze",
			Description: "Analyzes a pcap file using a driver",
			action: func(ctx context.Context, _ Command, protocolTypeAndPcapFile string) error {
				split := strings.Split(protocolTypeAndPcapFile, " ")
				if len(split) != 2 {
					return errors.Errorf("expect protocol and pcapfile")
				}
				protocolType := split[0]
				pcapFile := strings.TrimPrefix(protocolTypeAndPcapFile, protocolType+" ")
				cliConfig.PcapConfigInstance.Client = config.HostIp
				cliConfig.RootConfigInstance.HideProgressBar = true
				// disabled as we get this output anyway with the message call back
				//cliConfig.RootConfigInstance.Verbosity = 4
				return analyzer.AnalyzeWithOutputAndCallback(ctx, pcapFile, protocolType, tview.ANSIWriter(messageOutput), tview.ANSIWriter(messageOutput), func(parsed spi.Message) {
					spiNumberOfMessagesReceived++
					spiMessageReceived(spiNumberOfMessagesReceived, time.Now(), parsed)
				})
			},
			parameterSuggestions: func(currentText string) (entries []string) {
				for _, file := range loadedPcapFiles {
					for _, protocol := range protocolList {
						entries = append(entries, protocol+" "+file.path)
					}
				}
				return
			},
		},
		{
			Name:        "extract",
			Description: "Extract a pcap file using a driver",
			action: func(ctx context.Context, _ Command, protocolTypeAndPcapFile string) error {
				split := strings.Split(protocolTypeAndPcapFile, " ")
				if len(split) != 2 {
					return errors.Errorf("expect protocol and pcapfile")
				}
				protocolType := split[0]
				pcapFile := strings.TrimPrefix(protocolTypeAndPcapFile, protocolType+" ")
				cliConfig.PcapConfigInstance.Client = config.HostIp
				cliConfig.RootConfigInstance.HideProgressBar = true
				cliConfig.RootConfigInstance.Verbosity = 4
				return extractor.ExtractWithOutput(ctx, pcapFile, protocolType, tview.ANSIWriter(messageOutput), tview.ANSIWriter(messageOutput))
			},
			parameterSuggestions: func(currentText string) (entries []string) {
				for _, file := range loadedPcapFiles {
					for _, protocol := range protocolList {
						entries = append(entries, protocol+" "+file.path)
					}
				}
				return
			},
		},
		{
			Name:        "host",
			Description: "The host which is assumed to be the sender (important for protocols that are directional)",
			subCommands: []Command{
				{
					Name: "set",
					action: func(_ context.Context, _ Command, host string) error {
						config.HostIp = host
						return nil
					},
				},
				{
					Name: "get",
					action: func(_ context.Context, _ Command, host string) error {
						_, _ = fmt.Fprintf(commandOutput, "current set host %s", config.HostIp)
						return nil
					},
				},
			},
		},
		{
			Name:        "register",
			Description: "register a driver in the subsystem",
			action: func(_ context.Context, _ Command, driver string) error {
				return registerDriver(driver)
			},
			parameterSuggestions: func(currentText string) (entries []string) {
				for _, protocol := range protocolList {
					if strings.HasPrefix(protocol, currentText) {
						entries = append(entries, protocol)
					}
				}
				return
			},
		},
		{
			Name:        "quit",
			Description: "Quits the application",
		},
		{
			Name:        "log",
			Description: "Log related operations",
			subCommands: []Command{
				{
					Name:        "get",
					Description: "Get a log level",
					action: func(_ context.Context, _ Command, _ string) error {
						_, _ = fmt.Fprintf(commandOutput, "Current log level %s", log.Logger.GetLevel())
						return nil
					},
				},
				{
					Name:        "set",
					Description: "Sets a log level",
					action: func(_ context.Context, _ Command, level string) error {
						parseLevel, err := zerolog.ParseLevel(level)
						if err != nil {
							return errors.Wrapf(err, "Error setting log level")
						}
						setLevel(parseLevel)
						log.Logger = log.Logger.Level(parseLevel)
						return nil
					},
					parameterSuggestions: func(currentText string) (entries []string) {
						levels := []string{
							zerolog.LevelTraceValue,
							zerolog.LevelDebugValue,
							zerolog.LevelInfoValue,
							zerolog.LevelWarnValue,
							zerolog.LevelErrorValue,
							zerolog.LevelFatalValue,
							zerolog.LevelPanicValue,
						}
						for _, level := range levels {
							entries = append(entries, level)
						}
						return
					},
				},
			},
		},
		{
			Name:        "conf",
			Description: "Various settings for plc4xpcapanalyzer",
			subCommands: []Command{
				{
					Name:        "list",
					Description: "list config values with their current settings",
					action: func(_ context.Context, _ Command, _ string) error {
						allCliConfigsValue := reflect.ValueOf(allCliConfigsInstances)
						for i := 0; i < allCliConfigsValue.NumField(); i++ {
							allConfigField := allCliConfigsValue.Field(i)
							allConfigFieldType := allCliConfigsValue.Type().Field(i)
							_, _ = fmt.Fprintf(commandOutput, "%s:\n", allConfigFieldType.Name)
							configInstanceReflectValue := reflect.ValueOf(allConfigField.Interface())
							if configInstanceReflectValue.Kind() == reflect.Ptr {
								configInstanceReflectValue = configInstanceReflectValue.Elem()
							}
							for j := 0; j < configInstanceReflectValue.NumField(); j++ {
								configField := configInstanceReflectValue.Field(j)
								configFieldType := configInstanceReflectValue.Type().Field(j)
								if configFieldType.Tag.Get("json") == "-" {
									// Ignore those
									continue
								}
								_, _ = fmt.Fprintf(commandOutput, "  %s: %s\t= %v\n", configFieldType.Name, configFieldType.Type, configField.Interface())
							}
						}
						return nil
					},
				},
				{
					Name:        "set",
					Description: "sets a config value",
					subCommands: func() []Command {
						var configCommand []Command
						allCliConfigsValue := reflect.ValueOf(allCliConfigsInstances)
						for i := 0; i < allCliConfigsValue.NumField(); i++ {
							allConfigField := allCliConfigsValue.Field(i)
							allConfigFieldType := allCliConfigsValue.Type().Field(i)
							configCommand = append(configCommand, Command{
								Name:        allConfigFieldType.Name,
								Description: fmt.Sprintf("Setting for %s", allConfigFieldType.Name),
								subCommands: func() []Command {
									var configElementCommands []Command
									configInstanceReflectValue := reflect.ValueOf(allConfigField.Interface())
									if configInstanceReflectValue.Kind() == reflect.Ptr {
										configInstanceReflectValue = configInstanceReflectValue.Elem()
									}
									for i := 0; i < configInstanceReflectValue.NumField(); i++ {
										field := configInstanceReflectValue.Field(i)
										fieldOfType := configInstanceReflectValue.Type().Field(i)
										if fieldOfType.Tag.Get("json") == "-" {
											// Ignore those
											continue
										}
										configElementCommands = append(configElementCommands, Command{
											Name:        fieldOfType.Name,
											Description: fmt.Sprintf("Sets value for %s", fieldOfType.Name),
											action: func(_ context.Context, _ Command, argument string) error {
												field.SetString(argument)
												return nil
											},
										})
									}
									return configElementCommands
								}(),
							})
						}
						return configCommand
					}(),
				},
				{
					Name:        "plc4xpcapanalyzer-debug",
					Description: "Prints out debug information of the pcap analyzer itself",
					subCommands: []Command{
						{
							Name:        "on",
							Description: "debug on",
							action: func(_ context.Context, _ Command, _ string) error {
								plc4xpcapanalyzerLog = zerolog.New(zerolog.ConsoleWriter{Out: tview.ANSIWriter(consoleOutput)})
								return nil
							},
						},
						{
							Name:        "off",
							Description: "debug off",
							action: func(_ context.Context, _ Command, _ string) error {
								plc4xpcapanalyzerLog = zerolog.Nop()
								return nil
							},
						},
					},
				},
				{
					Name:        "auto-register",
					Description: "autoregister driver at startup",
					subCommands: []Command{
						{
							Name: "list",
							action: func(_ context.Context, currentCommand Command, argument string) error {
								_, _ = fmt.Fprintf(commandOutput, "Auto-register enabled drivers:\n  %s\n", strings.Join(config.AutoRegisterDrivers, "\n  "))
								return nil
							},
						},
						{
							Name: "enable",
							action: func(_ context.Context, _ Command, argument string) error {
								return enableAutoRegister(argument)
							},
							parameterSuggestions: func(currentText string) (entries []string) {
								for _, protocol := range protocolList {
									if strings.HasPrefix(protocol, currentText) {
										entries = append(entries, protocol)
									}
								}
								return
							},
						},
						{
							Name: "disable",
							action: func(_ context.Context, _ Command, argument string) error {
								return disableAutoRegister(argument)
							},
							parameterSuggestions: func(currentText string) (entries []string) {
								for _, protocol := range protocolList {
									if strings.HasPrefix(protocol, currentText) {
										entries = append(entries, protocol)
									}
								}
								return
							},
						},
					},
				},
			},
		},
		{
			Name:        "plc4x-conf",
			Description: "plc4x related settings",
			subCommands: []Command{
				{
					Name:        "TraceTransactionManagerWorkers",
					Description: "print information about transaction manager workers",
					subCommands: []Command{
						{
							Name:        "on",
							Description: "trace on",
							action: func(_ context.Context, _ Command, _ string) error {
								plc4xconfig.TraceTransactionManagerWorkers = true
								return nil
							},
						},
						{
							Name:        "off",
							Description: "trace off",
							action: func(_ context.Context, _ Command, _ string) error {
								plc4xconfig.TraceTransactionManagerWorkers = false
								return nil
							},
						},
					},
				},
				{
					Name:        "TraceTransactionManagerTransactions",
					Description: "print information about transaction manager transactions",
					subCommands: []Command{
						{
							Name:        "on",
							Description: "trace on",
							action: func(_ context.Context, _ Command, _ string) error {
								plc4xconfig.TraceTransactionManagerTransactions = true
								return nil
							},
						},
						{
							Name:        "off",
							Description: "trace off",
							action: func(_ context.Context, _ Command, _ string) error {
								plc4xconfig.TraceTransactionManagerTransactions = false
								return nil
							},
						},
					},
				},
				{
					Name:        "TraceDefaultMessageCodecWorker",
					Description: "print information about message codec workers",
					subCommands: []Command{
						{
							Name:        "on",
							Description: "trace on",
							action: func(_ context.Context, _ Command, _ string) error {
								plc4xconfig.TraceDefaultMessageCodecWorker = true
								return nil
							},
						},
						{
							Name:        "off",
							Description: "trace off",
							action: func(_ context.Context, _ Command, _ string) error {
								plc4xconfig.TraceDefaultMessageCodecWorker = false
								return nil
							},
						},
					},
				},
			},
		},
		{
			Name:        "history",
			Description: "outputs the last commands",
			action: func(_ context.Context, _ Command, _ string) error {
				outputCommandHistory()
				return nil
			},
		},
		{
			Name:        "clear",
			Description: "clear all outputs",
			action: func(_ context.Context, _ Command, _ string) error {
				messageOutputClear()
				consoleOutputClear()
				commandOutputClear()
				return nil
			},
			subCommands: []Command{
				{
					Name:        "message",
					Description: "clears message output",
					action: func(_ context.Context, _ Command, _ string) error {
						messageOutputClear()
						return nil
					},
				},
				{
					Name:        "console",
					Description: "clears console output",
					action: func(_ context.Context, _ Command, _ string) error {
						consoleOutputClear()
						return nil
					},
				},
				{
					Name:        "command",
					Description: "clears command output",
					action: func(_ context.Context, _ Command, _ string) error {
						commandOutputClear()
						return nil
					},
				},
			},
		},
		{
			Name:        "abort",
			Description: "abort currently running jobs",
			action: func(_ context.Context, _ Command, _ string) error {
				for _, cancelFunc := range cancelFunctions {
					cancelFunc()
				}
				return nil
			},
		},
	},
}

func init() {
	// Because of the cycle we need to define the help command here as it needs access to the to command
	rootCommand.subCommands = append(rootCommand.subCommands, Command{
		Name:        "help",
		Description: "prints out this help",
		action: func(_ context.Context, _ Command, _ string) error {
			_, _ = fmt.Fprintf(commandOutput, "[#0000ff]Available commands[white]\n")
			rootCommand.visit(0, func(currentIndent int, command Command) {
				indentString := strings.Repeat("  ", currentIndent)
				description := command.Description
				if description == "" {
					description = command.Name + "s"
				}
				_, _ = fmt.Fprintf(commandOutput, "%s [#00ff00]%s[white]: %s\n", indentString, command.Name, description)
			})
			return nil
		},
	})
}

var NotDirectlyExecutable = errors.New("Not directly executable")

type Command struct {
	Name                 string
	Description          string
	action               func(ctx context.Context, currentCommand Command, argument string) error
	subCommands          []Command
	parameterSuggestions func(currentText string) (entries []string)
}

func (c Command) Completions(currentCommandText string) (entries []string) {
	if c.Name == rootCommandIndicator && len(currentCommandText) == 0 {
		// We don't return anything here to not pollute the command text by default
		return
	}
	if c.acceptsCurrentText(currentCommandText) {
		currentCommandPrefix := c.currentCommandPrefix()
		doesCommandTextTargetSubCommand := c.doesCommandTextTargetSubCommand(currentCommandPrefix)
		if c.hasDirectExecution() && !doesCommandTextTargetSubCommand {
			if c.parameterSuggestions != nil {
				preparedForParameters := c.prepareForParameters(currentCommandText)
				for _, parameterSuggestion := range c.parameterSuggestions(preparedForParameters) {
					entries = append(entries, currentCommandPrefix+parameterSuggestion)
				}
			} else if currentCommandText == "" {
				entries = append(entries, c.Name)
			}
		}
		if doesCommandTextTargetSubCommand {
			remainder := c.prepareForSubCommand(currentCommandText)
			for _, command := range c.subCommands {
				for _, subCommandCompletions := range command.Completions(remainder) {
					entries = append(entries, currentCommandPrefix+subCommandCompletions)
				}
			}
		}
	} else if strings.HasPrefix(c.Name, currentCommandText) {
		// Suggest ourselves if we start with the current letter
		entries = append(entries, c.Name)
	}
	return
}

func (c Command) acceptsCurrentText(currentCommandText string) bool {
	if c.Name == rootCommandIndicator {
		return true
	}
	hasThePrefix := strings.HasPrefix(currentCommandText, c.Name)
	hasNoMatchingAlternative := !strings.HasPrefix(currentCommandText, c.Name+"-")
	accepts := hasThePrefix && hasNoMatchingAlternative
	plc4xpcapanalyzerLog.Debug().
		Stringer("c", c).
		Bool("accepts", accepts).
		Msg("c accepts accepts")
	return accepts
}

func (c Command) doesCommandTextTargetSubCommand(currentCommandText string) bool {
	if c.Name == rootCommandIndicator {
		return true
	}
	if len(c.subCommands) == 0 {
		return false
	}
	return strings.HasPrefix(currentCommandText, c.currentCommandPrefix())
}

func (c Command) prepareForParameters(currentCommandText string) string {
	if currentCommandText == c.Name {
		return ""
	}
	return strings.TrimPrefix(currentCommandText, c.currentCommandPrefix())
}
func (c Command) prepareForSubCommand(currentCommandText string) string {
	return strings.TrimPrefix(currentCommandText, c.currentCommandPrefix())
}

func (c Command) currentCommandPrefix() string {
	if c.Name == rootCommandIndicator {
		return ""
	}
	return c.Name + " "
}

func (c Command) hasDirectExecution() bool {
	return c.action != nil
}

func Execute(ctx context.Context, commandText string) error {
	err := rootCommand.Execute(ctx, commandText)
	if err == nil {
		addCommandHistoryEntry(commandText)
	}
	return err
}

func (c Command) Execute(ctx context.Context, commandText string) (err error) {
	defer func() {
		if recoveredErr := recover(); recoveredErr != nil {
			if log.Debug().Enabled() {
				log.Error().
					Str("stack", string(debug.Stack())).
					Interface("err", err).
					Msg("panic-ed")
			}
			err = errors.Errorf("panic occurred: %v.", recoveredErr)
		}
	}()
	plc4xpcapanalyzerLog.Debug().
		Stringer("c", c).Str("commandText", commandText).
		Msg("c executes commandText")
	if !c.acceptsCurrentText(commandText) {
		return errors.Errorf("%s doesn't understand %s", c.Name, commandText)
	}
	if c.doesCommandTextTargetSubCommand(commandText) {
		prepareForSubCommandForSubCommand := c.prepareForSubCommand(commandText)
		for _, command := range c.subCommands {
			if command.acceptsCurrentText(prepareForSubCommandForSubCommand) {
				plc4xpcapanalyzerLog.Debug().
					Stringer("c", c).
					Str("commandText", commandText).
					Msg("c delegates to sub command")
				return command.Execute(ctx, prepareForSubCommandForSubCommand)
			}
		}
		return errors.Errorf("%s not accepted by any subcommands of %s", commandText, c.Name)
	} else {
		if c.action == nil {
			return NotDirectlyExecutable
		}
		plc4xpcapanalyzerLog.Debug().
			Stringer("c", c).
			Str("commandText", commandText).
			Msg("c executes commandText directly")
		preparedForParameters := c.prepareForParameters(commandText)
		return c.action(ctx, c, preparedForParameters)
	}
}

func (c Command) visit(i int, f func(currentIndent int, command Command)) {
	f(i, c)
	for _, subCommand := range c.subCommands {
		subCommand.visit(i+1, f)
	}
}

func (c Command) String() string {
	return c.Name
}
