package events

import (
	eopts "agent/client/events/opts"
	"agent/commons/adata"
	ibase64 "agent/commons/base64"
	"agent/commons/debug"
	"agent/commons/utils"
	"agent/commons/utime"
	"agent/defines/derrs"
	"agent/defines/devent/daction"
	"agent/defines/devent/dcategory"
	"agent/defines/devent/dfield"
	"agent/defines/devent/dkind"
	"agent/defines/devent/dtype"
	"bufio"
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"strconv"
	"strings"

	"github.com/google/uuid"
)

type NewEventOpts struct {
	Uuid        uuid.UUID
	Type        EventType
	Priority    EventPriority
	Dest        any
	Data        adata.IData
	DataMaxSize int64
	ErrData     string
	Created     int64
	Cleanup     bool
	Request     *EventRequestOpts
	Response    *EventResponseOpts
}

func NewEvent(
	kind EventKind, category []EventCategory, action EventAction,
	meta map[string]any, opts ...NewEventOpts,
) IEvent {
	raw := &RawEvent{
		Kind:     kind,
		Category: category,
		Action:   action,
	}

	if len(meta) > 0 {
		raw.Meta = meta
	} else {
		raw.Meta = make(map[string]any)
	}

	event := evnt{
		rawEvent: raw,
		opts:     map[any]any{},
	}

	var req *EventRequestOpts
	var resp *EventResponseOpts

	if len(opts) > 0 {
		o := opts[0]
		if o.Uuid != uuid.Nil {
			raw.VirtualUuid = o.Uuid
		}
		if o.Type > 0 {
			raw.Type = o.Type
		}
		if o.Priority > 0 {
			raw.Priority = o.Priority
		}
		if o.Dest != nil {
			eopts.SetDest(&event, o.Dest)
		}
		if o.Data != nil {
			dataSize, err := o.Data.Size()
			if err != nil {
				debug.LogErrf("Event data size error: %v", err)
			}
			if raw.DataSize = dataSize; raw.DataSize > 0 {
				eopts.SetData(&event, o.Data)
			}
		}
		if o.DataMaxSize > 0 {
			raw.DataMaxSize = o.DataMaxSize
		}
		if len(o.ErrData) > 0 {
			eopts.SetErrData(&event, o.ErrData)
		}
		if o.Created > 0 {
			eopts.SetCreated(&event, o.Created)
		}
		if o.Cleanup {
			eopts.SetCleanup(&event, true)
		}

		req = o.Request
		resp = o.Response
	}

	// Обязательные поля, которые содержат параметры для отправки/приема данных.
	// Каждый ивент, может самостоятельно настраивать, как нужно отправлять/получать данные.

	eopts.SetRequest(&event, NewRequest(req))
	eopts.SetResponse(&event, NewResponse(resp))

	return &event
}

func NewEvent2(rawEvent *RawEvent, data adata.IData) (event IEvent, err error) {
	if data == nil && len(rawEvent.Data) > 0 {
		eventData, err := ibase64.DecodeString(rawEvent.Data)
		if err != nil {
			return nil, err
		}
		data = adata.NewDataFromMem(eventData)
	}
	event = NewEvent(
		rawEvent.Kind, rawEvent.Category, rawEvent.Action, rawEvent.Meta,
		NewEventOpts{
			Uuid: rawEvent.VirtualUuid,
			Type: rawEvent.Type,
			Data: data,
		},
	)
	return event, nil
}

type NewResultEventOpts struct {
	Meta    map[string]any
	Data    adata.IData
	ErrData string
	Cleanup bool
}

// NewResultEvent используется для создания результирующих ивентов к сервису.
// Такие ивенты содержат только Uuid + Type + [Meta|Data|ErrData].
func NewResultEvent(event IEvent, eventType EventType, opts ...NewResultEventOpts) IEvent {
	resEvent := RawEvent{
		VirtualUuid: event.Uuid(),
	}
	return NewResultEvent2(&resEvent, eventType, opts...)
}

// NewResultEvent2 используется для создания результирующих ивентов к сервису.
// Такие ивенты содержат только Uuid + Type + [Meta|Data|ErrData].
func NewResultEvent2(rawEvent *RawEvent, eventType EventType, opts ...NewResultEventOpts) IEvent {
	var cleanup bool
	var data adata.IData
	var errData string
	var meta map[string]any

	if len(opts) > 0 {
		o := opts[0]
		if o.Data != nil {
			data = o.Data
		}
		if len(o.ErrData) > 0 {
			errData = o.ErrData
		}
		if o.Meta != nil {
			meta = o.Meta
		}
		cleanup = o.Cleanup
	}

	return NewEvent(
		0, nil, 0, meta,
		NewEventOpts{
			Response: &EventResponseOpts{
				// При отправке реузльтатов, мы не ожидаем ответов
				Exist: utils.PointerTo(false),
			},
			Uuid:    rawEvent.VirtualUuid,
			Type:    EventType(eventType),
			Data:    data,
			ErrData: errData,
			Cleanup: cleanup,
		},
	)
}

