package store

import (
	"context"
	"errors"
	"fmt"
	"strings"

	"github.com/tech/sendico/ledger/storage"
	"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"
	pkm "github.com/tech/sendico/pkg/model"
	"github.com/tech/sendico/pkg/model/account_role"
	"github.com/tech/sendico/pkg/mservice"
	"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 accountsStore struct {
	logger mlogger.Logger
	repo   repository.Repository
}

const (
	orgCurrencyRoleNonOperatingPrefix  = "org_currency_role_non_operating_unique"
	orgCurrencyRoleSystemOperatingName = "org_currency_role_system_operating_unique"
)

var nonOperatingUniqueRoles = []account_role.AccountRole{
	account_role.AccountRoleHold,
	account_role.AccountRoleTransit,
	account_role.AccountRoleSettlement,
	account_role.AccountRoleClearing,
	account_role.AccountRolePending,
	account_role.AccountRoleReserve,
	account_role.AccountRoleLiquidity,
	account_role.AccountRoleFee,
	account_role.AccountRoleChargeback,
	account_role.AccountRoleAdjustment,
}

func nonOperatingRoleIndexName(role account_role.AccountRole) string {
	return fmt.Sprintf("%s_%s", orgCurrencyRoleNonOperatingPrefix, role)
}

func NewAccounts(logger mlogger.Logger, db *mongo.Database) (storage.AccountsStore, error) {
	repo := repository.CreateMongoRepository(db, mservice.LedgerAccounts)

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

	// Keep role uniqueness for non-operating organization accounts.
	// Some Mongo-compatible backends reject partial filters that use negation ($ne/$not).
	// Build one equality-based partial index per non-operating role for compatibility.
	for _, role := range nonOperatingUniqueRoles {
		roleIndex := &ri.Definition{
			Keys: []ri.Key{
				{Field: "organizationRef", Sort: ri.Asc},
				{Field: "currency", Sort: ri.Asc},
				{Field: "role", Sort: ri.Asc},
			},
			Unique: true,
			Name:   nonOperatingRoleIndexName(role),
			PartialFilter: repository.Query().
				Filter(repository.Field("scope"), pkm.LedgerAccountScopeOrganization).
				Filter(repository.Field("role"), role),
		}
		if err := repo.CreateIndex(roleIndex); err != nil {
			logger.Error("Failed to ensure accounts role index", zap.String("role", string(role)), zap.Error(err))
			return nil, err
		}
	}

	// Ensure only one system-tagged operating role per organization/currency.
	systemOperatingRoleIndex := &ri.Definition{
		Keys: []ri.Key{
			{Field: "organizationRef", Sort: ri.Asc},
			{Field: "currency", Sort: ri.Asc},
			{Field: "role", Sort: ri.Asc},
			{Field: "metadata.system", Sort: ri.Asc},
		},
		Unique: true,
		Name:   orgCurrencyRoleSystemOperatingName,
		PartialFilter: repository.Query().
			Filter(repository.Field("scope"), pkm.LedgerAccountScopeOrganization).
			Filter(repository.Field("role"), account_role.AccountRoleOperating).
			Filter(repository.Field("metadata.system"), "true"),
	}
	if err := repo.CreateIndex(systemOperatingRoleIndex); err != nil {
		logger.Error("Failed to ensure system operating role index", zap.Error(err))
		return nil, err
	}

	// Create compound index on scope + systemPurpose + currency (unique) for system accounts
	systemIndex := &ri.Definition{
		Keys: []ri.Key{
			{Field: "scope", Sort: ri.Asc},
			{Field: "systemPurpose", Sort: ri.Asc},
			{Field: "currency", Sort: ri.Asc},
		},
		Unique:        true,
		PartialFilter: repository.Filter("scope", pkm.LedgerAccountScopeSystem),
	}
	if err := repo.CreateIndex(systemIndex); err != nil {
		logger.Error("Failed to ensure system accounts index", zap.Error(err))
		return nil, err
	}

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

	childLogger := logger.Named(mservice.LedgerAccounts)
	childLogger.Info("Accounts store initialised", zap.String("collection", mservice.LedgerAccounts))

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

func (a *accountsStore) Create(ctx context.Context, account *pkm.LedgerAccount) error {
	if account == nil {
		a.logger.Warn("Attempt to create nil account")
		return merrors.InvalidArgument("accountsStore: nil account")
	}

	if err := a.repo.Insert(ctx, account, nil); err != nil {
		if mongo.IsDuplicateKeyError(err) {
			a.logger.Warn("Duplicate account code", zap.String("account_code", account.AccountCode),
				zap.String("currency", account.Currency))
			return merrors.DataConflict("account with this code and currency already exists")
		}
		a.logger.Warn("Failed to create account", zap.Error(err))
		return err
	}

	a.logger.Debug("Account created", zap.String("account_code", account.AccountCode),
		zap.String("currency", account.Currency))
	return nil
}

func (a *accountsStore) Get(ctx context.Context, accountRef bson.ObjectID) (*pkm.LedgerAccount, error) {
	if accountRef.IsZero() {
		a.logger.Warn("Attempt to get account with zero ID")
		return nil, merrors.InvalidArgument("accountsStore: zero account ID")
	}

	result := &pkm.LedgerAccount{}
	if err := a.repo.Get(ctx, accountRef, result); err != nil {
		if errors.Is(err, merrors.ErrNoData) {
			a.logger.Debug("Account not found", mzap.AccRef(accountRef))
			return nil, storage.ErrAccountNotFound
		}
		a.logger.Warn("Failed to get account", zap.Error(err), mzap.AccRef(accountRef))
		return nil, err
	}

	a.logger.Debug("Account loaded", mzap.AccRef(accountRef), zap.String("account_code", result.AccountCode))
	return result, nil
}

func (a *accountsStore) GetByAccountCode(ctx context.Context, orgRef bson.ObjectID, accountCode, currency string) (*pkm.LedgerAccount, error) {
	if orgRef.IsZero() {
		a.logger.Warn("Attempt to get account with zero organization ID")
		return nil, merrors.InvalidArgument("accountsStore: zero organization ID")
	}
	if accountCode == "" {
		a.logger.Warn("Attempt to get account with empty code")
		return nil, merrors.InvalidArgument("accountsStore: empty account code")
	}
	if currency == "" {
		a.logger.Warn("Attempt to get account with empty currency")
		return nil, merrors.InvalidArgument("accountsStore: empty currency")
	}

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

	result := &pkm.LedgerAccount{}
	if err := a.repo.FindOneByFilter(ctx, query, result); err != nil {
		if errors.Is(err, merrors.ErrNoData) {
			a.logger.Debug("Account not found by code", zap.String("account_code", accountCode), zap.String("currency", currency))
			return nil, storage.ErrAccountNotFound
		}
		a.logger.Warn("Failed to get account by code", zap.Error(err), zap.String("account_code", accountCode))
		return nil, err
	}

	a.logger.Debug("Account loaded by code", zap.String("account_code", accountCode), zap.String("currency", currency))
	return result, nil
}

func (a *accountsStore) GetByRole(ctx context.Context, orgRef bson.ObjectID, currency string, role account_role.AccountRole) (*pkm.LedgerAccount, error) {
	if orgRef.IsZero() {
		a.logger.Warn("Attempt to get account with zero organization ID")
		return nil, merrors.InvalidArgument("accountsStore: zero organization ID")
	}
	if currency == "" {
		a.logger.Warn("Attempt to get account with empty currency")
		return nil, merrors.InvalidArgument("accountsStore: empty currency")
	}
	if strings.TrimSpace(string(role)) == "" {
		a.logger.Warn("Attempt to get account with empty role")
		return nil, merrors.InvalidArgument("accountsStore: empty role")
	}

	result := &pkm.LedgerAccount{}
	limit := int64(1)

	// Prefer topology/system-tagged account when present.
	systemQuery := repository.Query().
		Filter(repository.Field("organizationRef"), orgRef).
		Filter(repository.Field("currency"), currency).
		Filter(repository.Field("role"), role).
		Filter(repository.Field("scope"), pkm.LedgerAccountScopeOrganization).
		Filter(repository.Field("metadata.system"), "true").
		Limit(&limit)
	if err := a.repo.FindOneByFilter(ctx, systemQuery, result); err == nil {
		a.logger.Debug("System account loaded by role", mzap.ObjRef("accountRef", *result.GetID()),
			zap.String("currency", currency), zap.String("role", string(role)))
		return result, nil
	} else if !errors.Is(err, merrors.ErrNoData) {
		a.logger.Warn("Failed to get account by role", zap.Error(err), mzap.ObjRef("organization_ref", orgRef),
			zap.String("currency", currency), zap.String("role", string(role)))
		return nil, err
	}

	// Fallback to any organization account with the role.
	query := repository.Query().
		Filter(repository.Field("organizationRef"), orgRef).
		Filter(repository.Field("currency"), currency).
		Filter(repository.Field("role"), role).
		Filter(repository.Field("scope"), pkm.LedgerAccountScopeOrganization).
		Limit(&limit)
	if err := a.repo.FindOneByFilter(ctx, query, result); err != nil {
		if errors.Is(err, merrors.ErrNoData) {
			a.logger.Debug("Account not found by role", zap.String("currency", currency),
				zap.String("role", string(role)), mzap.ObjRef("organization_ref", orgRef))
			return nil, storage.ErrAccountNotFound
		}
		a.logger.Warn("Failed to get account by role", zap.Error(err), mzap.ObjRef("organization_ref", orgRef),
			zap.String("currency", currency), zap.String("role", string(role)))
		return nil, err
	}

	a.logger.Debug("Account loaded by role", mzap.ObjRef("accountRef", *result.GetID()),
		zap.String("currency", currency), zap.String("role", string(role)))
	return result, nil
}

func (a *accountsStore) GetSystemAccount(ctx context.Context, purpose pkm.SystemAccountPurpose, currency string) (*pkm.LedgerAccount, error) {
	if strings.TrimSpace(string(purpose)) == "" {
		a.logger.Warn("Attempt to get system account with empty purpose")
		return nil, merrors.InvalidArgument("accountsStore: empty system purpose")
	}
	normalizedCurrency := strings.ToUpper(strings.TrimSpace(currency))
	if normalizedCurrency == "" {
		a.logger.Warn("Attempt to get system account with empty currency")
		return nil, merrors.InvalidArgument("accountsStore: empty currency")
	}

	limit := int64(1)
	query := repository.Query().
		Filter(repository.Field("scope"), pkm.LedgerAccountScopeSystem).
		Filter(repository.Field("systemPurpose"), purpose).
		Filter(repository.Field("currency"), normalizedCurrency).
		Limit(&limit)

	result := &pkm.LedgerAccount{}
	if err := a.repo.FindOneByFilter(ctx, query, result); err != nil {
		if errors.Is(err, merrors.ErrNoData) {
			a.logger.Debug("System account not found", zap.String("currency", normalizedCurrency),
				zap.String("purpose", string(purpose)))
			return nil, storage.ErrAccountNotFound
		}
		a.logger.Warn("Failed to get system account", zap.Error(err),
			zap.String("currency", normalizedCurrency), zap.String("purpose", string(purpose)))
		return nil, err
	}

	a.logger.Debug("System account loaded", mzap.ObjRef("accountRef", *result.GetID()),
		zap.String("currency", normalizedCurrency), zap.String("purpose", string(purpose)))
	return result, nil
}

func (a *accountsStore) GetDefaultSettlement(ctx context.Context, orgRef bson.ObjectID, currency string) (*pkm.LedgerAccount, error) {
	if orgRef.IsZero() {
		a.logger.Warn("Attempt to get default settlement with zero organization ID")
		return nil, merrors.InvalidArgument("accountsStore: zero organization ID")
	}
	if currency == "" {
		a.logger.Warn("Attempt to get default settlement with empty currency")
		return nil, merrors.InvalidArgument("accountsStore: empty currency")
	}

	limit := int64(1)
	query := repository.Query().
		Filter(repository.Field("organizationRef"), orgRef).
		Filter(repository.Field("currency"), currency).
		Filter(repository.Field("role"), account_role.AccountRoleSettlement).
		Limit(&limit)

	result := &pkm.LedgerAccount{}
	if err := a.repo.FindOneByFilter(ctx, query, result); err != nil {
		if errors.Is(err, merrors.ErrNoData) {
			a.logger.Debug("Default settlement account not found",
				zap.String("currency", currency),
				mzap.ObjRef("organization_ref", orgRef))
			return nil, storage.ErrAccountNotFound
		}
		a.logger.Warn("Failed to get default settlement account", zap.Error(err),
			mzap.ObjRef("organization_ref", orgRef),
			zap.String("currency", currency))
		return nil, err
	}

	a.logger.Debug("Default settlement account loaded", mzap.ObjRef("accountRef", *result.GetID()), zap.String("currency", currency))
	return result, nil
}

func (a *accountsStore) ListByOrganization(ctx context.Context, orgRef bson.ObjectID, filter *storage.AccountsFilter, limit int, offset int) ([]*pkm.LedgerAccount, error) {
	if orgRef.IsZero() {
		a.logger.Warn("Attempt to list accounts with zero organization reference")
		return nil, merrors.InvalidArgument("accountsStore: zero organization reference")
	}

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

	if filter != nil && filter.OwnerRefFilter != nil {
		if filter.OwnerRefFilter.IsZero() {
			// Filter for accounts with nil owner_ref
			query = query.Filter(repository.Field("ownerRef"), nil)
		} else {
			// Filter for accounts matching owner_ref
			query = query.Filter(repository.Field("ownerRef"), *filter.OwnerRefFilter)
		}
	}

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

	a.logger.Debug("Listed accounts", zap.Int("count", len(accounts)))
	return accounts, nil
}

func (a *accountsStore) UpdateStatus(ctx context.Context, accountRef bson.ObjectID, status pkm.LedgerAccountStatus) error {
	if accountRef.IsZero() {
		a.logger.Warn("Attempt to update account status with zero reference")
		return merrors.InvalidArgument("accountsStore: zero account reference")
	}

	patch := repository.Patch().Set(repository.Field("status"), status)
	if err := a.repo.Patch(ctx, accountRef, patch); err != nil {
		a.logger.Warn("Failed to update account status", zap.Error(err), mzap.AccRef(accountRef))
		return err
	}

	a.logger.Debug("Account status updated", mzap.AccRef(accountRef), zap.String("status", string(status)))
	return nil
}
