package builderimp

import (
	"github.com/tech/sendico/pkg/db/repository/builder"
	"github.com/tech/sendico/pkg/mservice"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/mongo"
)

type unwindOpts = builder.UnwindOpts

// UnwindOption is the same type defined in the builder package.
type UnwindOption = builder.UnwindOption

// NewUnwindOpts applies all UnwindOption's to a fresh unwindOpts.
func NewUnwindOpts(opts ...UnwindOption) *unwindOpts {
	cfg := &unwindOpts{}
	for _, opt := range opts {
		opt(cfg)
	}
	return cfg
}

type PipelineImp struct {
	pipeline mongo.Pipeline
}

func (b *PipelineImp) Match(filter builder.Query) builder.Pipeline {
	b.pipeline = append(b.pipeline, filter.BuildPipeline())
	return b
}

func (b *PipelineImp) Lookup(from mservice.Type, localField, foreignField, as builder.Field) builder.Pipeline {
	b.pipeline = append(b.pipeline, bson.D{{Key: string(builder.Lookup), Value: bson.D{
		{Key: string(builder.MKFrom), Value: from},
		{Key: string(builder.MKLocalField), Value: localField.Build()},
		{Key: string(builder.MKForeignField), Value: foreignField.Build()},
		{Key: string(builder.MKAs), Value: as.Build()},
	}}})
	return b
}

func (b *PipelineImp) LookupWithPipeline(
	from mservice.Type,
	nested builder.Pipeline,
	as builder.Field,
	let *map[string]builder.Field,
) builder.Pipeline {
	lookupStage := bson.D{
		{Key: string(builder.MKFrom), Value: from},
		{Key: string(builder.MKPipeline), Value: nested.Build()},
		{Key: string(builder.MKAs), Value: as.Build()},
	}

	// only add "let" if provided and not empty
	if let != nil && len(*let) > 0 {
		letDoc := bson.D{}
		for varName, fld := range *let {
			letDoc = append(letDoc, bson.E{Key: varName, Value: fld.Build()})
		}
		lookupStage = append(lookupStage, bson.E{Key: string(builder.MKLet), Value: letDoc})
	}

	b.pipeline = append(b.pipeline, bson.D{{Key: string(builder.Lookup), Value: lookupStage}})
	return b
}

func (b *PipelineImp) Unwind(path builder.Field, opts ...UnwindOption) builder.Pipeline {
	cfg := NewUnwindOpts(opts...)

	var stageValue interface{}
	// if no options, shorthand
	if !cfg.PreserveNullAndEmptyArrays && cfg.IncludeArrayIndex == "" {
		stageValue = path.Build()
	} else {
		d := bson.D{{Key: string(builder.MKPath), Value: path.Build()}}
		if cfg.PreserveNullAndEmptyArrays {
			d = append(d, bson.E{Key: string(builder.MKPreserveNullAndEmptyArrays), Value: true})
		}
		if cfg.IncludeArrayIndex != "" {
			d = append(d, bson.E{Key: string(builder.MKIncludeArrayIndex), Value: cfg.IncludeArrayIndex})
		}
		stageValue = d
	}

	b.pipeline = append(b.pipeline, bson.D{{Key: string(builder.Unwind), Value: stageValue}})
	return b
}

func (b *PipelineImp) Count(field builder.Field) builder.Pipeline {
	b.pipeline = append(b.pipeline, bson.D{{Key: string(builder.Count), Value: field.Build()}})
	return b
}

func (b *PipelineImp) Group(groupBy builder.Alias, accumulators ...builder.GroupAccumulator) builder.Pipeline {
	groupDoc := groupBy.Build()
	for _, acc := range accumulators {
		groupDoc = append(groupDoc, acc.Build()...)
	}

	b.pipeline = append(b.pipeline, bson.D{
		{Key: string(builder.Group), Value: groupDoc},
	})
	return b
}

func (b *PipelineImp) Project(projections ...builder.Projection) builder.Pipeline {
	projDoc := bson.D{}
	for _, pr := range projections {
		projDoc = append(projDoc, pr.Build()...)
	}
	b.pipeline = append(b.pipeline, bson.D{{Key: string(builder.Project), Value: projDoc}})
	return b
}

func (b *PipelineImp) ReplaceRoot(newRoot builder.Expression) builder.Pipeline {
	b.pipeline = append(b.pipeline, bson.D{{Key: string(builder.ReplaceRoot), Value: bson.D{
		{Key: string(builder.MKNewRoot), Value: newRoot.Build()},
	}}})
	return b
}

func (b *PipelineImp) Build() mongo.Pipeline {
	return b.pipeline
}

func NewPipelineImp() builder.Pipeline {
	return &PipelineImp{
		pipeline: mongo.Pipeline{},
	}
}
