package providers

import (
	"context"
	"fmt"
	"strings"

	"github.com/niels/banking-app/backend/internal/domain"
	"github.com/niels/banking-app/backend/internal/integrations/bankledger"
	"github.com/niels/banking-app/backend/internal/integrations/blockchainanalytics"
	"github.com/niels/banking-app/backend/internal/integrations/cardissuer"
	"github.com/niels/banking-app/backend/internal/integrations/custody"
	"github.com/niels/banking-app/backend/internal/integrations/identityprovider"
	"github.com/niels/banking-app/backend/internal/integrations/sanctions"
)

type AdapterMode string

const (
	ModeLocal      AdapterMode = "local"
	ModeSandbox    AdapterMode = "sandbox"
	ModeContracted AdapterMode = "contracted"
	ModeDisabled   AdapterMode = "disabled"
)

type AdapterOptions struct {
	ProviderType string
	ProviderName string
	Mode         AdapterMode
}

func WrapBankLedger(next bankledger.Provider, executor *Executor, options AdapterOptions) bankledger.Provider {
	return bankLedgerWrapper{next: next, executor: executor, options: normalizeAdapterOptions(options, "bank_ledger", "local_bank_ledger")}
}

func WrapCardIssuer(next cardissuer.Issuer, executor *Executor, options AdapterOptions) cardissuer.Issuer {
	return cardIssuerWrapper{next: next, executor: executor, options: normalizeAdapterOptions(options, "card_issuer", "local_card_issuer")}
}

func WrapIdentityProvider(next identityprovider.Provider, executor *Executor, options AdapterOptions) identityprovider.Provider {
	return identityWrapper{next: next, executor: executor, options: normalizeAdapterOptions(options, "identity", "local_identity")}
}

func WrapSanctionsProvider(next sanctions.Provider, executor *Executor, options AdapterOptions) sanctions.Provider {
	return sanctionsWrapper{next: next, executor: executor, options: normalizeAdapterOptions(options, "sanctions", "local_sanctions")}
}

func WrapCustodyProvider(next custody.Provider, executor *Executor, options AdapterOptions) custody.Provider {
	return custodyWrapper{next: next, executor: executor, options: normalizeAdapterOptions(options, "custody", "local_custody")}
}

func WrapBlockchainAnalytics(next blockchainanalytics.Provider, executor *Executor, options AdapterOptions) blockchainanalytics.Provider {
	return blockchainAnalyticsWrapper{next: next, executor: executor, options: normalizeAdapterOptions(options, "blockchain_analytics", "local_blockchain_analytics")}
}

type bankLedgerWrapper struct {
	next     bankledger.Provider
	executor *Executor
	options  AdapterOptions
}

func (w bankLedgerWrapper) IssueAccount(ctx context.Context, params bankledger.IssueAccountParams) (bankledger.IssuedAccount, error) {
	if err := w.available(); err != nil {
		return bankledger.IssuedAccount{}, err
	}
	return Execute(ctx, w.executor, w.meta("issue_account", params.UserID+"|"+params.AccountNumber), params, func(ctx context.Context) (bankledger.IssuedAccount, error) {
		return w.next.IssueAccount(ctx, params)
	})
}

type cardIssuerWrapper struct {
	next     cardissuer.Issuer
	executor *Executor
	options  AdapterOptions
}

func (w cardIssuerWrapper) CreateVirtualCard(ctx context.Context, params cardissuer.CreateVirtualCardParams) (cardissuer.CreatedVirtualCard, error) {
	if err := w.available(); err != nil {
		return cardissuer.CreatedVirtualCard{}, err
	}
	return Execute(ctx, w.executor, w.meta("create_virtual_card", params.UserID+"|"+params.AccountID), params, func(ctx context.Context) (cardissuer.CreatedVirtualCard, error) {
		return w.next.CreateVirtualCard(ctx, params)
	})
}

