import itToBrowserReadableStream from 'it-to-browser-readablestream';
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string';
import { errorToObject } from "./error-to-object.js";
import { rangeToOffsetAndLength } from "./get-offset-and-length.js";
import { getContentRangeHeader } from "./response-headers.js";
function setField(response, name, value) {
    Object.defineProperty(response, name, {
        enumerable: true,
        configurable: false,
        set: () => { },
        get: () => value
    });
}
function setType(response, value) {
    if (response.type !== value) {
        setField(response, 'type', value);
    }
}
function setUrl(response, value) {
    value = value.toString();
    const fragmentStart = value.indexOf('#');
    if (fragmentStart > -1) {
        value = value.substring(0, fragmentStart);
    }
    if (response.url !== value) {
        setField(response, 'url', value);
    }
}
function setRedirected(response) {
    setField(response, 'redirected', true);
}
function isArrayBufferBacked(buf) {
    return buf.buffer instanceof ArrayBuffer;
}
function withArrayBuffer(buf) {
    if (isArrayBufferBacked(buf)) {
        return buf;
    }
    const arr = new ArrayBuffer(buf.byteLength);
    const u8a = new Uint8Array(arr);
    u8a.set(buf);
    return u8a;
}
function isReadableStream(obj) {
    if (obj == null) {
        return false;
    }
    return typeof obj.pipeThrough === 'function' && typeof obj.getReader === 'function';
}
function normalizeBody(body) {
    if (body == null) {
        return body;
    }
    if (body instanceof Uint8Array) {
        return withArrayBuffer(body);
    }
    if (isReadableStream(body)) {
        return body.pipeThrough(new TransformStream({
            transform(chunk, controller) {
                controller.enqueue(withArrayBuffer(chunk));
            }
        }));
    }
    return body;
}
export function okResponse(url, body, init) {
    const response = new Response(normalizeBody(body), {
        ...(init ?? {}),
        status: 200,
        statusText: 'OK'
    });
    if (init?.redirected === true) {
        setRedirected(response);
    }
    setType(response, 'basic');
    setUrl(response, url);
    return response;
}
export function internalServerErrorResponse(url, err, init) {
    const response = new Response(JSON.stringify({
        error: errorToObject(err)
    }), {
        ...(init ?? {}),
        status: 500,
        statusText: 'Internal Server Error'
    });
    response.headers.set('X-Content-Type-Options', 'nosniff'); // see https://specs.ipfs.tech/http-gateways/path-gateway/#x-content-type-options-response-header
    response.headers.set('content-type', 'application/json');
    response.headers.set('x-error-message', btoa(err.message));
    setType(response, 'basic');
    setUrl(response, url);
    return response;
}
/**
 * A 504 Gateway Timeout for when a request made to an upstream server timed out
 */
export function gatewayTimeoutResponse(url, body, init) {
    const response = new Response(normalizeBody(body), {
        ...(init ?? {}),
        status: 504,
        statusText: 'Gateway Timeout'
    });
    setType(response, 'basic');
    setUrl(response, url);
    return response;
}
/**
 * A 502 Bad Gateway is for when an invalid response was received from an
 * upstream server.
 */
