package repo

import (
	"box-cost/db"
	"box-cost/log"
	"context"
	"fmt"

	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

type RepoSession struct {
	Ctx    context.Context
	Client *db.MongoDB
}

const (
	CollectionMaterial      = "material"
	CollectionCraft         = "craft"
	CollectionProduct       = "product"
	CollectionSupplier      = "supplier"
	CollectionSupplierPrice = "supplier-price"
	CollectionPack          = "pack"
	CollectionProductPlan   = "product-plan"
	CollectionBillPurchase  = "bill-purchase"
	CollectionBillProduce   = "bill-produce"
	CollectionBillProduct   = "bill-product" // 成品采购

	CollectionSupplierMatprice     = "supplier-mats"
	CollectionSupplierCraftprice   = "supplier-crafts"
	CollectionSupplierProductprice = "supplier-product" // 成品采购
	CollectionIncrement            = "increment"
	CollectionSignature            = "signature"
	CollectionUsers                = "users"
)

type Map map[string]interface{}

type PageResult struct {
	List  []map[string]interface{} `json:"list"`
	Total int64                    `json:"total"`
	Page  int64                    `json:"page"`
	Size  int64                    `json:"size"`
}

type PageSearchOptions struct {
	Db          string
	CollectName string
	Page        int64
	Size        int64
	Query       map[string]interface{}
	Project     []string
	Sort        interface{}
}

type DocSearchOptions struct {
	Db          string
	CollectName string
	Query       Map
	Project     []string
	Sort        bson.M
}
type DocFilterOptions struct {
	Db          string
	CollectName string
	Query       Map
}

func NewDocSearchOptions(filter Map, project []string) *DocSearchOptions {
	return &DocSearchOptions{
		Query:   filter,
		Project: project,
	}
}

func RepoAddDoc(ctx *RepoSession, collectName string, doc interface{}) (string, error) {
	users := ctx.Client.GetCollection(collectName)
	result, err := users.InsertOne(ctx.Ctx, doc)
	if err != nil {
		return "", err
	}
	return result.InsertedID.(primitive.ObjectID).Hex(), nil
}

func RepoDbAddDoc(ctx *RepoSession, dbName string, collectName string, doc interface{}) (string, error) {
	users := ctx.Client.GetDbCollection(dbName, collectName)
	result, err := users.InsertOne(ctx.Ctx, doc)
	if err != nil {
		return "", err
	}
	return result.InsertedID.(primitive.ObjectID).Hex(), nil
}

func RepoDeleteDoc(ctx *RepoSession, collectName string, id string) (interface{}, error) {

	uid, _ := primitive.ObjectIDFromHex(id)

	colls := ctx.Client.GetCollection(collectName)

	return colls.DeleteOne(ctx.Ctx, &bson.M{"_id": uid})
}

func RepoDeleteDbDoc(ctx *RepoSession, dbName string, collectName string, id string) (interface{}, error) {

	uid, _ := primitive.ObjectIDFromHex(id)

	colls := ctx.Client.GetDbCollection(dbName, collectName)

	return colls.DeleteOne(ctx.Ctx, &bson.M{"_id": uid})
}

func RepoDeleteDocs(ctx *RepoSession, collectName string, query interface{}) (interface{}, error) {
	colls := ctx.Client.GetCollection(collectName)

	return colls.DeleteMany(ctx.Ctx, query)
}

func RepoUpdateSetDoc(ctx *RepoSession, collectName string, idstr string, model interface{}) (*mongo.UpdateResult, error) {

	colls := ctx.Client.GetCollection(collectName)
	update := bson.M{"$set": model}

	uid, _ := primitive.ObjectIDFromHex(idstr)

	return colls.UpdateByID(ctx.Ctx, uid, update)
}

func RepoUpdateSeDbDoc(ctx *RepoSession, db string, collectName string, idstr string, model interface{}) (*mongo.UpdateResult, error) {

	colls := ctx.Client.GetDbCollection(db, collectName)

	update := bson.M{"$set": model}

	uid, _ := primitive.ObjectIDFromHex(idstr)

	return colls.UpdateByID(ctx.Ctx, uid, update)
}

func RepoUpdateSetDocProps(ctx *RepoSession, collectName string, idstr string, update interface{}) (*mongo.UpdateResult, error) {

	colls := ctx.Client.GetCollection(collectName)
	// update := bson.M{"$set": model}
	uid, _ := primitive.ObjectIDFromHex(idstr)

	return colls.UpdateByID(ctx.Ctx, uid, update)
}

func RepoUpdateSetDocsProps(ctx *RepoSession, filter *DocFilterOptions, model interface{}) (*mongo.UpdateResult, error) {

	colls := ctx.Client.GetCollection(filter.CollectName)
	if len(filter.Db) > 0 {
		colls = ctx.Client.GetDbCollection(filter.Db, filter.CollectName)
	}
	update := bson.M{"$set": model}

	filterParams := bson.M{}
	if len(filter.Query) > 0 {
		for k, v := range filter.Query {
			if k == "_id" {
				if uid, ok := v.(string); ok {
					docId, _ := primitive.ObjectIDFromHex(uid)
					filterParams["_id"] = docId
					continue
				}
			}
			filterParams[k] = v
		}
	}

	return colls.UpdateMany(ctx.Ctx, filterParams, update)
}

func RepoUpdateSetDbDocProps(ctx *RepoSession, db string, collectName string, idstr string, update interface{}) (*mongo.UpdateResult, error) {

	colls := ctx.Client.GetDbCollection(db, collectName)
	// update := bson.M{"$set": model}
	uid, _ := primitive.ObjectIDFromHex(idstr)
	return colls.UpdateByID(ctx.Ctx, uid, update)
}

func RepoSeachDoc(ctx *RepoSession, param *DocSearchOptions, v interface{}) (bool, error) {

	colls := ctx.Client.GetDbCollection(param.Db, param.CollectName)

	opt := &options.FindOneOptions{}
	if len(param.Project) > 0 {
		prj := bson.M{}
		for _, v := range param.Project {
			prj[v] = 1
		}
		opt.SetProjection(prj)
	}
	if len(param.Sort) > 0 {
		opt.Sort = param.Sort
	}
	filter := bson.M{}
	if len(param.Query) > 0 {
		for k, v := range param.Query {
			if k == "_id" {
				if uid, ok := v.(string); ok {
					docId, _ := primitive.ObjectIDFromHex(uid)
					filter["_id"] = docId
					continue
				}
			}
			filter[k] = v
		}
	}
	err := colls.FindOne(ctx.Ctx, filter, opt).Decode(v)
	if err == mongo.ErrNoDocuments {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	return true, nil
}

func RepoSeachDocMap(ctx *RepoSession, param *DocSearchOptions) (bool, map[string]interface{}) {

	ret := map[string]interface{}{}
	ok := true
	colls := ctx.Client.GetDbCollection(param.Db, param.CollectName)
	opt := &options.FindOneOptions{}
	if len(param.Project) > 0 {
		prj := bson.M{}
		for _, v := range param.Project {
			prj[v] = 1
		}
		opt.SetProjection(prj)
	}
	if len(param.Sort) > 0 {
		opt.Sort = param.Sort
	}
	filter := bson.M{}
	if len(param.Query) > 0 {
		for k, v := range param.Query {
			if k == "_id" {
				if uid, ok := v.(string); ok {
					docId, _ := primitive.ObjectIDFromHex(uid)
					filter["_id"] = docId
					continue
				}
			}
			filter[k] = v
		}
	}
	ok = true
	err := colls.FindOne(ctx.Ctx, filter, opt).Decode(ret)
	if err == mongo.ErrNoDocuments {
		ok = false
	}
	if err != nil {
		ok = false
	}

	return ok, ret
}

// PageSearch 单表分页查询
func RepoPageSearch(ctx *RepoSession, para *PageSearchOptions) (out *PageResult, err error) {

	var colls *mongo.Collection
	if len(para.Db) > 0 {
		colls = ctx.Client.GetDbCollection(para.Db, para.CollectName)
	} else {
		colls = ctx.Client.GetCollection(para.CollectName)
	}
	findoptions := &options.FindOptions{}

	if para.Size > 0 {
		findoptions.SetLimit(para.Size)
		findoptions.SetSkip(para.Size * (para.Page - 1))
	}
	if para.Sort != nil {
		findoptions.SetSort(para.Sort)
	}

	if len(para.Project) > 0 {
		prj := bson.M{}
		for _, v := range para.Project {
			prj[v] = 1
		}
		findoptions.SetProjection(prj)
	}

	filter := bson.M{}
	if len(para.Query) > 0 {
		for k, v := range para.Query {

			if value, ok := v.(string); ok {
				if len(value) > 0 {
					filter[k] = v
					continue
				}
			} else if v != nil {
				filter[k] = v
			}
		}
	}

	cur, err := colls.Find(ctx.Ctx, filter, findoptions)

	out = &PageResult{
		List:  []map[string]interface{}{},
		Total: 0,
		Page:  para.Page,
		Size:  para.Size,
	}

	if err != nil {
		return out, err
	}

	defer cur.Close(ctx.Ctx)

	err = cur.All(ctx.Ctx, &out.List)

	out.Total, _ = colls.CountDocuments(ctx.Ctx, filter)
	return
}

func RepoDbCountDoc(ctx *RepoSession, db string, collectionName string, Query Map) (int64, error) {
	colls := ctx.Client.GetDbCollection(db, collectionName)
	filter := bson.M{}
	if len(Query) > 0 {
		for k, v := range Query {
			if value, ok := v.(string); ok {
				if len(value) > 0 {
					filter[k] = v
					continue
				}
			} else {
				filter[k] = v
			}
		}
	}
	return colls.CountDocuments(ctx.Ctx, filter)
}

func RepoCountDoc(ctx *RepoSession, collectionName string, Query Map) (int64, error) {

	colls := ctx.Client.GetCollection(collectionName)
	filter := bson.M{}
	if len(Query) > 0 {
		for k, v := range Query {
			if value, ok := v.(string); ok {
				if len(value) > 0 {
					filter[k] = v
					continue
				}
			} else {
				filter[k] = v
			}
		}
	}
	return colls.CountDocuments(ctx.Ctx, filter)
}

// PageSearch 单表分页查询
func RepoDocsSearch(ctx *RepoSession, para *PageSearchOptions, out interface{}) (err error) {

	colls := ctx.Client.GetCollection(para.CollectName)
	if len(para.Db) > 0 {
		colls = ctx.Client.GetDbCollection(para.Db, para.CollectName)
	}

	findoptions := &options.FindOptions{}

	if para.Size > 0 {
		findoptions.SetLimit(para.Size)
		findoptions.SetSkip(para.Size * (para.Page - 1))
	}
	if para.Sort != nil {
		findoptions.SetSort(para.Sort)
	}

	if len(para.Project) > 0 {
		prj := bson.M{}
		for _, v := range para.Project {
			prj[v] = 1
		}
		findoptions.SetProjection(prj)
	}

	filter := bson.M{}
	if len(para.Query) > 0 {
		for k, v := range para.Query {

			if value, ok := v.(string); ok {
				if len(value) > 0 {
					filter[k] = v
					continue
				}
			} else {
				filter[k] = v
			}
		}
	}

	cur, err := colls.Find(ctx.Ctx, filter, findoptions)
	if err != nil {
		return err
	}
	defer cur.Close(ctx.Ctx)

	err = cur.All(ctx.Ctx, out)

	return
}

func RepoDocArrayAppend(ctx *RepoSession, collectName string, idstr string, fieldpath string, arrayItem interface{}) (*mongo.UpdateResult, error) {

	colls := ctx.Client.GetCollection(collectName)
	arrayOp := bson.M{}
	arrayOp[fieldpath] = arrayItem

	update := bson.M{"$push": arrayOp}

	uid, _ := primitive.ObjectIDFromHex(idstr)

	return colls.UpdateByID(ctx.Ctx, uid, update)
}

// { _id: 4, "grades.grade": 85 },
// { $set: { "grades.$.std" : 6 } }

type ArrayOneUpdateOption struct {
	Query       Map
	Set         Map
	CollectName string
	Id          string
}

// if len(scene.Stickers) > 0 {
// 	optSet["scenes.$.stickers"] = scene.Stickers
// }

//	option := &repo.ArrayOneUpdateOption{
//		CollectName: repo.CollectionDesigns,
//		Id:          id,
//		Query:       repo.Map{"scenes.id": scene.Id},
//		Set:         optSet,
//	}
func RepoDocArrayOneUpdate(ctx *RepoSession, options *ArrayOneUpdateOption) (*mongo.UpdateResult, error) {

	colls := ctx.Client.GetCollection(options.CollectName)
	docId, _ := primitive.ObjectIDFromHex(options.Id)

	query := bson.M{"_id": docId}
	if len(options.Query) > 0 {
		for k, v := range options.Query {
			query[k] = v
		}
	}

	setOp := bson.M{}
	for k, v := range options.Set {
		setOp[k] = v
	}
	update := bson.M{"$set": setOp}

	return colls.UpdateOne(ctx.Ctx, query, update)
}

type ArrayOneRemoveOption struct {
	ArrayQuery  Map
	CollectName string
	Id          string
}

// { $pull: { "items" : { id: 23 } } }
func RepoDocArrayOneRemove(ctx *RepoSession, options *ArrayOneRemoveOption) (*mongo.UpdateResult, error) {

	colls := ctx.Client.GetCollection(options.CollectName)
	docId, _ := primitive.ObjectIDFromHex(options.Id)

	query := bson.M{"_id": docId}

	arrayQuery := bson.M{}
	if len(options.ArrayQuery) > 0 {
		for k, v := range options.ArrayQuery {
			arrayQuery[k] = v
		}
	}
	update := bson.M{"$pull": arrayQuery}

	return colls.UpdateOne(ctx.Ctx, query, update)
}

type ArrayOneSearchOption struct {
	ArrayQuery  Map
	CollectName string
	Id          string
	Field       string
}

func RepoDocArraySearch(ctx *RepoSession, options *ArrayOneSearchOption, entity interface{}) error {

	colls := ctx.Client.GetCollection(options.CollectName)
	docId, _ := primitive.ObjectIDFromHex(options.Id)

	match := []bson.E{}
	match = append(match, bson.E{"_id", docId})
	matchStage := bson.D{
		{"$match", match},
	}

	unwindStage := bson.D{
		{"$unwind", fmt.Sprintf("%s%s", "$", options.Field)},
	}

	pipe := mongo.Pipeline{matchStage, unwindStage}

	if len(options.ArrayQuery) > 0 {
		match2 := []bson.E{}

		for k, v := range options.ArrayQuery {
			match2 = append(match2, bson.E{k, v})
		}
		match2Stage := bson.D{
			{"$match", match2},
		}
		pipe = append(pipe, match2Stage)
	}

	curr, err := colls.Aggregate(ctx.Ctx, pipe)

	if err != nil {
		return err
	}
	defer curr.Close(ctx.Ctx)

	if curr.Next(ctx.Ctx) {
		err = curr.Decode(entity)
		if err != nil {
			return err
		}
		return nil
	}
	return nil
}

type DocsSearchOptions struct {
	CollectName string
	Query       map[string]interface{}
	Project     []string
	Sort        interface{} //bson.D{ bson.E{"update_time", -1},	bson.E{"goods_id", -1},}
}

func RepoSeachDocsMap(ctx *RepoSession, param *DocsSearchOptions) (ok bool, list []map[string]interface{}) {

	colls := ctx.Client.GetCollection(param.CollectName)
	findoptions := &options.FindOptions{}

	if len(param.Project) > 0 {
		prj := bson.M{}
		for _, v := range param.Project {
			prj[v] = 1
		}
		findoptions.SetProjection(prj)
	}

	if param.Sort != nil {
		findoptions.SetSort(param.Sort)
	}

	filter := bson.M{}
	if len(param.Query) > 0 {
		for k, v := range param.Query {

			if value, ok := v.(string); ok {
				if len(value) > 0 {
					filter[k] = v
					continue
				}
			} else {
				filter[k] = v
			}
		}
	}
	cur, err := colls.Find(ctx.Ctx, filter, findoptions)

	if err != nil {
		ok = false
		return
	}

	defer cur.Close(ctx.Ctx)

	listRes := []map[string]interface{}{}

	err = cur.All(ctx.Ctx, &listRes)

	if err != nil {
		log.Error(err)
		ok = false
		return
	}

	list = listRes
	ok = true
	return
}

type DbDocsSearchOptions struct {
	Db          string
	CollectName string
	Query       map[string]interface{}
	Project     []string
	Sort        interface{} //bson.D{ bson.E{"update_time", -1},	bson.E{"goods_id", -1},}
}

func DbRepoSeachDocsMap(ctx *RepoSession, param *DbDocsSearchOptions) (ok bool, list []map[string]interface{}) {

	colls := ctx.Client.GetDbCollection(param.Db, param.CollectName)
	findoptions := &options.FindOptions{}

	if len(param.Project) > 0 {
		prj := bson.M{}
		for _, v := range param.Project {
			prj[v] = 1
		}
		findoptions.SetProjection(prj)
	}

	if param.Sort != nil {
		findoptions.SetSort(param.Sort)
	}

	filter := bson.M{}
	if len(param.Query) > 0 {
		for k, v := range param.Query {

			if value, ok := v.(string); ok {
				if len(value) > 0 {
					filter[k] = v
					continue
				}
			} else {
				filter[k] = v
			}
		}
	}
	cur, err := colls.Find(ctx.Ctx, filter, findoptions)

	if err != nil {
		ok = false
		return
	}

	defer cur.Close(ctx.Ctx)

	listRes := []map[string]interface{}{}

	err = cur.All(ctx.Ctx, &listRes)

	if err != nil {
		log.Error(err)
		ok = false
		return
	}

	list = listRes
	ok = true
	return
}
