package client

import (
	"encoding/binary"
	"fmt"
	"io"
	"math/rand"
	"sync"
	"time"
)

// TCP block framing constants.
// After the HTTP handshake completes, SoftEther switches to raw TCP framing:
//
//	Send: uint32(numBlocks) + [uint32(blockSize) + blockData]...
//	Recv: same format
//	Keepalive: uint32(0xFFFFFFFF) + uint32(randSize) + randData
//
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Connection.c#L1654 (TcpSockRecv)
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Connection.c#L1761 (TcpSockSend)
const (
	keepAliveMagic    uint32 = 0xFFFFFFFF // Magic value indicating a keepalive packet
	maxKeepaliveSize  uint32 = 512        // Max random data in keepalive
	keepAliveInterval        = 3 * time.Second
)

// Tunnel handles bidirectional TCP block framing for Ethernet frames over a
// SoftEther VPN session. Each "block" is one Ethernet frame.
type Tunnel struct {
	sess    *Session
	stopCh  chan struct{}
	stopped sync.Once
}

// NewTunnel creates a tunnel from an established session.
// Call StartKeepalive() before reading/writing frames.
func NewTunnel(sess *Session) *Tunnel {
	return &Tunnel{
		sess:   sess,
		stopCh: make(chan struct{}),
	}
}

// Close stops the keepalive goroutine and closes the underlying connection.
func (t *Tunnel) Close() error {
	t.stopped.Do(func() { close(t.stopCh) })
	return t.sess.Conn.Close()
}

// ReadFrames reads a batch of Ethernet frames from the server.
// Returns nil (no error) for keepalive packets. Blocks until data arrives.
func (t *Tunnel) ReadFrames() ([][]byte, error) {
	var numBlocks uint32
	if err := binary.Read(t.sess.Conn, binary.BigEndian, &numBlocks); err != nil {
		return nil, fmt.Errorf("read num blocks: %w", err)
	}

	// Keepalive: server sends 0xFFFFFFFF + uint32(size) + random data
	if numBlocks == keepAliveMagic {
		var size uint32
		if err := binary.Read(t.sess.Conn, binary.BigEndian, &size); err != nil {
			return nil, fmt.Errorf("read keepalive size: %w", err)
		}
		if _, err := io.CopyN(io.Discard, t.sess.Conn, int64(size)); err != nil {
			return nil, fmt.Errorf("discard keepalive: %w", err)
		}
		return nil, nil
	}

	frames := make([][]byte, 0, numBlocks)
	for i := uint32(0); i < numBlocks; i++ {
		var size uint32
		if err := binary.Read(t.sess.Conn, binary.BigEndian, &size); err != nil {
			return nil, fmt.Errorf("read block size: %w", err)
		}
		buf := make([]byte, size)
		if _, err := io.ReadFull(t.sess.Conn, buf); err != nil {
			return nil, fmt.Errorf("read block data: %w", err)
		}
		frames = append(frames, buf)
	}
	return frames, nil
}

// WriteFrames sends a batch of Ethernet frames to the server.
func (t *Tunnel) WriteFrames(frames [][]byte) error {
	if err := binary.Write(t.sess.Conn, binary.BigEndian, uint32(len(frames))); err != nil {
		return fmt.Errorf("write num blocks: %w", err)
	}
	for _, frame := range frames {
		if err := binary.Write(t.sess.Conn, binary.BigEndian, uint32(len(frame))); err != nil {
			return fmt.Errorf("write block size: %w", err)
		}
		if _, err := t.sess.Conn.Write(frame); err != nil {
			return fmt.Errorf("write block data: %w", err)
		}
	}
	return nil
}

// StartKeepalive sends periodic keepalive packets to prevent the server from
// timing out the connection. Must be called after the session enters tunnel mode.
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Connection.c#L1779
func (t *Tunnel) StartKeepalive() {
	go func() {
		ticker := time.NewTicker(keepAliveInterval)
		defer ticker.Stop()
		rng := rand.New(rand.NewSource(time.Now().UnixNano()))
		for {
			select {
			case <-t.stopCh:
				return
			case <-ticker.C:
				size := uint32(rng.Intn(int(maxKeepaliveSize))) + 1
				if err := binary.Write(t.sess.Conn, binary.BigEndian, keepAliveMagic); err != nil {
					return
				}
				if err := binary.Write(t.sess.Conn, binary.BigEndian, size); err != nil {
					return
				}
				randData := make([]byte, size)
				rng.Read(randData)
				if _, err := t.sess.Conn.Write(randData); err != nil {
					return
				}
			}
		}
	}()
}
