import { browser } from "@web/core/browser/browser";
import { useService } from "@web/core/utils/hooks";
import { patch } from "@web/core/utils/patch";
import { WebClient } from "@web/webclient/webclient";
import { onWillDestroy } from "@odoo/owl";
import { _t } from "@web/core/l10n/translation";

const USER_DEVICES_MODEL = "mail.push.device";

patch(WebClient.prototype, {
    /**
     * @override
     */
    setup() {
        super.setup();
        this.orm = useService("orm");
        this.notification = useService("notification");
        if (this._canSendNativeNotification) {
            this._subscribePush();
        }
        if (browser.navigator.permissions) {
            let notificationPerm;
            const onPermissionChange = () => {
                if (this._canSendNativeNotification) {
                    this._subscribePush();
                } else {
                    this._unsubscribePush();
                }
            };
            browser.navigator.permissions.query({ name: "notifications" }).then((perm) => {
                notificationPerm = perm;
                notificationPerm.addEventListener("change", onPermissionChange);
            });
            onWillDestroy(() => {
                notificationPerm?.removeEventListener("change", onPermissionChange);
            });
        }
    },
    /**
     *
     * @returns {boolean}
     * @private
     */
    get _canSendNativeNotification() {
        return browser.Notification?.permission === "granted";
    },

    /**
     * Subscribe device from push notification
     *
     * @private
     * @return {Promise<void>}
     */
    async _subscribePush(numberTry = 1) {
        const pushManager = await this.pushManager();
        if (!pushManager) {
            return;
        }
        let subscription = await pushManager.getSubscription();
        const previousEndpoint = browser.localStorage.getItem(`${USER_DEVICES_MODEL}_endpoint`);
        // This may occur if the subscription was refreshed by the browser,
        // but it may also happen if the subscription has been revoked or lost.
        if (!subscription) {
            try {
                subscription = await pushManager.subscribe({
                    userVisibleOnly: true,
                    applicationServerKey: await this._getApplicationServerKey(),
                });
            } catch (error) {
                console.warn(error);
                this.notification.add(error.message, {
                    title: _t("Failed to enable push notifications"),
                    type: "danger",
                    sticky: true,
                });
                if (await navigator.brave?.isBrave()) {
                    this.notification.add(
                        _t(
                            "Brave: enable 'Google Services for Push Messaging' to enable push notifications"
                        ),
                        {
                            type: "warning",
                            sticky: true,
                        }
                    );
                }
                return;
            }
            browser.localStorage.setItem(`${USER_DEVICES_MODEL}_endpoint`, subscription.endpoint);
        }
        const kwargs = subscription.toJSON();
        if (previousEndpoint && subscription.endpoint !== previousEndpoint) {
            kwargs.previous_endpoint = previousEndpoint;
        }
        try {
            kwargs.vapid_public_key = this._arrayBufferToBase64(
                subscription.options.applicationServerKey
            );
            await this.orm.call(USER_DEVICES_MODEL, "register_devices", [], kwargs);
        } catch (e) {
            const invalidVapidErrorClass = "odoo.addons.mail.tools.jwt.InvalidVapidError";
            const warningMessage = "Error sending subscription information to the server";
            if (e.data?.name === invalidVapidErrorClass) {
                const MAX_TRIES = 2;
                if (numberTry < MAX_TRIES) {
                    await subscription.unsubscribe();
                    this._subscribePush(numberTry + 1);
                } else {
                    console.warn(warningMessage);
                }
            } else {
                console.warn(`${warningMessage}: ${e.data?.debug}`);
            }
        }
    },

    /**
     * Unsubscribe device from push notification
     *
     * @private
     * @return {Promise<void>}
     */
    async _unsubscribePush() {
        const pushManager = await this.pushManager();
        if (!pushManager) {
            return;
        }
        const subscription = await pushManager.getSubscription();
        if (!subscription) {
            return;
        }
        await this.orm.call(USER_DEVICES_MODEL, "unregister_devices", [], {
            endpoint: subscription.endpoint,
        });
        await subscription.unsubscribe();
        browser.localStorage.removeItem(`${USER_DEVICES_MODEL}_endpoint`);
    },

    /**
     * Retrieve the PushManager interface of the Push API provides a way to receive notifications from third-party
     * servers as well as request URLs for push notifications.
     *
     * @return {Promise<PushManager>}
     */
    async pushManager() {
        const registration = await browser.navigator.serviceWorker?.getRegistration();
        return registration?.pushManager;
    },

    /**
     *
     * The Application Server Key is need to be an Uint8Array.
     * This format is used when the exchanging secret key between client and server.
     * This base64 to Uint8Array implementation is inspired by https://github.com/gbhasha/base64-to-uint8array
     *
     * @private
     * @return {Uint8Array}
     */
    async _getApplicationServerKey() {
        const vapid_public_key_base64 = await this.orm.call(
            USER_DEVICES_MODEL,
            "get_web_push_vapid_public_key"
        );
        const padding = "=".repeat((4 - (vapid_public_key_base64.length % 4)) % 4);
        const base64 = (vapid_public_key_base64 + padding).replace(/-/g, "+").replace(/_/g, "/");
        const rawData = atob(base64);
        const outputArray = new Uint8Array(rawData.length);
        for (let i = 0; i < rawData.length; ++i) {
            outputArray[i] = rawData.charCodeAt(i);
        }
        return outputArray;
    },

    /**
     * Convert an ArrayBuffer to a base64 string without padding
     * @param buffer {ArrayBuffer}
     * @return {string}
     * @private
     */
    _arrayBufferToBase64(buffer) {
        const bytes = new Uint8Array(buffer);
        let binary = "";
        for (let i = 0; i < bytes.byteLength; i++) {
            binary += String.fromCharCode(bytes[i]);
        }
        return window.btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
    },
});
