var Connection;
(function (Connection) {
    Connection["Wifi"] = "wifi";
    Connection["Cellular"] = "cellular";
    Connection["Offline"] = "offline";
})(Connection || (Connection = {}));
var ContentType;
(function (ContentType) {
    ContentType["Dice"] = "Dice";
    ContentType["Source"] = "Source";
    ContentType["Compendium"] = "Compendium";
    ContentType["Single"] = "Single";
    ContentType["Bundle"] = "Bundle";
})(ContentType || (ContentType = {}));
var Device;
(function (Device) {
    Device["Tablet"] = "tablet";
    Device["Smartphone"] = "smartphone";
    Device["Unknown"] = "unknown";
})(Device || (Device = {}));
var Orientation;
(function (Orientation) {
    Orientation["Portrait"] = "portrait";
    Orientation["Landscape"] = "landscape";
    Orientation["Unknown"] = "unknown";
})(Orientation || (Orientation = {}));
var Platform;
(function (Platform) {
    Platform["Sigil"] = "Sigil";
    Platform["Web"] = "Web";
    Platform["MobileAndroid"] = "MobileAndroid";
    Platform["MobileIos"] = "MobileIOS";
    Platform["MobileWeb"] = "MobileWeb";
})(Platform || (Platform = {}));
var Region;
(function (Region) {
    Region["UsEast1"] = "us-east-1";
    Region["UsEast2"] = "us-east-2";
})(Region || (Region = {}));
var Source;
(function (Source) {
    Source["Web"] = "ddb.web";
    Source["Mobile"] = "ddb.mobile";
})(Source || (Source = {}));
var SubContentType;
(function (SubContentType) {
    SubContentType["Family"] = "Family";
    SubContentType["Set"] = "Set";
    SubContentType["Rule"] = "Rule";
    SubContentType["Adventure"] = "Adventure";
    SubContentType["Map"] = "Map";
    SubContentType["Legendary"] = "Legendary";
    SubContentType["Source"] = "Source";
    SubContentType["MagicItem"] = "MagicItem";
    SubContentType["Monster"] = "Monster";
    SubContentType["Feat"] = "Feat";
    SubContentType["Background"] = "Background";
    SubContentType["Subrace"] = "Subrace";
    SubContentType["Class"] = "Class";
    SubContentType["Subclass"] = "Subclass";
    SubContentType["Spell"] = "Spell";
    SubContentType["Vehicle"] = "Vehicle";
    SubContentType["Race"] = "Race";
})(SubContentType || (SubContentType = {}));
var SubscriptionLevel;
(function (SubscriptionLevel) {
    SubscriptionLevel["Visitor"] = "Visitor";
    SubscriptionLevel["RegisteredUser"] = "Registered User";
    SubscriptionLevel["Hero"] = "Hero";
    SubscriptionLevel["Master"] = "Master";
    SubscriptionLevel["Legendary"] = "Legendary";
})(SubscriptionLevel || (SubscriptionLevel = {}));

const byteToHex = [];
for (let i = 0; i < 256; ++i) {
    byteToHex.push((i + 0x100).toString(16).slice(1));
}
function unsafeStringify(arr, offset = 0) {
    return (byteToHex[arr[offset + 0]] +
        byteToHex[arr[offset + 1]] +
        byteToHex[arr[offset + 2]] +
        byteToHex[arr[offset + 3]] +
        '-' +
        byteToHex[arr[offset + 4]] +
        byteToHex[arr[offset + 5]] +
        '-' +
        byteToHex[arr[offset + 6]] +
        byteToHex[arr[offset + 7]] +
        '-' +
        byteToHex[arr[offset + 8]] +
        byteToHex[arr[offset + 9]] +
        '-' +
        byteToHex[arr[offset + 10]] +
        byteToHex[arr[offset + 11]] +
        byteToHex[arr[offset + 12]] +
        byteToHex[arr[offset + 13]] +
        byteToHex[arr[offset + 14]] +
        byteToHex[arr[offset + 15]]).toLowerCase();
}

let getRandomValues;
const rnds8 = new Uint8Array(16);
function rng() {
    if (!getRandomValues) {
        if (typeof crypto === 'undefined' || !crypto.getRandomValues) {
            throw new Error('crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported');
        }
        getRandomValues = crypto.getRandomValues.bind(crypto);
    }
    return getRandomValues(rnds8);
}

const randomUUID = typeof crypto !== 'undefined' && crypto.randomUUID && crypto.randomUUID.bind(crypto);
var native = { randomUUID };

function _v4(options, buf, offset) {
    options = options || {};
    const rnds = options.random ?? options.rng?.() ?? rng();
    if (rnds.length < 16) {
        throw new Error('Random bytes length must be >= 16');
    }
    rnds[6] = (rnds[6] & 0x0f) | 0x40;
    rnds[8] = (rnds[8] & 0x3f) | 0x80;
    return unsafeStringify(rnds);
}
function v4(options, buf, offset) {
    if (native.randomUUID && true && !options) {
        return native.randomUUID();
    }
    return _v4(options);
}

