/*
 * 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 xdsserver

import (
	"fmt"
	"strings"
	"time"

	"gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git/golib/eows"
	"gerrit.automotivelinux.org/gerrit/src/xds/xds-server/lib/xsapiv1"
	socketio "github.com/googollee/go-socket.io"
	uuid "github.com/satori/go.uuid"
)

// ITARGET interface implementation for standard targets

// TermSSH .
type TermSSH struct {
	*Context
	termCfg  xsapiv1.TerminalConfig
	targetID string
	sshWS    *eows.ExecOverWS
}

// NewTermSSH Create a new instance of TermSSH
func NewTermSSH(ctx *Context, cfg xsapiv1.TerminalConfig, targetID string) *TermSSH {

	// Allocate and set default settings
	t := TermSSH{
		Context: ctx,
		termCfg: xsapiv1.TerminalConfig{
			ID:      cfg.ID,
			Name:    "ssh",
			Type:    xsapiv1.TypeTermSSH,
			Status:  xsapiv1.StatusTermClose,
			User:    "",
			Options: []string{""},
			Cols:    80,
			Rows:    24,
		},
		targetID: targetID,
	}

	t.UpdateConfig(cfg)
	return &t
}

// NewUID Get a UUID
func (t *TermSSH) _NewUID(suffix string) string {
	uuid := uuid.NewV1().String()
	if len(suffix) > 0 {
		uuid += "_" + suffix
	}
	return uuid
}

// GetConfig Get public part of terminal config
func (t *TermSSH) GetConfig() xsapiv1.TerminalConfig {
	return t.termCfg
}

// UpdateConfig Update terminal config
func (t *TermSSH) UpdateConfig(newCfg xsapiv1.TerminalConfig) *xsapiv1.TerminalConfig {

	if t.termCfg.ID == "" {
		if newCfg.ID != "" {
			t.termCfg.ID = newCfg.ID
		} else {
			t.termCfg.ID = t._NewUID("")
		}
	}
	if newCfg.Name != "" {
		t.termCfg.Name = newCfg.Name
	}
	if newCfg.User != "" {
		t.termCfg.User = newCfg.User
	}
	if len(newCfg.Options) > 0 {
		t.termCfg.Options = newCfg.Options
	}

	// Adjust terminal size
	t.Resize(newCfg.Cols, newCfg.Rows)

	return &t.termCfg
}

// Open a new terminal - execute ssh command and bind stdin/stdout to WebSocket
func (t *TermSSH) Open(sock *socketio.Socket, sessID string) (*xsapiv1.TerminalConfig, error) {

	// Get target info to retrieve IP
	tgt := t.targets.Get(t.targetID)
	if tgt == nil {
		return nil, fmt.Errorf("Cannot retrieve target definition")
	}
	tgtCfg := (*tgt).GetConfig()

	// Sanity check
	if tgtCfg.IP == "" {
		return nil, fmt.Errorf("null target IP")
	}
	userStr := ""
	if t.termCfg.User != "" {
		userStr = t.termCfg.User + "@"
	}

	// Compute ssh command
	cmd := "ssh"
	cmdID := "ssh_term_" + t.termCfg.ID
	args := t.termCfg.Options
	args = append(args, userStr+tgtCfg.IP)

	t.sshWS = eows.New(cmd, args, sock, sessID, cmdID)
	t.sshWS.Log = t.Log
	t.sshWS.PtyMode = true

	// Define callback for input (stdin)
	t.sshWS.InputEvent = xsapiv1.TerminalInEvent
	t.sshWS.InputCB = func(e *eows.ExecOverWS, stdin []byte) ([]byte, error) {
		if t.LogLevelSilly {
			t.Log.Debugf("STDIN <<%v>> %s", stdin, string(stdin))
		}
		return stdin, nil
	}

	// Define callback for output (stdout+stderr)
	t.sshWS.OutputCB = func(e *eows.ExecOverWS, stdout, stderr []byte) {
		// IO socket can be nil when disconnected
		so := t.sessions.IOSocketGet(e.Sid)
		if so == nil {
			t.Log.Infof("%s not emitted: WS closed (sid:%s, CmdID:%s)", xsapiv1.TerminalOutEvent, e.Sid, e.CmdID)
			return
		}

		// Retrieve project ID and RootPath
		data := e.UserData
		termID := (*data)["ID"].(string)

		if t.LogLevelSilly {
			t.Log.Debugf("%s emitted - WS sid[4:] %s - id:%s - termID:%s", xsapiv1.TerminalOutEvent, e.Sid[4:], e.CmdID, termID)
			if len(stdout) > 0 {
				t.Log.Debugf("STDOUT <<%v>>", strings.Replace(string(stdout), "\n", "\\n", -1))
			}
			if len(stderr) > 0 {
				t.Log.Debugf("STDERR <<%v>>", strings.Replace(string(stderr), "\n", "\\n", -1))
			}
		}

		// FIXME replace by .BroadcastTo a room
		err := (*so).Emit(xsapiv1.TerminalOutEvent, xsapiv1.TerminalOutMsg{
			TermID:    termID,
			Timestamp: time.Now().String(),
			Stdout:    stdout,
			Stderr:    stderr,
		})
		if err != nil {
			t.Log.Errorf("WS Emit : %v", err)
		}
	}

	// Define callback for output
	t.sshWS.ExitCB = func(e *eows.ExecOverWS, code int, err error) {
		t.Log.Debugf("Command [Cmd ID %s] exited: code %d, error: %v", e.CmdID, code, err)

		// IO socket can be nil when disconnected
		so := t.sessions.IOSocketGet(e.Sid)
		if so == nil {
			t.Log.Infof("%s not emitted - WS closed (id:%s)", xsapiv1.TerminalExitEvent, e.CmdID)
			return
		}

		// Retrieve project ID and RootPath
		data := e.UserData
		termID := (*data)["ID"].(string)

		// FIXME replace by .BroadcastTo a room
		errSoEmit := (*so).Emit(xsapiv1.TerminalExitEvent, xsapiv1.TerminalExitMsg{
			TermID:    termID,
			Timestamp: time.Now().String(),
			Code:      code,
			Error:     err,
		})
		if errSoEmit != nil {
			t.Log.Errorf("WS Emit : %v", errSoEmit)
		}

		t.termCfg.Status = xsapiv1.StatusTermClose
		t.sshWS = nil
	}

	// data (used within callbacks)
	data := make(map[string]interface{})
	data["ID"] = t.termCfg.ID
	t.sshWS.UserData = &data

	// Start ssh command
	t.Log.Infof("Execute [Cmd ID %s]: %v %v", t.sshWS.CmdID, t.sshWS.Cmd, t.sshWS.Args)

	if err := t.sshWS.Start(); err != nil {
		return &t.termCfg, err
	}

	t.termCfg.Status = xsapiv1.StatusTermOpen

	return &t.termCfg, nil
}

// Close a terminal
func (t *TermSSH) Close() (*xsapiv1.TerminalConfig, error) {
	// nothing to do when not open or closing
    if ! (t.termCfg.Status == xsapiv1.StatusTermOpen || t.termCfg.Status == xsapiv1.StatusTermClosing) {
		return &t.termCfg, nil
	}

	err := t.sshWS.Signal("SIGTERM")

	t.termCfg.Status = xsapiv1.StatusTermClosing

	return &t.termCfg, err
}

// Resize a terminal
func (t *TermSSH) Resize(cols, rows uint16) (*xsapiv1.TerminalConfig, error) {
	if t.sshWS == nil {
		return &t.termCfg, fmt.Errorf("ssh session not initialized")
	}

	if cols > 0 {
		t.termCfg.Cols = cols
	}
	if rows > 0 {
		t.termCfg.Rows = rows
	}

	t.LogSillyf("Terminal resize id=%v, cols=%v, rows=%v", t.termCfg.ID, cols, rows)

	err := t.sshWS.TerminalSetSize(t.termCfg.Rows, t.termCfg.Cols)
	if err != nil {
		t.Log.Errorf("Error ssh TerminalSetSize: %v", err)
	}

	return &t.termCfg, err
}

// Signal Send a signal to a terminal
func (t *TermSSH) Signal(sigName string) error {
	return t.sshWS.Signal(sigName)
}