export function badGatewayResponse(url, body, init) {
    const response = new Response(normalizeBody(body), {
        ...(init ?? {}),
        status: 502,
        statusText: 'Bad Gateway'
    });
    setType(response, 'basic');
    setUrl(response, url);
    return response;
}
export function notImplementedResponse(url, body, init) {
    const response = new Response(normalizeBody(body), {
        ...(init ?? {}),
        status: 501,
        statusText: 'Not Implemented'
    });
    response.headers.set('X-Content-Type-Options', 'nosniff'); // see https://specs.ipfs.tech/http-gateways/path-gateway/#x-content-type-options-response-header
    setType(response, 'basic');
    setUrl(response, url);
    return response;
}
export function notAcceptableResponse(url, requested, acceptable, init) {
    const headers = new Headers(init?.headers);
    headers.set('content-type', 'application/json');
    const response = new Response(JSON.stringify({
        requested: requested.map(contentType => contentType.mediaType),
        acceptable: acceptable.map(contentType => contentType.mediaType)
    }), {
        ...(init ?? {}),
        status: 406,
        statusText: 'Not Acceptable',
        headers
    });
    setType(response, 'basic');
    setUrl(response, url);
    return response;
}
export function notFoundResponse(url, body, init) {
    const response = new Response(normalizeBody(body), {
        ...(init ?? {}),
        status: 404,
        statusText: 'Not Found'
    });
    setType(response, 'basic');
    setUrl(response, url);
    return response;
}
function isArrayOfErrors(body) {
    return Array.isArray(body) && body.every(e => e instanceof Error);
}
export function badRequestResponse(url, errors, init) {
    // stacktrace of the single error, or the stacktrace of the last error in the array
    let stack;
    let convertedErrors;
    if (isArrayOfErrors(errors)) {
        stack = errors[errors.length - 1].stack;
        convertedErrors = errors.map(e => ({ message: e.message, stack: e.stack ?? '' }));
    }
    else if (errors instanceof Error) {
        stack = errors.stack;
        convertedErrors = [{ message: errors.message, stack: errors.stack ?? '' }];
    }
    const bodyJson = JSON.stringify({
        stack,
        errors: convertedErrors
    });
    const response = new Response(bodyJson, {
        status: 400,
        statusText: 'Bad Request',
        ...(init ?? {}),
        headers: {
            ...(init?.headers ?? {}),
            'Content-Type': 'application/json'
        }
    });
    setType(response, 'basic');
    setUrl(response, url);
    return response;
}
export function movedPermanentlyResponse(url, location, init) {
    const response = new Response(null, {
        ...(init ?? {}),
        status: 301,
        statusText: 'Moved Permanently',
        headers: {
            ...(init?.headers ?? {}),
            location
        }
    });
    setType(response, 'basic');
    setUrl(response, url);
    return response;
}
export function notModifiedResponse(url, headers, init) {
    const response = new Response(null, {
        ...(init ?? {}),
        status: 304,
        statusText: 'Not Modified'
    });
    // These fields would be present on a 200 and must be sent as part of a 304
    // for the same resource
    // https://www.rfc-editor.org/rfc/rfc9110.html#name-304-not-modified
    const copyHeaders = [
        'cache-control',
        'content-location',
        'date',
        'etag',
        'expires',
        'vary'
    ];
    copyHeaders.forEach(key => {
        const value = headers.get(key);
        if (value != null) {
            response.headers.set(key, value);
        }
    });
    setType(response, 'basic');
    setUrl(response, url);
    return response;
}
export function partialContentResponse(url, getSlice, range, documentSize, init) {
    let response;
    if (range.ranges.length === 1) {
        response = singleRangeResponse(url, getSlice, range.ranges[0], documentSize, init);
    }
    else if (range.ranges.length > 1) {
        response = multiRangeResponse(url, getSlice, range, documentSize, init);
    }
    else {
        return notSatisfiableResponse(url, documentSize);
    }
    if (init?.redirected === true) {
        setRedirected(response);
    }
    setType(response, 'basic');
    setUrl(response, url);
    return response;
}
function singleRangeResponse(url, getSlice, range, documentSize, init) {
    try {
        // create headers object with any initial headers from init
        const headers = new Headers(init?.headers);
        const { offset, length } = rangeToOffsetAndLength(documentSize, range.start, range.end);
        headers.set('content-length', `${length}`);
        // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Range
        headers.set('content-range', getContentRangeHeader(documentSize, range.start, range.end));
        const stream = itToBrowserReadableStream(getSlice(offset, length));
        return new Response(stream, {
            ...(init ?? {}),
            status: 206,
            statusText: 'Partial Content',
            headers
        });
    }
    catch (err) {
        if (err.name === 'InvalidRangeError') {
            return notSatisfiableResponse(url, documentSize, init);
        }
        return internalServerErrorResponse(url, err, init);
    }
}
/**
 * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Range_requests
 */
function multiRangeResponse(url, getSlice, range, documentSize, init) {
    // create headers object with any initial headers from init
    const headers = new Headers(init?.headers);
    const contentType = headers.get('content-type');
    if (contentType == null) {
        throw new Error('Content-Type header must be set');
    }
    headers.delete('content-type');
    let contentLength = 0n;
    // calculate content range based on range headers
    const rangeHeaders = range.ranges.map(({ start, end }) => {
        const header = uint8ArrayFromString([
            `--${range.multipartBoundary}`,
            // content-type of multipart part
            `Content-Type: ${contentType}`,
            // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Range
            `Content-Range: ${getContentRangeHeader(documentSize, start, end)}`,
            '',
            ''
        ].join('\r\n'));
        contentLength += BigInt(header.byteLength) + (BigInt(end ?? documentSize) - BigInt(start ?? 0));
        return header;
    });
    const trailer = uint8ArrayFromString([
        `--${range.multipartBoundary}--`,
        ''
    ].join('\r\n'));
    contentLength += BigInt(trailer.byteLength);
    // content length is the expected length of all multipart parts
    headers.set('content-length', `${contentLength}`);
    // content type of response is multipart
    headers.set('content-type', `multipart/byteranges; boundary=${range.multipartBoundary}`);
    const stream = itToBrowserReadableStream(async function* () {
        for (let i = 0; i < rangeHeaders.length; i++) {
            yield rangeHeaders[i];
            const { offset, length } = rangeToOffsetAndLength(documentSize, range.ranges[i].start, range.ranges[i].end);
            yield* getSlice(offset, length);
            yield uint8ArrayFromString('\r\n');
        }
        yield trailer;
    }());
    return new Response(stream, {
        ...(init ?? {}),
        status: 206,
        statusText: 'Partial Content',
        headers
    });
}
/**
 * We likely need to catch errors handled by upstream helia libraries if
 * range-request throws an error. Some examples:
 *
 * - The range is out of bounds
 * - The range is invalid
 * - The range is not supported for the given type
 */
export function notSatisfiableResponse(url, documentSize, init) {
    const headers = new Headers(init?.headers);
    if (documentSize != null) {
        headers.set('content-range', `bytes */${documentSize}`);
    }
    const response = new Response('Range Not Satisfiable', {
        ...init,
        headers,
        status: 416,
        statusText: 'Range Not Satisfiable'
    });
    setType(response, 'basic');
    setUrl(response, url);
    return response;
}
/**
 * Error to indicate that request was formally correct, but Gateway is unable to
 * return requested data under the additional (usually cache-related) conditions
 * sent by the client.
 *
 * @see https://specs.ipfs.tech/http-gateways/path-gateway/#412-precondition-failed
 */
export function preconditionFailedResponse(url, init) {
    const headers = new Headers(init?.headers);
    const response = new Response('Precondition Failed', {
        ...init,
        headers,
        status: 412,
        statusText: 'Precondition Failed'
    });
    setType(response, 'basic');
    setUrl(response, url);
    return response;
}
//# sourceMappingURL=responses.js.map