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"
	"github.com/tech/sendico/pkg/model/account_role"
	"github.com/tech/sendico/pkg/mutil/mzap"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.uber.org/zap"
)

const LedgerTopologyVersion = 1

var RequiredRolesV1 = []account_role.AccountRole{
	account_role.AccountRoleOperating,
	account_role.AccountRoleHold,
	account_role.AccountRolePending,
	account_role.AccountRoleTransit,
	account_role.AccountRoleSettlement,
}

func isRequiredTopologyRole(role account_role.AccountRole) bool {
	for _, required := range RequiredRolesV1 {
		if role == required {
			return true
		}
	}
	return false
}

func (s *Service) ensureLedgerTopology(ctx context.Context, orgRef bson.ObjectID, currency string) error {
	if s.storage == nil || s.storage.Accounts() == nil {
		return errStorageNotInitialized
	}
	if orgRef.IsZero() {
		return merrors.InvalidArgument("organization_ref is required")
	}
	normalizedCurrency := strings.ToUpper(strings.TrimSpace(currency))
	if normalizedCurrency == "" {
		return merrors.InvalidArgument("currency is required")
	}

	for _, role := range RequiredRolesV1 {
		if _, err := s.ensureRoleAccount(ctx, orgRef, normalizedCurrency, role); err != nil {
			return err
		}
	}
	return nil
}

func (s *Service) ensureRoleAccount(ctx context.Context, orgRef bson.ObjectID, currency string, role account_role.AccountRole) (*pmodel.LedgerAccount, error) {
	if s.storage == nil || s.storage.Accounts() == nil {
		return nil, errStorageNotInitialized
	}
	if orgRef.IsZero() {
		return nil, merrors.InvalidArgument("organization_ref is required")
	}
	normalizedCurrency := strings.ToUpper(strings.TrimSpace(currency))
	if normalizedCurrency == "" {
		return nil, merrors.InvalidArgument("currency is required")
	}
	if strings.TrimSpace(string(role)) == "" {
		return nil, merrors.InvalidArgument("role is required")
	}

	account, err := s.storage.Accounts().GetByRole(ctx, orgRef, normalizedCurrency, role)
	if err == nil {
		if isSystemTaggedAccount(account) {
			return account, nil
		}
		s.logger.Info("Found non-system account for topology role; creating missing system account",
			mzap.ObjRef("organization_ref", orgRef),
			zap.String("currency", normalizedCurrency),
			zap.String("role", string(role)))
	}
	if err != nil && !errors.Is(err, storage.ErrAccountNotFound) {
		s.logger.Warn("Failed to resolve ledger account by role", zap.Error(err),
			mzap.ObjRef("organization_ref", orgRef), zap.String("currency", normalizedCurrency),
			zap.String("role", string(role)))
		return nil, merrors.Internal("failed to resolve ledger account")
	}

	account = newSystemAccount(orgRef, normalizedCurrency, role)
	if err := s.storage.Accounts().Create(ctx, account); err != nil {
		if errors.Is(err, merrors.ErrDataConflict) {
			existing, lookupErr := s.storage.Accounts().GetByRole(ctx, orgRef, normalizedCurrency, role)
			if lookupErr == nil && existing != nil {
				return existing, nil
			}
			s.logger.Warn("Duplicate ledger account create but failed to load existing",
				zap.Error(lookupErr),
				mzap.ObjRef("organization_ref", orgRef),
				zap.String("currency", normalizedCurrency),
				zap.String("role", string(role)))
			return nil, merrors.Internal("failed to resolve ledger account after conflict")
		}
		s.logger.Warn("Failed to create system ledger account", zap.Error(err),
			mzap.ObjRef("organization_ref", orgRef), zap.String("currency", normalizedCurrency),
			zap.String("role", string(role)), zap.String("account_code", account.AccountCode))
		return nil, merrors.Internal("failed to create ledger account")
	}

	s.logger.Info("System ledger account created", mzap.ObjRef("organization_ref", orgRef),
		zap.String("currency", normalizedCurrency), zap.String("role", string(role)),
		zap.String("account_code", account.AccountCode))
	return account, nil
}

func isSystemTaggedAccount(account *pmodel.LedgerAccount) bool {
	if account == nil || account.Metadata == nil {
		return false
	}
	return strings.EqualFold(strings.TrimSpace(account.Metadata["system"]), "true")
}

func newSystemAccount(orgRef bson.ObjectID, currency string, role account_role.AccountRole) *pmodel.LedgerAccount {
	ref := bson.NewObjectID()
	account := &pmodel.LedgerAccount{
		AccountCode:   generateAccountCode(pmodel.LedgerAccountTypeAsset, currency, ref),
		AccountType:   pmodel.LedgerAccountTypeAsset,
		Currency:      currency,
		Status:        pmodel.LedgerAccountStatusActive,
		AllowNegative: false,
		Role:          role,
		Scope:         pmodel.LedgerAccountScopeOrganization,
		Metadata: map[string]string{
			"system": "true",
		},
	}
	account.OrganizationRef = &orgRef
	account.SetID(ref)
	return account
}
