package ledger

import (
	"context"
	"errors"
	"strings"

	"github.com/tech/sendico/ledger/storage"
	"github.com/tech/sendico/pkg/merrors"
	pmodel "github.com/tech/sendico/pkg/model"
	"go.mongodb.org/mongo-driver/v2/bson"
)

// EnsureSystemAccounts initializes required system accounts once at startup.
func (s *Service) EnsureSystemAccounts(ctx context.Context) error {
	return s.ensureSystemAccounts(ctx)
}

func (s *Service) ensureSystemAccounts(ctx context.Context) error {
	if s == nil || s.storage == nil || s.storage.Accounts() == nil {
		return errStorageNotInitialized
	}
	if ctx == nil {
		ctx = context.Background()
	}

	for _, currency := range pmodel.SupportedCurrencies {
		normalized := strings.ToUpper(strings.TrimSpace(string(currency)))
		if normalized == "" {
			continue
		}
		if err := s.ensureSystemAccountForCurrency(ctx, pmodel.SystemAccountPurposeExternalSource, normalized); err != nil {
			return err
		}
		if err := s.ensureSystemAccountForCurrency(ctx, pmodel.SystemAccountPurposeExternalSink, normalized); err != nil {
			return err
		}
	}
	return nil
}

func (s *Service) ensureSystemAccountForCurrency(ctx context.Context, purpose pmodel.SystemAccountPurpose, currency string) error {
	account, err := s.storage.Accounts().GetSystemAccount(ctx, purpose, currency)
	if err == nil && account != nil {
		s.cacheSystemAccount(purpose, currency, account)
		return nil
	}
	if err != nil && !errors.Is(err, storage.ErrAccountNotFound) {
		return err
	}

	account = newExternalSystemAccount(purpose, currency)
	if err := s.storage.Accounts().Create(ctx, account); err != nil {
		if errors.Is(err, merrors.ErrDataConflict) {
			existing, lookupErr := s.storage.Accounts().GetSystemAccount(ctx, purpose, currency)
			if lookupErr != nil {
				return lookupErr
			}
			s.cacheSystemAccount(purpose, currency, existing)
			return nil
		}
		return err
	}
	s.cacheSystemAccount(purpose, currency, account)
	return nil
}

func (s *Service) systemAccount(ctx context.Context, purpose pmodel.SystemAccountPurpose, currency string) (*pmodel.LedgerAccount, error) {
	if s == nil || s.storage == nil || s.storage.Accounts() == nil {
		return nil, errStorageNotInitialized
	}
	normalized := strings.ToUpper(strings.TrimSpace(currency))
	if normalized == "" {
		return nil, merrors.InvalidArgument("currency is required")
	}

	if acc := s.cachedSystemAccount(purpose, normalized); acc != nil {
		return acc, nil
	}

	account, err := s.storage.Accounts().GetSystemAccount(ctx, purpose, normalized)
	if err != nil {
		return nil, err
	}
	s.cacheSystemAccount(purpose, normalized, account)
	return account, nil
}

func (s *Service) cachedSystemAccount(purpose pmodel.SystemAccountPurpose, currency string) *pmodel.LedgerAccount {
	s.systemAccounts.mu.RLock()
	defer s.systemAccounts.mu.RUnlock()

	switch purpose {
	case pmodel.SystemAccountPurposeExternalSource:
		if s.systemAccounts.externalSource == nil {
			return nil
		}
		return s.systemAccounts.externalSource[currency]
	case pmodel.SystemAccountPurposeExternalSink:
		if s.systemAccounts.externalSink == nil {
			return nil
		}
		return s.systemAccounts.externalSink[currency]
	default:
		return nil
	}
}

func (s *Service) cacheSystemAccount(purpose pmodel.SystemAccountPurpose, currency string, account *pmodel.LedgerAccount) {
	if account == nil {
		return
	}
	s.systemAccounts.mu.Lock()
	defer s.systemAccounts.mu.Unlock()

	switch purpose {
	case pmodel.SystemAccountPurposeExternalSource:
		if s.systemAccounts.externalSource == nil {
			s.systemAccounts.externalSource = make(map[string]*pmodel.LedgerAccount)
		}
		s.systemAccounts.externalSource[currency] = account
	case pmodel.SystemAccountPurposeExternalSink:
		if s.systemAccounts.externalSink == nil {
			s.systemAccounts.externalSink = make(map[string]*pmodel.LedgerAccount)
		}
		s.systemAccounts.externalSink[currency] = account
	}
}

func newExternalSystemAccount(purpose pmodel.SystemAccountPurpose, currency string) *pmodel.LedgerAccount {
	ref := bson.NewObjectID()
	purposeCopy := purpose
	account := &pmodel.LedgerAccount{
		AccountCode:   generateAccountCode(pmodel.LedgerAccountTypeAsset, currency, ref),
		AccountType:   pmodel.LedgerAccountTypeAsset,
		Currency:      currency,
		Status:        pmodel.LedgerAccountStatusActive,
		AllowNegative: true,
		Scope:         pmodel.LedgerAccountScopeSystem,
		SystemPurpose: &purposeCopy,
		Metadata: map[string]string{
			"system": "true",
		},
	}
	account.SetID(ref)
	return account
}
