package utils

import (
	"encoding/json"
	"errors"
	"os"

	"cloud.o-forge.io/core/oc-lib/dbs"
	"cloud.o-forge.io/core/oc-lib/dbs/mongo"
	"github.com/google/uuid"
	mgb "go.mongodb.org/mongo-driver/mongo"
)

type Owner struct {
	Name string `json:"name,omitempty" bson:"name,omitempty"`
	Logo string `json:"logo,omitempty" bson:"logo,omitempty"`
}

func VerifyAccess(a Accessor, id string) error {
	if a == nil {
		return errors.New("no accessor to verify access")
	}
	data, _, err := a.LoadOne(id)
	if err != nil {
		return err
	}
	if a.ShouldVerifyAuth() && !data.VerifyAuth("get", a.GetRequest()) {
		return errors.New("you are not allowed to access :" + a.GetType().String())
	}
	return nil
}

// GenericLoadOne loads one object from the database (generic)
func GenericStoreOne(data DBObject, a Accessor) (DBObject, int, error) {
	if data.GetID() == "" {
		data.GenerateID()
	}
	data.StoreDraftDefault()
	data.UpToDate(a.GetUser(), a.GetPeerID(), true)
	data.Unsign()
	data.Sign()
	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 a.ShouldVerifyAuth() && !data.VerifyAuth("store", a.GetRequest()) {
		return nil, 403, errors.New("you are not allowed to access : " + a.GetType().String())
	}
	if cursor, _, _ := a.Search(&f, "", data.IsDrafted(), 0, 10); len(cursor) > 0 {
		return nil, 409, errors.New(a.GetType().String() + " with name " + data.GetName() + " already exists")
	}
	err := validate.Struct(data)
	if err != nil {
		return nil, 422, errors.New("error when validating the received struct: " + err.Error())
	}
	id, code, err := mongo.MONGOService.StoreOne(data, data.GetID(), a.GetType().String())
	if err != nil {
		a.GetLogger().Error().Msg("Could not store " + data.GetName() + " to db. Error: " + err.Error())
		return nil, code, err
	}
	result, rcode, rerr := a.LoadOne(id)
	if rerr == nil && result != nil {
		go NotifyChange(a.GetType(), result.GetID(), result, false)
	}
	return result, rcode, rerr
}

// GenericLoadOne loads one object from the database (generic)
func GenericDeleteOne(id string, a Accessor) (DBObject, int, error) {
	res, code, err := a.LoadOne(id)
	if err != nil {
		return res, code, err
	}
	if res == nil {
		return res, code, errors.New("not found")
	}
	return GenericDelete(res, a)
}

func GenericDelete(res DBObject, a Accessor) (DBObject, int, error) {
	if !res.CanDelete() {
		return nil, 403, errors.New("you are not allowed to delete :" + a.GetType().String())
	}
	if a.ShouldVerifyAuth() && !res.VerifyAuth("delete", a.GetRequest()) {
		return nil, 403, errors.New("you are not allowed to access " + a.GetType().String())
	}
	_, code, err := mongo.MONGOService.DeleteOne(res.GetID(), a.GetType().String())
	if err != nil {
		a.GetLogger().Error().Msg("Could not delete " + res.GetID() + " to db. Error: " + err.Error())
		return nil, code, err
	}
	go NotifyChange(a.GetType(), res.GetID(), res, true)
	return res, 200, nil
}

