package docker

import (
	"bytes"
	json2 "encoding/json"
	"fmt"
	"io"
	"regexp"
	"strconv"
	"sync"
	"time"

	"github.com/gorilla/websocket"
	"github.com/sirupsen/logrus"
	"golang.org/x/crypto/ssh"
)

func NewSshClient(user, password string) (*ssh.Client, error) {

	// connet to ssh
	// addr = fmt.Sprintf("%s:%d", host, port)

	config := &ssh.ClientConfig{
		Timeout:         time.Second * 5,
		User:            user,
		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
		//HostKeyCallback: ,
		//HostKeyCallback: hostKeyCallBackFunc(h.Host),
	}
	//if h.Type == "password" {
	config.Auth = []ssh.AuthMethod{ssh.Password(password)}
	//} else {
	//	config.Auth = []ssh.AuthMethod{publicKeyAuthFunc(h.Key)}
	//}
	addr := fmt.Sprintf("%s:%d", "127.0.0.1", 22)
	c, err := ssh.Dial("tcp", addr, config)
	if err != nil {
		return nil, err
	}
	return c, nil
}

// setup ssh shell session
// set Session and StdinPipe here,
// and the Session.Stdout and Session.Sdterr are also set.
func NewSshConn(cols, rows int, sshClient *ssh.Client) (*SshConn, error) {
	sshSession, err := sshClient.NewSession()
	if err != nil {
		return nil, err
	}

	stdinP, err := sshSession.StdinPipe()
	if err != nil {
		return nil, err
	}
	comboWriter := new(wsBufferWriter)

	sshSession.Stdout = comboWriter
	sshSession.Stderr = comboWriter

	modes := ssh.TerminalModes{
		ssh.ECHO:          1,     // disable echo
		ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
		ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
	}
	// Request pseudo terminal
	if err := sshSession.RequestPty("xterm", rows, cols, modes); err != nil {
		return nil, err
	}
	// Start remote shell
	if err := sshSession.Shell(); err != nil {
		return nil, err
	}
	return &SshConn{StdinPipe: stdinP, ComboOutput: comboWriter, Session: sshSession}, nil
}

type SshConn struct {
	// calling Write() to write data into ssh server
	StdinPipe io.WriteCloser
	// Write() be called to receive data from ssh server
	ComboOutput *wsBufferWriter
	Session     *ssh.Session
}
type wsBufferWriter struct {
	buffer bytes.Buffer
	mu     sync.Mutex
}

func (w *wsBufferWriter) Write(p []byte) (int, error) {
	w.mu.Lock()
	defer w.mu.Unlock()
	return w.buffer.Write(p)
}
func (s *SshConn) Close() {
	if s.Session != nil {
		s.Session.Close()
	}

}

const (
	wsMsgCmd    = "cmd"
	wsMsgResize = "resize"
)

//ReceiveWsMsg  receive websocket msg do some handling then write into ssh.session.stdin
func ReceiveWsMsgUser(wsConn *websocket.Conn, logBuff *bytes.Buffer) string {
	//tells other go routine quit
	username := ""
	for {

		//read websocket msg
		_, wsData, err := wsConn.ReadMessage()
		if err != nil {

			return ""
		}

		msgObj := wsMsg{}
		if err := json2.Unmarshal(wsData, &msgObj); err != nil {
			msgObj.Type = "cmd"
			msgObj.Cmd = string(wsData)
		}
		//if err := json.Unmarshal(wsData, &msgObj); err != nil {
		//	logrus.WithError(err).WithField("wsData", string(wsData)).Error("unmarshal websocket message failed")
		//}
		switch msgObj.Type {
		case wsMsgCmd:
			//handle xterm.js stdin
			//decodeBytes, err := base64.StdEncoding.DecodeString(msgObj.Cmd)
			decodeBytes := []byte(msgObj.Cmd)
			if msgObj.Cmd == "\u007f" {
				if len(username) == 0 {
					continue
				}
				wsConn.WriteMessage(websocket.TextMessage, []byte("\b\x1b[K"))
				username = username[:len(username)-1]
				continue
			}
			if msgObj.Cmd == "\r" {
				return username
			}
			username += msgObj.Cmd

			if err := wsConn.WriteMessage(websocket.TextMessage, decodeBytes); err != nil {
				logrus.WithError(err).Error("ws cmd bytes write to ssh.stdin pipe failed")
			}
			//write input cmd to log buffer
			if _, err := logBuff.Write(decodeBytes); err != nil {
				logrus.WithError(err).Error("write received cmd into log buffer failed")
			}
		}

	}
}