func (w cardIssuerWrapper) FreezeVirtualCard(ctx context.Context, externalCardID string) error {
	if err := w.available(); err != nil {
		return err
	}
	_, err := Execute(ctx, w.executor, w.meta("freeze_virtual_card", externalCardID), map[string]string{"external_card_id": externalCardID}, func(ctx context.Context) (map[string]string, error) {
		return map[string]string{"status": "frozen"}, w.next.FreezeVirtualCard(ctx, externalCardID)
	})
	return err
}

func (w cardIssuerWrapper) UnfreezeVirtualCard(ctx context.Context, externalCardID string) error {
	if err := w.available(); err != nil {
		return err
	}
	_, err := Execute(ctx, w.executor, w.meta("unfreeze_virtual_card", externalCardID), map[string]string{"external_card_id": externalCardID}, func(ctx context.Context) (map[string]string, error) {
		return map[string]string{"status": "active"}, w.next.UnfreezeVirtualCard(ctx, externalCardID)
	})
	return err
}

func (w cardIssuerWrapper) CancelVirtualCard(ctx context.Context, externalCardID string) error {
	if err := w.available(); err != nil {
		return err
	}
	_, err := Execute(ctx, w.executor, w.meta("cancel_virtual_card", externalCardID), map[string]string{"external_card_id": externalCardID}, func(ctx context.Context) (map[string]string, error) {
		return map[string]string{"status": "canceled"}, w.next.CancelVirtualCard(ctx, externalCardID)
	})
	return err
}

func (w cardIssuerWrapper) SyncSpendingLimit(ctx context.Context, externalCardID string, spendingLimitCents int64) (cardissuer.LimitSyncResult, error) {
	if err := w.available(); err != nil {
		return cardissuer.LimitSyncResult{}, err
	}
	request := map[string]any{"external_card_id": externalCardID, "spending_limit_cents": spendingLimitCents}
	return Execute(ctx, w.executor, w.meta("sync_spending_limit", externalCardID), request, func(ctx context.Context) (cardissuer.LimitSyncResult, error) {
		return w.next.SyncSpendingLimit(ctx, externalCardID, spendingLimitCents)
	})
}

type identityWrapper struct {
	next     identityprovider.Provider
	executor *Executor
	options  AdapterOptions
}

func (w identityWrapper) StartVerification(ctx context.Context, params identityprovider.StartVerificationParams) (identityprovider.Verification, error) {
	if err := w.available(); err != nil {
		return identityprovider.Verification{}, err
	}
	return Execute(ctx, w.executor, w.meta("start_verification", params.UserID), params, func(ctx context.Context) (identityprovider.Verification, error) {
		return w.next.StartVerification(ctx, params)
	})
}

type sanctionsWrapper struct {
	next     sanctions.Provider
	executor *Executor
	options  AdapterOptions
}

func (w sanctionsWrapper) ScreenPerson(ctx context.Context, params sanctions.ScreenPersonParams) (sanctions.ScreeningResult, error) {
	if err := w.available(); err != nil {
		return sanctions.ScreeningResult{}, err
	}
	return Execute(ctx, w.executor, w.meta("screen_person", params.UserID+"|"+params.Country), params, func(ctx context.Context) (sanctions.ScreeningResult, error) {
		return w.next.ScreenPerson(ctx, params)
	})
}

type custodyWrapper struct {
	next     custody.Provider
	executor *Executor
	options  AdapterOptions
}

func (w custodyWrapper) CreateWallet(ctx context.Context, params custody.CreateWalletParams) (custody.CreatedWallet, error) {
	if err := w.available(); err != nil {
		return custody.CreatedWallet{}, err
	}
	return Execute(ctx, w.executor, w.meta("create_wallet", params.UserID+"|"+params.Name), params, func(ctx context.Context) (custody.CreatedWallet, error) {
		return w.next.CreateWallet(ctx, params)
	})
}

