// deno-lint-ignore-file no-explicit-any
import { Context, EffectsList, executeEffects } from '@worker-tools/middleware';
import { internalServerError, notFound } from '@worker-tools/response-creators';
import { ResolvablePromise } from '@worker-tools/resolvable-promise';

import type { URLPatternInit, URLPatternComponentResult, URLPatternInput, URLPatternResult } from '@worker-tools/middleware'
export type { URLPatternInit, URLPatternComponentResult, URLPatternInput, URLPatternResult }

import { AggregateError } from "./utils/aggregate-error.js";
import { ErrorEvent } from './utils/error-event.js';

export type Awaitable<T> = T | PromiseLike<T>;

export interface RouteContext extends Context {
  /** 
   * The match that resulted in the execution of this route. It is the full result produced by the URL Pattern API. 
   * If you are looking for a `params`-like object similar to outer routers, use the `basics` middleware 
   * or `match.pathname.groups`.
   */
  match: URLPatternResult
}

export interface ErrorContext extends RouteContext {
  /**
   * If the exception is well-known and caused by middleware, this property is populated with a `Response` object 
   * with an appropriate status code and text set. 
   * 
   * You can use it to customize the error response, e.g.: `new Response('...', response)`.
   */
  response: Response,

  /**
   * If an unknown error occurred, the sibling `response` property is set to be an "internal server error" while
   * the `error` property contains thrown error.
   */
  error?: unknown,
}

export type Middleware<RX extends RouteContext, X extends RouteContext> = (x: Awaitable<RX>) => Awaitable<X>

export type Handler<X extends RouteContext> = (request: Request, ctx: X) => Awaitable<Response>;
export type ErrorHandler<X extends ErrorContext> = (request: Request, ctx: X) => Awaitable<Response>;

export type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';

// Internal types...  these are not the types you are looking for
type MethodWildcard = 'ANY';
type RouteHandler = (x: RouteContext) => Awaitable<Response>
type RecoverRouteHandler = (x: ErrorContext) => Awaitable<Response>

interface Route {
  method: Method | MethodWildcard
  pattern: URLPattern
  handler: RouteHandler | RecoverRouteHandler
}

/** 
 * Turns a pathname pattern into a `URLPattern` that works across worker runtimes.
 * 
 * Specifically in the case of Service Workers, this ensures requests to external domains that happen to have the same
 * pathname aren't matched. 
 * If a worker environment has a location set (e.g. deno with `--location` or CF workers with a location polyfill), 
 * this is essentially a noop since only matching requests can reach deployed workers in the first place.
 */
const toPattern = navigator.userAgent?.includes('Cloudflare-Workers') && self.location?.hostname === 'localhost'
  ? (pathname: string) => new URLPattern({ pathname })
  : (pathname: string) => {
    const pattern = new URLPattern({
      pathname,
      protocol: self.location?.protocol,
      hostname: self.location?.hostname,
      port: self.location?.port,
    })
    // Note that `undefined` becomes a `*` pattern.
    return pattern;
  }

export interface WorkerRouterOptions {
  fatal?: boolean
}

// const anyResult = Object.freeze(toPattern('*').exec(new Request('/').url)!);
// const anyPathResult = Object.freeze(toPattern('/*').exec(new Request('/').url)!);

export class WorkerRouter<RX extends RouteContext = RouteContext> extends EventTarget implements EventListenerObject {
  #middleware: Middleware<RouteContext, RX>
  #routes: Route[] = [];
  #recoverRoutes: Route[] = [];
  #fatal: boolean

  constructor(middleware?: Middleware<RouteContext, RX> | null, opts: WorkerRouterOptions = {}) {
    super();
    this.#middleware = middleware ?? (_ => _ as RX);
    this.#fatal = opts?.fatal ?? false;
  }

  get fatal() {
    return this.#fatal;
  }

