// Package store provides ...
package store

import (
	"context"
	"sort"
	"time"

	"github.com/eiblog/eiblog/pkg/model"

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

// example:
//   driver: mongodb
//   source: mongodb://localhost:27017

const (
	mongoDBName       = "eiblog"
	collectionAccount = "account"
	collectionArticle = "article"
	collectionBlogger = "blogger"
	collectionCounter = "counter"
	collectionSerie   = "serie"

	counterNameSerie   = "serie"
	counterNameArticle = "article"
)

type mongodb struct {
	*mongo.Client
}

// Init init mongodb client
func (db *mongodb) Init(name, source string) (Store, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	opts := options.Client().ApplyURI(source)
	client, err := mongo.Connect(ctx, opts)
	if err != nil {
		return nil, err
	}
	err = client.Ping(ctx, readpref.Primary())
	if err != nil {
		return nil, err
	}
	db.Client = client
	// create index
	indexModel := mongo.IndexModel{
		Keys:    bson.D{bson.E{Key: "username", Value: 1}},
		Options: options.Index().SetUnique(true).SetSparse(true),
	}
	db.Database(mongoDBName).Collection(collectionAccount).
		Indexes().
		CreateOne(context.Background(), indexModel)
	indexModel = mongo.IndexModel{
		Keys:    bson.D{bson.E{Key: "slug", Value: 1}},
		Options: options.Index().SetUnique(true).SetSparse(true),
	}
	db.Database(mongoDBName).Collection(collectionArticle).
		Indexes().
		CreateOne(context.Background(), indexModel)
	indexModel = mongo.IndexModel{
		Keys:    bson.D{bson.E{Key: "slug", Value: 1}},
		Options: options.Index().SetUnique(true).SetSparse(true),
	}
	db.Database(mongoDBName).Collection(collectionSerie).
		Indexes().
		CreateOne(context.Background(), indexModel)
	return db, nil
}

// LoadInsertBlogger 读取或创建博客
func (db *mongodb) LoadInsertBlogger(ctx context.Context,
	blogger *model.Blogger) (created bool, err error) {

	collection := db.Database(mongoDBName).Collection(collectionBlogger)

	filter := bson.M{}
	result := collection.FindOne(ctx, filter)
	err = result.Err()
	if err != nil {
		if err != mongo.ErrNoDocuments {
			return
		}
		_, err = collection.InsertOne(ctx, blogger)
		created = true
	} else {
		err = result.Decode(blogger)
	}
	return
}

// UpdateBlogger 更新博客
func (db *mongodb) UpdateBlogger(ctx context.Context,
	fields map[string]interface{}) error {

	collection := db.Database(mongoDBName).Collection(collectionBlogger)

	filter := bson.M{}
	params := bson.M{}
	for k, v := range fields {
		params[k] = v
	}
	update := bson.M{"$set": params}
	_, err := collection.UpdateOne(ctx, filter, update)
	return err
}

// LoadInsertAccount 读取或创建账户
func (db *mongodb) LoadInsertAccount(ctx context.Context,
	acct *model.Account) (created bool, err error) {

	collection := db.Database(mongoDBName).Collection(collectionAccount)

	filter := bson.M{"username": acct.Username}
	result := collection.FindOne(ctx, filter)
	err = result.Err()
	if err != nil {
		if err != mongo.ErrNoDocuments {
			return
		}
		_, err = collection.InsertOne(ctx, acct)
		created = true
	} else {
		err = result.Decode(acct)
	}
	return
}

// UpdateAccount 更新账户
func (db *mongodb) UpdateAccount(ctx context.Context, name string,
	fields map[string]interface{}) error {

	collection := db.Database(mongoDBName).Collection(collectionAccount)

	filter := bson.M{"username": name}
	params := bson.M{}
	for k, v := range fields {
		params[k] = v
	}
	update := bson.M{"$set": params}
	_, err := collection.UpdateOne(ctx, filter, update)
	return err
}

// InsertSerie 创建专题
func (db *mongodb) InsertSerie(ctx context.Context, serie *model.Serie) error {
	collection := db.Database(mongoDBName).Collection(collectionSerie)

	serie.ID = db.nextValue(ctx, counterNameSerie)
	_, err := collection.InsertOne(ctx, serie)
	return err
}

// RemoveSerie 删除专题
func (db *mongodb) RemoveSerie(ctx context.Context, id int) error {
	collection := db.Database(mongoDBName).Collection(collectionSerie)

	filter := bson.M{"id": id}
	_, err := collection.DeleteOne(ctx, filter)
	return err
}

// UpdateSerie 更新专题
func (db *mongodb) UpdateSerie(ctx context.Context, id int,
	fields map[string]interface{}) error {

	collection := db.Database(mongoDBName).Collection(collectionSerie)

	filter := bson.M{"id": id}
	params := bson.M{}
	for k, v := range fields {
		params[k] = v
	}
	update := bson.M{"$set": params}
	_, err := collection.UpdateOne(ctx, filter, update)
	return err
}

// LoadAllSerie 查询所有专题
func (db *mongodb) LoadAllSerie(ctx context.Context) (model.SortedSeries, error) {
	collection := db.Database(mongoDBName).Collection(collectionSerie)

	opts := options.Find().SetSort(bson.M{"id": -1})
	filter := bson.M{}
	cur, err := collection.Find(ctx, filter, opts)
	if err != nil {
		return nil, err
	}
	defer cur.Close(ctx)

	var series model.SortedSeries
	for cur.Next(ctx) {
		obj := model.Serie{}
		err = cur.Decode(&obj)
		if err != nil {
			return nil, err
		}
		series = append(series, &obj)
	}
	return series, nil
}

// InsertArticle 创建文章
func (db *mongodb) InsertArticle(ctx context.Context, article *model.Article, startID int) error {
	// 可手动分配ID或者分配ID, 占位至起始id
	for article.ID == 0 {
		id := db.nextValue(ctx, counterNameArticle)
		if id < startID {
			continue
		} else {
			article.ID = id
		}
	}

	collection := db.Database(mongoDBName).Collection(collectionArticle)
	_, err := collection.InsertOne(ctx, article)
	return err
}

// RemoveArticle 硬删除文章
func (db *mongodb) RemoveArticle(ctx context.Context, id int) error {
	collection := db.Database(mongoDBName).Collection(collectionArticle)

	filter := bson.M{"id": id}
	_, err := collection.DeleteOne(ctx, filter)
	return err
}

// CleanArticles 清理回收站文章
func (db *mongodb) CleanArticles(ctx context.Context, exp time.Time) error {
	collection := db.Database(mongoDBName).Collection(collectionArticle)

	// 超过两天自动删除
	filter := bson.M{"deleted_at": bson.M{"$gt": time.Time{}, "$lt": exp}}
	_, err := collection.DeleteMany(ctx, filter)
	return err
}

// UpdateArticle 更新文章
func (db *mongodb) UpdateArticle(ctx context.Context, id int,
	fields map[string]interface{}) error {

	collection := db.Database(mongoDBName).Collection(collectionArticle)

	filter := bson.M{"id": id}
	params := bson.M{}
	for k, v := range fields {
		params[k] = v
	}
	update := bson.M{"$set": params}
	_, err := collection.UpdateOne(ctx, filter, update)
	return err
}

// LoadArticle 查找文章
func (db *mongodb) LoadArticle(ctx context.Context, id int) (*model.Article, error) {
	collection := db.Database(mongoDBName).Collection(collectionArticle)

	filter := bson.M{"id": id}
	result := collection.FindOne(ctx, filter)
	err := result.Err()
	if err != nil {
		return nil, err
	}
	article := &model.Article{}
	err = result.Decode(article)
	return article, err
}

// LoadArticleList 获取文章列表
func (db *mongodb) LoadArticleList(ctx context.Context, search SearchArticles) (
	model.SortedArticles, int, error) {
	collection := db.Database(mongoDBName).Collection(collectionArticle)

	filter := bson.M{}
	for k, v := range search.Fields {
		switch k {
		case SearchArticleDraft:
			if ok := v.(bool); ok {
				filter["is_draft"] = true
			} else {
				filter["is_draft"] = false
				filter["deleted_at"] = bson.M{"$eq": time.Time{}}
			}
		case SearchArticleTitle:
			filter["title"] = bson.M{
				"$regex":   v.(string),
				"$options": "$i",
			}
		case SearchArticleSerieID:
			filter["serie_id"] = v.(int)
		case SearchArticleTrash:
			filter["deleted_at"] = bson.M{"$ne": time.Time{}}
		}
	}
	// search count
	count, err := collection.CountDocuments(ctx, filter)
	if err != nil {
		return nil, 0, err
	}
	opts := options.Find().SetLimit(int64(search.Limit)).
		SetSkip(int64((search.Page - 1) * search.Limit)).
		SetSort(bson.M{"created_at": -1})
	cur, err := collection.Find(ctx, filter, opts)
	if err != nil {
		return nil, 0, err
	}
	defer cur.Close(ctx)

	var articles model.SortedArticles
	for cur.Next(ctx) {
		obj := model.Article{}
		err = cur.Decode(&obj)
		if err != nil {
			return nil, 0, err
		}
		articles = append(articles, &obj)
	}
	sort.Sort(articles)
	return articles, int(count), nil
}

// DropDatabase drop eiblog database
func (db *mongodb) DropDatabase(ctx context.Context) error {
	return db.Database(mongoDBName).Drop(ctx)
}

// counter counter
type counter struct {
	Name    string
	NextVal int
}

// nextValue counter value
func (db *mongodb) nextValue(ctx context.Context, name string) int {
	collection := db.Database(mongoDBName).Collection(collectionCounter)

	opts := options.FindOneAndUpdate().SetUpsert(true).
		SetReturnDocument(options.After)
	filter := bson.M{"name": name}
	update := bson.M{"$inc": bson.M{"nextval": 1}}

	next := counter{}
	err := collection.FindOneAndUpdate(ctx, filter, update, opts).Decode(&next)
	if err != nil {
		return -1
	}
	return next.NextVal
}

// register store
func init() {
	Register("mongodb", &mongodb{})
}
