// Copyright 2020 Pilosa Corp.
//
// 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 pg

import (
	"bufio"
	"bytes"
	"context"
	"crypto/tls"
	"encoding/binary"
	"encoding/hex"
	"fmt"
	"io"
	"io/ioutil"
	"net"
	"strings"
	"sync"
	"time"

	"github.com/pilosa/pilosa/v2/pg/message"
	"github.com/pkg/errors"
)

// Protocol is a Postgres protocol version.
type Protocol uint32

const (
	// ProtocolPostgres30 is version 3.0 of the Postgres wire protocol.
	ProtocolPostgres30 Protocol = (3 << 16)

	// ProtocolCancel is the protocol used for query cancellation.
	ProtocolCancel Protocol = (1234 << 16) | 5678

	// ProtocolSSL is the protocol used for SSL upgrades.
	ProtocolSSL Protocol = (1234 << 16) | 5679

	// ProtocolSupported is the main protocol version supported by this package.
	ProtocolSupported Protocol = ProtocolPostgres30
)

// Major returns the major revision of the protocol.
func (p Protocol) Major() uint16 {
	return uint16(p >> 16)
}

// Minor returns the minor revision of the protocol.
func (p Protocol) Minor() uint16 {
	return uint16(p)
}

func (p Protocol) String() string {
	switch p {
	case ProtocolCancel:
		return "cancel"
	case ProtocolSSL:
		return "SSL"
	}

	return fmt.Sprintf("v%d.%d", p.Major(), p.Minor())
}

// handle reads the startup packet and dispatches an appropriate protocol handler for the connection.
func (s *Server) handle(ctx context.Context, conn net.Conn) (err error) {
	var hasTLS bool

	defer func() {
		cerr := conn.Close()
		if cerr != nil && err == nil {
			if hasTLS {
				if nerr, ok := cerr.(net.Error); ok && nerr.Timeout() {
					// TLS does this sometimes.
					return
				}
			}
			err = errors.Wrap(cerr, "closing connection")
		}
	}()

	if tcpconn, ok := conn.(*net.TCPConn); ok {
		// Postgres does not have any real mechanism for confirming that a connection is still alive.
		// Without this, a connection that breaks while idle would live indefinitely.
		// With a TCP keepalive, this should return an error after approximately 2 hours (depending on OS configuration).
		err := tcpconn.SetKeepAlive(true)
		if err != nil {
			return errors.Wrap(err, "enabling TCP keepalive")
		}
	}

	var startupDeadline time.Time
	if s.StartupTimeout > 0 {
		// Set deadline for processing the startup.
		startupDeadline = time.Now().Add(s.StartupTimeout)
		err = conn.SetDeadline(startupDeadline)
		if err != nil {
			return errors.Wrap(err, "setting deadline on protocol startup")
		}
	}

startup:
	// Read startup packet.
	var buf [4]byte
	_, err = io.ReadFull(conn, buf[:])
	if err != nil {
		return errors.Wrap(err, "reading startup message length")
	}
	size := binary.BigEndian.Uint32(buf[:])
	if size < 4 {
		return errors.Errorf("invalid startup packet length: %d bytes", size)
	}
	maxLen := s.MaxStartupSize
	if maxLen == 0 {
		maxLen = 1024 * 1024
	}
	if size > maxLen {
		return errors.Errorf("oversized startup frame of %d bytes (max: %d bytes)", size, maxLen)
	}
	data := make([]byte, size-4)
	_, err = io.ReadFull(conn, data)
	if err != nil {
		return errors.Wrap(err, "reading startup packet")
	}

	// Extract protocol ID.
	if len(data) < 4 {
		return errors.Errorf("startup packet is too small for protocol ID: %d bytes", len(data))
	}
	proto := Protocol(binary.BigEndian.Uint32(data))
	data = data[4:]

	if proto == ProtocolSSL {
		if s.TLSConfig != nil {
			// Upgrade the connection to TLS and renegotiate on the tunneled connection.
			_, err = conn.Write([]byte{'S'})
			if err != nil {
				return errors.Wrap(err, "sending SSL support confirmation")
			}
			conn = tls.Server(conn, s.TLSConfig)
			if s.StartupTimeout > 0 {
				err := conn.SetDeadline(startupDeadline)
				if err != nil {
					return errors.Wrap(err, "transferring startup deadline to TLS connection")
				}
			}
			hasTLS = true
			goto startup
		}

		// Inform the client that SSL is not available and try again.
		s.Logger.Debugf("client at %s requested a secure postgres connection but TLS is not configured", conn.RemoteAddr())
		_, err = conn.Write([]byte{'N'})
		if err != nil {
			return errors.Wrap(err, "sending SSL unsupported notification")
		}
		goto startup
	}

	if s.TLSConfig != nil && !hasTLS {
		// Reject the unsecured connection.
		return errors.Errorf("client at %s attempted to initiate an unsecured postgres conenction", conn.RemoteAddr())
	}

	switch proto {
	case ProtocolCancel:
		// Handle cancellation.
		return s.handleCancel(ctx, conn, data)
	default:
		// Handle regular postgres.
		return s.handleStandard(ctx, proto, conn, data)
	}
}

