package store

import (
	"context"
	"errors"
	"strings"

	"github.com/tech/sendico/billing/documents/storage"
	"github.com/tech/sendico/billing/documents/storage/model"
	"github.com/tech/sendico/pkg/db/repository"
	"github.com/tech/sendico/pkg/db/repository/builder"
	ri "github.com/tech/sendico/pkg/db/repository/index"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.uber.org/zap"
)

type Documents struct {
	logger mlogger.Logger
	repo   repository.Repository
}

// NewDocuments constructs a Mongo-backed documents store.
func NewDocuments(logger mlogger.Logger, db *mongo.Database) (*Documents, error) {
	if db == nil {
		return nil, merrors.InvalidArgument("documentsStore: database is nil")
	}

	repo := repository.CreateMongoRepository(db, model.DocumentRecordsCollection)

	indexes := []*ri.Definition{
		{
			Keys:   []ri.Key{{Field: "paymentRef", Sort: ri.Asc}},
			Unique: true,
		},
	}

	for _, def := range indexes {
		if err := repo.CreateIndex(def); err != nil {
			logger.Error("Failed to ensure documents index", zap.Error(err), zap.String("collection", repo.Collection()))

			return nil, err
		}
	}

	childLogger := logger.Named("documents")
	childLogger.Debug("Documents store initialised")

	return &Documents{
		logger: childLogger,
		repo:   repo,
	}, nil
}

func (d *Documents) Create(ctx context.Context, record *model.DocumentRecord) error {
	if record == nil {
		return merrors.InvalidArgument("documentsStore: nil record")
	}

	record.Normalize()

	if record.PaymentRef == "" {
		return merrors.InvalidArgument("documentsStore: empty paymentRef")
	}

	record.Update()
	if err := d.repo.Insert(ctx, record, repository.Filter("paymentRef", record.PaymentRef)); err != nil {
		if errors.Is(err, merrors.ErrDataConflict) {
			return storage.ErrDuplicateDocument
		}

		return err
	}

	d.logger.Debug("Document record created", zap.String("payment_ref", record.PaymentRef))

	return nil
}

func (d *Documents) Update(ctx context.Context, record *model.DocumentRecord) error {
	if record == nil {
		return merrors.InvalidArgument("documentsStore: nil record")
	}

	if record.ID.IsZero() {
		return merrors.InvalidArgument("documentsStore: missing record id")
	}

	record.Normalize()
	record.Update()
	if err := d.repo.Update(ctx, record); err != nil {
		if errors.Is(err, merrors.ErrNoData) {
			return storage.ErrDocumentNotFound
		}

		return err
	}

	return nil
}

func (d *Documents) GetByPaymentRef(ctx context.Context, paymentRef string) (*model.DocumentRecord, error) {
	paymentRef = strings.TrimSpace(paymentRef)
	if paymentRef == "" {
		return nil, merrors.InvalidArgument("documentsStore: empty paymentRef")
	}

	entity := &model.DocumentRecord{}
	if err := d.repo.FindOneByFilter(ctx, repository.Filter("paymentRef", paymentRef), entity); err != nil {
		if errors.Is(err, merrors.ErrNoData) {
			return nil, storage.ErrDocumentNotFound
		}

		return nil, err
	}

	return entity, nil
}

func (d *Documents) ListByPaymentRefs(ctx context.Context, paymentRefs []string) ([]*model.DocumentRecord, error) {
	refs := make([]string, 0, len(paymentRefs))
	for _, ref := range paymentRefs {
		clean := strings.TrimSpace(ref)
		if clean == "" {
			continue
		}

		refs = append(refs, clean)
	}

	if len(refs) == 0 {
		return []*model.DocumentRecord{}, nil
	}

	query := repository.Query().Comparison(repository.Field("paymentRef"), builder.In, refs)
	records := make([]*model.DocumentRecord, 0)

	decoder := func(cur *mongo.Cursor) error {
		var rec model.DocumentRecord
		if err := cur.Decode(&rec); err != nil {
			d.logger.Warn("Failed to decode document record", zap.Error(err))

			return err
		}

		records = append(records, &rec)

		return nil
	}

	if err := d.repo.FindManyByFilter(ctx, query, decoder); err != nil {
		return nil, err
	}

	return records, nil
}

var _ storage.DocumentsStore = (*Documents)(nil)