// NewRawResponseEvent используется для возврата ответов, которые были отправлены с пометкой Raw.
func NewRawResponseEvent(data adata.IData) IEvent {
	return NewEvent(
		dkind.None, []EventCategory{dcategory.Event, dcategory.Raw, dcategory.Response},
		daction.None, nil, NewEventOpts{
			Uuid: uuid.Nil,
			Data: data,
		},
	)
}

// NewDataEvent используется для возврата ответов, которые были отправлены с пометкой DataOnly.
func NewDataEvent(data adata.IData) IEvent {
	return NewEvent(
		dkind.None, []EventCategory{dcategory.Event, dcategory.Data},
		daction.None, nil, NewEventOpts{
			Uuid: uuid.Nil,
			Data: data,
		},
	)
}

func IsDataEvent(event IEvent) bool {
	if event == nil {
		return false
	}
	return utils.SliceEqual(event.Category(), []EventCategory{dcategory.Event, dcategory.Data})
}

func NewErrEvent(errData ...any) IEvent {
	opts := NewEventOpts{
		Uuid: uuid.Nil,
	}
	if len(errData) > 0 {
		switch err := errData[0].(type) {
		case string:
			opts.ErrData = err
		case error:
			opts.ErrData = err.Error()
		default:
			opts.ErrData = fmt.Sprintf("%v", err)
		}
	}
	return NewEvent(
		dkind.None, []EventCategory{dcategory.Event, dcategory.Error},
		daction.None, nil, opts,
	)
}

func IsResultEvent(event IEvent) bool {
	if event == nil {
		return false
	}
	return event.Type() == dtype.Result
}

func IsResultErrorEvent(event IEvent) bool {
	if event == nil {
		return false
	}
	return event.Type() == dtype.ResultError
}

func IsErrEvent(event IEvent) bool {
	if event == nil {
		return false
	}
	if _, ok := eopts.ErrData(event); ok {
		return true
	}
	if event.Category() != nil && len(event.Category()) > 0 {
		if utils.SliceEqual(event.Category(), []EventCategory{dcategory.Event, dcategory.Error}) {
			return true
		}
		if utils.SliceEqual(event.Category(), []EventCategory{dcategory.Service, dcategory.Internal, dcategory.Error}) {
			return true
		}
	}
	return false
}

func IsLoginEvent(event IEvent) bool {
	if event == nil {
		return false
	}
	return event.Action() == daction.Login
}

func IsPingEvent(event IEvent) bool {
	if event == nil {
		return false
	}
	return event.Action() == daction.Ping
}

func IsCheckHealthEvent(event IEvent) bool {
	if event == nil {
		return false
	}
	if len(event.Category()) != 2 {
		return false
	}
	category := event.Category()
	if category[0] == dcategory.Service && category[1] == dcategory.Health {
		return event.Action() == daction.Check
	}
	return false
}

type evnt struct {
	rawEvent *RawEvent
	opts     map[any]any
}

func (e *evnt) CmdId() int {
	return e.rawEvent.CmdId
}

func (e *evnt) CmdTag() string {
	return e.rawEvent.CmdTag
}

func (e *evnt) Signature() string {
	var items []string
	if e.rawEvent.Kind > 0 {
		items = append(items, "k-"+strconv.Itoa(int(e.rawEvent.Kind)))
	}
	if e.rawEvent.Action > 0 {
		items = append(items, "a-"+strconv.Itoa(int(e.rawEvent.Action)))
	}
	if len(e.rawEvent.Category) > 0 {
		cat := make([]string, 0, len(e.rawEvent.Category))
		for _, v := range e.rawEvent.Category {
			cat = append(cat, strconv.Itoa(int(v)))
		}
		items = append(items, "ct-"+strings.Join(cat, "-"))
	}
	if len(items) > 0 {
		return strings.Join(items, "/")
	}

	if e.rawEvent.CmdId > 0 {
		return "c-" + string(strconv.Itoa(e.rawEvent.CmdId))
	} else if len(e.rawEvent.CmdTag) > 0 {
		return "t-" + e.rawEvent.CmdTag
	} else {
		if e.rawEvent.Type == 0 {
			panic(derrs.NewUnknownError(`rawEvent.Type == 0`))
		}
		return "et-" + strconv.Itoa(int(e.rawEvent.Type))
	}
}

func (e *evnt) Uuid() uuid.UUID {
	return e.rawEvent.VirtualUuid
}

func (e *evnt) Kind() EventKind {
	return e.rawEvent.Kind
}

func (e *evnt) Category() []EventCategory {
	return e.rawEvent.Category
}

func (e *evnt) Action() EventAction {
	return e.rawEvent.Action
}

func (e *evnt) Type() EventType {
	return e.rawEvent.Type
}

func (e *evnt) Status() EventStatus {
	return e.rawEvent.Status
}

