package utils

import (
	"encoding/json"
	"errors"
	"time"

	"cloud.o-forge.io/core/oc-lib/dbs"
	"cloud.o-forge.io/core/oc-lib/dbs/mongo"
	"cloud.o-forge.io/core/oc-lib/logs"
	"cloud.o-forge.io/core/oc-lib/static"
	"cloud.o-forge.io/core/oc-lib/tools"
	"github.com/go-playground/validator/v10"
	"github.com/google/uuid"
	"github.com/rs/zerolog"
)

// single instance of the validator used in every model Struct to validate the fields
var validate = validator.New(validator.WithRequiredStructEnabled())

/*
* AbstractObject is a struct that represents the basic fields of an object
* it defines the object id and name
* every data in base root model should inherit from this struct (only exception is the ResourceModel)
 */
type AbstractObject struct {
	UUID           string    `json:"id,omitempty" bson:"id,omitempty" validate:"required"`
	Name           string    `json:"name,omitempty" bson:"name,omitempty" validate:"required"`
	UpdateDate     time.Time `json:"update_date" bson:"update_date"`
	LastPeerWriter string    `json:"last_peer_writer" bson:"last_peer_writer"`
}

// GetID returns the id of the object (abstract)
func (ao *AbstractObject) GetID() string {
	return ao.UUID
}

// GetName returns the name of the object (abstract)
func (ao *AbstractObject) GetName() string {
	return ao.Name
}

func (ao *AbstractObject) UpToDate() {
	ao.UpdateDate = time.Now()
	ao.LastPeerWriter, _ = static.GetMyLocalJsonPeer()
}

// GetAccessor returns the accessor of the object (abstract)
func (dma *AbstractObject) GetAccessor(caller *tools.HTTPCaller) Accessor {
	return nil
}

func (dma *AbstractObject) Deserialize(j map[string]interface{}) DBObject {
	b, err := json.Marshal(j)
	if err != nil {
		return nil
	}
	json.Unmarshal(b, dma)
	return dma
}

func (dma *AbstractObject) Serialize() map[string]interface{} {
	var m map[string]interface{}
	b, err := json.Marshal(dma)
	if err != nil {
		return nil
	}
	json.Unmarshal(b, &m)
	return m
}

func (r *AbstractObject) GenerateID() {
	if r.UUID == "" {
		r.UUID = uuid.New().String()
	}
}

type AbstractAccessor struct {
	Logger zerolog.Logger    // Logger is the logger of the accessor, it's a specilized logger for the accessor
	Type   string            // Type is the data type of the accessor
	Caller *tools.HTTPCaller // Caller is the http caller of the accessor (optionnal) only need in a peer connection
}

func (dma *AbstractAccessor) GetType() string {
	return dma.Type
}

func (dma *AbstractAccessor) GetCaller() *tools.HTTPCaller {
	return dma.Caller
}

// Init initializes the accessor with the data type and the http caller
func (dma *AbstractAccessor) Init(t tools.DataType, caller *tools.HTTPCaller) {
	dma.Logger = logs.CreateLogger(t.String()) // Create a logger with the data type
	dma.Caller = caller                        // Set the caller
	dma.Type = t.String()                      // Set the data type
}

// GenericLoadOne loads one object from the database (generic)
func (wfa *AbstractAccessor) GenericStoreOne(data DBObject, accessor Accessor) (DBObject, int, error) {
	data.GenerateID()
	f := dbs.Filters{
		Or: map[string][]dbs.Filter{
			"abstractresource.abstractobject.name": {{
				Operator: dbs.LIKE.String(),
				Value:    data.GetName(),
			}},
			"abstractobject.name": {{
				Operator: dbs.LIKE.String(),
				Value:    data.GetName(),
			}},
		},
	}
	if cursor, _, _ := accessor.Search(&f, ""); len(cursor) > 0 {
		return nil, 409, errors.New(accessor.GetType() + " with name " + data.GetName() + " already exists")
	}
	err := validate.Struct(data)
	if err != nil {
		return nil, 422, err
	}
	id, code, err := mongo.MONGOService.StoreOne(data, data.GetID(), wfa.GetType())
	if err != nil {
		wfa.Logger.Error().Msg("Could not store " + data.GetName() + " to db. Error: " + err.Error())
		return nil, code, err
	}
	return accessor.LoadOne(id)
}

// GenericLoadOne loads one object from the database (generic)
func (dma *AbstractAccessor) GenericDeleteOne(id string, accessor Accessor) (DBObject, int, error) {
	res, code, err := accessor.LoadOne(id)
	if err != nil {
		dma.Logger.Error().Msg("Could not retrieve " + id + " to db. Error: " + err.Error())
		return nil, code, err
	}
	_, code, err = mongo.MONGOService.DeleteOne(id, accessor.GetType())
	if err != nil {
		dma.Logger.Error().Msg("Could not delete " + id + " to db. Error: " + err.Error())
		return nil, code, err
	}
	return res, 200, nil
}

// GenericLoadOne loads one object from the database (generic)
// json expected in entry is a flatted object no need to respect the inheritance hierarchy
func (dma *AbstractAccessor) GenericUpdateOne(set DBObject, id string, accessor Accessor, new DBObject) (DBObject, int, error) {
	r, c, err := accessor.LoadOne(id)
	if err != nil {
		return nil, c, err
	}
	change := set.Serialize() // get the changes
	loaded := r.Serialize()   // get the loaded object

	for k, v := range change { // apply the changes, with a flatten method
		loaded[k] = v
	}
	id, code, err := mongo.MONGOService.UpdateOne(new.Deserialize(loaded), id, accessor.GetType())
	if err != nil {
		dma.Logger.Error().Msg("Could not update " + id + " to db. Error: " + err.Error())
		return nil, code, err
	}
	return accessor.LoadOne(id)
}