  async #route(fqURL: string, ctx: Omit<Context, 'effects' | 'handled'>): Promise<Response> {
    const result = this.#execPatterns(fqURL, ctx.request)
    try {
      if (!result) throw notFound();
      const [handler, match] = result;
      const handle = new ResolvablePromise<Response>()
      const handled = Promise.resolve(handle)
      const userCtx = Object.assign(ctx, { match, handled, effects: new EffectsList() })
      const response = await handler(userCtx)
      handle.resolve(ctx.event?.handled?.then(() => response) ?? response)
      return response;
    }
    catch (err) {
      const recoverResult = this.#execPatterns(fqURL, ctx.request, this.#recoverRoutes)
      if (recoverResult) {
        try {
          const [handler, match] = recoverResult;
          const [response, error] = err instanceof Response ? [err, undefined] : [internalServerError(), err];
          const handle = new ResolvablePromise<Response>()
          const handled = Promise.resolve(handle)
          const userCtx = Object.assign(ctx, { response, error, match, handled, effects: new EffectsList() })
          const res = await handler(userCtx);
          handle.resolve(ctx.event?.handled?.then(() => res) ?? res)
          return res;
        }
        catch (recoverErr) {
          const aggregateErr = new AggregateError([err, recoverErr], 'Route handler and recover handler failed')
          if (this.#fatal) throw aggregateErr;
          this.#fireError(aggregateErr);
          if (recoverErr instanceof Response) return recoverErr;
          if (err instanceof Response) return err;
          return internalServerError();
        }
      }
      if (this.#fatal) throw err;
      this.#fireError(err);
      if (err instanceof Response) return err;
      return internalServerError();
    }
  }

  #fireError(error: unknown) {
    const message = error instanceof Response
      ? `${error.status} ${error.statusText}`
      : error instanceof Error
        ? error.message
        : '' + error;
    this.dispatchEvent(new ErrorEvent('error', { message, error }));
  }

