package store

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

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

const (
	treasuryRequestsCollection = "treasury_requests"

	fieldTreasuryRequestID      = "requestId"
	fieldTreasuryLedgerAccount  = "ledgerAccountId"
	fieldTreasuryIdempotencyKey = "idempotencyKey"
	fieldTreasuryStatus         = "status"
	fieldTreasuryScheduledAt    = "scheduledAt"
	fieldTreasuryCreatedAt      = "createdAt"
	fieldTreasuryActive         = "active"
)

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

func NewTreasuryRequests(logger mlogger.Logger, db *mongo.Database) (*TreasuryRequests, error) {
	if db == nil {
		return nil, merrors.InvalidArgument("mongo database is nil")
	}
	if logger == nil {
		logger = zap.NewNop()
	}
	logger = logger.Named("treasury_requests").With(zap.String("collection", treasuryRequestsCollection))

	repo := repository.CreateMongoRepository(db, treasuryRequestsCollection)
	if err := repo.CreateIndex(&ri.Definition{
		Keys:   []ri.Key{{Field: fieldTreasuryRequestID, Sort: ri.Asc}},
		Unique: true,
	}); err != nil {
		logger.Error("Failed to create treasury requests request_id index", zap.Error(err), zap.String("index_field", fieldTreasuryRequestID))
		return nil, err
	}
	if err := repo.CreateIndex(&ri.Definition{
		Keys:   []ri.Key{{Field: fieldTreasuryIdempotencyKey, Sort: ri.Asc}},
		Unique: true,
	}); err != nil {
		logger.Error("Failed to create treasury requests idempotency index", zap.Error(err), zap.String("index_field", fieldTreasuryIdempotencyKey))
		return nil, err
	}
	if err := repo.CreateIndex(&ri.Definition{
		Keys: []ri.Key{
			{Field: fieldTreasuryLedgerAccount, Sort: ri.Asc},
			{Field: fieldTreasuryActive, Sort: ri.Asc},
		},
		Unique:        true,
		PartialFilter: repository.Filter(fieldTreasuryActive, true),
	}); err != nil {
		logger.Error("Failed to create treasury requests active-account index", zap.Error(err))
		return nil, err
	}
	if err := repo.CreateIndex(&ri.Definition{
		Keys: []ri.Key{
			{Field: fieldTreasuryStatus, Sort: ri.Asc},
			{Field: fieldTreasuryScheduledAt, Sort: ri.Asc},
		},
	}); err != nil {
		logger.Error("Failed to create treasury requests execution index", zap.Error(err))
		return nil, err
	}
	if err := repo.CreateIndex(&ri.Definition{
		Keys: []ri.Key{
			{Field: fieldTreasuryLedgerAccount, Sort: ri.Asc},
			{Field: fieldTreasuryCreatedAt, Sort: ri.Asc},
		},
	}); err != nil {
		logger.Error("Failed to create treasury requests daily-amount index", zap.Error(err))
		return nil, err
	}

	t := &TreasuryRequests{
		logger: logger,
		repo:   repo,
	}
	t.logger.Debug("Treasury requests store initialised")
	return t, nil
}

