/* eslint-disable @typescript-eslint/ban-types */

import * as React from 'react';
import { ContextConsumer } from '../context';
import ApplePayElementComponent from './elements/applepay-element';
import SamsungPayElementComponent from './elements/samsungpay-element';
import BankElementComponent from './elements/bank-element';
import CardElementComponent from './elements/card-element';
import GooglePayElementComponent from './elements/googlepay-element';
import IBANElementComponent from './elements/iban-element';
import PaypalElementComponent from './elements/paypal-element';

import type {
    FramePayApplePayProps,
    FramePaySamsungPayProps,
    FramePayBankProps,
    FramePayCardProps,
    FramePayComponentProps,
    FramePayGooglePayProps,
    FramePayIBANProps,
    FramePayPaypalProps,
} from '../../../types/injector';
import type { FramePayContext } from '../../../types/context';
import type {
    ApplePayProps,
    BankProps,
    CardProps,
    FramepayProps,
    GooglePayProps,
    IBANProps,
    PaypalProps,
    SamsungPayProps,
} from '../../../types/elements';
import type { FramePay } from '@rebilly/framepay';

// Create the main framepay prop for all the components.
// We take the normal framepay object and add error and ready on top of it.
// Todo: Refactor this to use a different approach where we do not decorate the framepay object.
const makeFramepayProps = (data: FramePayContext): FramepayProps => {
    const apiTarget = data.api ?? {}; // Use an empty object if framepay is null or undefined

    const apiProxy = new Proxy(apiTarget, {
        get(target: FramePay, prop: keyof FramePay | 'error' | 'ready') {
            // If it's a property of the FramePay object...
            if (prop in target) {
                const key = prop as keyof FramePay;

                // If it's a function, return a wrapper that calls the original function
                // otherwise private properties will break for proxies, see:
                // https://stackoverflow.com/a/75153646
                if (typeof target[key] === 'function') {
                    return (...params: unknown[]) => {
                        return (target[key] as Function)(...params);
                    };
                }

                // Otherwise, it is not a function but a property, return it as is
                return target[key];
            }

            // If it's one of the additional properties, return it from data
            if (prop === 'error') return data.error;
            if (prop === 'ready') return data.ready;

            // Default behavior: return undefined for unknown properties
            return undefined;
        },
    });

    // Return the proxy which includes both the API properties and augmented properties
    return apiProxy as FramepayProps;
};

function Hoc<P extends object>(
    name: string,
    WrappedComponent: React.ComponentType<P>,
    provider: (data: FramePayContext) => object,
) {
    return class extends React.Component<P, {}> {
        static readonly displayName = `withFramePay${name}(${
            WrappedComponent.displayName || WrappedComponent.name || 'Component'
        })`;

        render() {
            return (
                <ContextConsumer>
                    {(data: FramePayContext) => {
                        const provided = provider(data);
                        return (
                            <WrappedComponent
                                {...{ ...this.props, ...provided }}
                            />
                        );
                    }}
                </ContextConsumer>
            );
        }
    };
}

