package handshake

import (
	"crypto/tls"
	"errors"
	"fmt"
	"io"

	"github.com/lucas-clemente/quic-go/internal/crypto"
	"github.com/lucas-clemente/quic-go/internal/protocol"
	"github.com/lucas-clemente/quic-go/internal/utils"
	"github.com/marten-seemann/qtls"
)

type messageType uint8

// TLS handshake message types.
const (
	typeClientHello         messageType = 1
	typeServerHello         messageType = 2
	typeEncryptedExtensions messageType = 8
	typeCertificate         messageType = 11
	typeCertificateRequest  messageType = 13
	typeCertificateVerify   messageType = 15
	typeFinished            messageType = 20
)

func (m messageType) String() string {
	switch m {
	case typeClientHello:
		return "ClientHello"
	case typeServerHello:
		return "ServerHello"
	case typeEncryptedExtensions:
		return "EncryptedExtensions"
	case typeCertificate:
		return "Certificate"
	case typeCertificateRequest:
		return "CertificateRequest"
	case typeCertificateVerify:
		return "CertificateVerify"
	case typeFinished:
		return "Finished"
	default:
		return fmt.Sprintf("unknown message type: %d", m)
	}
}

type cryptoSetup struct {
	tlsConf *qtls.Config

	messageChan chan []byte

	readEncLevel  protocol.EncryptionLevel
	writeEncLevel protocol.EncryptionLevel

	handleParamsCallback func(*TransportParameters)

	// There are two ways that an error can occur during the handshake:
	// 1. as a return value from qtls.Handshake()
	// 2. when new data is passed to the crypto setup via HandleData()
	// handshakeErrChan is closed when qtls.Handshake() errors
	handshakeErrChan chan struct{}
	// HandleData() sends errors on the messageErrChan
	messageErrChan chan error
	// handshakeDone is closed as soon as the go routine running qtls.Handshake() returns
	handshakeDone chan struct{}
	// transport parameters are sent on the receivedTransportParams, as soon as they are received
	receivedTransportParams <-chan TransportParameters
	// is closed when Close() is called
	closeChan chan struct{}

	clientHelloWritten     bool
	clientHelloWrittenChan chan struct{}

	initialStream io.Writer
	initialAEAD   crypto.AEAD

	handshakeStream io.Writer
	handshakeOpener Opener
	handshakeSealer Sealer

	opener Opener
	sealer Sealer
	// TODO: add a 1-RTT stream (used for session tickets)

	receivedWriteKey chan struct{}
	receivedReadKey  chan struct{}

	logger utils.Logger

	perspective protocol.Perspective
}

var _ qtls.RecordLayer = &cryptoSetup{}
var _ CryptoSetup = &cryptoSetup{}

// NewCryptoSetupClient creates a new crypto setup for the client
func NewCryptoSetupClient(
	initialStream io.Writer,
	handshakeStream io.Writer,
	origConnID protocol.ConnectionID,
	connID protocol.ConnectionID,
	params *TransportParameters,
	handleParams func(*TransportParameters),
	tlsConf *tls.Config,
	initialVersion protocol.VersionNumber,
	supportedVersions []protocol.VersionNumber,
	currentVersion protocol.VersionNumber,
	logger utils.Logger,
	perspective protocol.Perspective,
) (CryptoSetup, <-chan struct{} /* ClientHello written */, error) {
	extHandler, receivedTransportParams := newExtensionHandlerClient(
		params,
		origConnID,
		initialVersion,
		supportedVersions,
		currentVersion,
		logger,
	)
	return newCryptoSetup(
		initialStream,
		handshakeStream,
		connID,
		extHandler,
		receivedTransportParams,
		handleParams,
		tlsConf,
		logger,
		perspective,
	)
}

// NewCryptoSetupServer creates a new crypto setup for the server
func NewCryptoSetupServer(
	initialStream io.Writer,
	handshakeStream io.Writer,
	connID protocol.ConnectionID,
	params *TransportParameters,
	handleParams func(*TransportParameters),
	tlsConf *tls.Config,
	supportedVersions []protocol.VersionNumber,
	currentVersion protocol.VersionNumber,
	logger utils.Logger,
	perspective protocol.Perspective,
) (CryptoSetup, error) {
	extHandler, receivedTransportParams := newExtensionHandlerServer(
		params,
		supportedVersions,
		currentVersion,
		logger,
	)
	cs, _, err := newCryptoSetup(
		initialStream,
		handshakeStream,
		connID,
		extHandler,
		receivedTransportParams,
		handleParams,
		tlsConf,
		logger,
		perspective,
	)
	return cs, err
}

