package srequest

import (
	"encoding/json"

	"github.com/tech/sendico/pkg/merrors"
)

type EndpointType string

const (
	EndpointTypeLedger        EndpointType = "ledger"
	EndpointTypeManagedWallet EndpointType = "managedWallet"
	EndpointTypeExternalChain EndpointType = "cryptoAddress"
	EndpointTypeCard          EndpointType = "card"
	EndpointTypeCardToken     EndpointType = "cardToken"
	EndpointTypeWallet        EndpointType = "wallet"
	EndpointTypeBankAccount   EndpointType = "bankAccount"
	EndpointTypeIBAN          EndpointType = "iban"
)

// Endpoint is a discriminated union for payment endpoints.
type Endpoint struct {
	Type     EndpointType      `json:"type"`
	Data     json.RawMessage   `json:"data"`
	Metadata map[string]string `json:"metadata,omitempty"`
}

func newEndpoint(kind EndpointType, payload interface{}, metadata map[string]string) (Endpoint, error) {
	data, err := json.Marshal(payload)
	if err != nil {
		return Endpoint{}, merrors.Internal("marshal endpoint payload failed")
	}
	return Endpoint{
		Type:     kind,
		Data:     data,
		Metadata: cloneStringMap(metadata),
	}, nil
}

func (e Endpoint) decodePayload(expected EndpointType, dst interface{}) error {
	actual := normalizeEndpointType(e.Type)
	if actual == "" {
		return merrors.InvalidArgument("endpoint type is required")
	}
	if actual != expected {
		return merrors.InvalidArgument("expected endpoint type " + string(expected) + ", got " + string(e.Type))
	}
	if len(e.Data) == 0 {
		return merrors.InvalidArgument("endpoint data is required for type " + string(expected))
	}
	if err := json.Unmarshal(e.Data, dst); err != nil {
		return merrors.InvalidArgument("decode " + string(expected) + " endpoint: " + err.Error())
	}
	return nil
}

func (e *Endpoint) UnmarshalJSON(data []byte) error {
	var envelope struct {
		Type     EndpointType      `json:"type"`
		Data     json.RawMessage   `json:"data"`
		Metadata map[string]string `json:"metadata"`
	}
	if err := json.Unmarshal(data, &envelope); err == nil {
		if envelope.Type != "" || len(envelope.Data) > 0 {
			if envelope.Type == "" {
				return merrors.InvalidArgument("endpoint type is required")
			}
			*e = Endpoint{
				Type:     normalizeEndpointType(envelope.Type),
				Data:     envelope.Data,
				Metadata: cloneStringMap(envelope.Metadata),
			}
			return nil
		}
	}

	var legacy LegacyPaymentEndpoint
	if err := json.Unmarshal(data, &legacy); err != nil {
		return err
	}
	endpoint, err := LegacyPaymentEndpointToEndpointDTO(&legacy)
	if err != nil {
		return err
	}
	if endpoint == nil {
		return merrors.InvalidArgument("endpoint payload is empty")
	}
	*e = *endpoint
	return nil
}

func NewLedgerEndpointDTO(payload LedgerEndpoint, metadata map[string]string) (Endpoint, error) {
	return newEndpoint(EndpointTypeLedger, payload, metadata)
}

func NewManagedWalletEndpointDTO(payload ManagedWalletEndpoint, metadata map[string]string) (Endpoint, error) {
	return newEndpoint(EndpointTypeManagedWallet, payload, metadata)
}

func NewExternalChainEndpointDTO(payload ExternalChainEndpoint, metadata map[string]string) (Endpoint, error) {
	return newEndpoint(EndpointTypeExternalChain, payload, metadata)
}

func NewCardEndpointDTO(payload CardEndpoint, metadata map[string]string) (Endpoint, error) {
	return newEndpoint(EndpointTypeCard, payload, metadata)
}

func NewCardTokenEndpointDTO(payload CardTokenEndpoint, metadata map[string]string) (Endpoint, error) {
	return newEndpoint(EndpointTypeCardToken, payload, metadata)
}

func NewWalletEndpointDTO(payload WalletEndpoint, metadata map[string]string) (Endpoint, error) {
	return newEndpoint(EndpointTypeWallet, payload, metadata)
}

func NewBankAccountEndpointDTO(payload BankAccountEndpoint, metadata map[string]string) (Endpoint, error) {
	return newEndpoint(EndpointTypeBankAccount, payload, metadata)
}

func NewIBANEndpointDTO(payload IBANEndpoint, metadata map[string]string) (Endpoint, error) {
	return newEndpoint(EndpointTypeIBAN, payload, metadata)
}

