import { URLPattern } from "urlpattern-polyfill";
import { getKeyValueStore } from "../../data/index.js";
import { getServiceWorkerId } from "./service-worker-config.js";
import { v4 } from "uuid";
import { ok } from "../../is.js";
import { caches } from "../../fetch/index.js";
export function isRouterURLPatternCondition(condition) {
    return "urlPattern" in condition;
}
export function isRouterRequestMethodCondition(condition) {
    return "requestMethod" in condition;
}
export function isRouterRequestModeCondition(condition) {
    return "requestMode" in condition;
}
export function isRouterRequestDestinationCondition(condition) {
    return "requestDestination" in condition;
}
export function isRouterCacheSource(source) {
    return isRouterSourceObject(source) && source.type === "cache";
}
export function isRouterRequestCondition(condition) {
    return (isRouterRequestMethodCondition(condition) ||
        isRouterRequestModeCondition(condition) ||
        isRouterRequestDestinationCondition(condition));
}
export function isRouterTimeCondition(condition) {
    return ("timeFrom" in condition ||
        "timeTo" in condition);
}
export function isRouterRunningStatusCondition(condition) {
    return "runningStatus" in condition;
}
export function isRouterAndCondition(condition) {
    return "and" in condition && Array.isArray(condition.and);
}
export function isRouterOrCondition(condition) {
    return "or" in condition && Array.isArray(condition.or);
}
export function isRouterNotCondition(condition) {
    return "not" in condition && Boolean(condition.not);
}
export function isRouterSourceObject(source) {
    return (typeof source === "object" &&
        "type" in source);
}
export function isRouterSourceType(source, type) {
    return (source === type ||
        (isRouterSourceObject(source) &&
            source.type === type));
}
export function isRouterNetworkSource(source) {
    return isRouterSourceObject(source) && source.type === "network";
}
const STORE_NAME = "serviceWorkerRoutes";
export function getServiceWorkerRoutesStore(serviceWorkerId = getServiceWorkerId()) {
    return getKeyValueStore(`${STORE_NAME}:${serviceWorkerId}`);
}
export async function addRoutes(rules) {
    if (!Array.isArray(rules)) {
        return addRoutes([rules]);
    }
    const store = getServiceWorkerRoutesStore();
    for (const rule of rules) {
        const safe = JSON.parse(JSON.stringify(rule, (key, value) => {
            // How fun right...
            // Will need to resolve this in the storage layer instead.
            // Should allow cloneable objects and extending this.
            if (value instanceof URLPattern) {
                return {
                    protocol: value.protocol,
                    username: value.username,
                    password: value.password,
                    hostname: value.hostname,
                    port: value.port,
                    pathname: value.pathname,
                    search: value.search,
                    hash: value.hash
                };
            }
            else if (value instanceof Request) {
                // a cache source can define a request to use as a key...
                // this isn't the exact same, but it will be used as the key in:
                //   cache.put(value.url, response)
                //
                // This would be different from what a real service worker might do, so
                // I will definitely need to find a solution for objects just like URLPattern
                return value.url;
            }
            else {
                return value;
            }
        }));
        await store.set(v4(), safe);
    }
}
export function listRoutes(serviceWorkerId = getServiceWorkerId()) {
    const store = getServiceWorkerRoutesStore(serviceWorkerId);
    return store.values();
}
export function isRouteMatchCondition({ baseURL }, route, input, init) {
    return isConditionsMatch(route.condition);
    function isConditionsMatch(conditions) {
        if (Array.isArray(conditions)) {
            if (!conditions.length) {
                throw new Error("Expected at least one condition");
            }
            for (const condition of conditions) {
                if (!isConditionMatch(condition)) {
                    return false;
                }
            }
            return true;
        }
        else {
            return isConditionMatch(conditions);
        }
    }
    function isRouterURLPatternConditionMatch(condition) {
        const url = input instanceof URL ?
            input :
            typeof input === "string" ?
                input :
                input.url;
        const { urlPattern } = condition;
        if (typeof urlPattern === "string") {
            // TODO, confirm this is correct
            // Explainer does mention:
            //
            //   For a USVString input, a ServiceWorker script's URL is used as a base URL.
            //
            // My current assumption is we are completely comparing the URL and search params here
            const matchInstance = new URL(url, baseURL);
            const patternInstance = new URL(urlPattern, baseURL);
            return matchInstance.toString() === patternInstance.toString();
        }
        else {
            if ("test" in urlPattern && urlPattern.test) {
                return urlPattern.test(url, baseURL);
            }
            else {
                const pattern = new URLPattern(urlPattern, typeof urlPattern === "string" ? baseURL : undefined);
                return pattern.test(url, baseURL);
            }
        }
    }
    function isRouterRequestConditionMatch(condition) {
        if (typeof input === "string" || input instanceof URL) {
            const { method, mode } = init || {};
            if (isRouterRequestMethodCondition(condition)) {
                if (method) {
                    if (method.toUpperCase() !== condition.requestMethod.toUpperCase()) {
                        return false;
                    }
                }
                else {
                    if (condition.requestMethod.toUpperCase() !== "GET") {
                        return false;
                    }
                }
            }
            if (isRouterRequestModeCondition(condition)) {
                if (mode) {
                    if (mode !== condition.requestMode) {
                        return false;
                    }
                }
                else {
                    // Assuming that fetch follows creating a new Request object
                    // From https://developer.mozilla.org/en-US/docs/Web/API/Request/mode
                    //   For example, when a Request object is created using the Request() constructor, the value of the mode property for that Request is set to cors.
                    //
                    // undici uses new Request
                    //
                    // https://github.com/nodejs/undici/blob/8535d8c8e3937d73037272ffb411c7b14b036917/lib/fetch/index.js#L136
                    // https://github.com/nodejs/undici/blob/8535d8c8e3937d73037272ffb411c7b14b036917/lib/fetch/request.js#L100
                    if (condition.requestMode !== "cors") {
                        return false;
                    }
                }
            }
            if (isRouterRequestDestinationCondition(condition)) {
                // default destination is ''
                // https://developer.mozilla.org/en-US/docs/Web/API/Request/destination#sect1
                if (condition.requestDestination !== "") {
                    return false;
                }
            }
        }
        else {
            const { method, mode, destination } = input;
            if (isRouterRequestMethodCondition(condition)) {
                if (method.toUpperCase() !== condition.requestMethod.toUpperCase()) {
                    return false;
                }
            }
            if (isRouterRequestModeCondition(condition)) {
                if (mode !== condition.requestMode) {
                    return false;
                }
            }
            if (isRouterRequestDestinationCondition(condition)) {
                if (destination !== condition.requestDestination) {
                    return false;
                }
            }
        }
        return true;
    }
    function isRouterTimeConditionMatch(condition) {
        // TODO... is this literally meant to be the condition of the current time?
        //  There is nothing else to compare
        //  Request.headers.get("date") should be the same current time if it were present...
        if (condition.timeFrom) {
            if (Date.now() < condition.timeFrom) {
                return false;
            }
        }
        if (condition.timeTo) {
            if (Date.now() > condition.timeTo) {
                return false;
            }
        }
        return true;
    }
    function isAndConditionMatch(condition) {
        return condition.and.every(isConditionMatch);
    }
    function isOrConditionMatch(condition) {
        const index = condition.or.findIndex(isNotConditionMatch);
        return index === -1;
    }
    function isNotConditionMatch(condition) {
        return !isConditionsMatch(condition.not);
    }
    function isConditionMatch(condition) {
        if (isRouterURLPatternCondition(condition)) {
            return isRouterURLPatternConditionMatch(condition);
        }
        if (isRouterRequestCondition(condition)) {
            return isRouterRequestConditionMatch(condition);
        }
        if (isRouterTimeCondition(condition)) {
            return isRouterTimeConditionMatch(condition);
        }
        if (isRouterAndCondition(condition)) {
            return isAndConditionMatch(condition);
        }
        if (isRouterOrCondition(condition)) {
            return isOrConditionMatch(condition);
        }
        if (isRouterNotCondition(condition)) {
            return isNotConditionMatch(condition);
        }
        return false;
    }
}
export async function createRouter(serviceWorkers) {
    const serviceWorkerRoutes = new Map();
    async function listServiceWorkerRoutes(serviceWorker) {
        const existing = serviceWorkerRoutes.get(serviceWorker);
        if (existing) {
            return existing;
        }
        const routes = await listRoutes(serviceWorker.durable.serviceWorkerId);
        if (routes.length) {
            serviceWorkerRoutes.set(serviceWorker, routes);
        }
        return routes;
    }
    async function match(input, init) {
        for (const serviceWorker of serviceWorkers) {
            const routes = await listServiceWorkerRoutes(serviceWorker);
            if (serviceWorkers.length === 1 && !routes.length) {
                // TODO install worker
            }
            for (const route of routes) {
                if (isRouteMatchCondition(serviceWorker.durable, route, input, init)) {
                    return {
                        serviceWorker,
                        route
                    };
                }
            }
        }
    }
    return async function (input, init) {
        const found = await match(input, init);
        if (!found) {
            throw new Error("FetchError: No match for this router");
        }
        const { serviceWorker, route } = found;
        return sources(route.source);
        async function sources(ruleSource) {
            if (Array.isArray(ruleSource)) {
                let firstResponse, lastError = undefined;
                for (const singleRuleSource of ruleSource) {
                    try {
                        const response = await source(singleRuleSource);
                        // TODO, should response.ok be true here as well
                        // TODO behaviorEnum
                        if (firstResponse) {
                            continue;
                        }
                        if (isRouterSourceObject(singleRuleSource)) {
                            if (singleRuleSource.behaviorEnum === "continue-discarding-latter-results") {
                                if (!firstResponse) {
                                    firstResponse = response;
                                }
                                continue;
                            }
                        }
                        if (response) {
                            return response;
                        }
                    }
                    catch (error) {
                        // Source failed, or could not match
                        lastError = error;
                    }
                }
                if (firstResponse) {
                    return firstResponse;
                }
                if (lastError) {
                    // Using promise reject here will give an original error stack for user...
                    throw await Promise.reject(lastError);
                }
                throw new Error("Could not resolve response");
            }
            else {
                return source(ruleSource);
            }
        }
        async function serviceWorkerFetch(source) {
            // TODO check if any fetch event handlers added.
            const { fetch } = serviceWorker;
            ok(fetch, "Expected to find fetcher for service worker, internal state corrupt");
            let resolvedInit = init;
            if (typeof source === "object" && source.tag) {
                resolvedInit = {
                    ...init,
                    tag: source.tag
                };
            }
            return fetch(clone(), resolvedInit);
        }
        async function source(ruleSource) {
            if (isRouterSourceType(ruleSource, "network")) {
                const response = await fetch(clone(), init);
                if (isRouterNetworkSource(ruleSource)) {
                    if (ruleSource.updatedCacheName) {
                        if (ruleSource.cacheErrorResponse || response.ok) {
                            const cache = await caches.open(ruleSource.updatedCacheName);
                            await cache.put(cacheKey(), response.clone());
                        }
                    }
                }
                return response;
            }
            if (isRouterSourceType(ruleSource, "fetch-event")) {
                return serviceWorkerFetch(ruleSource);
            }
            if (isRouterSourceType(ruleSource, "race-network-and-fetch-handler")) {
                return Promise.race([
                    source("network"),
                    serviceWorkerFetch(ruleSource)
                ]);
            }
            if (isRouterSourceType(ruleSource, "cache")) {
                if (isRouterCacheSource(ruleSource)) {
                    const cache = await caches.open(ruleSource.cacheName || "default");
                    return cache.match(cacheKey(ruleSource.request));
                }
                else {
                    const cache = await caches.open("default");
                    return cache.match(cacheKey());
                }
            }
            throw new Error("Unknown source type");
        }
        function cacheKey(request) {
            if (request) {
                if (request instanceof Request) {
                    return request.clone();
                }
                else {
                    return new Request(request);
                }
            }
            if (!init) {
                return clone();
            }
            else {
                return new Request(clone(), init);
            }
        }
        function clone() {
            if (input instanceof Request) {
                return input.clone();
            }
            else {
                return input;
            }
        }
    };
}
//# sourceMappingURL=router.js.map