func ModelGenericUpdateOne(change map[string]interface{}, id string, a Accessor) (DBObject, map[string]interface{}, int, error) {
	r, c, err := a.LoadOne(id)
	if err != nil {
		return nil, nil, c, err
	}
	obj := a.NewObj()
	b, _ := json.Marshal(r)
	json.Unmarshal(b, obj)
	if change["is_draft"] == true {
		obj.SetDraft(change["is_draft"] == true)
	}
	if !a.GetRequest().Admin {
		var ok bool
		ok, r = r.CanUpdate(obj)
		if !ok {
			return nil, nil, 403, errors.New("you are not allowed to update :" + a.GetType().String())
		}
		if a.ShouldVerifyAuth() && !r.VerifyAuth("update", a.GetRequest()) {
			return nil, nil, 403, errors.New("you are not allowed to access :" + a.GetType().String())
		}
	}
	r.UpToDate(a.GetUser(), a.GetPeerID(), false)
	if a.GetPeerID() == r.GetCreatorID() {
		r.Unsign()
		r.Sign()
	}

	loaded := r.Serialize(r)   // get the loaded object
	for k, v := range change { // apply the changes, with a flatten method
		loaded[k] = v
	}
	newObj := a.NewObj()
	b, err = json.Marshal(loaded)
	if err != nil {
		return nil, loaded, 400, nil
	}
	err = json.Unmarshal(b, newObj)
	if err != nil {
		return nil, loaded, 400, nil
	}
	return newObj, loaded, 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 GenericUpdateOne(change map[string]interface{}, id string, a Accessor) (DBObject, int, error) {
	obj, loaded, c, err := ModelGenericUpdateOne(change, id, a)

	if err != nil {
		return nil, c, err
	}
	id, code, err := mongo.MONGOService.UpdateOne(obj.Deserialize(loaded, obj), id, a.GetType().String())
	if err != nil {
		a.GetLogger().Error().Msg("Could not update " + id + " to db. Error: " + err.Error())
		return nil, code, err
	}
	result, rcode, rerr := a.LoadOne(id)
	if rerr == nil && result != nil {
		go NotifyChange(a.GetType(), result.GetID(), result, false)
	}
	return result, rcode, rerr
}

func GenericLoadOne[T DBObject](id string, data T, f func(DBObject) (DBObject, int, error), a Accessor) (DBObject, int, error) {
	res_mongo, code, err := mongo.MONGOService.LoadOne(id, a.GetType().String())
	if err != nil {
		return nil, code, err
	}
	if err = res_mongo.Decode(data); err != nil {
		return nil, 400, err
	}
	if a.ShouldVerifyAuth() && !data.VerifyAuth("get", a.GetRequest()) {
		return nil, 403, errors.New("you are not allowed to access :" + a.GetType().String())
	}
	return f(data)
}

func genericLoadAll[T DBObject](res *mgb.Cursor, code int, err error, onlyDraft bool, f func(DBObject) ShallowDBObject, a Accessor) ([]ShallowDBObject, int, error) {
	objs := []ShallowDBObject{}
	var results []T
	if err != nil {
		return nil, code, err
	}
	if err = res.All(mongo.MngoCtx, &results); err != nil {
		return nil, 404, err
	}
	for _, r := range results {
		if (a.ShouldVerifyAuth() && !r.VerifyAuth("get", a.GetRequest())) || f(r) == nil || (onlyDraft && !r.IsDrafted()) || (!onlyDraft && r.IsDrafted()) {
			continue
		}
		objs = append(objs, f(r))
	}
	return objs, 200, nil
}

func GenericLoadAll[T DBObject](f func(DBObject) ShallowDBObject, onlyDraft bool, wfa Accessor, opts ...int64) ([]ShallowDBObject, int, error) {
	offset := int64(0)
	limit := int64(0)
	if len(opts) > 1 {
		offset = opts[0]
	}
	res_mongo, code, err := mongo.MONGOService.LoadAll(wfa.GetType().String(), offset, limit)
	return genericLoadAll[T](res_mongo, code, err, onlyDraft, f, wfa)
}

func GenericSearch[T DBObject](filters *dbs.Filters, search string, defaultFilters *dbs.Filters,
	f func(DBObject) ShallowDBObject, onlyDraft bool, wfa Accessor, opts ...int64) ([]ShallowDBObject, int, error) {
	if filters == nil && search != "" {
		filters = defaultFilters
	}
	offset := int64(0)
	limit := int64(0)
	if len(opts) > 1 {
		offset = opts[0]
	}
	res_mongo, code, err := mongo.MONGOService.Search(filters, wfa.GetType().String(), offset, limit)
	return genericLoadAll[T](res_mongo, code, err, onlyDraft, f, wfa)
}

// GenericLoadOne loads one object from the database (generic)
// json expected in entry is a flatted object no need to respect the inheritance hierarchy
func GenericRawUpdateOne(set DBObject, id string, a Accessor) (DBObject, int, error) {
	id, code, err := mongo.MONGOService.UpdateOne(set, id, a.GetType().String())
	if err != nil {
		a.GetLogger().Error().Msg("Could not update " + id + " to db. Error: " + err.Error())
		return nil, code, err
	}
	result, rcode, rerr := a.LoadOne(id)
	if rerr == nil && result != nil {
		go NotifyChange(a.GetType(), result.GetID(), result, false)
	}
	return result, rcode, rerr
}

func GetMySelf(wfa Accessor) (ShallowDBObject, error) {
	datas, _, _ := wfa.Search(&dbs.Filters{
		And: map[string][]dbs.Filter{
			"relation": {{Operator: dbs.EQUAL.String(), Value: 1}},
		},
	}, "", false, 0, 1)
	if len(datas) > 0 && datas[0] != nil {
		return datas[0], nil
	}
	return nil, errors.New("peer not found")
}

func IsMySelf(peerID string, wfa Accessor) (bool, string) {
	pp, err := GetMySelf(wfa)
	if err != nil || pp == nil {
		return false, ""
	}
	return peerID == pp.GetID(), pp.GetID()
}

func GenerateNodeID() (string, error) {
	folderStatic := "/var/lib/opencloud-node"
	if _, err := os.Stat(folderStatic); err == nil {
		os.MkdirAll(folderStatic, 0644)
	}
	folderStatic += "/node_id"
	if _, err := os.Stat(folderStatic); os.IsNotExist(err) {
		hostname, err := os.Hostname()
		if err != nil {
			return "", err
		}
		id := uuid.NewSHA1(uuid.NameSpaceOID, []byte("oc-"+hostname))
		err = os.WriteFile(folderStatic, []byte(id.String()), 0644)
		return id.String(), err
	}
	data, err := os.ReadFile(folderStatic)
	return string(data), err
}