// parseParams parses a parameter list from a startup packet.
func parseParams(data []byte) (map[string]string, error) {
	params := make(map[string]string)
	for {
		idx := bytes.IndexByte(data, 0)
		switch idx {
		case 0:
			return params, nil
		case -1:
			return nil, errors.New("malformed startup parameter list")
		}

		key := string(data[:idx])
		data = data[idx+1:]

		idx = bytes.IndexByte(data, 0)
		if idx == -1 {
			return nil, errors.New("malformed startup parameter list")
		}
		val := string(data[:idx])
		data = data[idx+1:]

		params[key] = val
	}
}

// handleCancel handles cancel request connections.
func (s *Server) handleCancel(ctx context.Context, conn net.Conn, data []byte) error {
	if len(data) != 8 {
		return errors.New("malformed cancellation packet")
	}

	if s.CancellationManager == nil {
		return errors.New("cancellation is not configured")
	}

	pid := int32(binary.BigEndian.Uint32(data[:4]))
	key := int32(binary.BigEndian.Uint32(data[4:]))

	err := s.CancellationManager.Cancel(CancellationToken{PID: pid, Key: key})
	switch err {
	case nil:
	case ErrCancelledMissingConnection:
		// This is usually not a real error (race condition in the protocol).
		// This can happen if a client cancels a request and shuts down.
		s.Logger.Debugf("client at %v sent a mismatched cancellation token (is a load balancer misconfigured?)", conn.RemoteAddr())
	default:
		return err
	}

	return nil
}