function flattenPlugins(event) {
    const newObj = {};
    Object.keys(event).forEach((key) => {
        if (key === "plugins" && Array.isArray(event[key])) {
            event[key].forEach((plugin) => {
                Object.keys(plugin).forEach((pluginKey) => {
                    if (newObj[pluginKey]) {
                        throw new Error(`duplicate key ${pluginKey} attempting to be set!`);
                    }
                    else {
                        newObj[pluginKey] = plugin[pluginKey];
                    }
                });
            });
        }
        else {
            newObj[key] = event[key];
        }
    });
    return newObj;
}

// Regexps involved with splitting words in various case formats.
const SPLIT_LOWER_UPPER_RE = /([\p{Ll}\d])(\p{Lu})/gu;
const SPLIT_UPPER_UPPER_RE = /(\p{Lu})([\p{Lu}][\p{Ll}])/gu;
// Used to iterate over the initial split result and separate numbers.
const SPLIT_SEPARATE_NUMBER_RE = /(\d)\p{Ll}|(\p{L})\d/u;
// Regexp involved with stripping non-word characters from the result.
const DEFAULT_STRIP_REGEXP = /[^\p{L}\d]+/giu;
// The replacement value for splits.
const SPLIT_REPLACE_VALUE = "$1\0$2";
// The default characters to keep after transforming case.
const DEFAULT_PREFIX_SUFFIX_CHARACTERS = "";
/**
 * Split any cased input strings into an array of words.
 */
function split(value) {
    let result = value.trim();
    result = result
        .replace(SPLIT_LOWER_UPPER_RE, SPLIT_REPLACE_VALUE)
        .replace(SPLIT_UPPER_UPPER_RE, SPLIT_REPLACE_VALUE);
    result = result.replace(DEFAULT_STRIP_REGEXP, "\0");
    let start = 0;
    let end = result.length;
    // Trim the delimiter from around the output string.
    while (result.charAt(start) === "\0")
        start++;
    if (start === end)
        return [];
    while (result.charAt(end - 1) === "\0")
        end--;
    return result.slice(start, end).split(/\0/g);
}
/**
 * Split the input string into an array of words, separating numbers.
 */
function splitSeparateNumbers(value) {
    const words = split(value);
    for (let i = 0; i < words.length; i++) {
        const word = words[i];
        const match = SPLIT_SEPARATE_NUMBER_RE.exec(word);
        if (match) {
            const offset = match.index + (match[1] ?? match[2]).length;
            words.splice(i, 1, word.slice(0, offset), word.slice(offset));
        }
    }
    return words;
}
/**
 * Convert a string to constant case (`FOO_BAR`).
 */
function constantCase(input, options) {
    const [prefix, words, suffix] = splitPrefixSuffix(input, options);
    return (prefix +
        words.map(upperFactory(options?.locale)).join("_") +
        suffix);
}
function upperFactory(locale) {
    return locale === false
        ? (input) => input.toUpperCase()
        : (input) => input.toLocaleUpperCase(locale);
}
function splitPrefixSuffix(input, options = {}) {
    const splitFn = options.split ?? (options.separateNumbers ? splitSeparateNumbers : split);
    const prefixCharacters = options.prefixCharacters ?? DEFAULT_PREFIX_SUFFIX_CHARACTERS;
    const suffixCharacters = options.suffixCharacters ?? DEFAULT_PREFIX_SUFFIX_CHARACTERS;
    let prefixIndex = 0;
    let suffixIndex = input.length;
    while (prefixIndex < input.length) {
        const char = input.charAt(prefixIndex);
        if (!prefixCharacters.includes(char))
            break;
        prefixIndex++;
    }
    while (suffixIndex > prefixIndex) {
        const index = suffixIndex - 1;
        const char = input.charAt(index);
        if (!suffixCharacters.includes(char))
            break;
        suffixIndex = index;
    }
    return [
        input.slice(0, prefixIndex),
        splitFn(input.slice(prefixIndex, suffixIndex)),
        input.slice(suffixIndex),
    ];
}

function transformKeys(object, transformedObject = {}) {
    Object.entries(object).forEach(([key, value]) => {
        if (Array.isArray(value)) {
            value = object[key].map((item) => typeof item === "object" ? transformKeys(item) : item);
        }
        else if (typeof value === "object") {
            value = value ? transformKeys(value) : value;
        }
        transformedObject[constantCase(key)] = value ?? null;
    });
    return transformedObject;
}