const elementsFabric = (
    type:
        | 'card'
        | 'bankAccount'
        | 'iban'
        | 'applePay'
        | 'samsungPay'
        | 'googlePay'
        | 'paypal',
): object => {
    if (type === 'iban') {
        /**
         * IBAN
         */

        /**
         * IBAN number element
         */
        const IBANElement = Hoc(
            'IBANElement',
            IBANElementComponent,
            (data: FramePayContext) =>
                ({
                    Framepay: makeFramepayProps(data),
                    elementType: 'iban',
                } as IBANProps),
        );

        return {
            IBANElement,
        };
    }

    if (type === 'bankAccount') {
        /**
         * BankAccount
         * @see https://www.rebilly.com/docs/dev-docs/framepay-global-reference/#framepay.bban.mount
         */

        /**
         * Bank AccountType Element
         */
        const BankAccountTypeElement = Hoc(
            'BankAccountTypeElement',
            BankElementComponent,
            (data: FramePayContext) =>
                ({
                    Framepay: makeFramepayProps(data),
                    elementType: 'bankAccountType',
                } as BankProps),
        );

        /**
         * BankRoutingNumber Element
         */
        const BankRoutingNumberElement = Hoc(
            'BankRoutingNumberElement',
            BankElementComponent,
            (data: FramePayContext) =>
                ({
                    Framepay: makeFramepayProps(data),
                    elementType: 'bankRoutingNumber',
                } as BankProps),
        );

        /**
         * Bank AccountNumber Element
         */
        const BankAccountNumberElement = Hoc(
            'BankAccountNumberElement',
            BankElementComponent,
            (data: FramePayContext) =>
                ({
                    Framepay: makeFramepayProps(data),
                    elementType: 'bankAccountNumber',
                } as BankProps),
        );

        return {
            BankAccountNumberElement,
            BankAccountTypeElement,
            BankRoutingNumberElement,
        };
    }

    if (type === 'card') {
        /**
         * Default Card Element
         * @see https://www.rebilly.com/docs/dev-docs/framepay-global-reference/#framepay.card.mount
         */
        const CardElement = Hoc(
            'CardElement',
            CardElementComponent,
            (data: FramePayContext) =>
                ({
                    Framepay: makeFramepayProps(data),
                } as CardProps),
        );

        /**
         * Card CVV Element
         */
        const CardCvvElement = Hoc(
            'CardCvvElement',
            CardElementComponent,
            (data: FramePayContext) =>
                ({
                    Framepay: makeFramepayProps(data),
                    elementType: 'cardCvv',
                } as CardProps),
        );

        /**
         * Card Expiry Element
         */
        const CardExpiryElement = Hoc(
            'CardExpiryElement',
            CardElementComponent,
            (data: FramePayContext) =>
                ({
                    Framepay: makeFramepayProps(data),
                    elementType: 'cardExpiration',
                } as CardProps),
        );

        /**
         * Card Number Element
         */
        const CardNumberElement = Hoc(
            'CardNumberElement',
            CardElementComponent,
            (data: FramePayContext) =>
                ({
                    Framepay: makeFramepayProps(data),
                    elementType: 'cardNumber',
                } as CardProps),
        );

        return {
            CardCvvElement,
            CardElement,
            CardExpiryElement,
            CardNumberElement,
        };
    }

    if (type === 'applePay') {
        /**
         * Apple Pay
         */

        const ApplePayElement = Hoc(
            'ApplePayElement',
            ApplePayElementComponent,
            (data: FramePayContext) =>
                ({
                    Framepay: makeFramepayProps(data),
                } as ApplePayProps),
        );

        return {
            ApplePayElement,
        };
    }

    if (type === 'samsungPay') {
        /**
         * Samsung Pay
         */

        const SamsungPayElement = Hoc(
            'SamsungPayElement',
            SamsungPayElementComponent,
            (data: FramePayContext) =>
                ({
                    Framepay: makeFramepayProps(data),
                } as SamsungPayProps),
        );

        return {
            SamsungPayElement,
        };
    }

    if (type === 'googlePay') {
        /**
         * Google Pay
         */

        const GooglePayElement = Hoc(
            'GooglePayElement',
            GooglePayElementComponent,
            (data: FramePayContext) =>
                ({
                    Framepay: makeFramepayProps(data),
                } as GooglePayProps),
        );

        return {
            GooglePayElement,
        };
    }

    if (type === 'paypal') {
        /**
         * Paypal
         */

        const PaypalElement = Hoc(
            'PaypalElement',
            PaypalElementComponent,
            (data: FramePayContext) =>
                ({
                    Framepay: makeFramepayProps(data),
                } as PaypalProps),
        );

        return {
            PaypalElement,
        };
    }

    /**
     * Throw the error by default.
     */
    throw new Error(
        `Invalid PaymentMethod type, see PaymentMethodTypes declaration`,
    );
};

export function withFramePay<OriginalProps extends object>(
    WrappedComponent: React.ComponentType<
        OriginalProps & FramePayComponentProps
    >,
) {
    const elements = {
        ...elementsFabric('card'),
        ...elementsFabric('bankAccount'),
        ...elementsFabric('iban'),
        ...elementsFabric('applePay'),
        ...elementsFabric('samsungPay'),
        ...elementsFabric('googlePay'),
        ...elementsFabric('paypal'),
    } as FramePayComponentProps;
    return class extends React.Component<OriginalProps, {}> {
        static readonly displayName = `withFramePay(${
            WrappedComponent.displayName || WrappedComponent.name || 'Component'
        })`;

        render() {
            return (
                <ContextConsumer>
                    {(data: FramePayContext) => {
                        return (
                            <WrappedComponent
                                {...{
                                    ...this.props,
                                    ...elements,
                                    Framepay: makeFramepayProps(data),
                                }}
                            />
                        );
                    }}
                </ContextConsumer>
            );
        }
    };
}

export function withFramePayCardComponent<OriginalProps extends object>(
    WrappedComponent: React.ComponentType<OriginalProps & FramePayCardProps>,
) {
    const elements = elementsFabric('card') as FramePayCardProps;
    return class extends React.Component<OriginalProps, {}> {
        static readonly displayName = `withFramePayCardComponent(${
            WrappedComponent.displayName || WrappedComponent.name || 'Component'
        })`;

        render() {
            return (
                <ContextConsumer>
                    {(data: FramePayContext) => {
                        return (
                            <WrappedComponent
                                {...{
                                    ...this.props,
                                    ...elements,
                                    Framepay: makeFramepayProps(data),
                                }}
                            />
                        );
                    }}
                </ContextConsumer>
            );
        }
    };
}

