package store

import (
	"context"
	"errors"

	"github.com/tech/sendico/ledger/storage"
	"github.com/tech/sendico/ledger/storage/model"
	"github.com/tech/sendico/pkg/db/repository"
	ri "github.com/tech/sendico/pkg/db/repository/index"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/mutil/mzap"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.uber.org/zap"
)

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

func NewJournalEntries(logger mlogger.Logger, db *mongo.Database) (storage.JournalEntriesStore, error) {
	repo := repository.CreateMongoRepository(db, model.JournalEntriesCollection)

	// Create unique index on organizationRef + idempotencyKey
	uniqueIndex := &ri.Definition{
		Keys: []ri.Key{
			{Field: "organizationRef", Sort: ri.Asc},
			{Field: "idempotencyKey", Sort: ri.Asc},
		},
		Unique: true,
	}
	if err := repo.CreateIndex(uniqueIndex); err != nil {
		logger.Error("Failed to ensure journal entries idempotency index", zap.Error(err))
		return nil, err
	}

	// Create index on organizationRef for listing
	orgIndex := &ri.Definition{
		Keys: []ri.Key{
			{Field: "organizationRef", Sort: ri.Asc},
			{Field: "createdAt", Sort: ri.Desc},
		},
	}
	if err := repo.CreateIndex(orgIndex); err != nil {
		logger.Error("Failed to ensure journal entries organization index", zap.Error(err))
		return nil, err
	}

	childLogger := logger.Named(model.JournalEntriesCollection)
	childLogger.Debug("Journal entries store initialised", zap.String("collection", model.JournalEntriesCollection))

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

func (j *journalEntriesStore) Create(ctx context.Context, entry *model.JournalEntry) error {
	if entry == nil {
		j.logger.Warn("Attempt to create nil journal entry")
		return merrors.InvalidArgument("journalEntriesStore: nil journal entry")
	}

	if err := j.repo.Insert(ctx, entry, nil); err != nil {
		if mongo.IsDuplicateKeyError(err) {
			j.logger.Warn("Duplicate idempotency key", zap.String("idempotency_key", entry.IdempotencyKey))
			return storage.ErrDuplicateIdempotency
		}
		j.logger.Warn("Failed to create journal entry", zap.Error(err))
		return err
	}

	j.logger.Debug("Journal entry created", zap.String("idempotency_key", entry.IdempotencyKey),
		zap.String("entryType", string(entry.EntryType)))
	return nil
}

func (j *journalEntriesStore) Get(ctx context.Context, entryRef bson.ObjectID) (*model.JournalEntry, error) {
	if entryRef.IsZero() {
		j.logger.Warn("Attempt to get journal entry with zero ID")
		return nil, merrors.InvalidArgument("journalEntriesStore: zero entry ID")
	}

	result := &model.JournalEntry{}
	if err := j.repo.Get(ctx, entryRef, result); err != nil {
		if errors.Is(err, merrors.ErrNoData) {
			j.logger.Debug("Journal entry not found", mzap.ObjRef("entry_ref", entryRef))
			return nil, storage.ErrJournalEntryNotFound
		}
		j.logger.Warn("Failed to get journal entry", zap.Error(err), mzap.ObjRef("entry_ref", entryRef))
		return nil, err
	}

	j.logger.Debug("Journal entry loaded", mzap.ObjRef("entry_ref", entryRef),
		zap.String("idempotency_key", result.IdempotencyKey))
	return result, nil
}

func (j *journalEntriesStore) GetByIdempotencyKey(ctx context.Context, orgRef bson.ObjectID, idempotencyKey string) (*model.JournalEntry, error) {
	if orgRef.IsZero() {
		j.logger.Warn("Attempt to get journal entry with zero organization ID")
		return nil, merrors.InvalidArgument("journalEntriesStore: zero organization ID")
	}
	if idempotencyKey == "" {
		j.logger.Warn("Attempt to get journal entry with empty idempotency key")
		return nil, merrors.InvalidArgument("journalEntriesStore: empty idempotency key")
	}

	query := repository.Query().
		Filter(repository.Field("organizationRef"), orgRef).
		Filter(repository.Field("idempotencyKey"), idempotencyKey)

	result := &model.JournalEntry{}
	if err := j.repo.FindOneByFilter(ctx, query, result); err != nil {
		if errors.Is(err, merrors.ErrNoData) {
			j.logger.Debug("Journal entry not found by idempotency key", zap.String("idempotency_key", idempotencyKey))
			return nil, storage.ErrJournalEntryNotFound
		}
		j.logger.Warn("Failed to get journal entry by idempotency key", zap.Error(err),
			zap.String("idempotency_key", idempotencyKey))
		return nil, err
	}

	j.logger.Debug("Journal entry loaded by idempotency key", zap.String("idempotency_key", idempotencyKey))
	return result, nil
}

func (j *journalEntriesStore) ListByOrganization(ctx context.Context, orgRef bson.ObjectID, limit int, offset int) ([]*model.JournalEntry, error) {
	if orgRef.IsZero() {
		j.logger.Warn("Attempt to list journal entries with zero organization ID")
		return nil, merrors.InvalidArgument("journalEntriesStore: zero organization ID")
	}

	limit64 := int64(limit)
	offset64 := int64(offset)
	query := repository.Query().
		Filter(repository.Field("organizationRef"), orgRef).
		Limit(&limit64).
		Offset(&offset64).
		Sort(repository.Field("createdAt"), false) // false = descending

	entries := make([]*model.JournalEntry, 0)
	err := j.repo.FindManyByFilter(ctx, query, func(cur *mongo.Cursor) error {
		doc := &model.JournalEntry{}
		if err := cur.Decode(doc); err != nil {
			return err
		}
		entries = append(entries, doc)
		return nil
	})
	if err != nil {
		j.logger.Warn("Failed to list journal entries", zap.Error(err))
		return nil, err
	}

	j.logger.Debug("Listed journal entries", zap.Int("count", len(entries)))
	return entries, nil
}