// handleStandard handles a connection in the standard postgres wire protocol.
// The client is responsible for closing the connection when this finishes.
func (s *Server) handleStandard(ctx context.Context, proto Protocol, conn net.Conn, data []byte) error {
	// Wait for helper goroutines to finish.
	var wg sync.WaitGroup
	defer wg.Wait()

	// Set up context.
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	// Check the major version.
	if proto.Major() != ProtocolSupported.Major() {
		return errors.Errorf("unsupported protocol %v", proto)
	}

	// Parse the parameters bundled in the startup packet.
	params, err := parseParams(data)
	if err != nil {
		return errors.Wrap(err, "parsing parameters")
	}

	if user, ok := params["user"]; ok {
		// Log the connection.
		s.Logger.Debugf("new postgres connection from user %q at %v", user, conn.RemoteAddr())
	} else {
		// We do not use this much yet, but the wire protocol says that it is required.
		return errors.New("missing username")
	}

	// Set up message input and output.

	// Set up a reader that will preempt the connection when the context is canceled.
	ir := idleReader{
		conn:    conn,
		timeout: s.ReadTimeout,
	}
	wg.Add(1)
	go func() {
		defer wg.Done()

		<-ctx.Done()

		ir.preempt() //nolint:errcheck
	}()

	// Clear the startup deadline.
	err = conn.SetDeadline(time.Time{})
	if err != nil {
		return err
	}

	// Set up a message reader with buffering.
	rbuf := bufio.NewReader(&ir)
	r := message.NewWireReader(rbuf)

	// Set up a writer on the connection.
	var ww io.Writer = conn
	if s.WriteTimeout != 0 {
		// Apply the write timeout.
		ww = &timeoutWriter{
			conn:    conn,
			timeout: s.WriteTimeout,
		}
	}

	// Set up a message writer with buffering.
	w := message.NewWireWriter(bufio.NewWriter(ww))

	var encoder message.Encoder
	if proto.Minor() > ProtocolSupported.Minor() {
		// Negotiate the version down.
		s.Logger.Debugf("client requested unsupported protocol version %v; attempting to downgrade to %v", proto, ProtocolSupported)
		msg, err := encoder.NegotiateProtocolVersion(int32(ProtocolSupported.Minor()))
		if err != nil {
			return errors.Wrap(err, "negotiating version")
		}
		err = w.WriteMessage(msg)
		if err != nil {
			return errors.Wrap(err, "negotiating version")
		}
	}

	// TODO: real auth
	err = w.WriteMessage(message.AuthenticationOK)
	if err != nil {
		return errors.Wrap(err, "sending authentication confirmation")
	}

	var cancelNotify <-chan struct{}
	if s.CancellationManager != nil {
		notify, cancel, token, err := s.CancellationManager.Token()
		if err != nil {
			return errors.Wrap(err, "setting up cancellation")
		}
		defer cancel()

		msg, err := encoder.BackendKeyData(token.PID, token.Key)
		if err != nil {
			return errors.Wrap(err, "encoding cancellation key data")
		}
		err = w.WriteMessage(msg)
		if err != nil {
			return errors.Wrap(err, "sending cancellation key data")
		}
		cancelNotify = notify
	}

	var queryReady bool
	for {
		if !queryReady {
			// Indicate that we are ready for a query.
			// TODO: provide a valid transaction state.
			msg, err := encoder.ReadyForQuery(message.TransactionStatusActive)
			if err != nil {
				return errors.Wrap(err, "sending query ready status")
			}
			err = w.WriteMessage(msg)
			if err != nil {
				return errors.Wrap(err, "sending query ready status")
			}

			// Flush the write buffer so that the client can respond.
			err = w.Flush()
			if err != nil {
				return errors.Wrap(err, "flushing status")
			}

			if rbuf.Buffered() == 0 {
				// Put the connection into idle mode.
				err = ir.setIdle()
				if err != nil {
					return errors.Wrap(err, "setting idle mode")
				}
			} else {
				// If the client follows the spec, then it should not have sent anything more.
				// However, it seems that no clients completely follow the spec, so we shouldn't rely on anything that isn't entirely straightforward.
				s.Logger.Debugf("postgres client sent additional data without waiting for completion")
			}
		}

		// Read the next packet.
		msg, err := r.ReadMessage()
		if err != nil {
			if err == errPreempted {
				// The server is shutting down.
				return errors.Wrap(s.handleShutdown(
					conn, w, &encoder,

					message.NoticeField{
						Type: message.NoticeFieldSeverity,
						Data: "ERROR",
					},
					message.NoticeField{
						Type: message.NoticeFieldMessage,
						Data: "server shutting down",
					},
					message.NoticeField{
						Type: message.NoticeFieldHint,
						Data: "This is normal. This message is sent when a server is shutting down and terminating its connections.",
					},
				), "processing connection shutdown")
			}

			return err
		}

		switch msg.Type {
		case message.TypeTermination:
			// We are done.
			return w.Flush()

		case message.TypeSimpleQuery:
			// Execute a simple query.

			queryReady = false

			// Parse the query message (a null-terminated string).
			query := SimpleQuery(strings.TrimSuffix(string(msg.Data), "\x00"))

			// Execute the query.
			err := s.handleQuery(w, query, cancelNotify)
			if err != nil {
				return err
			}

		default:
			// The message is not supported yet.
			// Send an error.
			s.Logger.Printf("unrecognized postgres packet %v", msg)
			msg, err = encoder.Error(
				message.NoticeField{
					Type: message.NoticeFieldSeverity,
					Data: "ERROR",
				},
				message.NoticeField{
					Type: message.NoticeFieldMessage,
					Data: fmt.Sprintf("unrecognized message type %q", msg.Type),
				},
				message.NoticeField{
					Type: message.NoticeFieldDetail,
					Data: "message body:" + hex.Dump(msg.Data),
				},
			)
			if err != nil {
				return errors.Wrap(err, "sending unrecognized message error")
			}
			err = w.WriteMessage(msg)
			if err != nil {
				return errors.Wrap(err, "sending unrecognized message error")
			}
			err = w.Flush()
			if err != nil {
				return errors.Wrap(err, "sending unrecognized message error")
			}
		}
	}
}

