import { injectCssIntoNode, injectGlobalStyle } from '../internal/style-injector/src/index.mjs';
import wn, { unmountComponentAtNode as hn, flushSync as pn, render as q } from '../node_modules/preact/compat/dist/compat.module.mjs';

/**
 * See https://stytch.com/docs/sdks/ui-callbacks for B2C and https://stytch.com/docs/b2b/sdks/ui-callbacks for B2B
 * event types and data
 */ class StytchDOMEvent extends Event {
    stytchEventType;
    stytchEventData;
    constructor(event){
        // Event names are lowercase for Vue which only accepts lowercase events
        super('stytch-event', {
            composed: true
        });
        this.stytchEventType = event.type;
        this.stytchEventData = event.data;
    }
}
/**
 * DOM event wrapper around {@link StytchSDKUIError}
 */ class StytchError extends Event {
    error;
    constructor(error){
        super('stytch-error', {
            composed: true
        }), this.error = error;
    }
}
/**
 * Creates a web component based on a React component, and an imperative mount() function
 * (mostly for backwards compatibility).
 *
 * Implementation notes:
 * - The component allows for attributes, properties and events. Props and attributes need to be declared
 *   and are handled separately.
 * - Currently, attributes are just treated all as strings, but in the future if we want we can also write
 *   functions to cast to booleans and numbers. We reflect attributes to properties following
 *   https://web.dev/articles/custom-elements-best-practices#avoid_reentrancy_issues
 * - Events are regular DOM events and fired using regular DOM dispatchEvent function
 * - Props use a setter to trigger re-rendering. We use an internal, private #props mainly to make
 *   types work. It also prevents Props clobbering HTML element properties.
 * - To allow multiple props to be set all at once, we schedule a render rather than immediately rendering
 *   on setting props. We also expose a render() function that lets users set multiple props at once.
 * - For rare cases where users want to render immediately, we also expose a flushRender() function
 *   (similar to React)
 * - The types are quite messy. TS does not have great higher order types needed to support the stuff
 *   here so there's going to be a lot of casting. See comments for explanation. We also manually write
 *   types (StytchWebComponentConstructor) and cast them to it.
 */ /* @__NO_SIDE_EFFECTS__ */ function createWebComponent(Component, { propNames, attributeNames = [] }) {
    // If we're not in a browser env, return noop classes and components to avoid code trying to run the code below
    // which would only work in a browser
    if (typeof window === 'undefined') {
        return class {
            render() {
            // noop
            }
            flushRender() {
            // noop
            }
        };
    }
    // HTML attribute names are all lowercase, so we use a map of lowercase attribute name to camelCase prop name
    // https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute
    const attributeNameMap = Object.fromEntries(attributeNames.map((attr)=>[
            attr.toLowerCase(),
            attr
        ]));
    // This class only implements StytchWebComponentMethods. It is basically impossible to type the '& Props'
    // part of the type. We implement in the constructor using Object.defineProperties but TS do not have the
    // higher order typings needed to make this really work, so we make a best-effort to type it, then use casts
    // to make up for it
    return class WebComponent extends HTMLElement {
        /** Root element is the element our React component is mounted into */ #mountPoint;
        static observedAttributes = Object.keys(attributeNameMap);
        #shadow;
        #renderQueued = false;
        #props = {};
        constructor({ shadow } = {}){
            super();
            // The shadow attribute can be set either already on the element, or passed in via the constructor.
            this.#shadow = shadow ?? this.getAttribute('shadow') != null;
            // For other attributes, reflect attributes to property
            for (const attr of WebComponent.observedAttributes){
                const value = this.getAttribute(attr);
                if (value != null) {
                    const propName = attributeNameMap[attr];
                    this.#props[propName] = value;
                }
            }
            for (const propName of propNames){
                // In case properties were set before the component is registered
                if (Object.hasOwn(this, propName)) {
                    this.#props[propName] = this[propName];
                }
                // Make all accepted props reactive by defining getter/setter pairs for them
                // and triggering re-render on set
                Object.defineProperty(this, propName, {
                    get: ()=>this.#props[propName],
                    set: (value)=>{
                        // as string to exclude symbol and number even though they are not allowed in the Record key types
                        if (attributeNames.includes(propName)) this.setAttribute(propName, value);
                        this.#props[propName] = value;
                        this.#queueRender();
                    }
                });
            }
        }
        connectedCallback() {
            if (!this.#mountPoint) {
                this.#mountPoint = document.createElement('div');
                if (this.#shadow) {
                    this.setAttribute('shadow', 'true');
                    const shadowRoot = this.attachShadow({
                        mode: 'open'
                    });
                    shadowRoot.appendChild(injectCssIntoNode());
                    shadowRoot.appendChild(this.#mountPoint);
                } else {
                    injectGlobalStyle();
                    this.appendChild(this.#mountPoint);
                }
            }
            this.#queueRender();
        }
        disconnectedCallback() {
            if (this.#mountPoint) {
                hn(this.#mountPoint);
            }
        }
        attributeChangedCallback(name, oldValue, newValue) {
            const propName = attributeNameMap[name];
            this.#props[propName] = newValue;
        }
        render(props) {
            Object.assign(this, props);
        }
        flushRender() {
            pn(()=>{
                this.#renderInternal();
            });
        }
        // Internal methods
        #queueRender() {
            // We only want to call render once per tick
            if (!this.#renderQueued) {
                this.#renderQueued = true;
                window.queueMicrotask(()=>{
                    this.#renderInternal();
                });
            }
        }
        // This works with the one-tap slot defined in B2C GoogleOneTap. The node must be visible
        // to the outer DOM since the Google script looks for the node there, so use a slot we control
        // ourselves to do that
        #renderOneTap(props) {
            const config = getConfig(props);
            if (!config?.oauthOptions?.providers.some((config)=>typeof config === 'object' && config.type === 'google' && config.one_tap)) {
                return;
            }
            if (!this.querySelector('#google-parent-prompt')) {
                const mountPoint = document.createElement('div');
                mountPoint.id = 'google-parent-prompt';
                mountPoint.slot = 'one-tap';
                this.appendChild(mountPoint);
            }
        }
        #callbacks = {
            onError: (error)=>{
                getCallbacks(this.#props)?.onError?.(error);
                this.dispatchEvent(new StytchError(error));
            },
            onEvent: (event)=>{
                getCallbacks(this.#props)?.onEvent?.(event);
                this.dispatchEvent(new StytchDOMEvent(event));
            }
        };
        #renderInternal() {
            // This can happen if you try to update props before inserting this into the DOM
            if (!this.#mountPoint) return;
            this.#renderQueued = false;
            // Assume the component is not ready yet if there are no props
            if (Object.keys(this.#props).length === 0) {
                return;
            }
            this.#renderOneTap(this.#props);
            // Reflect shadow attr back onto enableShadowDOM config
            let props = this.#props;
            if (this.#shadow) {
                props = setShadow(props);
            }
            q(/*#__PURE__*/ wn.createElement(Component, {
                ...props,
                callbacks: this.#callbacks
            }), this.#mountPoint);
        }
    };
}
/* @__NO_SIDE_EFFECTS__ */ function createMountFn(defaultName, WebComponent) {
    if (typeof window === 'undefined') {
        return ()=>null;
    }
    return ({ elementId, ...props })=>{
        // Custom elements can only be defined once
        if (!customElements.get(defaultName)) {
            customElements.define(defaultName, WebComponent);
        }
        const targetNode = document.querySelector(elementId);
        if (!targetNode) {
            throw new Error(`The selector you specified (${elementId}) applies to no DOM elements that are currently on the page. Make sure the element exists on the page before calling mountLogin().`);
        }
        // Re-render if the component is already mounted
        let node;
        if (// HTML Node names all upper case always
        targetNode.firstChild?.nodeName.toLowerCase() === defaultName.toLowerCase()) {
            node = targetNode.firstChild;
            node.render(props);
        } else {
            const shadow = getShadow(props);
            node = new WebComponent({
                shadow
            });
            targetNode.appendChild(node);
        }
        return node;
    };
}
// Helper to avoid having to write the long cast everywhere.
// This duck types props have a config property which is either a B2C or B2B login config
function getConfig(props) {
    return props?.config;
}
function getCallbacks(props) {
    return props.callbacks;
}
function getShadow(props) {
    const { config, presentation } = props;
    return presentation?.options?.enableShadowDOM ?? config?.enableShadowDOM;
}
function setShadow(props) {
    const { presentation } = props;
    if (getShadow(props)) {
        return props;
    }
    return {
        ...props,
        presentation: {
            ...presentation,
            options: {
                ...presentation?.options,
                enableShadowDOM: true
            }
        }
    };
}

export { StytchDOMEvent, StytchError, createMountFn, createWebComponent };
//# sourceMappingURL=WebComponents.mjs.map