func ReceiveWsMsgPassword(wsConn *websocket.Conn, logBuff *bytes.Buffer) string {
	//tells other go routine quit
	password := ""
	for {

		//read websocket msg
		_, wsData, err := wsConn.ReadMessage()
		if err != nil {
			logrus.WithError(err).Error("reading webSocket message failed")
			return ""
		}

		msgObj := wsMsg{}
		if err := json2.Unmarshal(wsData, &msgObj); err != nil {
			msgObj.Type = "cmd"
			msgObj.Cmd = string(wsData)
		}
		//if err := json.Unmarshal(wsData, &msgObj); err != nil {
		//	logrus.WithError(err).WithField("wsData", string(wsData)).Error("unmarshal websocket message failed")
		//}
		switch msgObj.Type {
		case wsMsgCmd:
			//handle xterm.js stdin
			//decodeBytes, err := base64.StdEncoding.DecodeString(msgObj.Cmd)
			if msgObj.Cmd == "\r" {
				return password
			}

			if msgObj.Cmd == "\u007f" {
				if len(password) == 0 {
					continue
				}
				password = password[:len(password)-1]
				continue
			}
			password += msgObj.Cmd
		}

	}
}

//ReceiveWsMsg  receive websocket msg do some handling then write into ssh.session.stdin
func (ssConn *SshConn) ReceiveWsMsg(wsConn *websocket.Conn, logBuff *bytes.Buffer, exitCh chan bool) {
	//tells other go routine quit
	defer setQuit(exitCh)
	for {
		select {
		case <-exitCh:
			return
		default:
			//read websocket msg
			_, wsData, err := wsConn.ReadMessage()
			if err != nil {
				logrus.WithError(err).Error("reading webSocket message failed")
				return
			}
			//unmashal bytes into struct
			//msgObj := wsMsg{
			//	Type: "cmd",
			//	Cmd:  "",
			//	Rows: 50,
			//	Cols: 180,
			//}
			msgObj := wsMsg{}
			if err := json2.Unmarshal(wsData, &msgObj); err != nil {
				msgObj.Type = "cmd"
				msgObj.Cmd = string(wsData)
			}
			//if err := json.Unmarshal(wsData, &msgObj); err != nil {
			//	logrus.WithError(err).WithField("wsData", string(wsData)).Error("unmarshal websocket message failed")
			//}
			switch msgObj.Type {

			case wsMsgResize:
				//handle xterm.js size change
				if msgObj.Cols > 0 && msgObj.Rows > 0 {
					if err := ssConn.Session.WindowChange(msgObj.Rows, msgObj.Cols); err != nil {
						logrus.WithError(err).Error("ssh pty change windows size failed")
					}
				}
			case wsMsgCmd:
				//handle xterm.js stdin
				//decodeBytes, err := base64.StdEncoding.DecodeString(msgObj.Cmd)
				decodeBytes := []byte(msgObj.Cmd)
				if err != nil {
					logrus.WithError(err).Error("websock cmd string base64 decoding failed")
				}
				if _, err := ssConn.StdinPipe.Write(decodeBytes); err != nil {
					logrus.WithError(err).Error("ws cmd bytes write to ssh.stdin pipe failed")
				}
				//write input cmd to log buffer
				if _, err := logBuff.Write(decodeBytes); err != nil {
					logrus.WithError(err).Error("write received cmd into log buffer failed")
				}
			}
		}
	}
}

func (ssConn *SshConn) SendComboOutput(wsConn *websocket.Conn, exitCh chan bool) {
	//tells other go routine quit
	//defer setQuit(exitCh)

	//every 120ms write combine output bytes into websocket response
	tick := time.NewTicker(time.Millisecond * time.Duration(120))
	//for range time.Tick(120 * time.Millisecond){}
	defer tick.Stop()
	for {
		select {
		case <-tick.C:
			//write combine output bytes into websocket response
			if err := flushComboOutput(ssConn.ComboOutput, wsConn); err != nil {
				logrus.WithError(err).Error("ssh sending combo output to webSocket failed")
				return
			}
		case <-exitCh:
			return
		}
	}
}
func flushComboOutput(w *wsBufferWriter, wsConn *websocket.Conn) error {
	if w.buffer.Len() != 0 {
		err := wsConn.WriteMessage(websocket.TextMessage, w.buffer.Bytes())
		if err != nil {
			return err
		}
		w.buffer.Reset()
	}
	return nil
}

