package quic

import (
	"context"
	"fmt"
	"io"
	"net"
	"time"

	capnp "zombiezen.com/go/capnproto2"
	"zombiezen.com/go/capnproto2/rpc"

	"github.com/google/uuid"
	"github.com/rs/zerolog"

	"github.com/cloudflare/cloudflared/tunnelrpc"
	tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)

// ProtocolSignature defines the first 6 bytes of the stream, which is used to distinguish the type of stream. It
// ensures whoever performs a handshake does not write data before writing the metadata.
type ProtocolSignature [6]byte

var (
	// DataStreamProtocolSignature is a custom protocol signature for data stream
	DataStreamProtocolSignature = ProtocolSignature{0x0A, 0x36, 0xCD, 0x12, 0xA1, 0x3E}

	// RPCStreamProtocolSignature is a custom protocol signature for RPC stream
	RPCStreamProtocolSignature = ProtocolSignature{0x52, 0xBB, 0x82, 0x5C, 0xDB, 0x65}
)

type protocolVersion string

const (
	protocolV1 protocolVersion = "01"

	protocolVersionLength = 2

	HandshakeIdleTimeout = 5 * time.Second
	MaxIdleTimeout       = 5 * time.Second
	MaxIdlePingPeriod    = 1 * time.Second

	// MaxIncomingStreams is 2^60, which is the maximum supported value by Quic-Go
	MaxIncomingStreams = 1 << 60
)

// RequestServerStream is a stream to serve requests
type RequestServerStream struct {
	io.ReadWriteCloser
}

func NewRequestServerStream(stream io.ReadWriteCloser, signature ProtocolSignature) (*RequestServerStream, error) {
	if signature != DataStreamProtocolSignature {
		return nil, fmt.Errorf("RequestClientStream can only be created from data stream")
	}
	return &RequestServerStream{stream}, nil
}

// ReadConnectRequestData reads the handshake data from a QUIC stream.
func (rss *RequestServerStream) ReadConnectRequestData() (*ConnectRequest, error) {
	// This is a NO-OP for now. We could cause a branching if we wanted to use multiple versions.
	if _, err := readVersion(rss); err != nil {
		return nil, err
	}

	msg, err := capnp.NewDecoder(rss).Decode()
	if err != nil {
		return nil, err
	}

	r := &ConnectRequest{}
	if err := r.fromPogs(msg); err != nil {
		return nil, err
	}
	return r, nil
}

// WriteConnectResponseData writes response to a QUIC stream.
func (rss *RequestServerStream) WriteConnectResponseData(respErr error, metadata ...Metadata) error {
	var connectResponse *ConnectResponse
	if respErr != nil {
		connectResponse = &ConnectResponse{
			Error: respErr.Error(),
		}
	} else {
		connectResponse = &ConnectResponse{
			Metadata: metadata,
		}
	}

	msg, err := connectResponse.toPogs()
	if err != nil {
		return err
	}

	if err := writeDataStreamPreamble(rss); err != nil {
		return err
	}
	return capnp.NewEncoder(rss).Encode(msg)
}

type RequestClientStream struct {
	io.ReadWriteCloser
}

// WriteConnectRequestData writes requestMeta to a stream.
func (rcs *RequestClientStream) WriteConnectRequestData(dest string, connectionType ConnectionType, metadata ...Metadata) error {
	connectRequest := &ConnectRequest{
		Dest:     dest,
		Type:     connectionType,
		Metadata: metadata,
	}

	msg, err := connectRequest.toPogs()
	if err != nil {
		return err
	}

	if err := writeDataStreamPreamble(rcs); err != nil {
		return err
	}
	return capnp.NewEncoder(rcs).Encode(msg)
}

// ReadConnectResponseData reads the response to a RequestMeta in a stream.
func (rcs *RequestClientStream) ReadConnectResponseData() (*ConnectResponse, error) {
	signature, err := DetermineProtocol(rcs)
	if err != nil {
		return nil, err
	}
	if signature != DataStreamProtocolSignature {
		return nil, fmt.Errorf("wrong protocol signature %v", signature)
	}

	// This is a NO-OP for now. We could cause a branching if we wanted to use multiple versions.
	if _, err := readVersion(rcs); err != nil {
		return nil, err
	}

	msg, err := capnp.NewDecoder(rcs).Decode()
	if err != nil {
		return nil, err
	}

	r := &ConnectResponse{}
	if err := r.fromPogs(msg); err != nil {
		return nil, err
	}
	return r, nil
}

// RPCServerStream is a stream to serve RPCs. It is closed when the RPC client is done
type RPCServerStream struct {
	io.ReadWriteCloser
}

