import CachedResponseStateEnum from './CachedResponseStateEnum.js';
import Headers from '../../Headers.js';
import ResponseCacheFileSystem from './ResponseCacheFileSystem.js';
const UPDATE_RESPONSE_HEADERS = ['Cache-Control', 'Last-Modified', 'Vary', 'ETag'];
/**
 * Fetch response cache.
 *
 * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching
 * @see https://www.mnot.net/cache_docs/
 */
export default class ResponseCache {
    fileSystem;
    #entries = new Map();
    /**
     * Constructor.
     */
    constructor() {
        this.fileSystem = new ResponseCacheFileSystem(this.#entries);
    }
    /**
     * Returns cached response.
     *
     * @param request Request.
     * @returns Cached response.
     */
    get(request) {
        if (request.headers.get('Cache-Control')?.includes('no-cache')) {
            return null;
        }
        const url = request.url;
        const entries = this.#entries.get(url);
        if (entries) {
            for (let i = 0, max = entries.length; i < max; i++) {
                const entry = entries[i];
                let isMatch = entry.request.method === request.method;
                if (isMatch) {
                    for (const header of Object.keys(entry.vary)) {
                        const requestHeader = request.headers.get(header);
                        if (requestHeader !== null && entry.vary[header] !== requestHeader) {
                            isMatch = false;
                            break;
                        }
                    }
                }
                if (isMatch) {
                    if (entry.expires && entry.expires < Date.now()) {
                        if (entry.lastModified) {
                            entry.state = CachedResponseStateEnum.stale;
                        }
                        else if (!entry.etag) {
                            entries.splice(i, 1);
                            return null;
                        }
                    }
                    return entry;
                }
            }
        }
        return null;
    }
    /**
     * Adds a cache entity.
     *
     * @param request Request.
     * @param response Response.
     * @returns Cached response.
     */
    add(request, response) {
        // We should only cache GET and HEAD requests.
        if ((request.method !== 'GET' && request.method !== 'HEAD') ||
            request.headers.get('Cache-Control')?.includes('no-cache')) {
            return null;
        }
        const url = request.url;
        let cachedResponse = this.get(request);
        if (response.status === 304) {
            if (!cachedResponse) {
                throw new Error('ResponseCache: Cached response not found.');
            }
            for (const name of UPDATE_RESPONSE_HEADERS) {
                if (response.headers.has(name)) {
                    cachedResponse.response.headers.set(name, response.headers.get(name));
                }
            }
            cachedResponse.cacheUpdateTime = Date.now();
            cachedResponse.state = CachedResponseStateEnum.fresh;
        }
        else {
            if (cachedResponse) {
                const entries = this.#entries.get(url);
                if (entries) {
                    const index = entries.indexOf(cachedResponse);
                    if (index !== -1) {
                        entries.splice(index, 1);
                    }
                }
            }
            cachedResponse = {
                response: {
                    status: response.status,
                    statusText: response.statusText,
                    url: response.url,
                    headers: new Headers(response.headers),
                    // We need to wait for the body to be consumed and then populated if set to true (e.g. by using Response.text()).
                    waitingForBody: response.waitingForBody,
                    body: response.body ?? null
                },
                request: {
                    headers: request.headers,
                    method: request.method
                },
                vary: {},
                expires: null,
                etag: null,
                cacheUpdateTime: Date.now(),
                lastModified: null,
                mustRevalidate: false,
                staleWhileRevalidate: false,
                state: CachedResponseStateEnum.fresh,
                virtual: response.virtual ?? false
            };
            let entries = this.#entries.get(url);
            if (!entries) {
                entries = [];
                this.#entries.set(url, entries);
            }
            entries.push(cachedResponse);
        }
        if (response.headers.has('Cache-Control')) {
            const age = response.headers.get('Age');
            for (const part of response.headers.get('Cache-Control').split(',')) {
                const [key, value] = part.trim().split('=');
                switch (key) {
                    case 'max-age':
                        cachedResponse.expires =
                            Date.now() + parseFloat(value) * 1000 - (age ? parseFloat(age) * 1000 : 0);
                        break;
                    case 'no-cache':
                    case 'no-store':
                        const entries = this.#entries.get(url);
                        if (entries) {
                            const index = entries.indexOf(cachedResponse);
                            if (index !== -1) {
                                entries.splice(index, 1);
                            }
                        }
                        return null;
                    case 'must-revalidate':
                        cachedResponse.mustRevalidate = true;
                        break;
                    case 'stale-while-revalidate':
                        cachedResponse.staleWhileRevalidate = true;
                        break;
                }
            }
        }
        if (response.headers.has('Last-Modified')) {
            cachedResponse.lastModified = Date.parse(response.headers.get('Last-Modified'));
        }
        if (response.headers.has('Vary')) {
            for (const header of response.headers.get('Vary').split(',')) {
                const name = header.trim();
                const value = request.headers.get(name);
                if (value) {
                    cachedResponse.vary[name] = value;
                }
            }
        }
        if (response.headers.has('ETag')) {
            cachedResponse.etag = response.headers.get('ETag');
        }
        if (!cachedResponse.expires) {
            const expires = response.headers.get('Expires');
            if (expires) {
                cachedResponse.expires = Date.parse(expires);
            }
        }
        // Cache is invalid if it has expired and doesn't have an ETag.
        if (!cachedResponse.etag && (!cachedResponse.expires || cachedResponse.expires < Date.now())) {
            const entries = this.#entries.get(url);
            if (entries) {
                const index = entries.indexOf(cachedResponse);
                if (index !== -1) {
                    entries.splice(index, 1);
                }
            }
            return null;
        }
        return cachedResponse;
    }
    /**
     * Clears the cache.
     *
     * @param [options] Options.
     * @param [options.url] URL.
     * @param [options.toTime] Removes all entries that are older than this time. Time in MS.
     */
    clear(options) {
        if (options) {
            if (options.toTime) {
                for (const key of options.url ? [options.url] : this.#entries.keys()) {
                    const entry = this.#entries.get(key);
                    if (entry) {
                        for (let i = 0, max = entry.length; i < max; i++) {
                            if (entry[i].cacheUpdateTime < options.toTime) {
                                entry.splice(i, 1);
                                i--;
                                max--;
                            }
                        }
                    }
                }
            }
            else if (options.url) {
                this.#entries.delete(options.url);
            }
        }
        else {
            this.#entries.clear();
        }
    }
}
//# sourceMappingURL=ResponseCache.js.map