/*
 * Copyright (C) 2018 "IoT.bzh"
 * Author Sebastien Douheret <sebastien@iot.bzh>
 *
 * Licensed 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 main

import (
	"encoding/json"
	"fmt"
	"io"
	"os"
	"sort"
	"strings"
	"time"

	"gerrit.automotivelinux.org/gerrit/src/xds/xds-agent.git/lib/xaapiv1"
	"github.com/creack/goselect"
	"github.com/golang/crypto/ssh/terminal"
	"github.com/urfave/cli"
)

func initCmdTargets(cmdDef *[]cli.Command) {
	*cmdDef = append(*cmdDef, cli.Command{
		Name:     "targets",
		Aliases:  []string{"tgt"},
		HideHelp: true,
		Usage:    "targets commands group",
		Subcommands: []cli.Command{
			{
				Name:    "add",
				Aliases: []string{"a"},
				Usage:   "Add a new target",
				Action:  targetsAdd,
				Flags: []cli.Flag{
					cli.StringFlag{
						Name:  "name, n",
						Usage: "target name (free form string)",
					},
					cli.StringFlag{
						Name:  "ip",
						Usage: "IP address",
					},
					cli.BoolFlag{
						Name:  "short, s",
						Usage: "short output, only print create target id (useful from scripting)",
					},
					cli.StringFlag{
						Name:  "type, t",
						Usage: "target type (standard|std)",
					},
				},
			},
			{
				Name:   "get",
				Usage:  "Get properties of a target",
				Action: targetsGet,
				Flags: []cli.Flag{
					cli.StringFlag{
						Name:   "id",
						Usage:  "target id",
						EnvVar: "XDS_TARGET_ID",
					},
				},
			},
			{
				Name:    "list",
				Aliases: []string{"ls"},
				Usage:   "List existing targets",
				Action:  targetsList,
				Flags: []cli.Flag{
					cli.BoolFlag{
						Name:  "verbose, v",
						Usage: "display verbose output",
					},
				},
			},
			{
				Name:    "remove",
				Aliases: []string{"rm"},
				Usage:   "Remove an existing target",
				Action:  targetsRemove,
				Flags: []cli.Flag{
					cli.StringFlag{
						Name:   "id",
						Usage:  "target id",
						EnvVar: "XDS_TARGET_ID",
					},
					cli.BoolFlag{
						Name:  "force, f",
						Usage: "remove confirmation prompt before removal",
					},
				},
			},
			{
				Name:    "terminal",
				Aliases: []string{"term"},
				Usage:   "Open a target terminal",
				Action:  terminalOpen,
				Flags: []cli.Flag{
					cli.StringFlag{
						Name:   "id",
						Usage:  "target id",
						EnvVar: "XDS_TARGET_ID",
					},
					cli.StringSliceFlag{
						Name:  "options, o",
						Usage: "passthrough options set to command line used to start terminal",
					},
					cli.StringFlag{
						Name:   "termId, tid",
						Usage:  "terminal id",
						EnvVar: "XDS_TERMINAL_ID",
					},
					cli.StringFlag{
						Name:   "user, u",
						Usage:  "user name used to connect terminal",
						EnvVar: "XDS_TERMINAL_USER",
					},
				},
			},
			{
				Name:    "terminal-remove",
				Aliases: []string{"term-rm"},
				Usage:   "Remove a target terminal",
				Action:  terminalRemove,
				Flags: []cli.Flag{
					cli.StringFlag{
						Name:   "id",
						Usage:  "target id",
						EnvVar: "XDS_TARGET_ID",
					},
					cli.StringFlag{
						Name:   "termId, tid",
						Usage:  "terminal id",
						EnvVar: "XDS_TERMINAL_ID",
					},
				},
			},
		},
	})
}

func targetsList(ctx *cli.Context) error {
	// Get targets list
	tgts := []xaapiv1.TargetConfig{}
	if err := TargetsListGet(&tgts); err != nil {
		return cli.NewExitError(err.Error(), 1)
	}
	_displayTargets(tgts, ctx.Bool("verbose"))
	return nil
}

func targetsGet(ctx *cli.Context) error {
	id := GetID(ctx)
	if id == "" {
		return cli.NewExitError("id parameter or option must be set", 1)
	}
	tgts := make([]xaapiv1.TargetConfig, 1)
	url := XdsServerComputeURL("/targets/" + id)
	if err := HTTPCli.Get(url, &tgts[0]); err != nil {
		return cli.NewExitError(err, 1)
	}
	_displayTargets(tgts, true)
	return nil
}

func _displayTargets(tgts []xaapiv1.TargetConfig, verbose bool) {
	// Display result
	first := true
	writer := NewTableWriter()
	for _, tgt := range tgts {
		if verbose {
			if !first {
				fmt.Fprintln(writer)
			}
			fmt.Fprintln(writer, "ID:\t", tgt.ID)
			fmt.Fprintln(writer, "Name:\t", tgt.Name)
			fmt.Fprintln(writer, "Type:\t", tgt.Type)
			fmt.Fprintln(writer, "IP:\t", tgt.IP)
			fmt.Fprintln(writer, "Status:\t", tgt.Status)
			if len(tgt.Terms) > 0 {
				tmNfo := "\t\n"
				for _, tt := range tgt.Terms {
					tmNfo += "\t ID:\t" + tt.ID + "\n"
					tmNfo += "\t  Name:\t" + tt.Name + "\n"
					tmNfo += "\t  Type:\t" + string(tt.Type) + "\n"
					tmNfo += "\t  Status:\t" + tt.Status + "\n"
					tmNfo += "\t  User:\t" + tt.User + "\n"
					tmNfo += "\t  Options:\t" + strings.Join(tt.Options, " ") + "\n"
					tmNfo += fmt.Sprintf("\t  Size:\t%v x %v\n", tt.Cols, tt.Rows)
				}
				fmt.Fprintln(writer, "Terminals:", tmNfo)
			} else {
				fmt.Fprintln(writer, "Terminals:\t None")
			}

		} else {
			if first {
				fmt.Fprintln(writer, "ID\t Name\t IP\t Terminals #")
			}
			fmt.Fprintln(writer, tgt.ID[0:8], "\t", tgt.Name, "\t", tgt.IP, "\t", len(tgt.Terms))
		}
		first = false
	}
	writer.Flush()
}

func targetsAdd(ctx *cli.Context) error {

	// Decode target type
	var tType xaapiv1.TargetType
	switch strings.ToLower(ctx.String("type")) {
	case "standard", "std":
		tType = xaapiv1.TypeTgtStandard
	default:
		tType = xaapiv1.TypeTgtStandard
	}

	tgt := xaapiv1.TargetConfig{
		Name: ctx.String("name"),
		Type: tType,
		IP:   ctx.String("ip"),
	}

	Log.Infof("POST /target %v", tgt)
	newTgt := xaapiv1.TargetConfig{}
	err := HTTPCli.Post(XdsServerComputeURL("/targets"), tgt, &newTgt)
	if err != nil {
		return cli.NewExitError(err, 1)
	}

	if ctx.Bool("short") {
		fmt.Println(newTgt.ID)
	} else {
		fmt.Printf("New target '%s' (id %v) successfully created.\n", newTgt.Name, newTgt.ID)
	}

	return nil
}

func targetsRemove(ctx *cli.Context) error {
	var res xaapiv1.TargetConfig
	id := GetID(ctx)
	if id == "" {
		return cli.NewExitError("id parameter or option must be set", 1)
	}

	if !ctx.Bool("force") {
		if !Confirm("Do you permanently remove target id '" + id + "' [yes/No] ? ") {
			return nil
		}
	}

	if err := HTTPCli.Delete(XdsServerComputeURL("/targets/"+id), &res); err != nil {
		return cli.NewExitError(err, 1)
	}

	fmt.Println("Target ID " + res.ID + " successfully deleted.")
	return nil
}

func terminalOpen(ctx *cli.Context) error {

	tgt, term, err := GetTargetAndTerminalIDs(ctx, true)
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}
	if tgt == nil {
		return cli.NewExitError("cannot identify target", 1)
	}

	if term == nil {
		// Create a new terminal when needed
		newTerm := xaapiv1.TerminalConfig{
			Name:    "ssh session from xds-cli",
			Type:    xaapiv1.TypeTermSSH,
			User:    ctx.String("user"),
			Options: ctx.StringSlice("options"),
		}
		term = &newTerm
		url := XdsServerComputeURL("/targets/" + tgt.ID + "/terminals")
		if err := HTTPCli.Post(url, &newTerm, term); err != nil {
			return cli.NewExitError(err.Error(), 1)
		}
		Log.Debugf("New terminal created: %v", term)
	} else {
		// Update terminal config when needed
		needUp := false
		if ctx.String("user") != "" {
			term.User = ctx.String("user")
			needUp = true
		}
		if len(ctx.StringSlice("options")) > 0 {
			term.Options = ctx.StringSlice("options")
			needUp = true
		}
		if needUp {
			url := XdsServerComputeURL("/targets/" + tgt.ID + "/terminals/" + term.ID)
			if err := HTTPCli.Put(url, &term, term); err != nil {
				return cli.NewExitError(err.Error(), 1)
			}
			Log.Debugf("Update terminal config: %v", term)
		}
	}

	// Process Socket IO events
	type exitResult struct {
		error error
		code  int
	}
	exitChan := make(chan exitResult, 1)

	IOSkClient.On("disconnection", func(err error) {
		Log.Debugf("WS disconnection event with err: %v\n", err)
		exitChan <- exitResult{err, 2}
	})

	IOSkClient.On(xaapiv1.TerminalOutEvent, func(ev xaapiv1.TerminalOutMsg) {
		if len(ev.Stdout) > 0 {
			os.Stdout.Write(ev.Stdout)
		}
		if len(ev.Stderr) > 0 {
			os.Stderr.Write(ev.Stdout)
		}
	})

	IOSkClient.On(xaapiv1.TerminalExitEvent, func(ev xaapiv1.TerminalExitMsg) {
		exitChan <- exitResult{ev.Error, ev.Code}
	})

	// Setup terminal (raw mode to handle escape and control keys)
	if !terminal.IsTerminal(0) || !terminal.IsTerminal(1) {
		return cli.NewExitError("stdin/stdout should be terminal", 1)
	}
	oldState, err := terminal.MakeRaw(int(os.Stdin.Fd()))
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}
	defer terminal.Restore(int(os.Stdin.Fd()), oldState)

	// Send stdin though WS
	go func() {
		type exposeFd interface {
			Fd() uintptr
		}
		buff := make([]byte, 128)
		rdfs := &goselect.FDSet{}
		reader := io.ReadCloser(os.Stdin)
		defer reader.Close()

		for {
			rdfs.Zero()
			rdfs.Set(reader.(exposeFd).Fd())
			err := goselect.Select(1, rdfs, nil, nil, 50*time.Millisecond)
			if err != nil {
				terminal.Restore(int(os.Stdin.Fd()), oldState)
				exitChan <- exitResult{err, 3}
				return
			}
			if rdfs.IsSet(reader.(exposeFd).Fd()) {
				size, err := reader.Read(buff)

				if err != nil {
					Log.Debugf("Read error %v; err %v", size, err)
					if err == io.EOF {
						// CTRL-D exited scanner, so send it explicitly
						err := IOSkClient.Emit(xaapiv1.TerminalInEvent, "\x04\n")

						if err != nil {
							terminal.Restore(int(os.Stdin.Fd()), oldState)
							exitChan <- exitResult{err, 4}
							return
						}
						time.Sleep(time.Millisecond * 100)
						continue
					} else {
						terminal.Restore(int(os.Stdin.Fd()), oldState)
						exitChan <- exitResult{err, 5}
						return
					}
				}

				if size <= 0 {
					continue
				}

				data := buff[:size]
				LogSillyf("Terminal Send data <%v> (%s)", data, data)
				err = IOSkClient.Emit(xaapiv1.TerminalInEvent, data)
				if err != nil {
					terminal.Restore(int(os.Stdin.Fd()), oldState)
					exitChan <- exitResult{err, 6}
					return
				}
			}
		}
	}()

	// Handle signals
	err = OnSignals(func(sig os.Signal) {
		Log.Debugf("Send signal %v", sig)
		if IsWinResizeSignal(sig) {
			TerminalResize(tgt, term)
		} else if IsInterruptSignal(sig) {
			IOSkClient.Emit(xaapiv1.TerminalInEvent, "\x03\n")
		} else {
			TerminalSendSignal(tgt, term, sig)
		}
	})
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	// Send open command
	url := XdsServerComputeURL("/targets/" + tgt.ID + "/terminals/" + term.ID + "/open")
	LogPost("POST %v", url)
	if err := HTTPCli.Post(url, nil, term); err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	// Send init size
	TerminalResize(tgt, term)

	// Wait exit - blocking
	select {
	case res := <-IOSkClient.ServerDiscoChan:
		Log.Debugf("XDS Server disconnected %v", res)
		return cli.NewExitError(res.error, res.code)

	case res := <-exitChan:
		errStr := ""
		if res.code == 0 {
			Log.Debugln("Exit Target Terminal successfully")
		}
		if res.error != nil {
			Log.Debugln("Exit Target Terminal with ERROR: ", res.error.Error())
			errStr = res.error.Error()
		}
		return cli.NewExitError(errStr, res.code)
	}
}

func terminalRemove(ctx *cli.Context) error {

	tgt, term, err := GetTargetAndTerminalIDs(ctx, false)
	if err != nil {
		return cli.NewExitError(err.Error(), 1)
	}
	if tgt == nil || tgt.ID == "" {
		return cli.NewExitError("cannot identify target id", 1)
	}
	if term == nil || term.ID == "" {
		return cli.NewExitError("cannot identify terminal id", 1)
	}

	// Send delete command
	url := XdsServerComputeURL("/targets/" + tgt.ID + "/terminals/" + term.ID)
	LogPost("DELETE %v", url)
	if err := HTTPCli.Delete(url, term); err != nil {
		return cli.NewExitError(err.Error(), 1)
	}

	return nil
}

/**
 * utils functions
 */

