// Copyright 2018-2026 the Deno authors. MIT license.

(function () {
const { core, primordials } = __bootstrap;
const {
  op_cache_delete,
  op_cache_keys,
  op_cache_match,
  op_cache_put,
  op_cache_storage_delete,
  op_cache_storage_has,
  op_cache_storage_keys,
  op_cache_storage_open,
} = core.ops;
const {
  ArrayPrototypePush,
  ObjectPrototypeIsPrototypeOf,
  StringPrototypeSplit,
  StringPrototypeTrim,
  Symbol,
  SymbolFor,
  TypeError,
} = primordials;

const webidl = core.loadExtScript("ext:deno_webidl/00_webidl.js");
const {
  Request,
  RequestPrototype,
  toInnerRequest,
} = core.loadExtScript("ext:deno_fetch/23_request.js");
const { toInnerResponse, wireHeaderList } = core.loadExtScript(
  "ext:deno_fetch/23_response.js",
);
const { URLPrototype } = core.loadExtScript("ext:deno_web/00_url.js");
const { getHeader } = core.loadExtScript("ext:deno_fetch/20_headers.js");
const {
  getReadableStreamResourceBacking,
  readableStreamForRid,
  resourceForReadableStream,
} = core.loadExtScript("ext:deno_web/06_streams.js");
class CacheStorage {
  constructor() {
    webidl.illegalConstructor();
  }

  async open(cacheName) {
    webidl.assertBranded(this, CacheStoragePrototype);
    const prefix = "Failed to execute 'open' on 'CacheStorage'";
    webidl.requiredArguments(arguments.length, 1, prefix);
    cacheName = webidl.converters["DOMString"](cacheName, prefix, "Argument 1");
    const cacheId = await op_cache_storage_open(cacheName);
    const cache = webidl.createBranded(Cache);
    cache[_id] = cacheId;
    return cache;
  }

  async has(cacheName) {
    webidl.assertBranded(this, CacheStoragePrototype);
    const prefix = "Failed to execute 'has' on 'CacheStorage'";
    webidl.requiredArguments(arguments.length, 1, prefix);
    cacheName = webidl.converters["DOMString"](cacheName, prefix, "Argument 1");
    return await op_cache_storage_has(cacheName);
  }

  async delete(cacheName) {
    webidl.assertBranded(this, CacheStoragePrototype);
    const prefix = "Failed to execute 'delete' on 'CacheStorage'";
    webidl.requiredArguments(arguments.length, 1, prefix);
    cacheName = webidl.converters["DOMString"](cacheName, prefix, "Argument 1");
    return await op_cache_storage_delete(cacheName);
  }

  async keys() {
    webidl.assertBranded(this, CacheStoragePrototype);
    return await op_cache_storage_keys();
  }

  async match(request, options) {
    webidl.assertBranded(this, CacheStoragePrototype);
    const prefix = "Failed to execute 'match' on 'CacheStorage'";
    webidl.requiredArguments(arguments.length, 1, prefix);
    request = webidl.converters["RequestInfo_DOMString"](
      request,
      prefix,
      "Argument 1",
    );
    const cacheName = options?.cacheName;
    if (cacheName !== undefined) {
      if (!(await op_cache_storage_has(cacheName))) {
        return undefined;
      }
      const cache = await this.open(cacheName);
      // false positive: cache is a local Cache instance, not a global intrinsic
      // deno-lint-ignore deno-internal/prefer-primordials
      return await cache.match(request, options);
    }
    const names = await op_cache_storage_keys();
    for (let i = 0; i < names.length; ++i) {
      const cache = await this.open(names[i]);
      // false positive: cache is a local Cache instance, not a global intrinsic
      // deno-lint-ignore deno-internal/prefer-primordials
      const response = await cache.match(request, options);
      if (response !== undefined) {
        return response;
      }
    }
    return undefined;
  }

  [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
    return `${this.constructor.name} ${inspect({}, inspectOptions)}`;
  }
}

const _matchAll = Symbol("[[matchAll]]");
const _id = Symbol("id");

class Cache {
  /** @type {number} */
  [_id];

  constructor() {
    webidl.illegalConstructor();
  }

  /** See https://w3c.github.io/ServiceWorker/#dom-cache-put */
  async put(request, response) {
    webidl.assertBranded(this, CachePrototype);
    const prefix = "Failed to execute 'put' on 'Cache'";
    webidl.requiredArguments(arguments.length, 2, prefix);
    request = webidl.converters["RequestInfo_DOMString"](
      request,
      prefix,
      "Argument 1",
    );
    response = webidl.converters["Response"](response, prefix, "Argument 2");
    // Step 1.
    let innerRequest = null;
    // Step 2.
    if (ObjectPrototypeIsPrototypeOf(RequestPrototype, request)) {
      innerRequest = toInnerRequest(request);
    } else {
      // Step 3.
      innerRequest = toInnerRequest(new Request(request));
    }
    // Step 4.
    const reqUrl = new URL(innerRequest.url());
    if (reqUrl.protocol !== "http:" && reqUrl.protocol !== "https:") {
      throw new TypeError(
        `Request url protocol must be 'http:' or 'https:': received '${reqUrl.protocol}'`,
      );
    }
    if (innerRequest.method !== "GET") {
      throw new TypeError("Request method must be GET");
    }
    // Step 5.
    const innerResponse = toInnerResponse(response);
    // Step 6.
    if (innerResponse.status === 206) {
      throw new TypeError("Response status must not be 206");
    }
    // Step 7.
    const varyHeader = getHeader(innerResponse.headerList, "vary");
    if (varyHeader) {
      const fieldValues = StringPrototypeSplit(varyHeader, ",");
      for (let i = 0; i < fieldValues.length; ++i) {
        const field = fieldValues[i];
        if (StringPrototypeTrim(field) === "*") {
          throw new TypeError("Vary header must not contain '*'");
        }
      }
    }

    // Step 8.
    if (innerResponse.body !== null && innerResponse.body.unusable()) {
      throw new TypeError("Response body is already used");
    }

    const stream = innerResponse.body?.stream;
    let rid = null;
    if (stream) {
      const resourceBacking = getReadableStreamResourceBacking(
        innerResponse.body?.stream,
      );
      if (resourceBacking) {
        rid = resourceBacking.rid;
      } else {
        rid = resourceForReadableStream(stream, innerResponse.body?.length);
      }
    }

    // Remove fragment from request URL before put.
    reqUrl.hash = "";

    // Step 9-11.
    // Step 12-19: TODO(@satyarohith): do the insertion in background.
    await op_cache_put(
      {
        cacheId: this[_id],
        // deno-lint-ignore deno-internal/prefer-primordials
        requestUrl: reqUrl.toString(),
        responseHeaders: wireHeaderList(innerResponse),
        requestHeaders: innerRequest.headerList,
        responseStatus: innerResponse.status,
        responseStatusText: innerResponse.statusMessage,
        responseRid: rid,
      },
    );
  }

  /** See https://w3c.github.io/ServiceWorker/#cache-match */
  async match(request, options) {
    webidl.assertBranded(this, CachePrototype);
    const prefix = "Failed to execute 'match' on 'Cache'";
    webidl.requiredArguments(arguments.length, 1, prefix);
    request = webidl.converters["RequestInfo_DOMString"](
      request,
      prefix,
      "Argument 1",
    );
    const p = await this[_matchAll](request, options);
    if (p.length > 0) {
      return p[0];
    } else {
      return undefined;
    }
  }

  /** See https://w3c.github.io/ServiceWorker/#cache-delete */
  async delete(request, _options) {
    webidl.assertBranded(this, CachePrototype);
    const prefix = "Failed to execute 'delete' on 'Cache'";
    webidl.requiredArguments(arguments.length, 1, prefix);
    request = webidl.converters["RequestInfo_DOMString"](
      request,
      prefix,
      "Argument 1",
    );
    // Step 1.
    let r = null;
    // Step 2.
    if (ObjectPrototypeIsPrototypeOf(RequestPrototype, request)) {
      r = request;
      if (request.method !== "GET") {
        return false;
      }
    } else if (
      typeof request === "string" ||
      ObjectPrototypeIsPrototypeOf(URLPrototype, request)
    ) {
      r = new Request(request);
    }
    return await op_cache_delete({
      cacheId: this[_id],
      requestUrl: r.url,
    });
  }

  /** See https://w3c.github.io/ServiceWorker/#cache-keys */
  async keys(request, _options) {
    webidl.assertBranded(this, CachePrototype);
    const prefix = "Failed to execute 'keys' on 'Cache'";
    // Step 1-2.
    let requestUrl = null;
    if (request !== undefined) {
      request = webidl.converters["RequestInfo_DOMString"](
        request,
        prefix,
        "Argument 1",
      );
      let r = null;
      if (ObjectPrototypeIsPrototypeOf(RequestPrototype, request)) {
        r = request;
        // Only GET requests are stored in the cache.
        if (request.method !== "GET") {
          return [];
        }
      } else {
        r = new Request(request);
      }
      // Remove the fragment from the request URL before matching.
      const url = new URL(r.url);
      url.hash = "";
      // deno-lint-ignore deno-internal/prefer-primordials
      requestUrl = url.toString();
    }

    // Step 5: return the request keys in insertion order. When a request is
    // given, the backend filters by URL so we don't fetch the whole cache.
    const entries = await op_cache_keys({ cacheId: this[_id], requestUrl });
    const requests = [];
    for (let i = 0; i < entries.length; ++i) {
      const entry = entries[i];
      ArrayPrototypePush(
        requests,
        new Request(entry.requestUrl, { headers: entry.requestHeaders }),
      );
    }
    return requests;
  }

  /** See https://w3c.github.io/ServiceWorker/#cache-matchall
   *
   * Note: the function is private as we don't want to expose
   * this API to the public yet.
   *
   * The function will return an array of responses.
   */
  async [_matchAll](request, _options) {
    // Step 1.
    let r = null;
    // Step 2.
    if (ObjectPrototypeIsPrototypeOf(RequestPrototype, request)) {
      r = request;
      if (request.method !== "GET") {
        return [];
      }
    } else if (
      typeof request === "string" ||
      ObjectPrototypeIsPrototypeOf(URLPrototype, request)
    ) {
      r = new Request(request);
    }

    // Step 5.
    const responses = [];
    // Step 5.2
    if (r === null) {
      // Step 5.3
      // Note: we have to return all responses in the cache when
      // the request is null.
      // We deviate from the spec here and return an empty array
      // as we don't expose matchAll() API.
      return responses;
    } else {
      // Remove the fragment from the request URL.
      const url = new URL(r.url);
      url.hash = "";
      const innerRequest = toInnerRequest(r);
      const matchResult = await op_cache_match(
        {
          cacheId: this[_id],
          // deno-lint-ignore deno-internal/prefer-primordials
          requestUrl: url.toString(),
          requestHeaders: innerRequest.headerList,
        },
      );
      if (matchResult) {
        const { 0: meta, 1: responseBodyRid } = matchResult;
        let body = null;
        if (responseBodyRid !== null) {
          body = readableStreamForRid(responseBodyRid);
        }
        const response = new Response(
          body,
          {
            headers: meta.responseHeaders,
            status: meta.responseStatus,
            statusText: meta.responseStatusText,
          },
        );
        ArrayPrototypePush(responses, response);
      }
    }
    // Step 5.4-5.5: don't apply in this context.

    return responses;
  }

  [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
    return `${this.constructor.name} ${inspect({}, inspectOptions)}`;
  }
}

webidl.configureInterface(CacheStorage);
webidl.configureInterface(Cache);
const CacheStoragePrototype = CacheStorage.prototype;
const CachePrototype = Cache.prototype;

let cacheStorageStorage;
function cacheStorage() {
  if (!cacheStorageStorage) {
    cacheStorageStorage = webidl.createBranded(CacheStorage);
  }
  return cacheStorageStorage;
}

return { Cache, CacheStorage, cacheStorage };
})();
