/**
 * 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
 *
 *  http://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 cli

import (
	"encoding/hex"
	"encoding/json"
	"fmt"
	"sort"
	"strconv"

	"github.com/spf13/cobra"

	"mynewt.apache.org/newt/util"
	"mynewt.apache.org/newtmgr/newtmgr/nmutil"
	"mynewt.apache.org/newtmgr/nmxact/nmp"
	"mynewt.apache.org/newtmgr/nmxact/nmxutil"
	"mynewt.apache.org/newtmgr/nmxact/sesn"
	"mynewt.apache.org/newtmgr/nmxact/xact"
)

var optLogShowFull bool

// Converts the provided CBOR map to a JSON string.
func logCborMsgText(cborMap []byte) (string, error) {
	cm, err := nmxutil.DecodeCborMap(cborMap)
	if err != nil {
		return "", err
	}

	msg, err := json.Marshal(cm)
	if err != nil {
		return "", util.ChildNewtError(err)
	}

	return string(msg), nil
}

type logShowCfg struct {
	Name      string
	Last      bool
	Index     uint32
	Timestamp int64
}

func logShowParseArgs(args []string) (*logShowCfg, error) {
	cfg := &logShowCfg{}

	if len(args) < 1 {
		return cfg, nil
	}
	cfg.Name = args[0]

	if len(args) < 2 {
		return cfg, nil
	}

	if args[1] == "last" {
		cfg.Index = 0
		cfg.Timestamp = -1
	} else {
		u64, err := strconv.ParseUint(args[1], 0, 64)
		if err != nil {
			return nil, util.ChildNewtError(err)
		}
		if u64 > 0xffffffff {
			return nil, util.NewNewtError("index out of range")
		}
		cfg.Index = uint32(u64)
	}

	if len(args) < 3 || args[1] == "last" {
		return cfg, nil
	}

	ts, err := strconv.ParseInt(args[2], 0, 64)
	if err != nil {
		return nil, util.ChildNewtError(err)
	}
	cfg.Timestamp = ts

	return cfg, nil
}

func printLogShowRsp(rsp *nmp.LogShowRsp, printHdr bool) {
	if len(rsp.Logs) == 0 {
		fmt.Printf("(no logs retrieved)\n")
		return
	}

	for _, log := range rsp.Logs {
		if printHdr {
			fmt.Printf("Name: %s\n", log.Name)
			fmt.Printf("Type: %s\n", nmp.LogTypeToString(log.Type))

			fmt.Printf("%10s %22s | %16s %16s %6s %8s %s\n",
				"[index]", "[timestamp]", "[module]", "[level]", "[type]",
				"[img]", "[message]")
		}

		for _, entry := range log.Entries {
			modText := fmt.Sprintf("%s (%d)",
				nmp.LogModuleToString(int(entry.Module)), entry.Module)
			levText := fmt.Sprintf("%s (%d)",
				nmp.LogLevelToString(int(entry.Level)), entry.Level)

			var err error
			msgText := ""
			switch entry.Type {
			case nmp.LOG_ENTRY_TYPE_STRING:
				msgText = string(entry.Msg)
			case nmp.LOG_ENTRY_TYPE_CBOR:
				msgText, err = logCborMsgText(entry.Msg)
				if err != nil {
					fmt.Printf("Error decoding CBOR entry: %s; "+
						"idx=%d",
						err.Error(), entry.Index)
					msgText = hex.EncodeToString(entry.Msg)
				}

			case nmp.LOG_ENTRY_TYPE_BINARY:
				msgText = hex.EncodeToString(entry.Msg)

			default:
				fmt.Printf(
					"Error decoding entry: unknown entry type (%d); idx=%d",
					int(entry.Type), entry.Index)
				msgText = hex.EncodeToString(entry.Msg)
			}

			fmt.Printf("%10d %20dus | %16s %16s %6s %8s %s\n",
				entry.Index,
				entry.Timestamp,
				modText,
				levText,
				entry.Type,
				hex.EncodeToString(entry.ImgHash),
				msgText)
		}
	}
}

func logShowFullCmd(s sesn.Sesn, cfg *logShowCfg) error {
	if cfg.Name == "" {
		return util.FmtNewtError("must specify a single log to read when `-a` is used")
	}

	c := xact.NewLogShowFullCmd()
	c.SetTxOptions(nmutil.TxOptions())

	c.Name = cfg.Name
	c.Index = cfg.Index

	first := true
	c.ProgressCb = func(_ *xact.LogShowFullCmd, rsp *nmp.LogShowRsp) {
		printLogShowRsp(rsp, first)
		first = false
	}

	_, err := c.Run(s)
	if err != nil {
		return err
	}

	return nil
}

func logShowPartialCmd(s sesn.Sesn, cfg *logShowCfg) error {
	c := xact.NewLogShowCmd()
	c.SetTxOptions(nmutil.TxOptions())

	c.Name = cfg.Name
	c.Index = cfg.Index
	c.Timestamp = cfg.Timestamp

	res, err := c.Run(s)
	if err != nil {
		return err
	}

	sres := res.(*xact.LogShowResult)
	fmt.Printf("Status: %d\n", sres.Status())
	fmt.Printf("Next index: %d\n", sres.Rsp.NextIndex)
	if len(sres.Rsp.Logs) == 0 {
		fmt.Printf("(no logs retrieved)\n")
	} else {
		printLogShowRsp(sres.Rsp, true)
	}

	return nil
}

func logShowCmd(cmd *cobra.Command, args []string) {
	cfg, err := logShowParseArgs(args)
	if err != nil {
		nmUsage(cmd, err)
	}

	s, err := GetSesn()
	if err != nil {
		nmUsage(nil, err)
	}

	if optLogShowFull {
		err = logShowFullCmd(s, cfg)
	} else {
		err = logShowPartialCmd(s, cfg)
	}
	if err != nil {
		nmUsage(nil, err)
	}
}

func logListCmd(cmd *cobra.Command, args []string) {
	s, err := GetSesn()
	if err != nil {
		nmUsage(nil, err)
	}

	c := xact.NewLogListCmd()
	c.SetTxOptions(nmutil.TxOptions())

	res, err := c.Run(s)
	if err != nil {
		nmUsage(nil, util.ChildNewtError(err))
	}

	sres := res.(*xact.LogListResult)
	if sres.Rsp.Rc != 0 {
		fmt.Printf("error: %d\n", sres.Rsp.Rc)
		return
	}

	sort.Strings(sres.Rsp.List)

	fmt.Printf("available logs:\n")
	for _, log := range sres.Rsp.List {
		fmt.Printf("    %s\n", log)
	}
}

func logModuleListCmd(cmd *cobra.Command, args []string) {
	s, err := GetSesn()
	if err != nil {
		nmUsage(nil, err)
	}

	c := xact.NewLogModuleListCmd()
	c.SetTxOptions(nmutil.TxOptions())

	res, err := c.Run(s)
	if err != nil {
		nmUsage(nil, util.ChildNewtError(err))
	}

	sres := res.(*xact.LogModuleListResult)
	if sres.Rsp.Rc != 0 {
		fmt.Printf("error: %d\n", sres.Rsp.Rc)
		return
	}

	names := make([]string, 0, len(sres.Rsp.Map))
	for k, _ := range sres.Rsp.Map {
		names = append(names, k)
	}
	sort.Strings(names)

	fmt.Printf("available modules:\n")
	for _, name := range names {
		fmt.Printf("    %s (%d)\n", name, sres.Rsp.Map[name])
	}
}

func logLevelListCmd(cmd *cobra.Command, args []string) {
	s, err := GetSesn()
	if err != nil {
		nmUsage(nil, err)
	}

	c := xact.NewLogLevelListCmd()
	c.SetTxOptions(nmutil.TxOptions())

	res, err := c.Run(s)
	if err != nil {
		nmUsage(nil, util.ChildNewtError(err))
	}

	sres := res.(*xact.LogLevelListResult)
	if sres.Rsp.Rc != 0 {
		fmt.Printf("error: %d\n", sres.Rsp.Rc)
		return
	}

	vals := make([]int, 0, len(sres.Rsp.Map))
	revmap := make(map[int]string, len(sres.Rsp.Map))
	for name, val := range sres.Rsp.Map {
		vals = append(vals, val)
		revmap[val] = name
	}
	sort.Ints(vals)

	fmt.Printf("available levels:\n")
	for _, val := range vals {
		fmt.Printf("    %d: %s\n", val, revmap[val])
	}
}

func logClearCmd(cmd *cobra.Command, args []string) {
	s, err := GetSesn()
	if err != nil {
		nmUsage(nil, err)
	}

	c := xact.NewLogClearCmd()
	c.SetTxOptions(nmutil.TxOptions())

	res, err := c.Run(s)
	if err != nil {
		nmUsage(nil, util.ChildNewtError(err))
	}

	sres := res.(*xact.LogClearResult)
	if sres.Rsp.Rc != 0 {
		fmt.Printf("error: %d\n", sres.Rsp.Rc)
		return
	}

	fmt.Printf("done\n")
}

func logCmd() *cobra.Command {
	logCmd := &cobra.Command{
		Use:   "log",
		Short: "Manage logs on a device",
		Run: func(cmd *cobra.Command, args []string) {
			cmd.HelpFunc()(cmd, args)
		},
	}

	logShowHelpText := "Show logs on a device.  Optional log-name, min-index, and min-timestamp\nparameters can be specified to filter the logs to display.\n\n"
	logShowHelpText += "- log-name specifies the log to display.  If log-name is not specified, all\nlogs are displayed.\n\n"
	logShowHelpText += "- min-index specifies to only display the log entries with an index value equal to or higher than min-index.  "
	logShowHelpText += "If \"last\"  is specified for min-index, the last\nlog entry is displayed.\n\n"
	logShowHelpText += "- min-timestamp specifies to only display the log entries with a timestamp\nequal to or later than min-timestamp. Log entries with a timestamp equal to\nmin-timestamp are only displayed if the entry index is equal to or higher than min-index.\n"

	logShowEx := nmutil.ToolInfo.ExeName + " log show -c myserial\n"
	logShowEx += nmutil.ToolInfo.ExeName + " log show reboot_log -c myserial\n"
	logShowEx += nmutil.ToolInfo.ExeName + " log show reboot_log last -c myserial\n"
	logShowEx += nmutil.ToolInfo.ExeName + " log show reboot_log 5 -c myserial\n"
	logShowEx += nmutil.ToolInfo.ExeName + " log show reboot_log 3 1122222 -c myserial\n"

	showCmd := &cobra.Command{
		Use:     "show [log-name [min-index [min-timestamp]]] -c <conn_profile>",
		Example: logShowEx,
		Long:    logShowHelpText,
		Short:   "Show the logs on a device",
		Run:     logShowCmd,
	}
	showCmd.PersistentFlags().BoolVarP(&optLogShowFull, "all", "a", false, "read until end of log")
	logCmd.AddCommand(showCmd)

	clearCmd := &cobra.Command{
		Use:     "clear -c <conn_profile>",
		Short:   "Clear the logs on a device",
		Example: logShowEx,
		Run:     logClearCmd,
	}
	logCmd.AddCommand(clearCmd)

	moduleListCmd := &cobra.Command{
		Use:   "module_list -c <conn_profile>",
		Short: "Show the log module names",
		Run:   logModuleListCmd,
	}
	logCmd.AddCommand(moduleListCmd)

	levelListCmd := &cobra.Command{
		Use:   "level_list -c <conn_profile>",
		Short: "Show the log levels",
		Run:   logLevelListCmd,
	}

	logCmd.AddCommand(levelListCmd)

	ListCmd := &cobra.Command{
		Use:   "list -c <conn_profile>",
		Short: "Show the log names",
		Run:   logListCmd,
	}

	logCmd.AddCommand(ListCmd)

	return logCmd
}