func newCryptoSetup(
	initialStream io.Writer,
	handshakeStream io.Writer,
	connID protocol.ConnectionID,
	extHandler tlsExtensionHandler,
	transportParamChan <-chan TransportParameters,
	handleParams func(*TransportParameters),
	tlsConf *tls.Config,
	logger utils.Logger,
	perspective protocol.Perspective,
) (CryptoSetup, <-chan struct{} /* ClientHello written */, error) {
	initialAEAD, err := crypto.NewNullAEAD(connID, perspective)
	if err != nil {
		return nil, nil, err
	}
	cs := &cryptoSetup{
		initialStream:           initialStream,
		initialAEAD:             initialAEAD,
		handshakeStream:         handshakeStream,
		readEncLevel:            protocol.EncryptionInitial,
		writeEncLevel:           protocol.EncryptionInitial,
		handleParamsCallback:    handleParams,
		receivedTransportParams: transportParamChan,
		logger:                  logger,
		perspective:             perspective,
		handshakeDone:           make(chan struct{}),
		handshakeErrChan:        make(chan struct{}),
		messageErrChan:          make(chan error, 1),
		clientHelloWrittenChan:  make(chan struct{}),
		messageChan:             make(chan []byte, 100),
		receivedReadKey:         make(chan struct{}),
		receivedWriteKey:        make(chan struct{}),
		closeChan:               make(chan struct{}),
	}
	qtlsConf := tlsConfigToQtlsConfig(tlsConf)
	qtlsConf.AlternativeRecordLayer = cs
	qtlsConf.GetExtensions = extHandler.GetExtensions
	qtlsConf.ReceivedExtensions = extHandler.ReceivedExtensions
	cs.tlsConf = qtlsConf
	return cs, cs.clientHelloWrittenChan, nil
}

func (h *cryptoSetup) RunHandshake() error {
	var conn *qtls.Conn
	switch h.perspective {
	case protocol.PerspectiveClient:
		conn = qtls.Client(nil, h.tlsConf)
	case protocol.PerspectiveServer:
		conn = qtls.Server(nil, h.tlsConf)
	}
	// Handle errors that might occur when HandleData() is called.
	handshakeErrChan := make(chan error, 1)
	handshakeComplete := make(chan struct{})
	go func() {
		defer close(h.handshakeDone)
		if err := conn.Handshake(); err != nil {
			handshakeErrChan <- err
			return
		}
		close(handshakeComplete)
	}()

	select {
	case <-h.closeChan:
		close(h.messageChan)
		// wait until the Handshake() go routine has returned
		<-handshakeErrChan
		return errors.New("Handshake aborted")
	case <-handshakeComplete: // return when the handshake is done
		return nil
	case err := <-handshakeErrChan:
		// if handleMessageFor{server,client} are waiting for some qtls action, make them return
		close(h.handshakeErrChan)
		return err
	case err := <-h.messageErrChan:
		// If the handshake errored because of an error that occurred during HandleData(),
		// that error message will be more useful than the error message generated by Handshake().
		// Close the message chan that qtls is receiving messages from.
		// This will make qtls.Handshake() return.
		// Thereby the go routine running qtls.Handshake() will return.
		close(h.messageChan)
		return err
	}
}

func (h *cryptoSetup) Close() error {
	close(h.closeChan)
	// wait until qtls.Handshake() actually returned
	<-h.handshakeDone
	return nil
}

