import type {
  PortableBridgeRequest,
  PortableRegexBridge,
} from "../portable/PortableEngineAdapter";

const BRIDGE_PROTOCOL_VERSION = 1;

type BridgeResponse =
  | {
      readonly protocolVersion: typeof BRIDGE_PROTOCOL_VERSION;
      readonly requestId: number;
      readonly ok: true;
      readonly payload: unknown;
    }
  | {
      readonly protocolVersion: typeof BRIDGE_PROTOCOL_VERSION;
      readonly requestId: number;
      readonly ok: false;
      readonly error: {
        readonly name: string;
        readonly message: string;
      };
    };

interface PendingRequest {
  readonly resolve: (value: Readonly<Record<string, unknown>>) => void;
  readonly reject: (error: Error) => void;
}

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

function requirePayload(value: unknown): Readonly<Record<string, unknown>> {
  if (!isRecord(value)) {
    throw new Error("The WebPerl bridge returned a non-object payload.");
  }
  return value;
}

function requireResponse(value: unknown): BridgeResponse {
  if (
    !isRecord(value) ||
    value.protocolVersion !== BRIDGE_PROTOCOL_VERSION ||
    !Number.isSafeInteger(value.requestId) ||
    typeof value.ok !== "boolean"
  ) {
    throw new Error("The WebPerl bridge returned an invalid response.");
  }
  if (value.ok) {
    return value as BridgeResponse;
  }
  if (
    !isRecord(value.error) ||
    typeof value.error.name !== "string" ||
    typeof value.error.message !== "string"
  ) {
    throw new Error("The WebPerl bridge returned an invalid error.");
  }
  return value as BridgeResponse;
}

export class WebPerlWorkerBridge implements PortableRegexBridge {
  readonly #worker: Worker;
  readonly #pending = new Map<number, PendingRequest>();
  #requestId = 0;
  #loadPromise: Promise<Readonly<Record<string, unknown>>> | undefined;
  #terminated = false;

  constructor(workerUrl: URL) {
    this.#worker = new Worker(workerUrl, {
      type: "classic",
      name: "regex-tools-webperl-runtime",
    });
    this.#worker.onmessage = (event: MessageEvent<unknown>) => {
      let response: BridgeResponse;
      try {
        response = requireResponse(event.data);
      } catch (error) {
        this.#failAll(error);
        return;
      }
      const pending = this.#pending.get(response.requestId);
      if (!pending) return;
      this.#pending.delete(response.requestId);
      if (response.ok) {
        try {
          pending.resolve(requirePayload(response.payload));
        } catch (error) {
          pending.reject(
            error instanceof Error ? error : new Error(String(error)),
          );
        }
      } else {
        const failure = new Error(response.error.message);
        failure.name = response.error.name;
        pending.reject(failure);
      }
    };
    this.#worker.onerror = (event: ErrorEvent) => {
      this.#failAll(
        new Error(
          event.message || "The WebPerl classic worker failed unexpectedly.",
        ),
      );
    };
    this.#worker.onmessageerror = () => {
      this.#failAll(
        new Error("The WebPerl classic worker returned uncloneable data."),
      );
    };
  }

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

  async run(
    request: PortableBridgeRequest,
  ): Promise<Readonly<Record<string, unknown>>> {
    await this.load();
    return this.#send("run", request);
  }

  #send(
    kind: "load" | "run",
    request?: PortableBridgeRequest,
  ): Promise<Readonly<Record<string, unknown>>> {
    if (this.#terminated) {
      return Promise.reject(
        new Error("The WebPerl bridge has been terminated."),
      );
    }
    this.#requestId += 1;
    const requestId = this.#requestId;
    const result = new Promise<Readonly<Record<string, unknown>>>(
      (resolve, reject) => {
        this.#pending.set(requestId, { resolve, reject });
      },
    );
    this.#worker.postMessage({
      protocolVersion: BRIDGE_PROTOCOL_VERSION,
      requestId,
      kind,
      ...(request === undefined ? {} : { request }),
    });
    return result;
  }

  #failAll(error: unknown): void {
    const failure = error instanceof Error ? error : new Error(String(error));
    for (const pending of this.#pending.values()) {
      pending.reject(failure);
    }
    this.#pending.clear();
  }

  terminate(): void {
    if (this.#terminated) return;
    this.#terminated = true;
    this.#worker.terminate();
    this.#failAll(new Error("The WebPerl bridge has been terminated."));
  }
}