func (w custodyWrapper) CreateDepositAddress(ctx context.Context, params custody.CreateDepositAddressParams) (custody.DepositAddress, error) {
	if err := w.available(); err != nil {
		return custody.DepositAddress{}, err
	}
	return Execute(ctx, w.executor, w.meta("create_deposit_address", params.ExternalWalletID+"|"+params.Symbol+"|"+params.Network), params, func(ctx context.Context) (custody.DepositAddress, error) {
		return w.next.CreateDepositAddress(ctx, params)
	})
}

type blockchainAnalyticsWrapper struct {
	next     blockchainanalytics.Provider
	executor *Executor
	options  AdapterOptions
}

func (w blockchainAnalyticsWrapper) ScreenAddress(ctx context.Context, params blockchainanalytics.ScreenAddressParams) (blockchainanalytics.ScreeningResult, error) {
	if err := w.available(); err != nil {
		return blockchainanalytics.ScreeningResult{}, err
	}
	return Execute(ctx, w.executor, w.meta("screen_address", params.Network+"|"+params.Address), params, func(ctx context.Context) (blockchainanalytics.ScreeningResult, error) {
		return w.next.ScreenAddress(ctx, params)
	})
}

func (w bankLedgerWrapper) meta(operation, businessKey string) CallMeta {
	return callMeta(w.options, operation, businessKey)
}

func (w cardIssuerWrapper) meta(operation, businessKey string) CallMeta {
	return callMeta(w.options, operation, businessKey)
}

func (w identityWrapper) meta(operation, businessKey string) CallMeta {
	return callMeta(w.options, operation, businessKey)
}

func (w sanctionsWrapper) meta(operation, businessKey string) CallMeta {
	return callMeta(w.options, operation, businessKey)
}

func (w custodyWrapper) meta(operation, businessKey string) CallMeta {
	return callMeta(w.options, operation, businessKey)
}

func (w blockchainAnalyticsWrapper) meta(operation, businessKey string) CallMeta {
	return callMeta(w.options, operation, businessKey)
}

func (w bankLedgerWrapper) available() error          { return adapterAvailable(w.options) }
func (w cardIssuerWrapper) available() error          { return adapterAvailable(w.options) }
func (w identityWrapper) available() error            { return adapterAvailable(w.options) }
func (w sanctionsWrapper) available() error           { return adapterAvailable(w.options) }
func (w custodyWrapper) available() error             { return adapterAvailable(w.options) }
func (w blockchainAnalyticsWrapper) available() error { return adapterAvailable(w.options) }

func callMeta(options AdapterOptions, operation, businessKey string) CallMeta {
	return CallMeta{
		ProviderType: options.ProviderType,
		ProviderName: options.ProviderName,
		Operation:    operation,
		BusinessKey:  businessKey,
	}
}

func normalizeAdapterOptions(options AdapterOptions, providerType, fallbackName string) AdapterOptions {
	options.ProviderType = strings.ToLower(strings.TrimSpace(options.ProviderType))
	options.ProviderName = strings.TrimSpace(options.ProviderName)
	options.Mode = AdapterMode(strings.ToLower(strings.TrimSpace(string(options.Mode))))
	if options.ProviderType == "" {
		options.ProviderType = providerType
	}
	if options.ProviderName == "" {
		options.ProviderName = fallbackName
	}
	if options.Mode == "" {
		options.Mode = ModeLocal
	}
	return options
}

func adapterAvailable(options AdapterOptions) error {
	switch options.Mode {
	case ModeLocal, ModeSandbox:
		return nil
	case ModeDisabled:
		return fmt.Errorf("%w: %s provider is disabled", domain.ErrProviderUnavailable, options.ProviderType)
	case ModeContracted:
		return fmt.Errorf("%w: contracted %s adapter %q is not wired in this build", domain.ErrProviderUnavailable, options.ProviderType, options.ProviderName)
	default:
		return fmt.Errorf("%w: unsupported %s provider mode %q", domain.ErrProviderUnavailable, options.ProviderType, options.Mode)
	}
}
