package orm

import (
	"context"
	"errors"
	"fmt"
	"github.com/fatih/structtag"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.mongodb.org/mongo-driver/v2/mongo/options"
	"reflect"
	"time"
)

func serializeIDs(input interface{}, isJson bool, populated map[string]bool, parent string) interface{} {
	var key string
	if isJson {
		key = "json"
	} else {
		key = "bson"
	}
	vp := reflect.ValueOf(input)
	mt := reflect.TypeOf(input)
	var ret interface{}
	if vp.Kind() != reflect.Ptr {
		if vp.CanAddr() {
			vp = vp.Addr()
		} else {
			vp = makeSettable(vp, input)
		}
	}

	if mt.Kind() == reflect.Pointer {
		mt = mt.Elem()
	}
	getID := func(bbb interface{}) interface{} {
		mptr := reflect.ValueOf(bbb)
		if mptr.Kind() != reflect.Pointer {
			mptr = makeSettable(mptr, bbb)
		}
		ifc, ok := mptr.Interface().(HasID)
		if ok {
			return ifc.Id()
		} else {
			return nil
		}
	}
	switch vp.Elem().Kind() {
	case reflect.Struct:
		ret0 := bson.M{}
		for i := 0; i < vp.Elem().NumField(); i++ {
			fv := vp.Elem().Field(i)
			ft := mt.Field(i)
			var descent string
			if parent != "" {
				descent = parent + "." + ft.Name
			} else {
				descent = ft.Name
			}
			tag, err := structtag.Parse(string(ft.Tag))
			panik(err)
			bbson, err := tag.Get(key)
			if err != nil || bbson.Name == "-" {
				continue
			}
			if bbson.Name == "" {
				marsh, _ := bson.Marshal(fv.Interface())
				unmarsh := bson.M{}
				bson.Unmarshal(marsh, &unmarsh)
				for k, v := range unmarsh {
					ret0[k] = v
				}
			} else {
				_, terr := tag.Get("ref")
				if reflect.ValueOf(fv.Interface()).Type().Kind() != reflect.Pointer {
					vp1 := reflect.New(fv.Type())
					vp1.Elem().Set(reflect.ValueOf(fv.Interface()))
					fv.Set(vp1.Elem())
				}
				var ip bool
				for k1, v1 := range populated {
					if k1 == descent {
						ip = v1
						break
					}
				}
				if iidoc, ok := input.(IDocument); ok && !ip {
					for k1, v1 := range iidoc.getPopulated() {
						if k1 == descent || k1 == ft.Name {
							ip = v1
							break
						}
					}
				}
				if terr == nil {
					ifc, ok := fv.Interface().(HasID)
					if fv.Kind() == reflect.Slice {
						rarr := bson.A{}
						for j := 0; j < fv.Len(); j++ {
							if !isJson {
								rarr = append(rarr, getID(fv.Index(j).Interface()))
							} else {
								if ip {
									rarr = append(rarr, serializeIDs(fv.Index(j).Interface(), isJson, populated, descent))
								} else {
									rarr = append(rarr, getID(fv.Index(j).Interface()))
								}
							}
						}
						ret0[bbson.Name] = rarr
					} else if !ok {
						panic(fmt.Sprintf("referenced model slice at '%s.%s' does not implement HasID", nameOf(input), ft.Name))
					} else {
						if reflect.ValueOf(ifc).IsNil() || reflect.ValueOf(ifc.Id()).IsZero() {
							ret0[bbson.Name] = nil
						} else {
							if !isJson {
								ret0[bbson.Name] = ifc.Id()
							} else {
								if ip && bbson.Name != "-" {
									ret0[bbson.Name] = serializeIDs(fv.Interface(), isJson, populated, descent)
								} else if bbson.Name != "-" {
									ret0[bbson.Name] = ifc.Id()
								}
							}
						}
					}
				} else {
					if fv.Type() == reflect.TypeFor[time.Time]() {
						ret0[bbson.Name] = fv.Interface()
					} else {
						ret0[bbson.Name] = serializeIDs(fv.Interface(), isJson, populated, descent)
					}
				}
			}

			ret = ret0
		}
	case reflect.Slice:
		ret0 := bson.A{}
		for i := 0; i < vp.Elem().Len(); i++ {

			ret0 = append(ret0, serializeIDs(vp.Elem().Index(i).Addr().Interface(), isJson, populated, parent))
		}
		ret = ret0
	default:
		ret = vp.Elem().Interface()
	}
	return ret
}
func doSave(c *mongo.Collection, isNew bool, opts *SaveOptions, arg interface{}) error {
	var err error
	d, ok := arg.(IDocument)
	if !ok {
		return fmt.Errorf(errFmtNotAModel, nameOf(arg))
	}
	d.SetSelf(d)
	now := time.Now()
	selfo := reflect.ValueOf(d)
	vp := selfo
	if vp.Kind() != reflect.Ptr {
		vp = reflect.New(selfo.Type())
		vp.Elem().Set(selfo)
	}
	var asHasId = vp.Interface().(HasID)
	var asModel = vp.Interface().(IDocument)
	if (isNew && reflect.ValueOf(asHasId.Id()).IsZero()) && opts.SetTimestamps {
		d.setCreated(now)
	}
	if opts.SetTimestamps {
		d.setModified(now)
	}
	idxs := d.getModel().getIdxs()
	for _, i := range idxs {
		_, err = c.Indexes().CreateOne(context.TODO(), *i)
		if err != nil {
			return err
		}
	}

	if isNew {
		nid := getLastInColl(c.Name(), asHasId.Id())
		pnid := incrementInterface(nid)
		if reflect.ValueOf(asHasId.Id()).IsZero() {
			(asHasId).SetId(pnid)
		}
		incrementAll(asHasId)
		_, im, _ := ModelRegistry.HasByName(asModel.getModel().getTypeName())
		_ = gridFsSave(asHasId, *im)

		_, err = c.InsertOne(context.TODO(), d.serializeToStore())
		if err == nil {
			d.setExists(true)
		} else {
			_, err = c.UpdateOne(context.TODO(), bson.D{{Key: "_id", Value: d.(HasID).Id()}}, bson.M{
				"$set": d.serializeToStore(),
			})
			//_, err = c.ReplaceOne(context.TO_DO(), bson.D{{Key: "_id", Value: d.(HasID).Id()}}, d.serializeToStore())
		}
	} else {
		//_, err = c.ReplaceOne(context.TO_DO(), bson.D{{Key: "_id", Value: d.(HasID).Id()}}, d.serializeToStore())
		_, im, _ := ModelRegistry.HasByName(asModel.getModel().getTypeName())
		_ = gridFsSave(asHasId, *im)

		_, err = c.UpdateOne(context.TODO(), bson.D{{Key: "_id", Value: d.(HasID).Id()}}, bson.M{
			"$set": d.serializeToStore(),
		})
		if errors.Is(err, mongo.ErrNoDocuments) {
			_, err = c.InsertOne(context.TODO(), d.serializeToStore())
			if err == nil {
				d.setExists(true)
			}
		}
	}
	return err
}
func doDelete(d *Document, arg interface{}) error {
	self, ok := arg.(HasID)

	if !ok {
		return fmt.Errorf(errFmtNotHasID, nameOf(arg))
	}
	c := d.model.Collection()
	_, err := c.DeleteOne(context.TODO(), bson.M{"_id": self.Id()})
	if err == nil {
		d.exists = false
		err = gridFsDel(arg, *d.model)
	}
	return err
}
func incrementTagged(item interface{}) interface{} {
	rv := reflect.ValueOf(item)
	rt := reflect.TypeOf(item)
	if rv.Kind() != reflect.Pointer {
		rv = makeSettable(rv, item)
	}
	if rt.Kind() == reflect.Pointer {
		rt = rt.Elem()
	}
	if rt.Kind() != reflect.Struct {
		if rt.Kind() == reflect.Slice {
			for i := 0; i < rv.Elem().Len(); i++ {
				incrementTagged(rv.Elem().Index(i).Addr().Interface())
			}
		} else {
			return item
		}
	}
	for i := 0; i < rt.NumField(); i++ {
		structField := rt.Field(i)
		cur := rv.Elem().Field(i)
		tags, err := structtag.Parse(string(structField.Tag))
		if err != nil {
			continue
		}
		incTag, err := tags.Get("autoinc")
		if err != nil {
			continue
		}

		nid := getLastInColl(incTag.Name, cur.Interface())
		if cur.IsZero() {
			coerced := coerceInt(reflect.ValueOf(incrementInterface(nid)), cur)
			if coerced != nil {
				cur.Set(reflect.ValueOf(coerced))
			} else {
				cur.Set(reflect.ValueOf(incrementInterface(nid)))
			}
			counterColl := DB.Collection(COUNTER_COL)
			counterColl.UpdateOne(context.TODO(), bson.M{"collection": incTag.Name}, bson.M{"$set": bson.M{"collection": incTag.Name, "current": cur.Interface()}}, options.UpdateOne().SetUpsert(true))
		}

	}
	return rv.Elem().Interface()
}

