import type {
  PortableBridgeRequest,
  PortableRegexBridge,
} from "../portable/PortableEngineAdapter";
import {
  decodeBase64OutputEnvelope,
  maximumBase64JsonUtf16,
} from "../portable/base64-output";
import {
  MAXIMUM_PORTABLE_REQUEST_JSON_BYTES,
  stringifyBoundedJsonRequest,
} from "../portable/json-request";

const INSTALLER_NAME = "__regexToolsInstallGoBridge";
const MAXIMUM_IDENTITY_JSON_UTF16 = 16 * 1024;
const MAXIMUM_NON_OUTPUT_RESULT_JSON_UTF16 = 16 * 1024 * 1024;

type GoCallback = (encoded?: string) => unknown;

interface GoRuntime {
  readonly importObject: WebAssembly.Imports;
  run(instance: WebAssembly.Instance): Promise<void>;
}

interface GoConstructor {
  new (): GoRuntime;
}

type GoGlobal = typeof globalThis & {
  Go?: GoConstructor;
  [INSTALLER_NAME]?: (identity: GoCallback, run: GoCallback) => void;
};

function parseJsonText(
  value: unknown,
  maximumUtf16: number,
  label: string,
): Readonly<Record<string, unknown>> {
  if (typeof value !== "string") {
    throw new Error(`${label} did not return JSON text.`);
  }
  if (value.length > maximumUtf16) {
    throw new Error(`${label} returned JSON beyond its bounded contract.`);
  }
  let parsed: unknown;
  try {
    parsed = JSON.parse(value);
  } catch {
    throw new Error(`${label} returned invalid JSON.`);
  }
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
    throw new Error(`${label} returned a non-object JSON value.`);
  }
  return parsed as Readonly<Record<string, unknown>>;
}

async function instantiate(
  source: URL | WebAssembly.Module,
  imports: WebAssembly.Imports,
): Promise<WebAssembly.Instance> {
  if (source instanceof WebAssembly.Module) {
    return WebAssembly.instantiate(source, imports);
  }
  const response = await fetch(source);
  if (!response.ok) {
    throw new Error(
      `Unable to load Go WebAssembly (${response.status} ${response.statusText}).`,
    );
  }
  try {
    return (await WebAssembly.instantiateStreaming(response.clone(), imports))
      .instance;
  } catch {
    return (
      await WebAssembly.instantiate(await response.arrayBuffer(), imports)
    ).instance;
  }
}

export class GoWasmBridge implements PortableRegexBridge {
  readonly #runtimeModuleUrl: URL;
  readonly #wasmSource: URL | WebAssembly.Module;
  #loadPromise: Promise<Readonly<Record<string, unknown>>> | undefined;
  #identityCallback: GoCallback | undefined;
  #runCallback: GoCallback | undefined;
  #runtimeFailure: Error | undefined;
  #terminated = false;

  constructor(runtimeModuleUrl: URL, wasmSource: URL | WebAssembly.Module) {
    this.#runtimeModuleUrl = runtimeModuleUrl;
    this.#wasmSource = wasmSource;
  }

  load(): Promise<Readonly<Record<string, unknown>>> {
    this.#loadPromise ??= this.#start();
    return this.#loadPromise;
  }

  async #start(): Promise<Readonly<Record<string, unknown>>> {
    if (this.#terminated) {
      throw new Error("The Go WebAssembly bridge has been terminated.");
    }
    await import(/* @vite-ignore */ this.#runtimeModuleUrl.href);
    const global = globalThis as GoGlobal;
    const Go = global.Go;
    if (typeof Go !== "function") {
      throw new Error("The pinned Go wasm_exec runtime did not install Go.");
    }

    let resolveInstalled:
      ((callbacks: readonly [GoCallback, GoCallback]) => void) | undefined;
    let rejectInstalled: ((error: Error) => void) | undefined;
    const installed = new Promise<readonly [GoCallback, GoCallback]>(
      (resolve, reject) => {
        resolveInstalled = resolve;
        rejectInstalled = reject;
      },
    );
    const previousInstaller = global[INSTALLER_NAME];
    global[INSTALLER_NAME] = (identity, run) => {
      if (typeof identity !== "function" || typeof run !== "function") {
        rejectInstalled?.(
          new Error("The Go module installed malformed bridge callbacks."),
        );
        return;
      }
      resolveInstalled?.([identity, run]);
    };

    try {
      const runtime = new Go();
      const instance = await instantiate(
        this.#wasmSource,
        runtime.importObject,
      );
      void runtime.run(instance).then(
        () => {
          this.#runtimeFailure = new Error(
            "The Go regexp WebAssembly runtime exited unexpectedly.",
          );
          rejectInstalled?.(this.#runtimeFailure);
        },
        (error: unknown) => {
          this.#runtimeFailure =
            error instanceof Error ? error : new Error(String(error));
          rejectInstalled?.(this.#runtimeFailure);
        },
      );
      const [identity, run] = await installed;
      this.#identityCallback = identity;
      this.#runCallback = run;
    } finally {
      if (previousInstaller === undefined) {
        delete global[INSTALLER_NAME];
      } else {
        global[INSTALLER_NAME] = previousInstaller;
      }
    }

    return parseJsonText(
      this.#identityCallback(),
      MAXIMUM_IDENTITY_JSON_UTF16,
      "The Go identity callback",
    );
  }

  async run(
    request: PortableBridgeRequest,
  ): Promise<Readonly<Record<string, unknown>>> {
    await this.load();
    if (this.#terminated) {
      throw new Error("The Go WebAssembly bridge has been terminated.");
    }
    if (this.#runtimeFailure) throw this.#runtimeFailure;
    const callback = this.#runCallback;
    if (!callback) {
      throw new Error("The Go WebAssembly bridge is not loaded.");
    }
    const maximumOutputBytes =
      request.operation === "replace"
        ? (request.maximumOutputBytes ?? 0)
        : undefined;
    const label = "The Go run callback";
    const encodedRequest = stringifyBoundedJsonRequest(
      request,
      MAXIMUM_PORTABLE_REQUEST_JSON_BYTES,
      label,
    );
    const parsed = parseJsonText(
      callback(encodedRequest),
      maximumBase64JsonUtf16(
        MAXIMUM_NON_OUTPUT_RESULT_JSON_UTF16,
        maximumOutputBytes ?? 0,
      ),
      label,
    );
    return decodeBase64OutputEnvelope(parsed, maximumOutputBytes, label);
  }

  terminate(): void {
    this.#terminated = true;
    this.#identityCallback = undefined;
    this.#runCallback = undefined;
  }
}