func (t *TreasuryRequests) Create(ctx context.Context, record *model.TreasuryRequest) error {
	if record == nil {
		return merrors.InvalidArgument("treasury request is nil", "record")
	}
	record.RequestID = strings.TrimSpace(record.RequestID)
	record.TelegramUserID = strings.TrimSpace(record.TelegramUserID)
	record.LedgerAccountID = strings.TrimSpace(record.LedgerAccountID)
	record.LedgerAccountCode = strings.TrimSpace(record.LedgerAccountCode)
	record.OrganizationRef = strings.TrimSpace(record.OrganizationRef)
	record.ChatID = strings.TrimSpace(record.ChatID)
	record.Amount = strings.TrimSpace(record.Amount)
	record.Currency = strings.ToUpper(strings.TrimSpace(record.Currency))
	record.IdempotencyKey = strings.TrimSpace(record.IdempotencyKey)
	record.LedgerReference = strings.TrimSpace(record.LedgerReference)
	record.ErrorMessage = strings.TrimSpace(record.ErrorMessage)

	if record.RequestID == "" {
		return merrors.InvalidArgument("request_id is required", "request_id")
	}
	if record.TelegramUserID == "" {
		return merrors.InvalidArgument("telegram_user_id is required", "telegram_user_id")
	}
	if record.LedgerAccountID == "" {
		return merrors.InvalidArgument("ledger_account_id is required", "ledger_account_id")
	}
	if record.Amount == "" {
		return merrors.InvalidArgument("amount is required", "amount")
	}
	if record.Currency == "" {
		return merrors.InvalidArgument("currency is required", "currency")
	}
	if record.IdempotencyKey == "" {
		return merrors.InvalidArgument("idempotency_key is required", "idempotency_key")
	}
	if record.Status == "" {
		return merrors.InvalidArgument("status is required", "status")
	}

	err := t.repo.Insert(ctx, record, repository.Filter(fieldTreasuryRequestID, record.RequestID))
	if errors.Is(err, merrors.ErrDataConflict) {
		return storage.ErrDuplicate
	}
	if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
		t.logger.Warn("Failed to create treasury request", zap.Error(err), zap.String("request_id", record.RequestID))
		return err
	}
	t.logger.Info("Treasury request created",
		zap.String("request_id", record.RequestID),
		zap.String("telegram_user_id", record.TelegramUserID),
		zap.String("chat_id", record.ChatID),
		zap.String("ledger_account_id", record.LedgerAccountID),
		zap.String("ledger_account_code", record.LedgerAccountCode),
		zap.String("operation_type", strings.TrimSpace(string(record.OperationType))),
		zap.String("status", strings.TrimSpace(string(record.Status))),
		zap.String("amount", record.Amount),
		zap.String("currency", record.Currency))
	return err
}

func (t *TreasuryRequests) FindByRequestID(ctx context.Context, requestID string) (*model.TreasuryRequest, error) {
	requestID = strings.TrimSpace(requestID)
	if requestID == "" {
		return nil, merrors.InvalidArgument("request_id is required", "request_id")
	}
	var result model.TreasuryRequest
	err := t.repo.FindOneByFilter(ctx, repository.Filter(fieldTreasuryRequestID, requestID), &result)
	if errors.Is(err, merrors.ErrNoData) {
		t.logger.Debug("Treasury request not found", zap.String("request_id", requestID))
		return nil, nil //nolint:nilnil // nil means treasury request does not exist
	}
	if err != nil {
		t.logger.Warn("Failed to load treasury request", zap.Error(err), zap.String("request_id", requestID))
		return nil, err
	}
	t.logger.Debug("Treasury request loaded",
		zap.String("request_id", requestID),
		zap.String("status", strings.TrimSpace(string(result.Status))),
		zap.String("ledger_account_id", strings.TrimSpace(result.LedgerAccountID)))
	return &result, nil
}

func (t *TreasuryRequests) FindActiveByLedgerAccountID(ctx context.Context, ledgerAccountID string) (*model.TreasuryRequest, error) {
	ledgerAccountID = strings.TrimSpace(ledgerAccountID)
	if ledgerAccountID == "" {
		return nil, merrors.InvalidArgument("ledger_account_id is required", "ledger_account_id")
	}
	var result model.TreasuryRequest
	query := repository.Query().
		Filter(repository.Field(fieldTreasuryLedgerAccount), ledgerAccountID).
		Filter(repository.Field(fieldTreasuryActive), true)
	err := t.repo.FindOneByFilter(ctx, query, &result)
	if errors.Is(err, merrors.ErrNoData) {
		t.logger.Debug("Active treasury request not found", zap.String("ledger_account_id", ledgerAccountID))
		return nil, nil //nolint:nilnil // nil means no active treasury request for ledger account
	}
	if err != nil {
		t.logger.Warn("Failed to load active treasury request", zap.Error(err), zap.String("ledger_account_id", ledgerAccountID))
		return nil, err
	}
	t.logger.Debug("Active treasury request loaded",
		zap.String("request_id", strings.TrimSpace(result.RequestID)),
		zap.String("ledger_account_id", ledgerAccountID),
		zap.String("status", strings.TrimSpace(string(result.Status))))
	return &result, nil
}