// handleMessage handles a TLS handshake message.
// It is called by the crypto streams when a new message is available.
// It returns if it is done with messages on the same encryption level.
func (h *cryptoSetup) HandleMessage(data []byte, encLevel protocol.EncryptionLevel) bool /* stream finished */ {
	msgType := messageType(data[0])
	h.logger.Debugf("Received %s message (%d bytes, encryption level: %s)", msgType, len(data), encLevel)
	if err := h.checkEncryptionLevel(msgType, encLevel); err != nil {
		h.messageErrChan <- err
		return false
	}
	h.messageChan <- data
	switch h.perspective {
	case protocol.PerspectiveClient:
		return h.handleMessageForClient(msgType)
	case protocol.PerspectiveServer:
		return h.handleMessageForServer(msgType)
	default:
		panic("")
	}
}

func (h *cryptoSetup) checkEncryptionLevel(msgType messageType, encLevel protocol.EncryptionLevel) error {
	var expected protocol.EncryptionLevel
	switch msgType {
	case typeClientHello,
		typeServerHello:
		expected = protocol.EncryptionInitial
	case typeEncryptedExtensions,
		typeCertificate,
		typeCertificateRequest,
		typeCertificateVerify,
		typeFinished:
		expected = protocol.EncryptionHandshake
	default:
		return fmt.Errorf("unexpected handshake message: %d", msgType)
	}
	if encLevel != expected {
		return fmt.Errorf("expected handshake message %s to have encryption level %s, has %s", msgType, expected, encLevel)
	}
	return nil
}

func (h *cryptoSetup) handleMessageForServer(msgType messageType) bool {
	switch msgType {
	case typeClientHello:
		select {
		case params := <-h.receivedTransportParams:
			h.handleParamsCallback(&params)
		case <-h.handshakeErrChan:
			return false
		}
		// get the handshake write key
		select {
		case <-h.receivedWriteKey:
		case <-h.handshakeErrChan:
			return false
		}
		// get the 1-RTT write key
		select {
		case <-h.receivedWriteKey:
		case <-h.handshakeErrChan:
			return false
		}
		// get the handshake read key
		// TODO: check that the initial stream doesn't have any more data
		select {
		case <-h.receivedReadKey:
		case <-h.handshakeErrChan:
			return false
		}
		return true
	case typeCertificate, typeCertificateVerify:
		// nothing to do
		return false
	case typeFinished:
		// get the 1-RTT read key
		select {
		case <-h.receivedReadKey:
		case <-h.handshakeErrChan:
			return false
		}
		return true
	default:
		panic("unexpected handshake message")
	}
}

func (h *cryptoSetup) handleMessageForClient(msgType messageType) bool {
	switch msgType {
	case typeServerHello:
		// get the handshake read key
		select {
		case <-h.receivedReadKey:
		case <-h.handshakeErrChan:
			return false
		}
		return true
	case typeEncryptedExtensions:
		select {
		case params := <-h.receivedTransportParams:
			h.handleParamsCallback(&params)
		case <-h.handshakeErrChan:
			return false
		}
		return false
	case typeCertificateRequest, typeCertificate, typeCertificateVerify:
		// nothing to do
		return false
	case typeFinished:
		// get the handshake write key
		select {
		case <-h.receivedWriteKey:
		case <-h.handshakeErrChan:
			return false
		}
		// While the order of these two is not defined by the TLS spec,
		// we have to do it on the same order as our TLS library does it.
		// get the handshake write key
		select {
		case <-h.receivedWriteKey:
		case <-h.handshakeErrChan:
			return false
		}
		// get the 1-RTT read key
		select {
		case <-h.receivedReadKey:
		case <-h.handshakeErrChan:
			return false
		}
		return true
	default:
		panic("unexpected handshake message: ")
	}
}

// ReadHandshakeMessage is called by TLS.
// It blocks until a new handshake message is available.
func (h *cryptoSetup) ReadHandshakeMessage() ([]byte, error) {
	// TODO: add some error handling here (when the session is closed)
	msg, ok := <-h.messageChan
	if !ok {
		return nil, errors.New("error while handling the handshake message")
	}
	return msg, nil
}