// handleQuery processes a single query on a connection.
func (s *Server) handleQuery(w message.Writer, query Query, cancelNotify <-chan struct{}) error {
	// Configure cancellation.
	// This is not the connection context, since we want the request to finish safely before connection shutdown.
	ctx := context.Background()
	if cancelNotify != nil {
		defer func() {
			// Flush any cancel notifications.
			// This works on a best-effort basis.
			// It is still entirely possible that the cancel notification may be delivered to the next request.
			// Regardless of what we do, we either get false positives or false negatives.
			// This code chooses false positives.
			for len(cancelNotify) > 0 {
				<-cancelNotify
			}
		}()

		var wg sync.WaitGroup
		defer wg.Add(1)

		var cancel context.CancelFunc
		ctx, cancel = context.WithCancel(ctx)
		defer cancel()

		wg.Add(1)
		go func() {
			defer wg.Done()

			select {
			case <-ctx.Done():
			case <-cancelNotify:
				cancel()
			}
		}()
	}

	// Set up a result writer.
	// SELECT is used as a default tag, which seems to be handled decently by clients.
	// The encoder is intentionally not re-used because its buffer may be huge.
	qwriter := &queryResultWriter{
		w:   w,
		te:  s.TypeEngine,
		tag: "SELECT",
	}

	// Dispatch the query handler.
	qerr := s.QueryHandler.HandleQuery(ctx, qwriter, query)
	if qerr != nil {
		// There was an error in processing the query.
		// Send the error back to the client and keep going.
		s.Logger.Debugf("failed to execute query %q: %v", query, qerr)
		msg, err := qwriter.enc.GoError(qerr)
		if err != nil {
			return errors.Wrap(err, "failed to send query error to client")
		}
		err = w.WriteMessage(msg)
		if err != nil {
			return errors.Wrap(err, "failed to send query error to client")
		}
	} else {
		if !qwriter.wroteHeaders {
			// The handler did not write headers.
			// Write back an empty set of headers.
			err := qwriter.WriteHeader()
			if err != nil {
				return errors.Wrap(err, "sending empty column headers")
			}
		}
		// The query completed normally.
		// Notify the client of completion.
		msg, err := qwriter.enc.CommandComplete(qwriter.tag)
		if err != nil {
			return errors.Wrap(err, "sending command completion notification")
		}
		err = w.WriteMessage(msg)
		if err != nil {
			return errors.Wrap(err, "sending command completion notification")
		}
	}

	// The data will be flushed after we write back the "ready for query" state.
	return nil
}

func (s *Server) handleShutdown(conn net.Conn, w message.Writer, encoder *message.Encoder, notice ...message.NoticeField) error {
	var wg sync.WaitGroup
	defer wg.Wait()

	// Try to send a message to the client before closing the connection.
	msg, err := encoder.Error(notice...)
	if err != nil {
		return errors.Wrap(err, "generating shutdown notification")
	}

	if s.WriteTimeout == 0 {
		// The client is likely to not listen for incoming messages.
		// Force a write timeout to ensure that this terminates.
		err := conn.SetWriteDeadline(time.Now().Add(time.Second))
		if err != nil {
			return errors.Wrap(err, "setting shutdown write deadline")
		}
	}

	// The client may be waiting on a write, so we need to drain the incoming data stream.
	err = conn.SetReadDeadline(time.Time{})
	if err != nil {
		return errors.Wrap(err, "clearing read deadline for shutdown")
	}
	defer conn.SetReadDeadline(time.Now()) //nolint:errcheck
	wg.Add(1)
	go func() {
		defer wg.Done()

		io.Copy(ioutil.Discard, conn) //nolint:errcheck
	}()

	// Attempt to send the shutdown notification.
	// This will fail under many scenarios, as the client is not necessarily reading.
	err = w.WriteMessage(msg)
	if err != nil {
		return nil
	}
	w.Flush()

	return nil
}
