/// <reference lib="webworker" />
import {
  DotNetEngineAdapter,
  DOTNET_ENGINE_VERSION,
} from "../regex/execution/adapters/dotnet/DotNetEngineAdapter";
import { importDotNetModule } from "../regex/execution/adapters/dotnet/dotnet-module";
import {
  WORKER_PROTOCOL_VERSION,
  workerError,
  type EngineWorkerOperation,
  type EngineWorkerResult,
  type WorkerRequest,
  type WorkerResponse,
} from "../regex/execution/worker-protocol";
import { workerStartupReady } from "../regex/execution/StartupBufferedWorker";

const scope = self as DedicatedWorkerGlobalScope;

function applicationRoot(): URL {
  const worker = new URL(scope.location.href);
  const assets = worker.pathname.lastIndexOf("/assets/");
  if (assets >= 0) {
    worker.pathname = worker.pathname.slice(0, assets + 1);
  } else {
    const sourceWorker = worker.pathname.lastIndexOf("/src/workers/");
    worker.pathname =
      sourceWorker >= 0 ? worker.pathname.slice(0, sourceWorker + 1) : "/";
  }
  worker.search = "";
  worker.hash = "";
  return worker;
}

async function loadAdapter(): Promise<DotNetEngineAdapter> {
  const moduleUrl = new URL(
    "engines/dotnet/_framework/dotnet.js",
    applicationRoot(),
  );
  const imported = await importDotNetModule(moduleUrl);
  const runtime = await imported.dotnet.withDiagnosticTracing(false).create();
  const config = runtime.getConfig();
  const exports = await runtime.getAssemblyExports(config.mainAssemblyName);
  // This is a browser reactor: JSExport methods are ready after assembly
  // loading. runMain() enters the runtime keep-alive lifecycle and therefore
  // must not be awaited by the worker's cold-start path.
  const adapter = new DotNetEngineAdapter(
    exports,
    `${config.mainAssemblyName} · ${scope.navigator.userAgent}`,
  );
  return adapter;
}

const startup:
  { readonly adapter: DotNetEngineAdapter } | { readonly error: unknown } =
  await loadAdapter().then(
    (adapter) => ({ adapter }),
    (error: unknown) => ({ error }),
  );

async function handleRequest(
  request: WorkerRequest<EngineWorkerOperation>,
): Promise<void> {
  if (request.protocolVersion !== WORKER_PROTOCOL_VERSION) return;
  let response: WorkerResponse<EngineWorkerResult>;
  try {
    if ("error" in startup) throw startup.error;
    const { adapter } = startup;
    const payload: EngineWorkerResult =
      request.payload.kind === "load"
        ? {
            kind: "load",
            info: await adapter.load().then((info) => {
              if (info.engineVersion !== DOTNET_ENGINE_VERSION) {
                throw new Error(
                  "Loaded .NET runtime identity does not match the adapter.",
                );
              }
              return info;
            }),
          }
        : request.payload.kind === "execute"
          ? {
              kind: "execute",
              result: await adapter.execute(request.payload.request),
            }
          : {
              kind: "replace",
              result: await adapter.replace(request.payload.request),
            };
    response = {
      protocolVersion: WORKER_PROTOCOL_VERSION,
      requestId: request.requestId,
      generation: request.generation,
      ok: true,
      payload,
    };
  } catch (error) {
    response = {
      protocolVersion: WORKER_PROTOCOL_VERSION,
      requestId: request.requestId,
      generation: request.generation,
      ok: false,
      error: workerError(error),
    };
  }
  scope.postMessage(response);
}

scope.onmessage = (
  event: MessageEvent<WorkerRequest<EngineWorkerOperation>>,
) => {
  void handleRequest(event.data);
};
scope.postMessage(workerStartupReady());
