package events

import (
	"agent/client/events/opts"
	ibytes "agent/commons/bytes"
	"agent/commons/debug"
	"agent/commons/utils"
	"agent/commons/vars"
	"agent/defines/derrs"
	"agent/defines/dresptype"
	"bytes"
	crand "crypto/rand"
	"encoding/json"
	"io"
	"sync"
)

func EventToStream(event IEvent, binary bool) (stream io.Reader, size int64, closer io.Closer, err error) {
	defer func() {
		if err != nil && closer != nil {
			closer.Close()
			closer = nil
		}
	}()

	// Не используется. Зарезервировано для будущего использования.
	respType := dresptype.Event
	respTypeWidth := ibytes.Width(dresptype.Event)

	// Не отправляем respType, если это event.
	if respType == dresptype.Event {
		respTypeWidth = 0
	}

	// Если binary == false, то event.data кодируется внутри event.body
	jsonEvent, err := event.JsonEvent(binary)
	closer = jsonEvent.Closer
	if err != nil {
		return nil, 0, closer, err
	}

	bodyRaw, err := json.Marshal(jsonEvent.Event)
	if err != nil {
		return nil, 0, closer, err
	}
	dataSizeWidth := ibytes.Width(len(bodyRaw))
	// Размер поля bodySize не должен превышать 3 байта
	if dataSizeWidth > 3 {
		return nil, 0, closer, derrs.NewIncorrectParamsError("event body size is too large")
	}

	trashSize := getTrashSize(event)
	trashSizeWidth := ibytes.Width(trashSize)

	debug.Logf(
		"EventToStream, sig: %s, bodySize: %d, trashSize: %d\n",
		event.Signature(), len(bodyRaw), trashSize,
	)

	header := make([]byte, 1+dataSizeWidth+respTypeWidth+trashSizeWidth+trashSize)
	// Размер поля bodySize находится в [0, 3)
	header[0] |= byte(dataSizeWidth)
	// Размер поля respType находится в [3, 5)
	header[0] |= byte(respTypeWidth) << 3
	// Размер поля trashSize находится в [5, 7)
	header[0] |= byte(trashSizeWidth) << 5
	// Последний бит рандом
	header[0] |= byte(utils.IntN(1)) << 7

	offset := 1
	// Устанавливаем занчения bodySize (может отсутствовать)
	if dataSizeWidth > 0 {
		offset = ibytes.Set(header, offset, len(bodyRaw), dataSizeWidth)
	}
	// Устанавливаем занчения respType (может отсутствовать)
	if respTypeWidth > 0 {
		offset = ibytes.Set(header, offset, int(respType), respTypeWidth)
	}
	// Устанавливаем занчения trashSize (может отсутствовать)
	if trashSize > 0 {
		offset = ibytes.Set(header, offset, trashSize, trashSizeWidth)
		// Добавляем мусор
		if _, err = crand.Read(header[offset:]); err != nil {
			return nil, 0, closer, derrs.Join(err, derrs.NewUnknownError("failed to read random bytes for trash"))
		}
	}

	// Собираем всё вместе
	source := []io.Reader{bytes.NewReader(header), bytes.NewReader(bodyRaw)}
	// Вычисляем размер запроса
	size = int64(len(header)) + int64(len(bodyRaw))

	// Если binary == true, добавляем данные ввиде бинарного потока, а не кодируем их внутри event.body
	if binary && event.DataSize() > 0 {
		source = append(source, jsonEvent.Data)
		size += event.DataSize()
	}

	data := io.MultiReader(source...)

	debug.Log("EventToStream, size:", size)

	return data, size, closer, nil
}

func getTrashSize(event IEvent) (res int) {
	if vars.TestTrash {
		return getTrashSize2(event)
	}

	reqOpts := opts.Request(event)

	min_ := vars.RequestMinTrashSize
	max_ := vars.RequestMaxTrashSize
	min := min_
	max := max_

	if reqOpts.MinTrashSize != nil && *reqOpts.MinTrashSize > -1 {
		min = int(*reqOpts.MinTrashSize)
	}
	if reqOpts.MaxTrashSize != nil && *reqOpts.MaxTrashSize > -1 {
		max = int(*reqOpts.MaxTrashSize)
	}

	if min < 0 {
		min = min_
	}
	if max < 0 {
		max = max_
	}

	if min == max {
		min = 0
	} else if min > max {
		max, min = min, max
	}

	if max > 4096 {
		max = 4096
	}

	if min > 0 || max > 0 {
		return utils.IntN(int(max-min)) + min
	}
	return 0
}

var STORE sync.Map

// Для тестирования, хранит последний размер мусора для каждого события и увеличивает его на 1
func getTrashSize2(event IEvent) (trashSize int) {
	trashSize = 0
	if raw, ok := STORE.Load(event.Signature()); ok {
		trashSize = raw.(int) + 1
	}
	STORE.Store(event.Signature(), trashSize)
	return trashSize
}
