package store

import (
	"context"
	"errors"
	"strings"
	"time"

	"github.com/tech/sendico/gateway/chain/storage"
	"github.com/tech/sendico/gateway/chain/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/mservice"
	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.uber.org/zap"
)

const (
	defaultDepositPageSize int64 = 100
	maxDepositPageSize     int64 = 500
)

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

// NewDeposits constructs a Mongo-backed deposits store.
func NewDeposits(logger mlogger.Logger, db *mongo.Database) (*Deposits, error) {
	if db == nil {
		return nil, merrors.InvalidArgument("mongo database is nil")
	}
	repo := repository.CreateMongoRepository(db, mservice.ChainDeposits)
	indexes := []*ri.Definition{
		{
			Keys:   []ri.Key{{Field: "depositRef", Sort: ri.Asc}},
			Unique: true,
		},
		{
			Keys: []ri.Key{{Field: "walletRef", Sort: ri.Asc}, {Field: "status", Sort: ri.Asc}},
		},
		{
			Keys:   []ri.Key{{Field: "txHash", Sort: ri.Asc}},
			Unique: true,
		},
	}
	for _, def := range indexes {
		if err := repo.CreateIndex(def); err != nil {
			logger.Error("Failed to ensure deposit index", zap.Error(err), zap.String("collection", repo.Collection()))
			return nil, err
		}
	}

	childLogger := logger.Named("deposits")
	childLogger.Debug("Deposits store initialised")

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

func (d *Deposits) Record(ctx context.Context, deposit *model.Deposit) error {
	if deposit == nil {
		return merrors.InvalidArgument("depositsStore: nil deposit")
	}
	deposit.Normalize()
	if strings.TrimSpace(deposit.DepositRef) == "" {
		return merrors.InvalidArgument("depositsStore: empty depositRef")
	}
	if deposit.Status == "" {
		deposit.Status = model.DepositStatusPending
	}
	if deposit.ObservedAt.IsZero() {
		deposit.ObservedAt = time.Now().UTC()
	}
	if deposit.RecordedAt.IsZero() {
		deposit.RecordedAt = time.Now().UTC()
	}
	if deposit.LastStatusAt.IsZero() {
		deposit.LastStatusAt = time.Now().UTC()
	}

	existing := &model.Deposit{}
	err := d.repo.FindOneByFilter(ctx, repository.Filter("depositRef", deposit.DepositRef), existing)
	switch {
	case err == nil:
		existing.Status = deposit.Status
		existing.ObservedAt = deposit.ObservedAt
		existing.RecordedAt = deposit.RecordedAt
		existing.LastStatusAt = time.Now().UTC()
		if deposit.Amount != nil {
			existing.Amount = deposit.Amount
		}
		if deposit.BlockID != "" {
			existing.BlockID = deposit.BlockID
		}
		if deposit.TxHash != "" {
			existing.TxHash = deposit.TxHash
		}
		if deposit.Network != "" {
			existing.Network = deposit.Network
		}
		if deposit.TokenSymbol != "" {
			existing.TokenSymbol = deposit.TokenSymbol
		}
		if deposit.ContractAddress != "" {
			existing.ContractAddress = deposit.ContractAddress
		}
		if deposit.SourceAddress != "" {
			existing.SourceAddress = deposit.SourceAddress
		}
		if err := d.repo.Update(ctx, existing); err != nil {
			return err
		}
		return nil
	case errors.Is(err, merrors.ErrNoData):
		if err := d.repo.Insert(ctx, deposit, repository.Filter("depositRef", deposit.DepositRef)); err != nil {
			return err
		}
		return nil
	default:
		return err
	}
}

func (d *Deposits) ListPending(ctx context.Context, network string, limit int32) ([]*model.Deposit, error) {
	query := repository.Query().Filter(repository.Field("status"), model.DepositStatusPending)
	if net := strings.TrimSpace(network); net != "" {
		query = query.Filter(repository.Field("network"), strings.ToLower(net))
	}
	pageSize := sanitizeDepositLimit(limit)
	query = query.Sort(repository.Field("observedAt"), true).Limit(&pageSize)

	deposits := make([]*model.Deposit, 0, pageSize)
	decoder := func(cur *mongo.Cursor) error {
		item := &model.Deposit{}
		if err := cur.Decode(item); err != nil {
			return err
		}
		deposits = append(deposits, item)
		return nil
	}

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

	return deposits, nil
}

func sanitizeDepositLimit(requested int32) int64 {
	if requested <= 0 {
		return defaultDepositPageSize
	}
	if requested > int32(maxDepositPageSize) {
		return maxDepositPageSize
	}
	return int64(requested)
}

var _ storage.DepositsStore = (*Deposits)(nil)