export function withFramePayBankComponent<OriginalProps extends object>(
    WrappedComponent: React.ComponentType<OriginalProps & FramePayBankProps>,
) {
    const elements = elementsFabric('bankAccount') as FramePayBankProps;
    return class extends React.Component<OriginalProps, {}> {
        static readonly displayName = `withFramePayBankComponent(${
            WrappedComponent.displayName || WrappedComponent.name || 'Component'
        })`;

        render() {
            return (
                <ContextConsumer>
                    {(data: FramePayContext) => {
                        return (
                            <WrappedComponent
                                {...{
                                    ...this.props,
                                    ...elements,
                                    Framepay: makeFramepayProps(data),
                                }}
                            />
                        );
                    }}
                </ContextConsumer>
            );
        }
    };
}

export function withFramePayIBANComponent<OriginalProps extends object>(
    WrappedComponent: React.ComponentType<OriginalProps & FramePayIBANProps>,
) {
    const elements = elementsFabric('iban') as FramePayIBANProps;
    return class extends React.Component<OriginalProps, {}> {
        static readonly displayName = `withFramePayIBANComponent(${
            WrappedComponent.displayName || WrappedComponent.name || 'Component'
        })`;

        render() {
            return (
                <ContextConsumer>
                    {(data: FramePayContext) => {
                        return (
                            <WrappedComponent
                                {...{
                                    ...this.props,
                                    ...elements,
                                    Framepay: makeFramepayProps(data),
                                }}
                            />
                        );
                    }}
                </ContextConsumer>
            );
        }
    };
}

export function withFramePayApplePayComponent<OriginalProps extends object>(
    WrappedComponent: React.ComponentType<
        OriginalProps & FramePayApplePayProps
    >,
) {
    const elements = elementsFabric('applePay') as FramePayApplePayProps;
    return class extends React.Component<OriginalProps, {}> {
        static readonly displayName = `withFramePayApplePayComponent(${
            WrappedComponent.displayName || WrappedComponent.name || 'Component'
        })`;

        render() {
            return (
                <ContextConsumer>
                    {(data: FramePayContext) => {
                        return (
                            <WrappedComponent
                                {...{
                                    ...this.props,
                                    ...elements,
                                    Framepay: makeFramepayProps(data),
                                }}
                            />
                        );
                    }}
                </ContextConsumer>
            );
        }
    };
}

export function withFramePaySamsungPayComponent<OriginalProps extends object>(
    WrappedComponent: React.ComponentType<
        OriginalProps & FramePaySamsungPayProps
    >,
) {
    const elements = elementsFabric('samsungPay') as FramePaySamsungPayProps;
    return class extends React.Component<OriginalProps, {}> {
        static readonly displayName = `withFramePaySamsungPayComponent(${
            WrappedComponent.displayName || WrappedComponent.name || 'Component'
        })`;

        render() {
            return (
                <ContextConsumer>
                    {(data: FramePayContext) => {
                        return (
                            <WrappedComponent
                                {...{
                                    ...this.props,
                                    ...elements,
                                    Framepay: makeFramepayProps(data),
                                }}
                            />
                        );
                    }}
                </ContextConsumer>
            );
        }
    };
}

export function withFramePayGooglePayComponent<OriginalProps extends object>(
    WrappedComponent: React.ComponentType<
        OriginalProps & FramePayGooglePayProps
    >,
) {
    const elements = elementsFabric('googlePay') as FramePayGooglePayProps;
    return class extends React.Component<OriginalProps, {}> {
        static readonly displayName = `withFramePayGooglePayComponent(${
            WrappedComponent.displayName || WrappedComponent.name || 'Component'
        })`;

        render() {
            return (
                <ContextConsumer>
                    {(data: FramePayContext) => {
                        return (
                            <WrappedComponent
                                {...{
                                    ...this.props,
                                    ...elements,
                                    Framepay: makeFramepayProps(data),
                                }}
                            />
                        );
                    }}
                </ContextConsumer>
            );
        }
    };
}

export function withFramePayPaypalComponent<OriginalProps extends object>(
    WrappedComponent: React.ComponentType<OriginalProps & FramePayPaypalProps>,
) {
    const elements = elementsFabric('paypal') as FramePayPaypalProps;
    return class extends React.Component<OriginalProps, {}> {
        static readonly displayName = `withFramePayPaypalComponent(${
            WrappedComponent.displayName || WrappedComponent.name || 'Component'
        })`;

        render() {
            return (
                <ContextConsumer>
                    {(data: FramePayContext) => {
                        return (
                            <WrappedComponent
                                {...{
                                    ...this.props,
                                    ...elements,
                                    Framepay: makeFramepayProps(data),
                                }}
                            />
                        );
                    }}
                </ContextConsumer>
            );
        }
    };
}
