package gquic

import (
	"bytes"
	"crypto/tls"
	"errors"
	"net"

	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/quic/gquic-go/internal/handshake"
	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/quic/gquic-go/internal/protocol"
	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/quic/gquic-go/internal/utils"
	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/quic/gquic-go/internal/wire"
	"github.com/bifurcation/mint"
)

type tlsSession struct {
	connID protocol.ConnectionID
	sess   quicSession
}

type serverTLS struct {
	conn            net.PacketConn
	config          *Config
	mintConf        *mint.Config
	params          *handshake.TransportParameters
	cookieGenerator *handshake.CookieGenerator

	newSession func(connection, sessionRunner, protocol.ConnectionID, protocol.ConnectionID, protocol.ConnectionID, protocol.PacketNumber, *Config, *mint.Config, <-chan handshake.TransportParameters, utils.Logger, protocol.VersionNumber) (quicSession, error)

	sessionRunner sessionRunner
	sessionChan   chan<- tlsSession

	logger utils.Logger
}

func newServerTLS(
	conn net.PacketConn,
	config *Config,
	runner sessionRunner,
	tlsConf *tls.Config,
	logger utils.Logger,
) (*serverTLS, <-chan tlsSession, error) {
	cookieGenerator, err := handshake.NewCookieGenerator()
	if err != nil {
		return nil, nil, err
	}
	params := &handshake.TransportParameters{
		StreamFlowControlWindow:     protocol.ReceiveStreamFlowControlWindow,
		ConnectionFlowControlWindow: protocol.ReceiveConnectionFlowControlWindow,
		IdleTimeout:                 config.IdleTimeout,
		MaxBidiStreams:              uint16(config.MaxIncomingStreams),
		MaxUniStreams:               uint16(config.MaxIncomingUniStreams),
		DisableMigration:            true,
		// TODO(#855): generate a real token
		StatelessResetToken: bytes.Repeat([]byte{42}, 16),
	}
	mconf, err := tlsToMintConfig(tlsConf, protocol.PerspectiveServer)
	if err != nil {
		return nil, nil, err
	}

	sessionChan := make(chan tlsSession)
	s := &serverTLS{
		conn:            conn,
		config:          config,
		mintConf:        mconf,
		sessionRunner:   runner,
		sessionChan:     sessionChan,
		cookieGenerator: cookieGenerator,
		params:          params,
		newSession:      newTLSServerSession,
		logger:          logger,
	}
	return s, sessionChan, nil
}

func (s *serverTLS) HandleInitial(p *receivedPacket) {
	// TODO: add a check that DestConnID == SrcConnID
	s.logger.Debugf("<- Received Initial packet.")
	sess, connID, err := s.handleInitialImpl(p)
	if err != nil {
		s.logger.Errorf("Error occurred handling initial packet: %s", err)
		return
	}
	if sess == nil { // a stateless reset was done
		return
	}
	s.sessionChan <- tlsSession{
		connID: connID,
		sess:   sess,
	}
}

func (s *serverTLS) handleInitialImpl(p *receivedPacket) (quicSession, protocol.ConnectionID, error) {
	hdr := p.header
	if len(hdr.Token) == 0 && hdr.DestConnectionID.Len() < protocol.MinConnectionIDLenInitial {
		return nil, nil, errors.New("dropping Initial packet with too short connection ID")
	}
	if len(hdr.Raw)+len(p.data) < protocol.MinInitialPacketSize {
		return nil, nil, errors.New("dropping too small Initial packet")
	}

	var cookie *handshake.Cookie
	if len(hdr.Token) > 0 {
		c, err := s.cookieGenerator.DecodeToken(hdr.Token)
		if err == nil {
			cookie = c
		}
	}
	if !s.config.AcceptCookie(p.remoteAddr, cookie) {
		// Log the Initial packet now.
		// If no Retry is sent, the packet will be logged by the session.
		p.header.Log(s.logger)
		return nil, nil, s.sendRetry(p.remoteAddr, hdr)
	}

	extHandler := handshake.NewExtensionHandlerServer(s.params, s.config.Versions, hdr.Version, s.logger)
	mconf := s.mintConf.Clone()
	mconf.ExtensionHandler = extHandler

	connID, err := protocol.GenerateConnectionID(s.config.ConnectionIDLength)
	if err != nil {
		return nil, nil, err
	}
	s.logger.Debugf("Changing connection ID to %s.", connID)
	sess, err := s.newSession(
		&conn{pconn: s.conn, currentAddr: p.remoteAddr},
		s.sessionRunner,
		hdr.DestConnectionID,
		hdr.SrcConnectionID,
		connID,
		1,
		s.config,
		mconf,
		extHandler.GetPeerParams(),
		s.logger,
		hdr.Version,
	)
	if err != nil {
		return nil, nil, err
	}
	go sess.run()
	sess.handlePacket(p)
	return sess, connID, nil
}

func (s *serverTLS) sendRetry(remoteAddr net.Addr, hdr *wire.Header) error {
	token, err := s.cookieGenerator.NewToken(remoteAddr)
	if err != nil {
		return err
	}
	connID, err := protocol.GenerateConnectionID(s.config.ConnectionIDLength)
	if err != nil {
		return err
	}
	replyHdr := &wire.Header{
		IsLongHeader:         true,
		Type:                 protocol.PacketTypeRetry,
		Version:              hdr.Version,
		SrcConnectionID:      connID,
		DestConnectionID:     hdr.SrcConnectionID,
		OrigDestConnectionID: hdr.DestConnectionID,
		Token:                token,
	}
	s.logger.Debugf("Changing connection ID to %s.\n-> Sending Retry", connID)
	replyHdr.Log(s.logger)
	buf := &bytes.Buffer{}
	if err := replyHdr.Write(buf, protocol.PerspectiveServer, hdr.Version); err != nil {
		return err
	}
	if _, err := s.conn.WriteTo(buf.Bytes(), remoteAddr); err != nil {
		s.logger.Debugf("Error sending Retry: %s", err)
	}
	return nil
}