func (e Endpoint) DecodeLedger() (LedgerEndpoint, error) {
	var payload LedgerEndpoint
	return payload, e.decodePayload(EndpointTypeLedger, &payload)
}

func (e Endpoint) DecodeManagedWallet() (ManagedWalletEndpoint, error) {
	var payload ManagedWalletEndpoint
	return payload, e.decodePayload(EndpointTypeManagedWallet, &payload)
}

func (e Endpoint) DecodeExternalChain() (ExternalChainEndpoint, error) {
	var payload ExternalChainEndpoint
	return payload, e.decodePayload(EndpointTypeExternalChain, &payload)
}

func (e Endpoint) DecodeCard() (CardEndpoint, error) {
	var payload CardEndpoint
	return payload, e.decodePayload(EndpointTypeCard, &payload)
}

func (e Endpoint) DecodeCardToken() (CardTokenEndpoint, error) {
	var payload CardTokenEndpoint
	return payload, e.decodePayload(EndpointTypeCardToken, &payload)
}

func (e Endpoint) DecodeWallet() (WalletEndpoint, error) {
	var payload WalletEndpoint
	return payload, e.decodePayload(EndpointTypeWallet, &payload)
}

func (e Endpoint) DecodeBankAccount() (BankAccountEndpoint, error) {
	var payload BankAccountEndpoint
	return payload, e.decodePayload(EndpointTypeBankAccount, &payload)
}

func (e Endpoint) DecodeIBAN() (IBANEndpoint, error) {
	var payload IBANEndpoint
	return payload, e.decodePayload(EndpointTypeIBAN, &payload)
}

func LegacyPaymentEndpointToEndpointDTO(old *LegacyPaymentEndpoint) (*Endpoint, error) {
	if old == nil {
		//nolint:nilnil // Nil legacy endpoint means no endpoint provided.
		return nil, nil
	}

	count := 0
	var endpoint Endpoint
	var err error

	if old.Ledger != nil {
		count++
		endpoint, err = NewLedgerEndpointDTO(*old.Ledger, old.Metadata)
	}
	if old.ManagedWallet != nil {
		count++
		endpoint, err = NewManagedWalletEndpointDTO(*old.ManagedWallet, old.Metadata)
	}
	if old.ExternalChain != nil {
		count++
		endpoint, err = NewExternalChainEndpointDTO(*old.ExternalChain, old.Metadata)
	}
	if old.Card != nil {
		count++
		endpoint, err = NewCardEndpointDTO(*old.Card, old.Metadata)
	}

	if err != nil {
		return nil, err
	}
	if count == 0 {
		return nil, merrors.InvalidArgument("exactly one endpoint must be set")
	}
	if count > 1 {
		return nil, merrors.InvalidArgument("only one endpoint can be set")
	}
	return &endpoint, nil
}

func EndpointDTOToLegacyPaymentEndpoint(new *Endpoint) (*LegacyPaymentEndpoint, error) {
	if new == nil {
		//nolint:nilnil // Nil endpoint DTO means no endpoint provided.
		return nil, nil
	}

	legacy := &LegacyPaymentEndpoint{
		Metadata: cloneStringMap(new.Metadata),
	}

	switch normalizeEndpointType(new.Type) {
	case EndpointTypeLedger:
		payload, err := new.DecodeLedger()
		if err != nil {
			return nil, err
		}
		legacy.Ledger = &payload
	case EndpointTypeManagedWallet:
		payload, err := new.DecodeManagedWallet()
		if err != nil {
			return nil, err
		}
		legacy.ManagedWallet = &payload
	case EndpointTypeExternalChain:
		payload, err := new.DecodeExternalChain()
		if err != nil {
			return nil, err
		}
		legacy.ExternalChain = &payload
	case EndpointTypeCard:
		payload, err := new.DecodeCard()
		if err != nil {
			return nil, err
		}
		legacy.Card = &payload
	default:
		return nil, merrors.InvalidArgument("unsupported endpoint type: " + string(new.Type))
	}
	return legacy, nil
}

var endpointTypeAliases = map[EndpointType]EndpointType{
	"managed_wallet": EndpointTypeManagedWallet,
	"external_chain": EndpointTypeExternalChain,
	"card_token":     EndpointTypeCardToken,
	"bank_account":   EndpointTypeBankAccount,
}

func normalizeEndpointType(t EndpointType) EndpointType {
	if canonical, ok := endpointTypeAliases[t]; ok {
		return canonical
	}
	return t
}

func cloneStringMap(src map[string]string) map[string]string {
	if len(src) == 0 {
		return nil
	}
	dst := make(map[string]string, len(src))
	for k, v := range src {
		dst[k] = v
	}
	return dst
}
