package srvconn

import (
	"bytes"
	"errors"
	"net"
	"regexp"
	"strconv"
	"time"

	gossh "golang.org/x/crypto/ssh"

	"github.com/jumpserver/koko/pkg/logger"
	"github.com/jumpserver/koko/pkg/model"
	"github.com/jumpserver/koko/pkg/service"
)

const (
	IAC  = 255
	DONT = 254
	DO   = 253
	WONT = 252
	WILL = 251
	SB   = 250

	TTYPE = 24
	SAG   = 3
	ECHO  = 1

	loginRegs          = "(?i)login:?\\s*$|username:?\\s*$|name:?\\s*$|用户名:?\\s*$|账\\s*号:?\\s*$|user:?\\s*$"
	passwordRegs       = "(?i)Password:?\\s*$|ssword:?\\s*$|passwd:?\\s*$|密\\s*码:?\\s*$"
	FailedRegs         = "(?i)incorrect|failed|失败|错误"
	DefaultSuccessRegs = "(?i)Last\\s*login|success|成功|#|>|\\$"
)

var (
	incorrectPattern, _ = regexp.Compile(FailedRegs)
	usernamePattern, _  = regexp.Compile(loginRegs)
	passwordPattern, _  = regexp.Compile(passwordRegs)
	successPattern, _   = regexp.Compile(DefaultSuccessRegs)
)

type AuthStatus int

const (
	AuthSuccess AuthStatus = iota
	AuthPartial
	AuthFailed
)

type ServerTelnetConnection struct {
	User                 *model.User
	Asset                *model.Asset
	SystemUser           *model.SystemUser
	Overtime             time.Duration
	CustomString         string
	CustomSuccessPattern *regexp.Regexp

	conn   net.Conn
	closed bool
}

func (tc *ServerTelnetConnection) Timeout() time.Duration {
	if tc.Overtime == 0 {
		tc.Overtime = 30 * time.Second
	}
	return tc.Overtime
}

func (tc *ServerTelnetConnection) Protocol() string {
	return "telnet"
}

func (tc *ServerTelnetConnection) optionNegotiate(data []byte) []byte {
	var buf bytes.Buffer
	optionData := bytes.Split(data, []byte{IAC})
	for _, item := range optionData {
		if len(item) == 0 {
			continue
		}
		buf.Write([]byte{IAC})
		switch item[0] {
		case DO:
			switch item[1] {
			case ECHO:
				buf.Write([]byte{WONT, ECHO})
			case TTYPE:
				buf.Write([]byte{WILL, TTYPE})
			default:
				buf.Write(bytes.ReplaceAll(item, []byte{DO}, []byte{WONT}))
			}
		case WILL:
			switch item[1] {
			case ECHO:
				buf.Write([]byte{DO, ECHO})
			case SAG:
				buf.Write([]byte{DO, ECHO})
			default:
				buf.Write(bytes.ReplaceAll(item, []byte{WILL}, []byte{DONT}))
			}
		case DONT:
			buf.Write(bytes.ReplaceAll(item, []byte{DONT}, []byte{WONT}))
		case WONT:
			buf.Write(bytes.ReplaceAll(item, []byte{WONT}, []byte{DONT}))
		case SB:
			switch item[1] {
			case TTYPE:
				if item[2] == 1 {
					buf.Write([]byte{SB, TTYPE, 0})
					buf.Write([]byte("XTERM-256COLOR"))
				}
			}
		default:
			buf.Write(item)
		}
	}
	return buf.Bytes()
}

func (tc *ServerTelnetConnection) login(data []byte) AuthStatus {
	if incorrectPattern.Match(data) {
		return AuthFailed
	} else if usernamePattern.Match(data) {
		_, _ = tc.conn.Write([]byte(tc.SystemUser.Username + "\r\n"))
		logger.Debugf("Username pattern match: %s", data)
		return AuthPartial
	} else if passwordPattern.Match(data) {
		_, _ = tc.conn.Write([]byte(tc.SystemUser.Password + "\r\n"))
		logger.Debugf("Password pattern: %s", data)
		return AuthPartial
	} else if successPattern.Match(data) {
		return AuthSuccess
	}
	if tc.CustomString != "" {
		if tc.CustomSuccessPattern != nil && tc.CustomSuccessPattern.Match(data) {
			return AuthSuccess
		}
	}
	return AuthPartial
}

func (tc *ServerTelnetConnection) Connect(h, w int, term string) (err error) {
	var ip = tc.Asset.IP
	var port = strconv.Itoa(tc.Asset.ProtocolPort("telnet"))
	var asset = tc.Asset
	var proxyConn *gossh.Client

	if asset.Domain != "" {
		sshConfig := MakeConfig(tc.Asset, tc.SystemUser, tc.Timeout())
		proxyConn, err = sshConfig.DialProxy()
		logger.Errorf("Proxy conn: %p", proxyConn)
		if err != nil {
			logger.Error("Dial proxy host error")
			return
		}
	}

	addr := net.JoinHostPort(ip, port)
	var conn net.Conn
	// 判断是否有合适的proxy连接
	if proxyConn != nil {
		logger.Debug("Connect host via proxy")
		conn, err = proxyConn.Dial("tcp", addr)
	} else {
		logger.Debug("Direct connect host")
		conn, err = net.DialTimeout("tcp", addr, tc.Timeout())
	}
	if err != nil {
		return
	}

	if tc.SystemUser.Password == "" {
		info := service.GetSystemUserAssetAuthInfo(tc.SystemUser.ID, asset.ID)
		tc.SystemUser.Password = info.Password
		tc.SystemUser.PrivateKey = info.PrivateKey
	}

	buf := make([]byte, 1024)
	tc.conn = conn
	var nr int
	for {
		nr, err = conn.Read(buf)
		if err != nil {
			return
		}
		if bytes.IndexByte(buf[:nr], IAC) == 0 {
			replayData := tc.optionNegotiate(buf[:nr])
			_, _ = conn.Write(replayData)
			continue
		} else {
			result := tc.login(buf[:nr])
			switch result {
			case AuthSuccess:
				return nil
			case AuthFailed:
				return errors.New("failed login")
			default:
				continue
			}
		}

	}
}

func (tc *ServerTelnetConnection) SetWinSize(w, h int) error {
	return nil
}

func (tc *ServerTelnetConnection) Read(p []byte) (n int, err error) {
	return tc.conn.Read(p)
}

func (tc *ServerTelnetConnection) Write(p []byte) (n int, err error) {
	return tc.conn.Write(p)
}

func (tc *ServerTelnetConnection) Close() (err error) {
	if tc.closed {
		return
	}
	err = tc.conn.Close()
	tc.closed = true
	return
}