const createEventEntry = ({ detail, detailType, eventBusName, source, }) => {
    const eventTime = new Date();
    const fullDetail = {
        ...detail,
        messageId: v4(),
        eventTime: eventTime.toISOString(),
    };
    const flattenedDetail = flattenPlugins(fullDetail);
    const transformedDetail = transformKeys(flattenedDetail);
    return {
        Detail: JSON.stringify(transformedDetail),
        DetailType: detailType,
        EventBusName: eventBusName,
        Source: source,
        Time: eventTime,
    };
};

const visitorId = "ddb_vid";
const sessionId = "ddb_sid";

/**
 * Checks if the environment is a test environment.
 * This is useful to avoid setting secure cookies in test environments
 * where the `window` object may not be available or secure cookies are not needed.
 */
const isTestEnv = () => {
    return (typeof process !== "undefined" &&
        process.env &&
        (process.env.NODE_ENV === "test" || process.env.VITEST));
};
const getCookie = (name) => document.cookie.match(`${name}=([^;]*)`)?.[1];
const setCookie = (name, value, opts) => {
    const parts = [`${name}=${value}`, `max-age=${opts["max-age"]}`];
    if (opts.secure && !isTestEnv()) {
        parts.push("secure", "domain=.dndbeyond.com", "path=/");
    }
    return (document.cookie = parts.join(";"));
};
const deleteCookie = (name) => setCookie(name, "", { "max-age": 0, secure: !isTestEnv() });
const createSessionAndVisitorCookies = () => {
    if (!getCookie(visitorId)) {
        setCookie(visitorId, v4(), {
            "max-age": 60 * 60 * 24 * 365, // 1 year
            secure: !isTestEnv(),
        });
    }
    if (!getCookie(sessionId)) {
        setCookie(sessionId, v4(), {
            "max-age": 60 * 60 * 4, // 4 hours
            secure: !isTestEnv(),
        });
    }
    else {
        extendSessionCookie();
    }
};
const initializeConsentCookieListener = (consentCallback) => {
    const maxRetries = 10;
    const retryInterval = 500;
    let retries = 0;
    let consentCallbackFulfilled = false;
    const tryInitialize = () => {
        if ("window" in globalThis && window.ketch) {
            /**
             * n.b. This callback will return immediately if the response is already available,
             * so this acts as both an initialization and a listener for future consent changes.
             */
            window.ketch("on", "consent", (consentData) => {
                if (!consentCallbackFulfilled) {
                    consentCallbackFulfilled = true;
                    consentCallback?.(consentData.purposes || null);
                }
                if (!consentData.purposes.analytics) {
                    deleteCookie(visitorId);
                    deleteCookie(sessionId);
                }
                else {
                    createSessionAndVisitorCookies();
                }
            });
            // For when the Ketch response never comes, we attempt a final callback
            setTimeout(() => {
                if (!consentCallbackFulfilled) {
                    console.warn("Ketch callback never fired, invoking fallback.");
                    consentCallbackFulfilled = true;
                    consentCallback?.(window.ketchConsent || null);
                }
            }, 5_000);
        }
        else if (retries < maxRetries) {
            retries++;
            setTimeout(tryInitialize, retryInterval);
        }
        else {
            console.warn("Failed to initialize session cookies.");
            consentCallbackFulfilled = true;
            consentCallback?.(null);
        }
    };
    tryInitialize();
};
const extendSessionCookie = () => {
    const session = getCookie(sessionId);
    if (session) {
        setCookie(sessionId, session, {
            "max-age": 60 * 60 * 4, // 4 hours
            secure: !isTestEnv(),
        });
    }
};

class EventPipeline {
    region;
    /**
     * Initializes the EventPipeline with the specified region and optional consent callback.
     * The consent callback will return with valid consent data or null if Ketch is not available.
     *
     * @param region - The region for the event pipeline.
     * @param consentCallback - Optional callback to handle Ketch consent data.
     */
    constructor(region, consentCallback) {
        this.region = region;
        try {
            initializeConsentCookieListener(consentCallback);
        }
        catch (error) {
            console.warn("Failed to initialize session cookies", error);
        }
    }
    async sendEvent(detail, source) {
        try {
            try {
                extendSessionCookie();
            }
            catch (error) {
                console.warn("Failed to extend session cookie", error);
            }
            const entry = createEventEntry({
                detail,
                source,
            });
            const response = await fetch(`https://${this.region === Region.UsEast2 ? "test-" : ""}api.dndbeyond.com/bi/events`, {
                body: JSON.stringify({
                    Entries: [entry],
                }),
                headers: {
                    "Content-Type": "application/json",
                },
                method: "POST",
            });
            const result = {
                success: response.ok,
                ...(response.ok
                    ? {}
                    : {
                        errorMessage: `Send to event pipeline failed (client-bi-event)`,
                    }),
            };
            response.ok || console.error(result.errorMessage);
            return result;
        }
        catch (error) {
            console.error(error);
            return {
                success: false,
                errorMessages: [error.message],
            };
        }
    }
}

export { EventPipeline };
