import PHP_BRIDGE_SOURCE from "../../../../../engines/php/bridge.php?raw";
import {
  PORTABLE_BRIDGE_ABI_VERSION,
  type PortableBridgeRequest,
  type PortableRegexBridge,
} from "../portable/PortableEngineAdapter";
import {
  decodeBase64OutputEnvelope,
  maximumBase64JsonUtf16,
} from "../portable/base64-output";
import type { PhpRuntime } from "./php-runtime";

const ROOT = "/regex-tools";
const BRIDGE_PATH = `${ROOT}/bridge.php`;
const REQUEST_PATH = `${ROOT}/request.json`;
const MAXIMUM_NON_OUTPUT_RESPONSE_UTF16 = 16 * 1024 * 1024;
const MAXIMUM_RUNTIME_ERROR_UTF16 = 2 * 1024;

function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

export class PhpWasmBridge implements PortableRegexBridge {
  readonly #php: PhpRuntime;
  #initialized = false;
  #terminated = false;
  #queue: Promise<void> = Promise.resolve();

  constructor(php: PhpRuntime) {
    this.#php = php;
  }

  load(): Promise<Readonly<Record<string, unknown>>> {
    return this.#enqueue(async () => {
      const result = await this.#invoke({
        abi: PORTABLE_BRIDGE_ABI_VERSION,
        operation: "identity",
      });
      if (!isRecord(result)) {
        throw new Error("The PHP identity bridge returned a non-object.");
      }
      return result;
    });
  }

  run(request: PortableBridgeRequest): Promise<unknown> {
    return this.#enqueue(() => this.#invoke(request));
  }

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

  #enqueue<T>(operation: () => Promise<T>): Promise<T> {
    const result = this.#queue.then(operation, operation);
    this.#queue = result.then(
      () => undefined,
      () => undefined,
    );
    return result;
  }

  #initialize(): void {
    if (this.#initialized) return;
    if (!this.#php.isDir(ROOT)) {
      this.#php.mkdir(ROOT);
    }
    this.#php.writeFile(BRIDGE_PATH, PHP_BRIDGE_SOURCE);
    this.#initialized = true;
  }

  async #invoke(request: object): Promise<unknown> {
    if (this.#terminated) {
      throw new Error("The PHP WebAssembly bridge has been terminated.");
    }
    this.#initialize();
    this.#php.writeFile(REQUEST_PATH, JSON.stringify(request));
    const response = await this.#php.run({ scriptPath: BRIDGE_PATH });
    if (this.#terminated) {
      throw new Error("The PHP WebAssembly bridge has been terminated.");
    }
    if (response.exitCode !== 0) {
      throw new Error(
        `The PHP runtime exited with status ${response.exitCode}.`,
      );
    }
    if (response.errors.length > 0) {
      throw new Error(
        `The PHP runtime wrote to stderr: ${response.errors.slice(0, MAXIMUM_RUNTIME_ERROR_UTF16)}`,
      );
    }
    const encoded = response.text;
    const requestedOutputBytes =
      isRecord(request) &&
      request.operation === "replace" &&
      Number.isSafeInteger(request.maximumOutputBytes) &&
      Number(request.maximumOutputBytes) > 0
        ? Number(request.maximumOutputBytes)
        : undefined;
    if (
      encoded.length === 0 ||
      encoded.length >
        maximumBase64JsonUtf16(
          MAXIMUM_NON_OUTPUT_RESPONSE_UTF16,
          requestedOutputBytes ?? 0,
        )
    ) {
      throw new Error("The PHP bridge response size is invalid.");
    }
    let parsed: unknown;
    try {
      parsed = JSON.parse(encoded);
    } catch {
      throw new Error("The PHP bridge returned malformed JSON.");
    }
    return isRecord(parsed)
      ? decodeBase64OutputEnvelope(
          parsed,
          requestedOutputBytes,
          "The PHP bridge",
        )
      : parsed;
  }
}