// TerminalResize Send command to resize target terminal
func TerminalResize(tgt *xaapiv1.TargetConfig, term *xaapiv1.TerminalConfig) {
	col, row, err := terminal.GetSize(int(os.Stdin.Fd()))
	if err != nil {
		Log.Errorf("Error cannot get terminal size: %v", err)
	}

	LogSillyf("Terminal resizing rows %v, cols %v", row, col)
	sz := xaapiv1.TerminalResizeArgs{Rows: uint16(row), Cols: uint16(col)}
	url := XdsServerComputeURL("/targets/" + tgt.ID + "/terminals/" + term.ID + "/resize")
	if err := HTTPCli.Post(url, &sz, nil); err != nil {
		Log.Errorf("Error while resizing terminal (term %v): %v", sz, err)
	}
}

// TerminalSendSignal Send a signal to a target terminal
func TerminalSendSignal(tgt *xaapiv1.TargetConfig, term *xaapiv1.TerminalConfig, sig os.Signal) {
	url := XdsServerComputeURL("/targets/" + tgt.ID + "/terminals/" + term.ID + "/signal/" + sig.String())
	if err := HTTPCli.Post(url, nil, nil); err != nil {
		Log.Errorf("Error to send signal %v: %v", sig, err)
	}
}

// GetTargetAndTerminalIDs Retrieve Target and Terminal definition from IDs
func GetTargetAndTerminalIDs(ctx *cli.Context, useFirstFree bool) (*xaapiv1.TargetConfig, *xaapiv1.TerminalConfig, error) {

	tgts := []xaapiv1.TargetConfig{}
	if err := TargetsListGet(&tgts); err != nil {
		return nil, nil, err
	}

	idArg := ctx.String("id")
	tidArg := ctx.String("termId")
	if tidArg == "" {
		tidArg = ctx.String("tid")
	}
	if idArg != "" || tidArg != "" {
		matching := 0
		ti := 0
		tj := 0
		for ii, tt := range tgts {
			for jj, ttm := range tt.Terms {
				if idArg == "" && compareID(ttm.ID, tidArg) {
					ti = ii
					tj = jj
					matching++
				}
				if idArg != "" && compareID(tt.ID, idArg) && compareID(ttm.ID, tidArg) {
					ti = ii
					tj = jj
					matching++
				}
			}
		}
		if matching > 1 {
			return nil, nil, fmt.Errorf("Multiple IDs found, please set -id and -tid with full ID notation")
		} else if matching == 1 {
			return &tgts[ti], &tgts[ti].Terms[tj], nil
		}
	}

	// Allow to create a new terminal when only target id is set
	idArg = GetIDName(ctx, "id")
	if idArg == "" {
		return nil, nil, fmt.Errorf("id or termId argument must be set")
	}

	for _, tt := range tgts {
		if compareID(tt.ID, idArg) {
			if useFirstFree {
				for _, ttm := range tt.Terms {
					if ttm.Type == xaapiv1.TypeTermSSH &&
						(ttm.Status == xaapiv1.StatusTermEnable || ttm.Status == xaapiv1.StatusTermClose) {
						return &tt, &ttm, nil
					}
				}
			}
			return &tt, nil, nil
		}
	}

	return nil, nil, fmt.Errorf("No matching id found")
}

// Sort targets by Name
type _TgtByName []xaapiv1.TargetConfig

func (s _TgtByName) Len() int           { return len(s) }
func (s _TgtByName) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
func (s _TgtByName) Less(i, j int) bool { return s[i].Name < s[j].Name }

// TargetsListGet Get the list of existing targets
func TargetsListGet(tgts *[]xaapiv1.TargetConfig) error {
	var data []byte
	if err := HTTPCli.HTTPGet(XdsServerComputeURL("/targets"), &data); err != nil {
		return err
	}
	Log.Debugf("Result of /targets: %v", string(data[:]))

	if err := json.Unmarshal(data, &tgts); err != nil {
		return err
	}

	sort.Sort(_TgtByName(*tgts))

	return nil
}