  #execPatterns(fqURL: string, request: Request, routes = this.#routes): readonly [RouteHandler, URLPatternResult] | null {
    for (const { method, pattern, handler } of routes) {
      if (method !== 'ANY' && method !== request.method.toUpperCase()) continue

      const match = pattern.exec(fqURL);
      if (!match) continue

      // @ts-ignore: FIXME
      return [handler, match] as const;
    }
    return null
  }

  #pushRoute(
    method: Method | MethodWildcard,
    pattern: URLPattern,
    handler: Handler<RX>,
  ) {
    this.#routes.push({
      method,
      pattern,
      handler: async (event: RouteContext) => {
        const ctx = await this.#middleware(event);
        const response = handler(event.request, ctx);
        return executeEffects(event.effects, response)
      },
    })
  }

  #pushMiddlewareRoute<X extends RX>(
    method: Method | MethodWildcard,
    pattern: URLPattern,
    middleware: Middleware<RX, X>,
    handler: Handler<X>,
  ) {
    this.#routes.push({
      method,
      pattern,
      handler: async (event: RouteContext) => {
        const ctx = await middleware(this.#middleware(event))
        const response = handler(event.request, ctx);
        return executeEffects(event.effects, response)
      },
    })
  }

  #registerPattern<X extends RX>(
    method: Method | MethodWildcard,
    argsN: number,
    pattern: URLPattern,
    middlewareOrHandler: Middleware<RX, X> | Handler<X>,
    handler?: Handler<X>,
  ): this {
    if (argsN === 2) {
      const handler = middlewareOrHandler as Handler<RX>
      this.#pushRoute(method, pattern, handler)
    } else if (argsN === 3) {
      const middleware = middlewareOrHandler as Middleware<RX, X>
      this.#pushMiddlewareRoute(method, pattern, middleware, handler!)
    } else {
      throw Error(`Router '${method.toLowerCase()}' called with invalid number of arguments`)
    }
    return this;
  }

  #registerRecoverPattern<X extends ErrorContext>(
    method: Method | MethodWildcard,
    argsN: number,
    pattern: URLPattern,
    middlewareOrHandler: Middleware<ErrorContext, X> | ErrorHandler<ErrorContext>,
    handler?: ErrorHandler<X>,
  ): this {
    if (argsN === 2) {
      const handler = middlewareOrHandler as ErrorHandler<ErrorContext>
      this.#pushRecoverRoute(method, pattern, handler)
    } else if (argsN === 3) {
      const middleware = middlewareOrHandler as Middleware<ErrorContext, X>
      this.#pushMiddlewareRecoverRoute(method, pattern, middleware, handler!)
    } else {
      throw Error(`Router '${method.toLowerCase()}' called with invalid number of arguments`)
    }
    return this;
  }

  #pushRecoverRoute(
    method: Method | MethodWildcard,
    pattern: URLPattern,
    handler: ErrorHandler<ErrorContext>,
  ) {
    this.#recoverRoutes.push({
      method,
      pattern,
      handler: (event: ErrorContext) => {
        const response = handler(event.request, event)
        return executeEffects(event.effects, response)
      },
    });
  }

  #pushMiddlewareRecoverRoute<X extends ErrorContext>(
    method: Method | MethodWildcard,
    pattern: URLPattern,
    middleware: Middleware<ErrorContext, X>,
    handler: Handler<X>,
  ) {
    this.#recoverRoutes.push({
      method,
      pattern,
      handler: async (event: ErrorContext) => {
        const ctx = await middleware(event)
        const response = handler(event.request, ctx);
        return executeEffects(event.effects, response)
      },
    });
  }

  /** Add a route that matches *any* HTTP method. */
  any<X extends RX>(path: string, handler: Handler<X>): this;
  any<X extends RX>(path: string, middleware: Middleware<RX, X>, handler: Handler<X>): this;
  any<X extends RX>(path: string, middlewareOrHandler: Middleware<RX, X> | Handler<X>, handler?: Handler<X>): this {
    return this.#registerPattern('ANY', arguments.length, toPattern(path), middlewareOrHandler, handler);
  }
  /** Alias for for the more appropriately named `any` method */
  all<X extends RX>(path: string, handler: Handler<X>): this;
  all<X extends RX>(path: string, middleware: Middleware<RX, X>, handler: Handler<X>): this;
  all<X extends RX>(path: string, middlewareOrHandler: Middleware<RX, X> | Handler<X>, handler?: Handler<X>): this {
    return this.#registerPattern('ANY', arguments.length, toPattern(path), middlewareOrHandler, handler);
  }
  /** Add a route that matches the `GET` method. */
  get<X extends RX>(path: string, handler: Handler<X>): this;
  get<X extends RX>(path: string, middleware: Middleware<RX, X>, handler: Handler<X>): this;
  get<X extends RX>(path: string, middlewareOrHandler: Middleware<RX, X> | Handler<X>, handler?: Handler<X>): this {
    return this.#registerPattern('GET', arguments.length, toPattern(path), middlewareOrHandler, handler);
  }
  /** Add a route that matches the `POST` method. */
  post<X extends RX>(path: string, handler: Handler<X>): this;
  post<X extends RX>(path: string, middleware: Middleware<RX, X>, handler: Handler<X>): this;
  post<X extends RX>(path: string, middlewareOrHandler: Middleware<RX, X> | Handler<X>, handler?: Handler<X>): this {
    return this.#registerPattern('POST', arguments.length, toPattern(path), middlewareOrHandler, handler);
  }
  /** Add a route that matches the `PUT` method. */
  put<X extends RX>(path: string, handler: Handler<X>): this;
  put<X extends RX>(path: string, middleware: Middleware<RX, X>, handler: Handler<X>): this;
  put<X extends RX>(path: string, middlewareOrHandler: Middleware<RX, X> | Handler<X>, handler?: Handler<X>): this {
    return this.#registerPattern('PUT', arguments.length, toPattern(path), middlewareOrHandler, handler);
  }
  /** Add a route that matches the `PATCH` method. */
  patch<X extends RX>(path: string, handler: Handler<X>): this;
  patch<X extends RX>(path: string, middleware: Middleware<RX, X>, handler: Handler<X>): this;
  patch<X extends RX>(path: string, middlewareOrHandler: Middleware<RX, X> | Handler<X>, handler?: Handler<X>): this {
    return this.#registerPattern('PATCH', arguments.length, toPattern(path), middlewareOrHandler, handler);
  }
  /** Add a route that matches the `DELETE` method. */
  delete<X extends RX>(path: string, handler: Handler<X>): this;
  delete<X extends RX>(path: string, middleware: Middleware<RX, X>, handler: Handler<X>): this;
  delete<X extends RX>(path: string, middlewareOrHandler: Middleware<RX, X> | Handler<X>, handler?: Handler<X>): this {
    return this.#registerPattern('DELETE', arguments.length, toPattern(path), middlewareOrHandler, handler);
  }
  /** Add a route that matches the `HEAD` method. */
  head<X extends RX>(path: string, handler: Handler<X>): this;
  head<X extends RX>(path: string, middleware: Middleware<RX, X>, handler: Handler<X>): this;
  head<X extends RX>(path: string, middlewareOrHandler: Middleware<RX, X> | Handler<X>, handler?: Handler<X>): this {
    return this.#registerPattern('HEAD', arguments.length, toPattern(path), middlewareOrHandler, handler);
  }
  /** Add a route that matches the `OPTIONS` method. */
  options<X extends RX>(path: string, handler: Handler<X>): this;
  options<X extends RX>(path: string, middleware: Middleware<RX, X>, handler: Handler<X>): this;
  options<X extends RX>(path: string, middlewareOrHandler: Middleware<RX, X> | Handler<X>, handler?: Handler<X>): this {
    return this.#registerPattern('OPTIONS', arguments.length, toPattern(path), middlewareOrHandler, handler);
  }

  /** 
   * Add a route that matches *any* method with the provided pattern. 
   * Note that the pattern here is interpreted as a `URLPatternInit` which has important implication for matching. 
   * Mostly, this is for use in Service Workers to intercept requests to external resources.
   * 
   * The name `external` is a bit of a misnomer. It simply forwards `init` to the `URLPattern` constructor,
   * instead of being limited to the `pathname` property in the general case.
   * @deprecated Might change name/API
   */
  external<X extends RX>(init: string | URLPatternInit, handler: Handler<X>): this;
  external<X extends RX>(init: string | URLPatternInit, middleware: Middleware<RX, X>, handler: Handler<X>): this;
  external<X extends RX>(init: string | URLPatternInit, middlewareOrHandler: Middleware<RX, X> | Handler<X>, handler?: Handler<X>): this {
    return this.#registerPattern('ANY', arguments.length, new URLPattern(init), middlewareOrHandler, handler);
  }

  /** Like `.external`, but only matches `GET` 
   * @deprecated Might change name/API */
  externalGET<X extends RX>(init: string | URLPatternInit, handler: Handler<X>): this;
  externalGET<X extends RX>(init: string | URLPatternInit, middleware: Middleware<RX, X>, handler: Handler<X>): this;
  externalGET<X extends RX>(init: string | URLPatternInit, middlewareOrHandler: Middleware<RX, X> | Handler<X>, handler?: Handler<X>): this {
    return this.#registerPattern('GET', arguments.length, new URLPattern(init), middlewareOrHandler, handler);
  }

  /** Like `.external`, but only matches `POST` 
   * @deprecated Might change name/API */
  externalPOST<X extends RX>(init: string | URLPatternInit, handler: Handler<X>): this;
  externalPOST<X extends RX>(init: string | URLPatternInit, middleware: Middleware<RX, X>, handler: Handler<X>): this;
  externalPOST<X extends RX>(init: string | URLPatternInit, middlewareOrHandler: Middleware<RX, X> | Handler<X>, handler?: Handler<X>): this {
    return this.#registerPattern('POST', arguments.length, new URLPattern(init), middlewareOrHandler, handler);
  }

  /** Like `.external`, but only matches `PUT` 
   * @deprecated Might change name/API */
  externalPUT<X extends RX>(init: string | URLPatternInit, handler: Handler<X>): this;
  externalPUT<X extends RX>(init: string | URLPatternInit, middleware: Middleware<RX, X>, handler: Handler<X>): this;
  externalPUT<X extends RX>(init: string | URLPatternInit, middlewareOrHandler: Middleware<RX, X> | Handler<X>, handler?: Handler<X>): this {
    return this.#registerPattern('PUT', arguments.length, new URLPattern(init), middlewareOrHandler, handler);
  }

  /** Like `.external`, but only matches `PATCH` 
   * @deprecated Might change name/API */
  externalPATCH<X extends RX>(init: string | URLPatternInit, handler: Handler<X>): this;
  externalPATCH<X extends RX>(init: string | URLPatternInit, middleware: Middleware<RX, X>, handler: Handler<X>): this;
  externalPATCH<X extends RX>(init: string | URLPatternInit, middlewareOrHandler: Middleware<RX, X> | Handler<X>, handler?: Handler<X>): this {
    return this.#registerPattern('PATCH', arguments.length, new URLPattern(init), middlewareOrHandler, handler);
  }

  /** Like `.external`, but only matches `DELETE` 
   * @deprecated Might change name/API */
  externalDELETE<X extends RX>(init: string | URLPatternInit, handler: Handler<X>): this;
  externalDELETE<X extends RX>(init: string | URLPatternInit, middleware: Middleware<RX, X>, handler: Handler<X>): this;
  externalDELETE<X extends RX>(init: string | URLPatternInit, middlewareOrHandler: Middleware<RX, X> | Handler<X>, handler?: Handler<X>): this {
    return this.#registerPattern('DELETE', arguments.length, new URLPattern(init), middlewareOrHandler, handler);
  }

  /** Like `.external`, but only matches `OPTIONS` 
   * @deprecated Might change name/API */
  externalOPTIONS<X extends RX>(init: string | URLPatternInit, handler: Handler<X>): this;
  externalOPTIONS<X extends RX>(init: string | URLPatternInit, middleware: Middleware<RX, X>, handler: Handler<X>): this;
  externalOPTIONS<X extends RX>(init: string | URLPatternInit, middlewareOrHandler: Middleware<RX, X> | Handler<X>, handler?: Handler<X>): this {
    return this.#registerPattern('OPTIONS', arguments.length, new URLPattern(init), middlewareOrHandler, handler);
  }

  /** Like `.external`, but only matches `HEAD` 
   * @deprecated Might change name/API */
  externalHEAD<X extends RX>(init: string | URLPatternInit, handler: Handler<X>): this;
  externalHEAD<X extends RX>(init: string | URLPatternInit, middleware: Middleware<RX, X>, handler: Handler<X>): this;
  externalHEAD<X extends RX>(init: string | URLPatternInit, middlewareOrHandler: Middleware<RX, X> | Handler<X>, handler?: Handler<X>): this {
    return this.#registerPattern('HEAD', arguments.length, new URLPattern(init), middlewareOrHandler, handler);
  }

  /**
   * Use a different `WorkerRouter` for the provided pattern. Keep in mind that:
   * 
   * - The pattern must end in a wildcard `*` 
   * - The corresponding match is the only part used for matching in the `subRouter`
   * - Forwards all HTTP methods
   * - Does not apply any middleware
   * 
   * #### Why does it not apply middleware?
   * 
   * There are 2 reasons: First, it interferes with type inference of middleware.
   * As a developer you'd have to provide the correct types at the point of defining the sub router, 
   * which is at least as cumbersome as providing the middleware itself.
   * 
   * Second, without this there would be no way to opt a route out of the router-level middleware. 
   * For example you might want to opt out all `/public*` urls from cookie parsing, authentication, etc.
   * but add a different caching policy instead.
   * 
   * @param path A pattern ending in a wildcard, e.g. `/items*`
   * @param subRouter A `WorkerRouter` that handles the remaining part of the URL, e.g. `/:category/:id`
   * @deprecated The name of this method might change to avoid confusion with `use` method known from other routers.
   */
  use<Y extends RouteContext>(path: string, subRouter: WorkerRouter<Y>): this {
    // if (this..fatal && !path.endsWith('*')) {
    //   console.warn('Path for \'use\' does not appear to end in a wildcard (*). This is likely to produce unexpected results.');
    // }

    this.#routes.push({
      method: 'ANY',
      pattern: toPattern(path),
      handler: subRouter.#routeHandler,
    })
    return this;
  }

  /** 
   * See `.external` and `.use`. 
   * @deprecated Might change name/API 
   */
  useExternal<Y extends RouteContext>(init: string | URLPatternInit, subRouter: WorkerRouter<Y>): this {
    const pattern = new URLPattern(init)

    // if (this.#opts.fatal && !pattern.pathname.endsWith('*')) {
    //   console.warn('Pathname pattern for \'use\' does not appear to end in a wildcard (*). This is likely to produce unexpected results.');
    // }

    this.#routes.push({
      method: 'ANY',
      pattern,
      handler: subRouter.#routeHandler,
    })
    return this;
  }

  /**
   * Register a special route to recover from an error during execution of a regular route. 
   * 
   * In addition to the usual context properties, the provided handler receives a `response` and `error` property. 
   * In case of a well-known error (typically caused by middleware),
   * the `response` contains a Fetch API `Response` object with matching status and status text set. 
   * In case of an unknown error, the `response` is a generic "internal server error" and the `error` property 
   * contains the value caught by the catch block.
   * 
   * Recover routes don't execute the router-level middleware (which might have caused the error), but
   * can have middleware specifically for this route. Note that if another error occurs during the execution of 
   * this middleware, there are no more safety nets and an internal server error response is returned.
   * 
   * If a global `DEBUG` variable is set (or `process.env.NODE_ENV` is set to `development` in case of webpack)
   * the router will throw on an unhandled error. This is to make it easier to spot problems during development. 
   * Otherwise, the router will not throw but instead dispatch a `error` event on itself before returning an empty 
   * internal server error response.
   */
  recover(path: string, handler: Handler<ErrorContext>): this;
  recover<X extends ErrorContext>(path: string, middleware: Middleware<ErrorContext, X>, handler: Handler<X>): this;
  recover<X extends ErrorContext>(path: string, middlewareOrHandler: Middleware<ErrorContext, X> | Handler<ErrorContext>, handler?: Handler<X>): this {
    return this.#registerRecoverPattern('ANY', arguments.length, toPattern(path), middlewareOrHandler, handler);
  }

  recoverExternal(init: string | URLPatternInit, handler: Handler<ErrorContext>): this;
  recoverExternal<X extends ErrorContext>(init: string | URLPatternInit, middleware: Middleware<ErrorContext, X>, handler: Handler<X>): this;
  recoverExternal<X extends ErrorContext>(init: string | URLPatternInit, middlewareOrHandler: Middleware<ErrorContext, X> | Handler<ErrorContext>, handler?: Handler<X>): this {
    return this.#registerRecoverPattern('ANY', arguments.length, new URLPattern(init), middlewareOrHandler, handler);
  }

  #routeHandler: RouteHandler = (ctx) => {
    // TODO: are these guaranteed to be ordered correctly??
    const values = Object.values(ctx.match?.pathname.groups ?? {});
    if (values.length) {
      const baseURL = new URL(ctx.request.url).origin;
      const subURL = new URL(values.at(-1)!, baseURL);
      return this.#route(subURL.href, ctx);
    }
    throw TypeError('Pattern not suitable for nested routing. Did you forget to add a wildcard (*)?')
  }

  /** @deprecated Name/API might change */
  handle = (request: Request, ctx?: Omit<Context, 'effects'>) => {
    return this.#route(request.url, {
      ...ctx,
      request,
      waitUntil: ctx?.waitUntil?.bind(ctx) ?? ((_f: any) => { })
    })
  }

  /**
   * Implements the (ancient) event listener object interface to allow passing to fetch event directly,
   * e.g. `self.addEventListener('fetch', router)`.
   */
  handleEvent = (object: Event) => {
    const event = object as any;
    event.respondWith(this.#route(event.request.url, {
      request: event.request,
      waitUntil: event.waitUntil.bind(event),
      event,
    }));
  }

  /**
   * Callback compatible with Cloudflare Worker's `fetch` module export.
   * E.g. `export default router`.
   */
  fetch = (request: Request, env?: any, ctx?: any): Promise<Response> => {
    return this.#route(request.url, {
      request,
      waitUntil: ctx?.waitUntil?.bind(ctx) ?? ((_f: any) => { }),
      env,
      ctx,
    });
  }

  /**
   * Callback that is compatible with Deno's `serve` function.
   * E.g. `serve(router.serveCallback)`.
   */
  serveCallback = (request: Request, connInfo: any): Promise<Response> => {
    return this.#route(request.url, { request, waitUntil: (_f: any) => { }, connInfo });
  }

  // Provide types for error handler:
  addEventListener(
    type: 'error',
    listener: TypedEventListenerOrEventListenerObject<ErrorEvent> | null,
    options?: boolean | AddEventListenerOptions,
  ): void;
  addEventListener(...args: Parameters<EventTarget['addEventListener']>) {
    super.addEventListener(...args)
  }

  removeEventListener(
    type: 'error',
    listener: TypedEventListenerOrEventListenerObject<ErrorEvent> | null,
    options?: EventListenerOptions | boolean,
  ): void;
  removeEventListener(...args: Parameters<EventTarget['removeEventListener']>) {
    super.removeEventListener(...args)
  }
}

type TypedEventListener<E extends Event> = (evt: E) => void | Promise<void>;
type TypedEventListenerObject<E extends Event> = { handleEvent(evt: E): void | Promise<void>; }
type TypedEventListenerOrEventListenerObject<E extends Event> = TypedEventListener<E> | TypedEventListenerObject<E>;