func NewRPCServerStream(stream io.ReadWriteCloser, protocol ProtocolSignature) (*RPCServerStream, error) {
	if protocol != RPCStreamProtocolSignature {
		return nil, fmt.Errorf("RPCStream can only be created from rpc stream")
	}
	return &RPCServerStream{stream}, nil
}

func (s *RPCServerStream) Serve(sessionManager tunnelpogs.SessionManager, configManager tunnelpogs.ConfigurationManager, logger *zerolog.Logger) error {
	// RPC logs are very robust, create a new logger that only logs error to reduce noise
	rpcLogger := logger.Level(zerolog.ErrorLevel)
	rpcTransport := tunnelrpc.NewTransportLogger(&rpcLogger, rpc.StreamTransport(s))
	defer rpcTransport.Close()

	main := tunnelpogs.CloudflaredServer_ServerToClient(sessionManager, configManager)
	rpcConn := rpc.NewConn(
		rpcTransport,
		rpc.MainInterface(main.Client),
		tunnelrpc.ConnLog(&rpcLogger),
	)
	defer rpcConn.Close()

	return rpcConn.Wait()
}

func DetermineProtocol(stream io.Reader) (ProtocolSignature, error) {
	signature, err := readSignature(stream)
	if err != nil {
		return ProtocolSignature{}, err
	}
	switch signature {
	case DataStreamProtocolSignature:
		return DataStreamProtocolSignature, nil
	case RPCStreamProtocolSignature:
		return RPCStreamProtocolSignature, nil
	default:
		return ProtocolSignature{}, fmt.Errorf("unknown signature %v", signature)
	}
}

func writeDataStreamPreamble(stream io.Writer) error {
	if err := writeSignature(stream, DataStreamProtocolSignature); err != nil {
		return err
	}

	return writeVersion(stream)
}

func writeVersion(stream io.Writer) error {
	_, err := stream.Write([]byte(protocolV1)[:protocolVersionLength])
	return err
}

func readVersion(stream io.Reader) (string, error) {
	version := make([]byte, protocolVersionLength)
	_, err := stream.Read(version)
	return string(version), err
}

func readSignature(stream io.Reader) (ProtocolSignature, error) {
	var signature ProtocolSignature
	if _, err := io.ReadFull(stream, signature[:]); err != nil {
		return ProtocolSignature{}, err
	}
	return signature, nil
}

func writeSignature(stream io.Writer, signature ProtocolSignature) error {
	_, err := stream.Write(signature[:])
	return err
}

// RPCClientStream is a stream to call methods of SessionManager
type RPCClientStream struct {
	client    tunnelpogs.CloudflaredServer_PogsClient
	transport rpc.Transport

	// Time we wait for the server to respond to a request before we close the connection.
	rpcUnregisterUDPSessionDeadline time.Duration
}

func NewRPCClientStream(ctx context.Context, stream io.ReadWriteCloser, rpcUnregisterUDPSessionDeadline time.Duration, logger *zerolog.Logger) (*RPCClientStream, error) {
	n, err := stream.Write(RPCStreamProtocolSignature[:])
	if err != nil {
		return nil, err
	}
	if n != len(RPCStreamProtocolSignature) {
		return nil, fmt.Errorf("expect to write %d bytes for RPC stream protocol signature, wrote %d", len(RPCStreamProtocolSignature), n)
	}
	transport := tunnelrpc.NewTransportLogger(logger, rpc.StreamTransport(stream))
	conn := rpc.NewConn(
		transport,
		tunnelrpc.ConnLog(logger),
	)
	return &RPCClientStream{
		client:                          tunnelpogs.NewCloudflaredServer_PogsClient(conn.Bootstrap(ctx), conn),
		transport:                       transport,
		rpcUnregisterUDPSessionDeadline: rpcUnregisterUDPSessionDeadline,
	}, nil
}

func (rcs *RPCClientStream) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeIdleAfterHint time.Duration, traceContext string) (*tunnelpogs.RegisterUdpSessionResponse, error) {
	return rcs.client.RegisterUdpSession(ctx, sessionID, dstIP, dstPort, closeIdleAfterHint, traceContext)
}

func (rcs *RPCClientStream) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID, message string) error {
	ctx, cancel := context.WithTimeout(ctx, rcs.rpcUnregisterUDPSessionDeadline)
	defer cancel()
	return rcs.client.UnregisterUdpSession(ctx, sessionID, message)
}

func (rcs *RPCClientStream) UpdateConfiguration(ctx context.Context, version int32, config []byte) (*tunnelpogs.UpdateConfigurationResponse, error) {
	return rcs.client.UpdateConfiguration(ctx, version, config)
}

func (rcs *RPCClientStream) Close() {
	_ = rcs.client.Close()
	_ = rcs.transport.Close()
}
