package repository

import (
	"context"

	"haixun-backend/internal/library/clock"
	app "haixun-backend/internal/library/errors"
	"haixun-backend/internal/library/errors/code"
	"haixun-backend/internal/model/setting/domain/entity"
	domrepo "haixun-backend/internal/model/setting/domain/repository"

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

type mongoRepository struct {
	collection *mongo.Collection
}

func NewMongoRepository(db *mongo.Database) domrepo.Repository {
	if db == nil {
		return &mongoRepository{}
	}
	return &mongoRepository{collection: db.Collection(entity.CollectionName)}
}

func (r *mongoRepository) EnsureIndexes(ctx context.Context) error {
	if r.collection == nil {
		return nil
	}
	_, err := r.collection.Indexes().CreateOne(ctx, mongo.IndexModel{
		Keys:    bson.D{{Key: "scope", Value: 1}, {Key: "scope_id", Value: 1}, {Key: "key", Value: 1}},
		Options: options.Index().SetUnique(true),
	})
	return err
}

func (r *mongoRepository) List(ctx context.Context, scope, scopeID string, offset, limit int64) ([]*entity.Setting, int64, error) {
	if r.collection == nil {
		return nil, 0, app.For(code.Setting).DBUnavailable("Mongo is not configured")
	}
	filter := bson.M{"scope": scope, "scope_id": scopeID}
	total, err := r.collection.CountDocuments(ctx, filter)
	if err != nil {
		return nil, 0, err
	}
	cursor, err := r.collection.Find(
		ctx,
		filter,
		options.Find().SetSort(bson.D{{Key: "key", Value: 1}}).SetSkip(offset).SetLimit(limit),
	)
	if err != nil {
		return nil, 0, err
	}
	defer cursor.Close(ctx)

	var items []*entity.Setting
	if err := cursor.All(ctx, &items); err != nil {
		return nil, 0, err
	}
	return items, total, nil
}

func (r *mongoRepository) Find(ctx context.Context, scope, scopeID, key string) (*entity.Setting, error) {
	if r.collection == nil {
		return nil, app.For(code.Setting).DBUnavailable("Mongo is not configured")
	}
	var item entity.Setting
	err := r.collection.FindOne(ctx, identityFilter(scope, scopeID, key)).Decode(&item)
	if err == mongo.ErrNoDocuments {
		return nil, app.For(code.Setting).ResNotFound("setting not found")
	}
	if err != nil {
		return nil, err
	}
	return &item, nil
}

func (r *mongoRepository) Upsert(ctx context.Context, setting *entity.Setting) (*entity.Setting, error) {
	if r.collection == nil {
		return nil, app.For(code.Setting).DBUnavailable("Mongo is not configured")
	}
	now := clock.NowUnixNano()
	setting.UpdateAt = now
	if setting.CreateAt == 0 {
		setting.CreateAt = now
	}
	if setting.Version <= 0 {
		setting.Version = 1
	}

	after := options.After
	result := r.collection.FindOneAndUpdate(
		ctx,
		identityFilter(setting.Scope, setting.ScopeID, setting.Key),
		bson.M{
			"$set": bson.M{
				"scope":     setting.Scope,
				"scope_id":  setting.ScopeID,
				"key":       setting.Key,
				"value":     setting.Value,
				"version":   setting.Version,
				"update_at": setting.UpdateAt,
			},
			"$setOnInsert": bson.M{"create_at": setting.CreateAt},
		},
		&options.FindOneAndUpdateOptions{
			Upsert:         ptr(true),
			ReturnDocument: &after,
		},
	)

	var saved entity.Setting
	if err := result.Decode(&saved); err != nil {
		return nil, err
	}
	return &saved, nil
}

func (r *mongoRepository) Delete(ctx context.Context, scope, scopeID, key string) error {
	if r.collection == nil {
		return app.For(code.Setting).DBUnavailable("Mongo is not configured")
	}
	_, err := r.collection.DeleteOne(ctx, identityFilter(scope, scopeID, key))
	return err
}

func identityFilter(scope, scopeID, key string) bson.M {
	return bson.M{"scope": scope, "scope_id": scopeID, "key": key}
}

func ptr[T any](v T) *T {
	return &v
}