func (e *evnt) Priority() EventPriority {
	return e.rawEvent.Priority
}

func (e *evnt) Meta() map[string]any {
	return e.rawEvent.Meta
}

func (e *evnt) MetaSize() int64 {
	data, _ := json.Marshal(e.rawEvent.Meta)
	return int64(len(data))
}

func (e *evnt) Data() (data adata.IData) {
	if e.DataSize() == 0 {
		return nil
	}
	return eopts.Data(e)
}

func (e *evnt) Data2() ([]byte, error) {
	if e.DataSize() == 0 {
		return nil, nil
	}
	xdata := eopts.Data(e)
	return xdata.ReadAll()
}

func (e *evnt) DataSize() int64 {
	return e.rawEvent.DataSize
}

func (e *evnt) SetDataSize(size int64) {
	e.rawEvent.DataSize = size
}

func (e *evnt) DataMaxSize() int64 {
	return e.rawEvent.DataMaxSize
}

func (e *evnt) DataType() EventDataType {
	return e.rawEvent.DataType
}

func (e *evnt) SetOptValue(key any, val any) {
	e.opts[key] = val
}

func (e *evnt) OptValue(key any) (val any, ok bool) {
	val, ok = e.opts[key]
	return
}

func (e *evnt) jsonEvent() map[string]any {
	r := e.rawEvent
	event := map[string]any{}

	if r.VirtualUuid != uuid.Nil {
		event[dfield.VirtualUuid] = r.VirtualUuid
	}
	if r.Kind > 0 {
		event[dfield.Kind] = r.Kind
	}
	if len(r.Category) > 0 {
		event[dfield.Category] = r.Category
	}
	if r.Action > 0 {
		event[dfield.Action] = r.Action
	}
	if r.Type > 0 {
		event[dfield.Type] = r.Type
	}
	if len(r.Meta) > 0 {
		event[dfield.Meta] = r.Meta
	}
	if r.Priority > 0 {
		event[dfield.Priority] = r.Priority
	}
	if dest := eopts.Dest(e); dest != nil {
		event[dfield.Dest] = dest
	}

	if created := eopts.Created(e); created > 0 {
		event[dfield.Created] = created
	} else {
		event[dfield.Created] = utime.NanoNow()
	}

	if r.DataMaxSize > 0 {
		event[dfield.DataMaxSize] = r.DataMaxSize
	}

	// Ошибка идет ввиде "string"
	if errData, ok := eopts.ErrData(e); ok {
		event[dfield.Err] = errData
	}

	// Response указывает сервису, как возвращать данные
	if resp := eopts.Response(e); resp != nil {
		event[dfield.Response] = resp
	}

	return event
}

func (e *evnt) JsonEvent(stream ...bool) (res JsonEvent, err error) {
	event := e.jsonEvent()
	data := eopts.Data(e)

	var eventData io.Reader
	var closer io.Closer

	dataSize, err := data.Size()
	if err != nil {
		return res, err
	}
	// Обновляем размер, он мог измениться (например, если это файл с которым активно работают)
	e.rawEvent.DataSize = dataSize

	if dataSize > 0 {
		if len(stream) > 0 && stream[0] {
			// В этом режиме, клиент сам решает как кодировать и отправлять данные, мы должны отдать ему только Reader
			// с данными
			switch x := data.(type) {
			case adata.IDataFromMem:
				eventData = x
				closer = io.NopCloser(nil)

			case adata.IDataFromFile:
				// Нужно открыть файл, иначе при чтении будет ошибка
				if err := x.Open(); err != nil {
					return res, derrs.Join(err, derrs.NewDataNotFoundError(
						fmt.Sprintf(`file "%s" not found`, x.Path()),
					))
				}
				// Используем StrictLimitReader. Если к моменту отправки ивента:
				// - Файл увеличится в размере, то StrictLimitReader ограничит чтение до dataSize, чтобы не сломать
				// протокол передачи данных.
				// - Файл уменьшится в размере, то при чтении будет ошибка и передача ивента будет прервана с ошибкой.
				stream := utils.NewStrictLimitReader(x, dataSize)
				stream = bufio.NewReaderSize(stream, 4096)
				eventData = stream
				closer = x

			default:
				// Читаем данные в память и отдаем как IDataFromMem
				bdata, err := data.ReadAll()
				if err != nil {
					return res, err
				}
				eventData = bytes.NewBuffer(bdata)
				closer = io.NopCloser(nil)
			}

		} else {
			bdata, err := data.ReadAll()
			if err != nil {
				return res, err
			}
			event[dfield.Data] = ibase64.EncodeToString(bdata)
			// Обновляем размер, он мог измениться (например, если это файл с которым активно работают)
			e.rawEvent.DataSize = int64(len(bdata))
		}

		// Устанавливаем размер данных в jsonEvent
		event[dfield.DataSize] = e.rawEvent.DataSize
	}

	return JsonEvent{
		Event:  event,
		Data:   eventData,
		Closer: closer,
	}, nil
}
