package tseriesimp

import (
	"context"
	"errors"
	"time"

	"github.com/tech/sendico/pkg/db/repository"
	"github.com/tech/sendico/pkg/db/repository/builder"
	rdecoder "github.com/tech/sendico/pkg/db/repository/decoder"
	tsoptions "github.com/tech/sendico/pkg/db/tseries/options"
	tspoint "github.com/tech/sendico/pkg/db/tseries/point"
	"github.com/tech/sendico/pkg/merrors"
	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.mongodb.org/mongo-driver/v2/mongo/options"
)

type TimeSeries struct {
	options    tsoptions.Options
	collection *mongo.Collection
}

func NewMongoTimeSeriesCollection(ctx context.Context, db *mongo.Database, tsOpts *tsoptions.Options) (*TimeSeries, error) {
	if tsOpts == nil {
		return nil, merrors.InvalidArgument("nil time-series options provided", "options")
	}
	// Configure time-series options
	granularity := tsOpts.Granularity.String()
	ts := &options.TimeSeriesOptionsBuilder{}
	// TimeField:   tsOpts.TimeField,
	// Granularity: &granularity,
	ts.SetTimeField(tsOpts.TimeField)
	ts.SetGranularity(granularity)
	if tsOpts.MetaField != "" {
		ts.SetMetaField(tsOpts.MetaField)
	}

	// Collection options
	collOpts := options.CreateCollection().SetTimeSeriesOptions(ts)

	// Set TTL if requested
	if tsOpts.ExpireAfter > 0 {
		secs := int64(tsOpts.ExpireAfter / time.Second)
		collOpts.SetExpireAfterSeconds(secs)
	}

	if err := db.CreateCollection(ctx, tsOpts.Collection, collOpts); err != nil {
		var cmdErr mongo.CommandError
		if !errors.As(err, &cmdErr) || cmdErr.Code != 48 {
			return nil, err
		}
	}

	return &TimeSeries{collection: db.Collection(tsOpts.Collection), options: *tsOpts}, nil
}

func (ts *TimeSeries) Aggregate(ctx context.Context, pipeline builder.Pipeline, decoder rdecoder.DecodingFunc) error {
	queryFunc := func(ctx context.Context, collection *mongo.Collection) (*mongo.Cursor, error) {
		return collection.Aggregate(ctx, pipeline.Build())
	}
	return ts.executeQuery(ctx, decoder, queryFunc)
}

func (ts *TimeSeries) Insert(ctx context.Context, timePoint tspoint.TimePoint) error {
	_, err := ts.collection.InsertOne(ctx, timePoint)
	return err
}

func (ts *TimeSeries) InsertMany(ctx context.Context, timePoints []tspoint.TimePoint) error {
	docs := make([]any, len(timePoints))
	for i, p := range timePoints {
		docs[i] = p
	}

	// ignore the result if you like, or capture it
	_, err := ts.collection.InsertMany(ctx, docs)
	return err
}

type QueryFunc func(ctx context.Context, collection *mongo.Collection) (*mongo.Cursor, error)

func (ts *TimeSeries) executeQuery(ctx context.Context, decoder rdecoder.DecodingFunc, queryFunc QueryFunc) error {
	cursor, err := queryFunc(ctx, ts.collection)
	if errors.Is(err, mongo.ErrNoDocuments) {
		return merrors.NoData("no_items_in_array")
	}
	if err != nil {
		return err
	}
	defer func() {
		_ = cursor.Close(ctx)
	}()

	for cursor.Next(ctx) {
		if err := cursor.Err(); err != nil {
			return err
		}
		if err = decoder(cursor); err != nil {
			return err
		}
	}

	return nil
}

func (ts *TimeSeries) Query(ctx context.Context, decoder rdecoder.DecodingFunc, query builder.Query, from, to *time.Time) error {
	timeLimitedQuery := query
	if from != nil {
		timeLimitedQuery = timeLimitedQuery.And(repository.Query().Comparison(repository.Field(ts.options.TimeField), builder.Gte, *from))
	}
	if to != nil {
		timeLimitedQuery = timeLimitedQuery.And(repository.Query().Comparison(repository.Field(ts.options.TimeField), builder.Lte, *to))
	}
	queryFunc := func(ctx context.Context, collection *mongo.Collection) (*mongo.Cursor, error) {
		return collection.Find(ctx, timeLimitedQuery.BuildQuery(), timeLimitedQuery.BuildOptions())
	}
	return ts.executeQuery(ctx, decoder, queryFunc)
}

func (ts *TimeSeries) Name() string {
	return ts.collection.Name()
}
