package orm

import (
	"context"
	"fmt"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.mongodb.org/mongo-driver/v2/mongo/options"
	"reflect"
	"unsafe"
)

// Model - type which contains "static" methods like
// Find, FindOne, etc.
type Model struct {
	Indexes          map[string][]InternalIndex
	Type             reflect.Type
	collection       string
	gridFSReferences map[string]gridFSReference
	idx              int
	references       map[string]Reference
	typeName         string `bson:"-"`
}

// HasID is a simple interface that you must implement
// in your models, using a pointer receiver.
// This allows for more flexibility in cases where
// your ID isn't an ObjectID (e.g., int, uint, string...).
//
// and yes, those darn ugly ObjectIDs are supported :)
type HasID interface {
	Id() any
	SetId(id any)
}

type HasIDSlice []HasID

type IModel interface {
	FindRaw(query interface{}, opts *options.FindOptionsBuilder) (*mongo.Cursor, error)
	Find(query interface{}, opts *options.FindOptionsBuilder) (*Query, error)
	FindByID(id interface{}) (*Query, error)
	FindOne(query interface{}, options *options.FindOneOptionsBuilder) (*Query, error)
	FindPaged(query interface{}, page int64, perPage int64, options *options.FindOptionsBuilder) (*Query, error)

	getColl() *mongo.Collection
	getIdxs() []*mongo.IndexModel
	getParsedIdxs() map[string][]InternalIndex
	getTypeName() string
	setTypeName(str string)
}

func (m *Model) getTypeName() string {
	return m.typeName
}

func (m *Model) setTypeName(str string) {
	m.typeName = str
}

func (m *Model) getColl() *mongo.Collection {
	_, ri, ok := ModelRegistry.HasByName(m.typeName)
	if !ok {
		panic(fmt.Sprintf(errFmtModelNotRegistered, m.typeName))
	}
	return DB.Collection(ri.collection)
}

func (m *Model) getIdxs() []*mongo.IndexModel {
	mi := make([]*mongo.IndexModel, 0)
	if mpi := m.getParsedIdxs(); mpi != nil {
		for _, v := range mpi {
			for _, i := range v {
				mi = append(mi, buildIndex(i))
			}
		}
		return mi
	}
	return nil
}

func (m *Model) getParsedIdxs() map[string][]InternalIndex {
	_, ri, ok := ModelRegistry.HasByName(m.typeName)
	if !ok {
		panic(fmt.Sprintf(errFmtModelNotRegistered, m.typeName))
	}
	return ri.Indexes
}

// FindRaw - find documents satisfying `query` and return a plain mongo cursor.
func (m *Model) FindRaw(query interface{}, opts *options.FindOptionsBuilder) (*mongo.Cursor, error) {
	coll := m.getColl()
	var fo options.FindOptions
	for _, setter := range opts.Opts {
		_ = setter(&fo)
	}
	cursor, err := coll.Find(context.TODO(), query, opts)
	return cursor, err
}

// Find - find all documents satisfying `query`.
// returns a pointer to a Query for further chaining.
func (m *Model) Find(query interface{}, opts *options.FindOptionsBuilder) (*Query, error) {
	qqn := ModelRegistry.new_(m.typeName)
	qqt := reflect.SliceOf(reflect.TypeOf(qqn))
	qqv := reflect.New(qqt)
	qqv.Elem().Set(reflect.MakeSlice(qqt, 0, 0))
	qq := &Query{
		model:      m,
		collection: m.getColl(),
		doc:        qqv.Interface(),
		op:         OP_FIND_ALL,
	}
	q, err := m.FindRaw(query, opts)
	idoc := (*DocumentSlice)(unsafe.Pointer(qqv.Elem().UnsafeAddr()))
	if err == nil {
		rawRes := bson.A{}
		err = q.All(context.TODO(), &rawRes)
		if err == nil {
			idoc.setExists(true)
		}

		qq.rawDoc = rawRes
		err = q.All(context.TODO(), &qq.doc)
		if err != nil {
			qq.reOrganize()
			err = nil
		}
		for _, doc := range *idoc {
			doc.setModel(*m)
		}
	}

	return qq, err
}

// FindPaged - Wrapper around FindAll with the Skip and Limit options populated.
// returns a pointer to a Query for further chaining.
func (m *Model) FindPaged(query interface{}, page int64, perPage int64, opts *options.FindOptionsBuilder) (*Query, error) {
	skipAmt := perPage * (page - 1)
	if skipAmt < 0 {
		skipAmt = 0
	}
	opts.SetSkip(skipAmt).SetLimit(perPage)
	q, err := m.Find(query, opts)
	q.op = OP_FIND_PAGED
	return q, err
}

// FindByID - find a single document by its _id field.
// Wrapper around FindOne with an ID query as its first argument
func (m *Model) FindByID(id interface{}) (*Query, error) {
	return m.FindOne(bson.D{{"_id", id}}, nil)
}

// FindOne - find a single document satisfying `query`.
// returns a pointer to a Query for further chaining.
func (m *Model) FindOne(query interface{}, options *options.FindOneOptionsBuilder) (*Query, error) {
	coll := m.getColl()
	rip := coll.FindOne(context.TODO(), query, options)
	raw := bson.M{}
	err := rip.Decode(&raw)
	if err != nil {
		return nil, err
	}
	qqn := ModelRegistry.new_(m.typeName)
	idoc, ok := qqn.(IDocument)

	qq := &Query{
		collection: m.getColl(),
		rawDoc:     raw,
		doc:        idoc,
		op:         OP_FIND_ONE,
		model:      m,
	}
	qq.rawDoc = raw
	err = rip.Decode(qq.doc)
	if err != nil {
		qq.reOrganize()
		err = nil
	}
	if ok {
		idoc.setExists(true)
		idoc.setModel(*m)
	}
	idoc.setSelf(idoc)
	return qq, err
}

func createBase(d any) (reflect.Value, int, string) {
	var n string
	var ri *Model
	var ok bool

	n, ri, ok = ModelRegistry.HasByName(nameOf(d))

	if !ok {
		ModelRegistry.Model(d)
		n, ri, _ = ModelRegistry.Has(d)
	}
	t := ri.Type
	v := valueOf(d)
	i := ModelRegistry.Index(n)

	r := reflect.New(t)

	r.Elem().Set(v)

	if reflect.ValueOf(d).Kind() == reflect.Pointer {
		r.Elem().Set(reflect.ValueOf(d).Elem())
	} else {
		r.Elem().Set(reflect.ValueOf(d))
	}
	ri.setTypeName(n)
	r.Interface().(IDocument).setModel(*ri)

	return r, i, n
}

// Create creates a new instance of a given Document
// type and returns a pointer to it.
func Create(d any) any {
	r, _, n := createBase(d)
	//df := r.Elem().Field(i)
	dm := r.Interface().(IDocument)
	dm.getModel().setTypeName(n)
	what := r.Interface()

	dm.setSelf(what)
	//df.Set(reflect.ValueOf(dm))
	return what
}

// CreateSlice - convenience method which creates a new slice
// of type *T (where T is a type which embeds Document) and
// returns it
func CreateSlice[T any](d T) []*T {
	r, _, _ := createBase(d)
	rtype := r.Type()
	rslice := reflect.SliceOf(rtype)
	newItem := reflect.New(rslice)
	newItem.Elem().Set(reflect.MakeSlice(rslice, 0, 0))
	return newItem.Elem().Interface().([]*T)
}