import { supportsAbortSignal } from '../core/constants.js';
import { isObject } from './is.js';
const replaceSymbol = Symbol('replaceOption');
const getReplaceState = (value) => isObject(value) && value[replaceSymbol] === true
    ? {
        isReplace: true,
        value: value.value,
    }
    : {
        isReplace: false,
        value,
    };
/**
Wraps a value so that `ky.extend()` will replace the parent value instead of merging with it. Works with hooks, headers, search parameters, context, and any other deep-merged option.

By default, `.extend()` deep-merges options with the parent instance: hooks get appended, headers get merged, and search parameters get accumulated. Use `replaceOption` when you want to fully replace a merged property instead.

@example
```
import ky, {replaceOption} from 'ky';

const base = ky.create({
    hooks: {beforeRequest: [addAuth, addTracking]},
});

// Replaces instead of appending
const extended = base.extend({
    hooks: replaceOption({beforeRequest: [onlyThis]}),
});
// hooks.beforeRequest is now [onlyThis], not [addAuth, addTracking, onlyThis]
```
*/
export const replaceOption = (value) => {
    const markedValue = { [replaceSymbol]: true, value };
    return markedValue;
};
export const validateAndMerge = (...sources) => {
    for (const source of sources) {
        if ((!isObject(source) || Array.isArray(source)) && source !== undefined) {
            throw new TypeError('The `options` argument must be an object');
        }
    }
    return deepMerge({}, ...sources);
};
export const mergeHeaders = (source1 = {}, source2 = {}) => {
    const result = new globalThis.Headers(source1);
    const isHeadersInstance = source2 instanceof globalThis.Headers;
    const source = new globalThis.Headers(source2);
    for (const [key, value] of source.entries()) {
        if ((isHeadersInstance && value === 'undefined') || value === undefined) {
            result.delete(key);
        }
        else {
            result.set(key, value);
        }
    }
    return result;
};
const isPlainObject = (value) => {
    if (!isObject(value) || Array.isArray(value)) {
        return false;
    }
    const prototype = Object.getPrototypeOf(value);
    return prototype === Object.prototype || prototype === null;
};
export const cloneShallow = (value) => {
    if (value instanceof URLSearchParams) {
        return new URLSearchParams(value);
    }
    if (value instanceof globalThis.Headers) {
        return new globalThis.Headers(value);
    }
    if (Array.isArray(value)) {
        return [...value];
    }
    if (isPlainObject(value)) {
        const copy = { ...value };
        return copy;
    }
    return value;
};
const normalizeHeaderObject = (headers) => Object.fromEntries(Object.entries(headers).filter((entry) => entry[1] !== undefined));
const mergeHeaderContainers = (source1, source2) => {
    if (isPlainObject(source1) && isPlainObject(source2)) {
        return normalizeHeaderObject({ ...source1, ...source2 });
    }
    return mergeHeaders(source1, source2);
};
function newHookValue(original, incoming, property) {
    return (Object.hasOwn(incoming, property) && incoming[property] === undefined)
        ? []
        : deepMerge(original[property] ?? [], incoming[property] ?? []);
}
export const mergeHooks = (original = {}, incoming = {}) => ({
    init: newHookValue(original, incoming, 'init'),
    beforeRequest: newHookValue(original, incoming, 'beforeRequest'),
    beforeRetry: newHookValue(original, incoming, 'beforeRetry'),
    beforeError: newHookValue(original, incoming, 'beforeError'),
    afterResponse: newHookValue(original, incoming, 'afterResponse'),
});
export const deletedParametersSymbol = Symbol('deletedParameters');
const appendSearchParameters = (target, source) => {
    const result = new URLSearchParams();
    const deleted = new Set();
    for (const input of [target, source]) {
        if (input === undefined) {
            continue;
        }
        if (input instanceof URLSearchParams) {
            for (const [key, value] of input.entries()) {
                result.append(key, value);
                deleted.delete(key);
            }
            const inputDeleted = input[deletedParametersSymbol];
            if (inputDeleted) {
                for (const key of inputDeleted) {
                    result.delete(key);
                    deleted.add(key);
                }
            }
        }
        else if (Array.isArray(input)) {
            for (const pair of input) {
                if (!Array.isArray(pair) || pair.length !== 2) {
                    throw new TypeError('Array search parameters must be provided in [[key, value], ...] format');
                }
                result.append(String(pair[0]), String(pair[1]));
                deleted.delete(String(pair[0]));
            }
        }
        else if (isObject(input)) {
            for (const [key, value] of Object.entries(input)) {
                if (value === undefined) {
                    result.delete(key);
                    deleted.add(key);
                }
                else {
                    result.append(key, String(value));
                    deleted.delete(key);
                }
            }
        }
        else {
            // String
            const parameters = new URLSearchParams(input);
            for (const [key, value] of parameters.entries()) {
                result.append(key, value);
                deleted.delete(key);
            }
        }
    }
    if (deleted.size > 0) {
        result[deletedParametersSymbol] = deleted;
    }
    return result;
};
// TODO: Make this strongly-typed (no `any`).
export const deepMerge = (...sources) => {
    let returnValue = {};
    let headers = {};
    let hooks = {};
    let searchParameters;
    const signals = [];
    for (const source of sources) {
        if (Array.isArray(source)) {
            if (!Array.isArray(returnValue)) {
                returnValue = [];
            }
            returnValue = [...returnValue, ...source];
        }
        else if (isObject(source)) {
            for (let [key, value] of Object.entries(source)) {
                // Special handling for AbortSignal instances
                if (key === 'signal' && value instanceof globalThis.AbortSignal) {
                    signals.push(value);
                    continue;
                }
                const replaceState = getReplaceState(value);
                const { isReplace } = replaceState;
                value = replaceState.value;
                // Special handling for context - shallow merge only
                if (key === 'context') {
                    if (value !== undefined && value !== null && (!isObject(value) || Array.isArray(value))) {
                        throw new TypeError('The `context` option must be an object');
                    }
                    // Shallow merge: always create a new object to prevent mutation bugs
                    returnValue = {
                        ...returnValue,
                        context: (value === undefined || value === null)
                            ? {}
                            : (isReplace
                                ? { ...value }
                                : { ...returnValue.context, ...value }),
                    };
                    continue;
                }
                // Special handling for searchParams
                if (key === 'searchParams') {
                    if (value === undefined || value === null) {
                        // Explicit undefined or null removes searchParams
                        searchParameters = undefined;
                    }
                    else if (isReplace) {
                        searchParameters = value;
                    }
                    else {
                        // First source: keep as-is to preserve type (string/object/URLSearchParams)
                        // Subsequent sources: merge and convert to URLSearchParams
                        searchParameters = searchParameters === undefined ? value : appendSearchParameters(searchParameters, value);
                    }
                    continue;
                }
                if (isObject(value) && !isReplace && key in returnValue) {
                    value = deepMerge(returnValue[key], value);
                }
                returnValue = { ...returnValue, [key]: value };
            }
            if (isObject(source.hooks)) {
                const { value: hookValue, isReplace } = getReplaceState(source.hooks);
                hooks = isReplace
                    ? mergeHooks({}, hookValue)
                    : mergeHooks(hooks, hookValue);
                returnValue.hooks = hooks;
            }
            if (isObject(source.headers)) {
                const { value: headerValue, isReplace } = getReplaceState(source.headers);
                headers = isReplace
                    ? cloneShallow(headerValue)
                    : mergeHeaderContainers(headers, headerValue);
                returnValue.headers = headers;
            }
        }
    }
    if (searchParameters !== undefined) {
        returnValue.searchParams = searchParameters;
    }
    if (signals.length > 0) {
        if (signals.length === 1) {
            returnValue.signal = signals[0];
        }
        else if (supportsAbortSignal) {
            returnValue.signal = AbortSignal.any(signals);
        }
        else {
            // When AbortSignal.any is not available, use the last signal
            // This maintains the previous behavior before signal merging was added
            // This can be remove when the `supportsAbortSignal` check is removed.`
            returnValue.signal = signals.at(-1);
        }
    }
    return returnValue;
};
//# sourceMappingURL=merge.js.map