func (t *TreasuryRequests) FindDueByStatus(ctx context.Context, statuses []model.TreasuryRequestStatus, now time.Time, limit int64) ([]model.TreasuryRequest, error) {
	if len(statuses) == 0 {
		return nil, nil
	}
	if limit <= 0 {
		limit = 100
	}
	statusValues := make([]any, 0, len(statuses))
	for _, status := range statuses {
		next := strings.TrimSpace(string(status))
		if next == "" {
			continue
		}
		statusValues = append(statusValues, next)
	}
	if len(statusValues) == 0 {
		return nil, nil
	}
	query := repository.Query().
		In(repository.Field(fieldTreasuryStatus), statusValues...).
		Comparison(repository.Field(fieldTreasuryScheduledAt), builder.Lte, now).
		Sort(repository.Field(fieldTreasuryScheduledAt), true).
		Limit(&limit)

	result, err := mutil.GetObjects[model.TreasuryRequest](ctx, t.logger, query, nil, t.repo)
	if err != nil && !errors.Is(err, merrors.ErrNoData) {
		t.logger.Warn("Failed to list due treasury requests",
			zap.Error(err),
			zap.Any("statuses", statusValues),
			zap.Time("scheduled_before", now),
			zap.Int64("limit", limit))
		return nil, err
	}
	t.logger.Debug("Due treasury requests loaded",
		zap.Any("statuses", statusValues),
		zap.Time("scheduled_before", now),
		zap.Int64("limit", limit),
		zap.Int("count", len(result)))
	return result, nil
}

func (t *TreasuryRequests) ClaimScheduled(ctx context.Context, requestID string) (bool, error) {
	requestID = strings.TrimSpace(requestID)
	if requestID == "" {
		return false, merrors.InvalidArgument("request_id is required", "request_id")
	}
	patch := repository.Patch().
		Set(repository.Field(fieldTreasuryStatus), string(model.TreasuryRequestStatusConfirmed))
	updated, err := t.repo.PatchMany(ctx, repository.Filter(fieldTreasuryRequestID, requestID).And(
		repository.Filter(fieldTreasuryStatus, string(model.TreasuryRequestStatusScheduled)),
	), patch)
	if err != nil {
		t.logger.Warn("Failed to claim scheduled treasury request", zap.Error(err), zap.String("request_id", requestID))
		return false, err
	}
	if updated > 0 {
		t.logger.Info("Scheduled treasury request claimed", zap.String("request_id", requestID))
	} else {
		t.logger.Debug("Scheduled treasury request claim skipped", zap.String("request_id", requestID))
	}
	return updated > 0, nil
}