func (h *cryptoSetup) SetReadKey(suite *qtls.CipherSuite, trafficSecret []byte) {
	key := crypto.HkdfExpandLabel(suite.Hash(), trafficSecret, "key", suite.KeyLen())
	iv := crypto.HkdfExpandLabel(suite.Hash(), trafficSecret, "iv", suite.IVLen())
	opener := newOpener(suite.AEAD(key, iv), iv)

	switch h.readEncLevel {
	case protocol.EncryptionInitial:
		h.readEncLevel = protocol.EncryptionHandshake
		h.handshakeOpener = opener
		h.logger.Debugf("Installed Handshake Read keys")
	case protocol.EncryptionHandshake:
		h.readEncLevel = protocol.Encryption1RTT
		h.opener = opener
		h.logger.Debugf("Installed 1-RTT Read keys")
	default:
		panic("unexpected read encryption level")
	}
	h.receivedReadKey <- struct{}{}
}

func (h *cryptoSetup) SetWriteKey(suite *qtls.CipherSuite, trafficSecret []byte) {
	key := crypto.HkdfExpandLabel(suite.Hash(), trafficSecret, "key", suite.KeyLen())
	iv := crypto.HkdfExpandLabel(suite.Hash(), trafficSecret, "iv", suite.IVLen())
	sealer := newSealer(suite.AEAD(key, iv), iv)

	switch h.writeEncLevel {
	case protocol.EncryptionInitial:
		h.writeEncLevel = protocol.EncryptionHandshake
		h.handshakeSealer = sealer
		h.logger.Debugf("Installed Handshake Write keys")
	case protocol.EncryptionHandshake:
		h.writeEncLevel = protocol.Encryption1RTT
		h.sealer = sealer
		h.logger.Debugf("Installed 1-RTT Write keys")
	default:
		panic("unexpected write encryption level")
	}
	h.receivedWriteKey <- struct{}{}
}

// WriteRecord is called when TLS writes data
func (h *cryptoSetup) WriteRecord(p []byte) (int, error) {
	switch h.writeEncLevel {
	case protocol.EncryptionInitial:
		// assume that the first WriteRecord call contains the ClientHello
		n, err := h.initialStream.Write(p)
		if !h.clientHelloWritten && h.perspective == protocol.PerspectiveClient {
			h.clientHelloWritten = true
			close(h.clientHelloWrittenChan)
		}
		return n, err
	case protocol.EncryptionHandshake:
		return h.handshakeStream.Write(p)
	default:
		return 0, fmt.Errorf("unexpected write encryption level: %s", h.writeEncLevel)
	}
}

func (h *cryptoSetup) GetSealer() (protocol.EncryptionLevel, Sealer) {
	if h.sealer != nil {
		return protocol.Encryption1RTT, h.sealer
	}
	if h.handshakeSealer != nil {
		return protocol.EncryptionHandshake, h.handshakeSealer
	}
	return protocol.EncryptionInitial, h.initialAEAD
}

func (h *cryptoSetup) GetSealerWithEncryptionLevel(level protocol.EncryptionLevel) (Sealer, error) {
	errNoSealer := fmt.Errorf("CryptoSetup: no sealer with encryption level %s", level.String())

	switch level {
	case protocol.EncryptionInitial:
		return h.initialAEAD, nil
	case protocol.EncryptionHandshake:
		if h.handshakeSealer == nil {
			return nil, errNoSealer
		}
		return h.handshakeSealer, nil
	case protocol.Encryption1RTT:
		if h.sealer == nil {
			return nil, errNoSealer
		}
		return h.sealer, nil
	default:
		return nil, errNoSealer
	}
}

func (h *cryptoSetup) OpenInitial(dst, src []byte, pn protocol.PacketNumber, ad []byte) ([]byte, error) {
	return h.initialAEAD.Open(dst, src, pn, ad)
}

func (h *cryptoSetup) OpenHandshake(dst, src []byte, pn protocol.PacketNumber, ad []byte) ([]byte, error) {
	if h.handshakeOpener == nil {
		return nil, errors.New("no handshake opener")
	}
	return h.handshakeOpener.Open(dst, src, pn, ad)
}

func (h *cryptoSetup) Open1RTT(dst, src []byte, pn protocol.PacketNumber, ad []byte) ([]byte, error) {
	if h.opener == nil {
		return nil, errors.New("no 1-RTT opener")
	}
	return h.opener.Open(dst, src, pn, ad)
}

func (h *cryptoSetup) ConnectionState() ConnectionState {
	// TODO: return the connection state
	return ConnectionState{}
}
