package obfs

import (
	"crypto/rand"
	"encoding/binary"
	"errors"
)

const (
	geckoFlagFragment = 0x80
	geckoHeaderSize   = 5

	geckoMinFragmentChunks = 2
	geckoMaxFragmentChunks = 8
)

var (
	errFrameTruncated = errors.New("gecko frame truncated")
	errFrameInvalid   = errors.New("gecko frame invalid")
)

// frameHeader is a Gecko fragment frame header.
// Wire layout (after Salamander decryption):
//
//	byte 0:   0x80 (fragment marker; low 7 bits reserved)
//	byte 1:   msgID
//	byte 2:   chunkIdx:4 | totalChunks:4
//	byte 3-4: padLen (uint16, big-endian)
//	then padLen random padding bytes, then the chunk payload
type frameHeader struct {
	padLen      uint16
	msgID       uint8
	chunkIdx    uint8 // < totalChunks
	totalChunks uint8 // [2, 8]
}

// encodeFrame writes a frame into out, filling the padding region with random
// bytes. out must be at least geckoHeaderSize + h.padLen + len(payload) long.
func encodeFrame(h frameHeader, payload, out []byte) (int, error) {
	if h.totalChunks < geckoMinFragmentChunks || h.totalChunks > geckoMaxFragmentChunks {
		return 0, errFrameInvalid
	}
	if h.chunkIdx >= h.totalChunks {
		return 0, errFrameInvalid
	}
	needed := geckoHeaderSize + int(h.padLen) + len(payload)
	if len(out) < needed {
		return 0, errFrameTruncated
	}
	out[0] = geckoFlagFragment
	out[1] = h.msgID
	out[2] = h.chunkIdx<<4 | h.totalChunks&0x0f
	binary.BigEndian.PutUint16(out[3:5], h.padLen)
	if _, err := rand.Read(out[geckoHeaderSize : geckoHeaderSize+int(h.padLen)]); err != nil {
		return 0, err
	}
	copy(out[geckoHeaderSize+int(h.padLen):], payload)
	return needed, nil
}

// decodeFrame parses a frame from in. The returned payload is a sub-slice of
// in (zero-copy) covering the bytes after the header and padding.
func decodeFrame(in []byte) (frameHeader, []byte, error) {
	if len(in) < geckoHeaderSize {
		return frameHeader{}, nil, errFrameTruncated
	}
	if in[0]&geckoFlagFragment == 0 {
		return frameHeader{}, nil, errFrameInvalid
	}
	h := frameHeader{
		msgID:       in[1],
		chunkIdx:    in[2] >> 4,
		totalChunks: in[2] & 0x0f,
		padLen:      binary.BigEndian.Uint16(in[3:5]),
	}
	if h.totalChunks < geckoMinFragmentChunks || h.totalChunks > geckoMaxFragmentChunks {
		return frameHeader{}, nil, errFrameInvalid
	}
	if h.chunkIdx >= h.totalChunks {
		return frameHeader{}, nil, errFrameInvalid
	}
	if geckoHeaderSize+int(h.padLen) > len(in) {
		return frameHeader{}, nil, errFrameTruncated
	}
	return h, in[geckoHeaderSize+int(h.padLen):], nil
}
