package repo

import (
	"context"
	"fmt"
	"scnict/db"
	"scnict/log"

	"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 (
	CollectionCategory = "category"
	CollectionPumps    = "pumps"
	CollectionParts    = "parts"
	CollectionMaintain = "maintains"
	CollectionProducts = "products"
	CollectionNews     = "news"
	CollectionRecruits = "recruits"
	CollectionPartners = "partners"
	CollectionBanners  = "banners"
)

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
}

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{}, opts ...*options.UpdateOptions) (*mongo.UpdateResult, error) {

	colls := ctx.Client.GetCollection(collectName)

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

	uid, _ := primitive.ObjectIDFromHex(idstr)

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

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{}, opts ...*options.UpdateOptions) (*mongo.UpdateResult, error) {

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

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

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

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

func RepoUpdateSetDocsProps(ctx *RepoSession, collectName string, query interface{}, update interface{}, opts *options.UpdateOptions) (*mongo.UpdateResult, error) {

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

	return colls.UpdateMany(ctx.Ctx, query, update, opts)
}

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)
	}
	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)
	}
	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)
}
func RepoDocArrayOneRemove1(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
	}
	remove := bson.M{"$pull": setOp}

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

func RepoDocArrayOneDelete(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
	}
	delete := bson.M{"$pull": setOp}

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

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 AggregateLookup struct {
	From         string
	LocalField   string
	ForeignField string
	As           string
}

type AggregateSearchOption struct {
	QueryBeforeLookup Map

	QueryAfterLookup        Map
	CollectName             string
	Lookup                  *AggregateLookup
	UnwindArrayAfterLookup  string
	UnwindArrayBeforeLookup string

	Project Map
	Size    int64
	Page    int64
	Sort    Map
}

func RepoAggregateSearch(ctx *RepoSession, options *AggregateSearchOption, entity interface{}) error {

	colls := ctx.Client.GetCollection(options.CollectName)
	pipe := mongo.Pipeline{}

	if len(options.QueryBeforeLookup) > 0 {
		match := []bson.E{}
		for k, v := range options.QueryBeforeLookup {
			match = append(match, bson.E{Key: k, Value: v})
		}
		matchStage := bson.D{
			{Key: "$match", Value: match},
		}
		pipe = append(pipe, matchStage)
	}

	if len(options.UnwindArrayBeforeLookup) > 0 {
		unwindStage := bson.D{
			{Key: "$unwind", Value: fmt.Sprintf("%s%s", "$", options.UnwindArrayBeforeLookup)},
		}
		pipe = append(pipe, unwindStage)
	}

	if options.Lookup != nil {
		lookup := bson.D{
			{
				Key: "$lookup", Value: bson.M{
					"from":         options.Lookup.From,
					"localField":   options.Lookup.LocalField,
					"foreignField": options.Lookup.ForeignField,
					"as":           options.Lookup.As,
				},
			},
		}
		pipe = append(pipe, lookup)
	}

	if len(options.QueryAfterLookup) > 0 {
		match := []bson.E{}
		for k, v := range options.QueryAfterLookup {
			match = append(match, bson.E{Key: k, Value: v})
		}
		matchStage := bson.D{
			{Key: "$match", Value: match},
		}
		pipe = append(pipe, matchStage)
	}

	if len(options.UnwindArrayAfterLookup) > 0 {
		unwindStage := bson.D{
			{Key: "$unwind", Value: fmt.Sprintf("%s%s", "$", options.UnwindArrayAfterLookup)},
		}
		pipe = append(pipe, unwindStage)
	}

	if len(options.Project) > 0 {
		ProjectStage := bson.D{
			{Key: "$project", Value: options.Project},
		}
		pipe = append(pipe, ProjectStage)
	}

	if len(options.Sort) > 0 {
		sort := bson.D{
			{Key: "$sort", Value: options.Sort},
		}
		pipe = append(pipe, sort)
	}

	if options.Size > 0 {
		page := options.Page
		if page == 0 {
			page = 1
		}
		skip := bson.D{
			{Key: "$skip", Value: (page - 1) * options.Size},
		}
		limit := bson.D{
			{Key: "$limit", Value: options.Size},
		}
		pipe = append(pipe, skip, limit)
	}

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

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

	return curr.All(ctx.Ctx, entity)
}

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

	Page int64
	Size int64
	Sort Map
}

func RepoDocArrayPageSearch(ctx *RepoSession, options *ArrayPageSearchOption, arrayEntity 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)
	}

	if options.Sort != nil {
		sort := bson.D{
			{Key: "$sort", Value: options.Sort},
		}
		pipe = append(pipe, sort)
	}

	if options.Size > 0 {
		page := options.Page
		if page < 1 {
			page = 1
		}

		limit := bson.D{
			{Key: "$limit", Value: options.Size},
		}
		pipe = append(pipe, limit)
		skip := bson.D{
			{Key: "$skip", Value: options.Size * (page - 1)},
		}
		pipe = append(pipe, skip)
	}

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

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

	err = curr.All(ctx.Ctx, arrayEntity)
	if err != nil {
		return err
	}
	return nil
}

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 {
	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 RepoSeachDocsMap(ctx *RepoSession, param *DocsSearchOptions) (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
}

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
}