func (t *TreasuryRequests) Update(ctx context.Context, record *model.TreasuryRequest) error {
	if record == nil {
		return merrors.InvalidArgument("treasury request is nil", "record")
	}
	record.RequestID = strings.TrimSpace(record.RequestID)
	record.TelegramUserID = strings.TrimSpace(record.TelegramUserID)
	record.LedgerAccountID = strings.TrimSpace(record.LedgerAccountID)
	record.LedgerAccountCode = strings.TrimSpace(record.LedgerAccountCode)
	record.OrganizationRef = strings.TrimSpace(record.OrganizationRef)
	record.ChatID = strings.TrimSpace(record.ChatID)
	record.Amount = strings.TrimSpace(record.Amount)
	record.Currency = strings.ToUpper(strings.TrimSpace(record.Currency))
	record.IdempotencyKey = strings.TrimSpace(record.IdempotencyKey)
	record.LedgerReference = strings.TrimSpace(record.LedgerReference)
	record.ErrorMessage = strings.TrimSpace(record.ErrorMessage)
	if record.RequestID == "" {
		return merrors.InvalidArgument("request_id is required", "request_id")
	}
	existing, err := t.FindByRequestID(ctx, record.RequestID)
	if err != nil {
		return err
	}
	if existing == nil {
		return merrors.NoData("treasury request not found")
	}

	patch := repository.Patch().
		Set(repository.Field("operationType"), record.OperationType).
		Set(repository.Field("telegramUserId"), record.TelegramUserID).
		Set(repository.Field("ledgerAccountId"), record.LedgerAccountID).
		Set(repository.Field("organizationRef"), record.OrganizationRef).
		Set(repository.Field("chatId"), record.ChatID).
		Set(repository.Field("amount"), record.Amount).
		Set(repository.Field("currency"), record.Currency).
		Set(repository.Field(fieldTreasuryStatus), record.Status).
		Set(repository.Field(fieldTreasuryIdempotencyKey), record.IdempotencyKey).
		Set(repository.Field(fieldTreasuryActive), record.Active)
	if record.LedgerAccountCode != "" {
		patch = patch.Set(repository.Field("ledgerAccountCode"), record.LedgerAccountCode)
	} else {
		patch = patch.Unset(repository.Field("ledgerAccountCode"))
	}
	if !record.ConfirmedAt.IsZero() {
		patch = patch.Set(repository.Field("confirmedAt"), record.ConfirmedAt)
	} else {
		patch = patch.Unset(repository.Field("confirmedAt"))
	}
	if !record.ScheduledAt.IsZero() {
		patch = patch.Set(repository.Field("scheduledAt"), record.ScheduledAt)
	} else {
		patch = patch.Unset(repository.Field("scheduledAt"))
	}
	if !record.ExecutedAt.IsZero() {
		patch = patch.Set(repository.Field("executedAt"), record.ExecutedAt)
	} else {
		patch = patch.Unset(repository.Field("executedAt"))
	}
	if !record.CancelledAt.IsZero() {
		patch = patch.Set(repository.Field("cancelledAt"), record.CancelledAt)
	} else {
		patch = patch.Unset(repository.Field("cancelledAt"))
	}
	if record.LedgerReference != "" {
		patch = patch.Set(repository.Field("ledgerReference"), record.LedgerReference)
	} else {
		patch = patch.Unset(repository.Field("ledgerReference"))
	}
	if record.ErrorMessage != "" {
		patch = patch.Set(repository.Field("errorMessage"), record.ErrorMessage)
	} else {
		patch = patch.Unset(repository.Field("errorMessage"))
	}
	if _, err := t.repo.PatchMany(ctx, repository.Filter(fieldTreasuryRequestID, record.RequestID), patch); err != nil {
		if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
			t.logger.Warn("Failed to update treasury request", zap.Error(err), zap.String("request_id", record.RequestID))
		}
		return err
	}
	t.logger.Info("Treasury request updated",
		zap.String("request_id", record.RequestID),
		zap.String("telegram_user_id", strings.TrimSpace(record.TelegramUserID)),
		zap.String("chat_id", strings.TrimSpace(record.ChatID)),
		zap.String("ledger_account_id", strings.TrimSpace(record.LedgerAccountID)),
		zap.String("ledger_account_code", strings.TrimSpace(record.LedgerAccountCode)),
		zap.String("operation_type", strings.TrimSpace(string(record.OperationType))),
		zap.String("status", strings.TrimSpace(string(record.Status))),
		zap.String("amount", strings.TrimSpace(record.Amount)),
		zap.String("currency", strings.TrimSpace(record.Currency)),
		zap.String("error_message", strings.TrimSpace(record.ErrorMessage)))
	return nil
}

func (t *TreasuryRequests) ListByAccountAndStatuses(ctx context.Context, ledgerAccountID string, statuses []model.TreasuryRequestStatus, dayStart, dayEnd time.Time) ([]model.TreasuryRequest, error) {
	ledgerAccountID = strings.TrimSpace(ledgerAccountID)
	if ledgerAccountID == "" {
		return nil, merrors.InvalidArgument("ledger_account_id is required", "ledger_account_id")
	}
	statusValues := make([]any, 0, len(statuses))
	for _, status := range statuses {
		next := strings.TrimSpace(string(status))
		if next == "" {
			continue
		}
		statusValues = append(statusValues, next)
	}
	if len(statusValues) == 0 {
		return nil, nil
	}
	query := repository.Query().
		Filter(repository.Field(fieldTreasuryLedgerAccount), ledgerAccountID).
		In(repository.Field(fieldTreasuryStatus), statusValues...).
		Comparison(repository.Field(fieldTreasuryCreatedAt), builder.Gte, dayStart).
		Comparison(repository.Field(fieldTreasuryCreatedAt), builder.Lt, dayEnd)

	result, err := mutil.GetObjects[model.TreasuryRequest](ctx, t.logger, query, nil, t.repo)
	if err != nil && !errors.Is(err, merrors.ErrNoData) {
		t.logger.Warn("Failed to list treasury requests by account and statuses",
			zap.Error(err),
			zap.String("ledger_account_id", ledgerAccountID),
			zap.Any("statuses", statusValues),
			zap.Time("day_start", dayStart),
			zap.Time("day_end", dayEnd))
		return nil, err
	}
	t.logger.Debug("Treasury requests loaded by account and statuses",
		zap.String("ledger_account_id", ledgerAccountID),
		zap.Any("statuses", statusValues),
		zap.Time("day_start", dayStart),
		zap.Time("day_end", dayEnd),
		zap.Int("count", len(result)))
	return result, nil
}

var _ storage.TreasuryRequestsStore = (*TreasuryRequests)(nil)