//ReceiveWsMsg  receive websocket msg do some handling then write into ssh.session.stdin
func (ssConn *SshConn) Login(wsConn *websocket.Conn, logBuff *bytes.Buffer, exitCh chan bool) {
	//tells other go routine quit
	defer setQuit(exitCh)
	for {
		select {
		case <-exitCh:
			return
		default:
			//read websocket msg
			_, wsData, err := wsConn.ReadMessage()
			if err != nil {
				logrus.WithError(err).Error("reading webSocket message failed")
				return
			}
			//unmashal bytes into struct
			//msgObj := wsMsg{
			//	Type: "cmd",
			//	Cmd:  "",
			//	Rows: 50,
			//	Cols: 180,
			//}
			msgObj := wsMsg{}
			if err := json2.Unmarshal(wsData, &msgObj); err != nil {
				msgObj.Type = "cmd"
				msgObj.Cmd = string(wsData)
			}
			//if err := json.Unmarshal(wsData, &msgObj); err != nil {
			//	logrus.WithError(err).WithField("wsData", string(wsData)).Error("unmarshal websocket message failed")
			//}
			switch msgObj.Type {

			case wsMsgResize:
				//handle xterm.js size change
				if msgObj.Cols > 0 && msgObj.Rows > 0 {
					if err := ssConn.Session.WindowChange(msgObj.Rows, msgObj.Cols); err != nil {
						logrus.WithError(err).Error("ssh pty change windows size failed")
					}
				}
			case wsMsgCmd:
				//handle xterm.js stdin
				//decodeBytes, err := base64.StdEncoding.DecodeString(msgObj.Cmd)
				decodeBytes := []byte(msgObj.Cmd)
				if err != nil {
					logrus.WithError(err).Error("websock cmd string base64 decoding failed")
				}
				if _, err := ssConn.StdinPipe.Write(decodeBytes); err != nil {
					logrus.WithError(err).Error("ws cmd bytes write to ssh.stdin pipe failed")
				}
				//write input cmd to log buffer
				if _, err := logBuff.Write(decodeBytes); err != nil {
					logrus.WithError(err).Error("write received cmd into log buffer failed")
				}
			}
		}
	}
}
func (ssConn *SshConn) SessionWait(quitChan chan bool) {
	if err := ssConn.Session.Wait(); err != nil {
		logrus.WithError(err).Error("ssh session wait failed")
		setQuit(quitChan)
	}
}

func setQuit(ch chan bool) {
	ch <- true
}

type wsMsg struct {
	Type string `json:"type"`
	Cmd  string `json:"cmd"`
	Cols int    `json:"cols"`
	Rows int    `json:"rows"`
}

// 将终端的输出转发到前端
func WsWriterCopy(reader io.Reader, writer *websocket.Conn) {
	buf := make([]byte, 8192)
	reg1 := regexp.MustCompile(`stty rows \d+ && stty cols \d+ `)
	for {
		nr, err := reader.Read(buf)
		if nr > 0 {
			result1 := reg1.FindIndex(buf[0:nr])
			if len(result1) > 0 {
				fmt.Println(result1)
			} else {
				err := writer.WriteMessage(websocket.BinaryMessage, buf[0:nr])
				if err != nil {
					return
				}
			}

		}
		if err != nil {
			return
		}
	}
}

// 将前端的输入转发到终端
func WsReaderCopy(reader *websocket.Conn, writer io.Writer) {
	for {
		messageType, p, err := reader.ReadMessage()
		if err != nil {
			return
		}
		if messageType == websocket.TextMessage {
			msgObj := wsMsg{}
			if err = json2.Unmarshal(p, &msgObj); err != nil {
				writer.Write(p)
			} else if msgObj.Type == wsMsgResize {
				writer.Write([]byte("stty rows " + strconv.Itoa(msgObj.Rows) + " && stty cols " + strconv.Itoa(msgObj.Cols) + " \r"))
			}
		}
	}
}
