package webtty

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"github.com/pkg/errors"
	"os"
	"strconv"
	"sync"
	"time"
)

// WebTTY bridges a PTY slave and its PTY master.
// To support text-based streams and side channel commands such as
// terminal resizing, WebTTY uses an original protocol.
type WebTTY struct {
	// PTY Master, which probably a connection to browser
	masterConn Master
	// PTY Slave
	slave Slave

	windowTitle []byte
	permitWrite bool
	columns     int
	rows        int
	reconnect   int // in seconds
	masterPrefs []byte

	bufferSize   int
	writeMutex   sync.Mutex
	lastPingTime time.Time
}

const (
	MaxBufferSize = 1024 * 1024 * 1
)

// New creates a new instance of WebTTY.
// masterConn is a connection to the PTY master,
// typically it's a websocket connection to a client.
// slave is a PTY slave such as a local command with a PTY.
func New(masterConn Master, slave Slave, options ...Option) (*WebTTY, error) {
	wt := &WebTTY{
		masterConn: masterConn,
		slave:      slave,

		permitWrite: false,
		columns:     0,
		rows:        0,

		bufferSize:   MaxBufferSize,
		lastPingTime: time.Now(),
	}

	for _, option := range options {
		option(wt)
	}

	return wt, nil
}

// Run starts the main process of the WebTTY.
// This method blocks until the context is canceled.
// Note that the master and slave are left intact even
// after the context is canceled. Closing them is caller's
// responsibility.
// If the connection to one end gets closed, returns ErrSlaveClosed or ErrMasterClosed.
func (wt *WebTTY) Run(ctx context.Context) error {
	err := wt.sendInitializeMessage()
	if err != nil {
		return errors.Wrapf(err, "failed to send initializing message")
	}

	errs := make(chan error, 3)

	slaveBuffer := make([]byte, wt.bufferSize)
	go func() {
		errs <- func() error {
			defer func() {
				if e := recover(); e != nil {
				}
			}()
			for {
				if slaveBuffer == nil {
					return ErrSlaveClosed
				}
				n, err := wt.slave.Read(slaveBuffer)
				if err != nil {
					return ErrSlaveClosed
				}
				err = wt.handleSlaveReadEvent(slaveBuffer[:n])
				if err != nil {
					return err
				}
			}
		}()
	}()
	masterBuffer := make([]byte, wt.bufferSize)
	go func() {
		errs <- func() error {
			defer func() {
				if e := recover(); e != nil {
				}
			}()
			for {
				if masterBuffer == nil {
					return ErrMasterClosed
				}
				n, err := wt.masterConn.Read(masterBuffer)
				if err != nil {
					return ErrMasterClosed
				}
				err = wt.handleMasterReadEvent(masterBuffer[:n])
				if err != nil {
					return err
				}
			}
		}()
	}()

	go func() {
		errs <- func() error {
			lostPingTimeout := time.Duration(180) * time.Second
			seconds, _err := strconv.Atoi(os.Getenv("LOST_PING_TIMEOUT_SECONDS"))
			if _err != nil && seconds > 30 {
				lostPingTimeout = time.Duration(seconds) * time.Second
			}
			for {
				time.Sleep(time.Duration(30) * time.Second)
				if err != nil {
					return err
				}
				if time.Now().After(wt.lastPingTime.Add(lostPingTimeout)) {
					return ErrConnectionLostPing
				}
			}
		}()
	}()

	defer func() {
		slaveBuffer = nil
		masterBuffer = nil
	}()

	select {
	case <-ctx.Done():
		err = ctx.Err()
	case err = <-errs:
	}

	return err
}

func (wt *WebTTY) sendInitializeMessage() error {
	err := wt.masterWrite(append([]byte{SetWindowTitle}, wt.windowTitle...))
	if err != nil {
		return errors.Wrapf(err, "failed to send window title")
	}

	if wt.reconnect > 0 {
		reconnect, _ := json.Marshal(wt.reconnect)
		err := wt.masterWrite(append([]byte{SetReconnect}, reconnect...))
		if err != nil {
			return errors.Wrapf(err, "failed to set reconnect")
		}
	}

	if wt.masterPrefs != nil {
		err := wt.masterWrite(append([]byte{SetPreferences}, wt.masterPrefs...))
		if err != nil {
			return errors.Wrapf(err, "failed to set preferences")
		}
	}

	return nil
}

func (wt *WebTTY) handleSlaveReadEvent(data []byte) error {
	safeMessage := base64.StdEncoding.EncodeToString(data)
	err := wt.masterWrite(append([]byte{Output}, []byte(safeMessage)...))
	if err != nil {
		return errors.Wrapf(err, "failed to send message to master")
	}

	return nil
}

func (wt *WebTTY) masterWrite(data []byte) error {
	wt.writeMutex.Lock()
	defer wt.writeMutex.Unlock()

	_, err := wt.masterConn.Write(data)
	if err != nil {
		return errors.Wrapf(err, "failed to write to master")
	}

	return nil
}

func (wt *WebTTY) handleMasterReadEvent(data []byte) error {
	if len(data) == 0 {
		return errors.New("unexpected zero length read from master")
	}

	switch data[0] {
	case Input:
		if !wt.permitWrite {
			return nil
		}

		if len(data) <= 1 {
			return nil
		}
		_, err := wt.slave.Write(data[1:])
		if err != nil {
			return errors.Wrapf(err, "failed to write received data to slave")
		}

	case Ping:
		err := wt.masterWrite([]byte{Pong})
		wt.lastPingTime = time.Now()
		if err != nil {
			return errors.Wrapf(err, "failed to return Pong message to master")
		}

	case ResizeTerminal:
		if wt.columns != 0 && wt.rows != 0 {
			break
		}

		if len(data) <= 1 {
			return errors.New("received malformed remote command for terminal resize: empty payload")
		}

		var args argResizeTerminal
		err := json.Unmarshal(data[1:], &args)
		if err != nil {
			return errors.Wrapf(err, "received malformed data for terminal resize")
		}
		rows := wt.rows
		if rows == 0 {
			rows = int(args.Rows)
		}

		columns := wt.columns
		if columns == 0 {
			columns = int(args.Columns)
		}

		return wt.slave.ResizeTerminal(columns, rows)
	default:
		return errors.Errorf("unknown message type `%c`", data[0])
	}

	return nil
}

type argResizeTerminal struct {
	Columns float64
	Rows    float64
}
