const crossFetch = require('cross-fetch');

/**
 * Metadata header names
 * @enum {string} The enum value is the name of the associated header.
 * @readonly
 */
const RequestMetadata = {
    /** The ID of the project associated with this request */
    ProjectId: 'X-Project-ID',
    /** The ID of the project run associated with this request */
    RunId: 'X-Run-ID'
};

/**
 * Metadata headers for requests
 * @type {Headers}
 */
const metadata = new crossFetch.Headers();

/**
 * Check if there is any metadata to apply.
 * @returns {boolean} true if `metadata` has contents, or false if it is empty.
 */
const hasMetadata = () => {
    /* global self */
    const searchParams = (
        typeof self !== 'undefined' &&
        self &&
        self.location &&
        self.location.search &&
        self.location.search.split(/[?&]/)
    ) || [];
    if (!searchParams.includes('scratchMetadata=1')) {
        // for now, disable this feature unless scratchMetadata=1
        // TODO: remove this check once we're sure the feature works correctly in production
        return false;
    }
    for (const _ of metadata) {
        return true;
    }
    return false;
};

/**
 * Non-destructively merge any metadata state (if any) with the provided options object (if any).
 * If there is metadata state but no options object is provided, make a new object.
 * If there is no metadata state, return the provided options parameter without modification.
 * If there is metadata and an options object is provided, modify a copy and return it.
 * Headers in the provided options object may override headers generated from metadata state.
 * @param {RequestInit} [options] The initial request options. May be null or undefined.
 * @returns {RequestInit|undefined} the provided options parameter without modification, or a new options object.
 */
const applyMetadata = options => {
    if (hasMetadata()) {
        const augmentedOptions = Object.assign({}, options);
        augmentedOptions.headers = new crossFetch.Headers(metadata);
        if (options && options.headers) {
            // the Fetch spec says options.headers could be:
            // "A Headers object, an object literal, or an array of two-item arrays to set request's headers."
            // turn it into a Headers object to be sure of how to interact with it
            const overrideHeaders = options.headers instanceof crossFetch.Headers ?
                options.headers : new crossFetch.Headers(options.headers);
            for (const [name, value] of overrideHeaders.entries()) {
                augmentedOptions.headers.set(name, value);
            }
        }
        return augmentedOptions;
    }
    return options;
};

/**
 * Make a network request.
 * This is a wrapper for the global fetch method, adding some Scratch-specific functionality.
 * @param {RequestInfo|URL} resource The resource to fetch.
 * @param {RequestInit} options Optional object containing custom settings for this request.
 * @see {@link https://developer.mozilla.org/docs/Web/API/fetch} for more about the fetch API.
 * @returns {Promise<Response>} A promise for the response to the request.
 */
const scratchFetch = (resource, options) => {
    const augmentedOptions = applyMetadata(options);
    return crossFetch.fetch(resource, augmentedOptions);
};

/**
 * Set the value of a named request metadata item.
 * Setting the value to `null` or `undefined` will NOT remove the item.
 * Use `unsetMetadata` for that.
 * @param {RequestMetadata} name The name of the metadata item to set.
 * @param {any} value The value to set (will be converted to a string).
 */
const setMetadata = (name, value) => {
    metadata.set(name, value);
};

/**
 * Remove a named request metadata item.
 * @param {RequestMetadata} name The name of the metadata item to remove.
 */
const unsetMetadata = name => {
    metadata.delete(name);
};

module.exports = {
    Headers: crossFetch.Headers,
    RequestMetadata,
    applyMetadata,
    scratchFetch,
    setMetadata,
    unsetMetadata
};

if (process.env.NODE_ENV === 'development') {
    /**
     * Retrieve a named request metadata item.
     * Only for use in tests.
     * @param {RequestMetadata} name The name of the metadata item to retrieve.
     * @returns {any} value The value of the metadata item, or `undefined` if it was not found.
     */
    const getMetadata = name => metadata.get(name);

    module.exports.getMetadata = getMetadata;
}