//                           _       _
// __      _____  __ ___   ___  __ _| |_ ___
// \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
//  \ V  V /  __/ (_| |\ V /| | (_| | ||  __/
//   \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
//
//  Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
//
//  CONTACT: hello@weaviate.io
//

package objects

import (
	"encoding/json"
	"fmt"

	"github.com/go-openapi/strfmt"
	"github.com/weaviate/weaviate/entities/models"
	"github.com/weaviate/weaviate/entities/storobj"
)

// VObject is a versioned object for detecting replication inconsistencies
type VObject struct {
	// ID of the Object.
	// Format: uuid
	ID strfmt.UUID `json:"id,omitempty"`

	Deleted bool `json:"deleted"`

	// Timestamp of the last Object update in milliseconds since epoch UTC.
	LastUpdateTimeUnixMilli int64 `json:"lastUpdateTimeUnixMilli,omitempty"`

	// LatestObject is to most up-to-date version of an object
	LatestObject *models.Object `json:"object,omitempty"`

	Vector  []float32      `json:"vector"`
	Vectors models.Vectors `json:"vectors"`

	// StaleUpdateTime is the LastUpdateTimeUnix of the stale object sent to the coordinator
	StaleUpdateTime int64 `json:"updateTime,omitempty"`

	// Version is the most recent incremental version number of the object
	Version uint64 `json:"version"`
}

// vobjectMarshaler is a helper for the methods implementing encoding.BinaryMarshaler
//
// Because models.Object has an optimized custom MarshalBinary method, that is what
// we want to use when serializing, rather than json.Marshal. This is just a thin
// wrapper around the model bytes resulting from the underlying call to MarshalBinary
type vobjectMarshaler struct {
	ID                      strfmt.UUID
	Deleted                 bool
	LastUpdateTimeUnixMilli int64
	StaleUpdateTime         int64
	Version                 uint64
	Vector                  []float32
	Vectors                 models.Vectors
	LatestObject            []byte
}

func (vo *VObject) MarshalBinary() ([]byte, error) {
	b := vobjectMarshaler{
		ID:                      vo.ID,
		Deleted:                 vo.Deleted,
		LastUpdateTimeUnixMilli: vo.LastUpdateTimeUnixMilli,
		StaleUpdateTime:         vo.StaleUpdateTime,
		Vector:                  vo.Vector,
		Vectors:                 vo.Vectors,
		Version:                 vo.Version,
	}
	if vo.LatestObject != nil {
		obj, err := vo.LatestObject.MarshalBinary()
		if err != nil {
			return nil, fmt.Errorf("marshal object: %w", err)
		}
		b.LatestObject = obj
	}

	return json.Marshal(b)
}

func (vo *VObject) UnmarshalBinary(data []byte) error {
	var b vobjectMarshaler

	err := json.Unmarshal(data, &b)
	if err != nil {
		return err
	}

	vo.ID = b.ID
	vo.Deleted = b.Deleted
	vo.LastUpdateTimeUnixMilli = b.LastUpdateTimeUnixMilli
	vo.StaleUpdateTime = b.StaleUpdateTime
	vo.Vector = b.Vector
	vo.Vectors = b.Vectors
	vo.Version = b.Version

	if b.LatestObject != nil {
		var obj models.Object
		err = obj.UnmarshalBinary(b.LatestObject)
		if err != nil {
			return fmt.Errorf("unmarshal object: %w", err)
		}
		vo.LatestObject = &obj
	}

	return nil
}

// Replica represents a replicated data item
type Replica struct {
	ID                      strfmt.UUID     `json:"id,omitempty"`
	Deleted                 bool            `json:"deleted"`
	Object                  *storobj.Object `json:"object,omitempty"`
	LastUpdateTimeUnixMilli int64           `json:"lastUpdateTimeUnixMilli"`
}

// robjectMarshaler is a helper for the methods implementing encoding.BinaryMarshaler
//
// Because *storobj.Object has an optimized custom MarshalBinary method, that is what
// we want to use when serializing, rather than json.Marshal. This is just a thin
// wrapper around the storobj bytes resulting from the underlying call to MarshalBinary
type robjectMarshaler struct {
	ID                      strfmt.UUID
	Deleted                 bool
	LastUpdateTimeUnixMilli int64
	Object                  []byte
}

func (r *Replica) MarshalBinary() ([]byte, error) {
	b := robjectMarshaler{
		ID:                      r.ID,
		Deleted:                 r.Deleted,
		LastUpdateTimeUnixMilli: r.LastUpdateTimeUnixMilli,
	}
	if r.Object != nil {
		obj, err := r.Object.MarshalBinary()
		if err != nil {
			return nil, fmt.Errorf("marshal object: %w", err)
		}
		b.Object = obj
	}

	return json.Marshal(b)
}

func (r *Replica) UnmarshalBinary(data []byte) error {
	var b robjectMarshaler

	err := json.Unmarshal(data, &b)
	if err != nil {
		return err
	}
	r.ID = b.ID
	r.Deleted = b.Deleted
	r.LastUpdateTimeUnixMilli = b.LastUpdateTimeUnixMilli

	if b.Object != nil {
		var obj storobj.Object
		err = obj.UnmarshalBinary(b.Object)
		if err != nil {
			return fmt.Errorf("unmarshal object: %w", err)
		}
		r.Object = &obj
	}

	return nil
}

type Replicas []Replica

func (ro Replicas) MarshalBinary() ([]byte, error) {
	ms := make([]robjectMarshaler, len(ro))

	for i, obj := range ro {
		m := robjectMarshaler{
			ID:                      obj.ID,
			Deleted:                 obj.Deleted,
			LastUpdateTimeUnixMilli: obj.LastUpdateTimeUnixMilli,
		}
		if obj.Object != nil {
			b, err := obj.Object.MarshalBinary()
			if err != nil {
				return nil, fmt.Errorf("marshal object %q: %w", obj.ID, err)
			}
			m.Object = b
		}
		ms[i] = m
	}

	return json.Marshal(ms)
}

func (ro *Replicas) UnmarshalBinary(data []byte) error {
	var ms []robjectMarshaler

	err := json.Unmarshal(data, &ms)
	if err != nil {
		return err
	}

	reps := make(Replicas, len(ms))
	for i, m := range ms {
		rep := Replica{
			ID:                      m.ID,
			Deleted:                 m.Deleted,
			LastUpdateTimeUnixMilli: m.LastUpdateTimeUnixMilli,
		}
		if m.Object != nil {
			var obj storobj.Object
			err = obj.UnmarshalBinary(m.Object)
			if err != nil {
				return fmt.Errorf("unmarshal object %q: %w", m.ID, err)
			}
			rep.Object = &obj
		}
		reps[i] = rep
	}

	*ro = reps
	return nil
}

// UpdateTime return update time if it exists and 0 otherwise
func (r Replica) UpdateTime() int64 {
	if r.Object != nil {
		return r.Object.LastUpdateTimeUnix()
	}
	return r.LastUpdateTimeUnixMilli
}