func incrementAll(item interface{}) {
	if item == nil {
		return
	}
	vp := reflect.ValueOf(item)
	el := vp
	if vp.Kind() == reflect.Pointer {
		el = vp.Elem()
	}
	if vp.Kind() == reflect.Pointer && vp.IsNil() {
		return
	}
	vt := el.Type()
	switch el.Kind() {
	case reflect.Struct:
		incrementTagged(item)
		for i := 0; i < el.NumField(); i++ {
			fv := el.Field(i)
			fst := vt.Field(i)
			if !fst.IsExported() {
				continue
			}
			incrementAll(fv.Interface())
		}
	case reflect.Slice:
		for i := 0; i < el.Len(); i++ {
			incd := incrementTagged(el.Index(i).Addr().Interface())
			if reflect.ValueOf(incd).Kind() == reflect.Pointer {
				el.Index(i).Set(reflect.ValueOf(incd).Elem())
			} else {
				el.Index(i).Set(reflect.ValueOf(incd))
			}
		}
	default:
	}
}

func (d *Document) markPopulated(field string) {
	d.newPopulationMap()
	d.populatedFields[field] = true
}

func (d *Document) markDepopulated(field string) {
	d.newPopulationMap()
	d.populatedFields[field] = false
}

func (d *Document) newPopulationMap() {
	if d.populatedFields == nil {
		d.populatedFields = make(map[string]bool)
	}
}

func (d *Document) getPopulated() map[string]bool {
	return d.populatedFields
}

func (d *Document) setRaw(raw any) {
	d.raw = raw
}

func (d *Document) getRaw() any {
	return d.raw
}