import api from '../../../node_modules/js-cookie/dist/js.cookie.mjs';
import { hasMultipleCookies, isLocalhost } from './utils/index.mjs';
import { getPersistentStorageKey, getKeyBoundStorage, safeLocalStorage, safeSessionStorage } from './utils/storage.mjs';
import { createDeepEqual } from '../../js-utils/src/createDeepEqual.mjs';
import { logger } from '../../core/src/utils/logger.mjs';

const STYTCH_SESSION_COOKIE = 'stytch_session';
const STYTCH_SESSION_JWT_COOKIE = 'stytch_session_jwt';
const STYTCH_INTERMEDIATE_SESSION_TOKEN_COOKIE = 'stytch_intermediate_session_token';
const SEEN_DOMAINS_KEY = 'seen_domains';
class SubscriptionDataLayer {
    publicToken;
    state;
    _opaqueTokenCookieName = null;
    _jwtCookieName = null;
    _cookiePath = null;
    _domain = null;
    _cookieAvailableToSubdomains = false;
    _istCookieName = null;
    subscriptions;
    _localStorage;
    browserSessionStorage;
    constructor(publicToken, options){
        this.publicToken = publicToken;
        this.state = null;
        this.subscriptions = {};
        // Initialize storage utilities
        this._localStorage = getKeyBoundStorage(safeLocalStorage, publicToken);
        this.browserSessionStorage = getKeyBoundStorage(safeSessionStorage, publicToken);
        if (options?.cookieOptions) {
            this._opaqueTokenCookieName = options.cookieOptions.opaqueTokenCookieName || null;
            this._jwtCookieName = options.cookieOptions.jwtCookieName || null;
            this._cookiePath = options.cookieOptions.path || null;
            this._domain = options.cookieOptions.domain || null;
            this._cookieAvailableToSubdomains = options.cookieOptions.availableToSubdomains || false;
            this._istCookieName = options.cookieOptions.istCookieName || null;
        }
        const localStorageState = this._localStorage.getItem('');
        if (!localStorageState) {
            return;
        }
        let parsedState;
        try {
            parsedState = JSON.parse(localStorageState);
        } catch  {
            // Overwrite the bad data with nulls
            this.syncToLocalStorage();
            return;
        }
        // TODO: Validate the data looks decent & matches Session/User format
        this.state = parsedState;
    }
    get opaqueTokenCookieName() {
        return this._opaqueTokenCookieName ?? STYTCH_SESSION_COOKIE;
    }
    get jwtCookieName() {
        return this._jwtCookieName ?? STYTCH_SESSION_JWT_COOKIE;
    }
    get istCookieName() {
        return this._istCookieName ?? STYTCH_INTERMEDIATE_SESSION_TOKEN_COOKIE;
    }
    readSessionCookie() {
        return {
            session_token: api.get(this.opaqueTokenCookieName),
            session_jwt: api.get(this.jwtCookieName)
        };
    }
    readIntermediateSessionTokenCookie() {
        return api.get(this.istCookieName);
    }
    writeSessionCookie(stateDiff) {
        const { state, session_token, session_jwt } = stateDiff;
        const cookieOpts = SubscriptionDataLayer.generateCookieOpts({
            expiresAt: state?.session?.expires_at ?? '',
            availableToSubdomains: this._cookieAvailableToSubdomains,
            path: this._cookiePath,
            domain: this._domain
        });
        if (cookieOpts.domain) {
            this.addSeenDomain(cookieOpts.domain);
        }
        api.set(this.opaqueTokenCookieName, session_token, cookieOpts);
        api.set(this.jwtCookieName, session_jwt, cookieOpts);
        /**
     * If a developer flips the boolean value of availableToSubdomains at any point,
     * there will be two cookies set which will could cause the user to log out since
     * the js-cookie API always returns the first cookie set. Thus, we will clear the
     * cookie that doesn't relate to the current cookie options.
     */ const alternateCookieOptions = SubscriptionDataLayer.generateCookieOpts({
            expiresAt: state?.session?.expires_at ?? '',
            availableToSubdomains: !this._cookieAvailableToSubdomains,
            path: this._cookiePath,
            domain: this._domain
        });
        if (alternateCookieOptions.domain) {
            this.addSeenDomain(alternateCookieOptions.domain);
        }
        if (hasMultipleCookies(this.jwtCookieName)) {
            api.remove(this.jwtCookieName, alternateCookieOptions);
        }
        if (hasMultipleCookies(this.opaqueTokenCookieName)) {
            api.remove(this.opaqueTokenCookieName, alternateCookieOptions);
        }
        if (hasMultipleCookies(this.jwtCookieName)) {
            logger.warn('Could not remove extraneous JWT cookie. This might happen if the cookie has been set using multiple `path` settings, and may produce unwanted behavior.');
        }
        if (hasMultipleCookies(this.opaqueTokenCookieName)) {
            logger.warn('Could not remove extraneous opaque token cookie.');
        }
    }
    writeIntermediateSessionTokenCookie(IST) {
        // ISTs are valid for 10 minutes
        const expiresAtTime = new Date(Date.now() + 10 * 60000);
        const cookieOpts = SubscriptionDataLayer.generateCookieOpts({
            expiresAt: expiresAtTime.toString(),
            availableToSubdomains: this._cookieAvailableToSubdomains,
            path: this._cookiePath,
            domain: this._domain
        });
        api.set(this.istCookieName, IST, cookieOpts);
    }
    removeSessionCookie() {
        this.removeCookies([
            this.opaqueTokenCookieName,
            this.jwtCookieName
        ]);
    }
    removeISTCookie() {
        this.removeCookies([
            this.istCookieName
        ]);
    }
    removeCookies(cookiesToRemove) {
        /**
     * Spray and Pray approach:
     * In order to delete a cookie, both the path and domain must match exactly
     * We don't always know how the cookie was set - we can use the path & info that the SDK was created with
     * but if the SDK settings have changed (e.g. a dev is developing) then things might get strange.
     * Note: it is _impossible_ to reliably delete a cookie if you don't know what path it was set at - thank,; internet
     * Our best effort approach is to just list off the likely combinations
     *
     * As of 2025-07-14, we track all domains that have been used to set cookies and try to delete
     * cookies from all of them to handle cases where domains change (e.g. something.com -> web.something.com)
     */ // Get all previously seen domains
        const trackedDomains = this.getSeenDomains();
        // Include the current domain and null (no domain) in our attempts
        const allDomains = [
            this._domain,
            null,
            ...trackedDomains
        ];
        // Remove duplicates
        const uniqueDomains = [
            ...new Set(allDomains)
        ];
        [
            true,
            false
        ].forEach((availableToSubdomains)=>{
            [
                this._cookiePath,
                null
            ].forEach((path)=>{
                uniqueDomains.forEach((domain)=>{
                    const cookieOpts = SubscriptionDataLayer.generateCookieOpts({
                        expiresAt: new Date(0).toString(),
                        availableToSubdomains,
                        path,
                        domain
                    });
                    cookiesToRemove.forEach((cookieName)=>{
                        api.remove(cookieName, cookieOpts);
                    });
                });
            });
        });
    }
    syncToLocalStorage() {
        this._localStorage.setItem('', JSON.stringify(this.state));
    }
    getItem(key) {
        return this._localStorage.getItem(key);
    }
    setItem(key, value) {
        this._localStorage.setItem(key, value);
    }
    removeItem(key) {
        this._localStorage.removeItem(key);
    }
    /**
   * Get all previously seen domains from local storage
   */ getSeenDomains() {
        const storedDomains = this.getItem(SEEN_DOMAINS_KEY);
        if (!storedDomains) {
            return [];
        }
        try {
            const parsed = JSON.parse(storedDomains);
            return Array.isArray(parsed) ? parsed : [];
        } catch  {
            return [];
        }
    }
    /**
   * Add a domain to the seen domains list
   */ addSeenDomain(domain) {
        if (!domain) {
            return;
        }
        const storedDomains = this.getSeenDomains();
        if (!storedDomains.includes(domain)) {
            storedDomains.push(domain);
            this.setItem(SEEN_DOMAINS_KEY, JSON.stringify(storedDomains));
        }
    }
    static generateCookieOpts({ path, domain, availableToSubdomains, expiresAt }) {
        const cookieOpts = {
            expires: new Date(expiresAt),
            sameSite: 'lax'
        };
        if (path) {
            cookieOpts.path = path;
        }
        if (isLocalhost()) {
            // We do not require HTTPS for localhost / local development
            // TODO: Could investigate disabling HTTPS for test projects
            cookieOpts.secure = false;
        } else {
            if (availableToSubdomains) {
                // Domain must be expressly configured in order for the cookie to
                // be sent to subdomains
                // UNLESS it is localhost, in which case domain should not be set
                // cf. https://stackoverflow.com/questions/1134290/cookies-on-localhost-with-explicit-domain
                cookieOpts.domain = domain || window.location.host;
            }
            cookieOpts.secure = true;
        }
        return cookieOpts;
    }
}
class ConsumerSubscriptionDataLayer extends SubscriptionDataLayer {
}
class B2BSubscriptionDataLayer extends SubscriptionDataLayer {
}
const consumerDataLayerCacheSymbol = Symbol.for('__stytch_DataLayer');
const b2bDataLayerCacheSymbol = Symbol.for('__stytch_b2b_DataLayer');
const getConsumerDataLayerCache = ()=>{
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const wdw = window;
    if (!wdw[consumerDataLayerCacheSymbol]) {
        wdw[consumerDataLayerCacheSymbol] = {};
    }
    return wdw[consumerDataLayerCacheSymbol];
};
const getB2BDataLayerCache = ()=>{
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const wdw = window;
    if (!wdw[b2bDataLayerCacheSymbol]) {
        wdw[b2bDataLayerCacheSymbol] = {};
    }
    return wdw[b2bDataLayerCacheSymbol];
};
const getConsumerDataLayer = (publicToken, options)=>{
    const dataLayerCache = getConsumerDataLayerCache();
    if (!dataLayerCache[publicToken]) {
        dataLayerCache[publicToken] = new ConsumerSubscriptionDataLayer(publicToken, options);
    }
    return dataLayerCache[publicToken];
};
const getB2BDataLayer = (publicToken, options)=>{
    const dataLayerCache = getB2BDataLayerCache();
    if (!dataLayerCache[publicToken]) {
        dataLayerCache[publicToken] = new B2BSubscriptionDataLayer(publicToken, options);
    }
    return dataLayerCache[publicToken];
};
const addSubscriber = (collection, subscriber)=>{
    const uniqueId = Math.random().toString(36).slice(-10);
    collection[uniqueId] = subscriber;
    return ()=>delete collection[uniqueId];
};
const notifySubscribers = (collection, value)=>{
    Object.values(collection).forEach((cb)=>cb(value));
};
const deepEqualData = createDeepEqual({
    KEYS_TO_EXCLUDE: [
        'last_accessed_at'
    ]
});
class BaseSubscriptionService {
    _publicToken;
    _datalayer;
    /**
   * Whether the state was retrieved from the cache and is awaiting a refresh
   */ fromCache = true;
    constructor(_publicToken, _datalayer, { usingCustomApiEndpoint }){
        this._publicToken = _publicToken;
        this._datalayer = _datalayer;
        window.addEventListener('storage', this._listen);
        // If a custom API endpoint is being used, the session may be being managed
        // by HttpOnly cookies, which we can't detect.
        if (!usingCustomApiEndpoint) {
            // If the session does not exist in localstorage (like if we are in an iframe)
            // then the cookie might still be set and we can retrieve the session via an API call
            const { session_token } = this._datalayer.readSessionCookie();
            if (!session_token) {
                this.destroySession();
                return;
            }
        }
    }
    // Listening for state changes across tabs
    _listen = (e)=>{
        if (e.key !== getPersistentStorageKey(this._publicToken, '')) {
            return;
        }
        const parsedValue = e.newValue === null || e.newValue === 'null' ? null : JSON.parse(e.newValue);
        this.updateState(parsedValue, true);
    };
    getTokens() {
        const { session_token, session_jwt } = this._datalayer.readSessionCookie();
        if (!(typeof session_token === 'string') || !(typeof session_jwt === 'string')) {
            return null;
        }
        return {
            session_token,
            session_jwt
        };
    }
    getIntermediateSessionToken() {
        return this._datalayer.readIntermediateSessionTokenCookie() || null;
    }
    destroyState() {
        this.updateStateAndTokens({
            state: null,
            session_token: null,
            session_jwt: null,
            intermediate_session_token: null
        });
    }
    destroySession() {
        this._datalayer.removeSessionCookie();
        this.updateState(null);
    }
    _updateStateAndTokensInternal(stateDiff, options) {
        const oldStateValue = this._datalayer.state;
        const newStateValue = stateDiff.state === null ? null : {
            ...this._datalayer.state,
            ...stateDiff.state
        };
        this._datalayer.state = newStateValue;
        const wasCached = this.getFromCache();
        this.setCacheRefreshed();
        // NOTE: This means that our tab executes first before we signal the state change to other tabs
        // TODO: Should we avoid notifying subscribers in the same thread? Should we put
        // notifications in a setImmediate or setTimeout(..., 0)?
        // Should subscribers let us know if they are active/passive when they sign up?
        if (wasCached || !deepEqualData(oldStateValue, newStateValue)) {
            let notification;
            if (newStateValue == null || options.fromCache) {
                notification = newStateValue;
            } else {
                notification = {
                    ...newStateValue,
                    sessionDurationMinutes: options.sessionDurationMinutes
                };
            }
            notifySubscribers(this._datalayer.subscriptions, notification);
        }
    }
    updateStateAndTokens(stateDiff, options = {
        fromCache: false
    }) {
        if (stateDiff.state) {
            if (typeof stateDiff.session_token === 'string') {
                this._datalayer.writeSessionCookie(stateDiff);
            } else {
                // The session token is opaque, so let's clear any residual session
                // cookies that may have been left over
                this._datalayer.removeSessionCookie();
            }
            this._datalayer.removeISTCookie();
        } else if (stateDiff.intermediate_session_token) {
            if (typeof stateDiff.intermediate_session_token === 'string') {
                this._datalayer.writeIntermediateSessionTokenCookie(stateDiff.intermediate_session_token);
            } else {
                // The intermediate session token is opaque, so let's clear any residual
                // intermediate session token cookies that may have been left over
                this._datalayer.removeISTCookie();
            }
            this._datalayer.removeSessionCookie();
        } else {
            this._datalayer.removeSessionCookie();
            this._datalayer.removeISTCookie();
        }
        this._updateStateAndTokensInternal(stateDiff, options);
        this._datalayer.syncToLocalStorage();
    }
    updateState(state, fromExternalSource = false) {
        const oldStateValue = this._datalayer.state;
        const newStateValue = state === null ? null : {
            ...this._datalayer.state,
            ...state
        };
        this._datalayer.state = newStateValue;
        const wasCached = this.getFromCache();
        this.setCacheRefreshed();
        const hasStateChanged = !deepEqualData(oldStateValue, newStateValue);
        if (wasCached || hasStateChanged) {
            notifySubscribers(this._datalayer.subscriptions, newStateValue);
            // If there is no state change, or if this update was itself triggered by
            // a storage event, there is no need to sync to local storage since the
            // state is already in sync.
            if (hasStateChanged && !fromExternalSource) {
                // Delay notifying other tabs until after we have refreshed ourselves
                this._datalayer.syncToLocalStorage();
            }
        }
    }
    updateTokens(tokens) {
        const { session_token, session_jwt } = tokens;
        const cookie = this._datalayer.readSessionCookie();
        const diff = {
            ...cookie,
            session_token,
            session_jwt
        };
        if (typeof session_token === 'string' || typeof session_jwt === 'string') {
            this._datalayer.writeSessionCookie(diff);
            this._datalayer.removeISTCookie();
        } else {
            this._datalayer.removeSessionCookie();
        }
    }
    subscribeToState(callback) {
        return addSubscriber(this._datalayer.subscriptions, callback);
    }
    getState() {
        return this._datalayer.state;
    }
    destroy() {
        window.removeEventListener('storage', this._listen);
    }
    syncFromDeviceStorage() {
        return null;
    }
    getFromCache() {
        return this.fromCache;
    }
    setCacheRefreshed() {
        this.fromCache = false;
    }
}
class ConsumerSubscriptionService extends BaseSubscriptionService {
    updateUser = (user)=>this.updateState({
            user
        });
    getUser = ()=>this.getState()?.user ?? null;
    getSession = ()=>this.getState()?.session ?? null;
    updateSession = (resp, options)=>{
        const { session, user, session_jwt, session_token } = resp;
        if (session_token && session_jwt) {
            this.updateStateAndTokens({
                state: {
                    session,
                    user
                },
                session_jwt,
                session_token,
                intermediate_session_token: null
            }, {
                fromCache: false,
                sessionDurationMinutes: options?.sessionDurationMinutes
            });
        } else {
            this.updateStateAndTokens({
                state: {
                    session,
                    user
                },
                session_token: true,
                session_jwt: true,
                intermediate_session_token: null
            }, {
                fromCache: false,
                sessionDurationMinutes: options?.sessionDurationMinutes
            });
        }
    };
    withUpdateSession = (authenticate)=>async (...args)=>{
            const resp = await authenticate(...args);
            if (resp != null) {
                const options = args.find((a)=>a != null && !(typeof a === 'string'));
                this.updateSession(resp, {
                    sessionDurationMinutes: options?.session_duration_minutes
                });
            }
            return resp;
        };
}
class B2BSubscriptionService extends BaseSubscriptionService {
    updateMember = (member)=>this.updateState({
            member
        });
    getMember = ()=>this.getState()?.member ?? null;
    updateOrganization = (organization)=>this.updateState({
            organization
        });
    getOrganization = ()=>this.getState()?.organization ?? null;
    getSession = ()=>this.getState()?.session ?? null;
    updateSession = (resp, options)=>{
        if ('member_session' in resp && resp.member_session) {
            const tokens = resp.session_token && resp.session_jwt ? {
                session_token: resp.session_token,
                session_jwt: resp.session_jwt
            } : {
                session_token: true,
                session_jwt: true
            };
            this.updateStateAndTokens({
                state: {
                    session: resp.member_session,
                    member: resp.member,
                    organization: resp.organization
                },
                ...tokens,
                intermediate_session_token: null
            }, {
                fromCache: false,
                sessionDurationMinutes: options?.sessionDurationMinutes
            });
        } else {
            this.updateStateAndTokens({
                state: null,
                session_token: null,
                session_jwt: null,
                intermediate_session_token: resp.intermediate_session_token || true
            }, {
                fromCache: false,
                sessionDurationMinutes: options?.sessionDurationMinutes
            });
        }
    };
    withUpdateSession = (authenticate)=>async (options)=>{
            const resp = await authenticate(options);
            this.updateSession(resp, {
                sessionDurationMinutes: options?.session_duration_minutes
            });
            return resp;
        };
}

export { B2BSubscriptionDataLayer, B2BSubscriptionService, BaseSubscriptionService, ConsumerSubscriptionDataLayer, ConsumerSubscriptionService, SubscriptionDataLayer, getB2BDataLayer, getConsumerDataLayer };
//# sourceMappingURL=SubscriptionService.mjs.map
