import type { RegexEngineCapabilities } from "../../../model/flavour";
import {
  PortableEngineAdapter,
  type PortableBridgeRequest,
  type PortableRegexBridge,
} from "../portable/PortableEngineAdapter";
import type { DotNetRegexExports } from "./dotnet-module";

export const DOTNET_FLAVOUR_VERSION = ".NET 10 System.Text.RegularExpressions";
export const DOTNET_ENGINE_VERSION = "10.0.10";

const MAXIMUM_NON_OUTPUT_RESPONSE_UTF16 = 16 * 1024 * 1024;
const BASE64_CHUNK_UTF16 = 32 * 1024;

const CAPABILITIES: RegexEngineCapabilities = {
  compilation: true,
  matching: true,
  replacement: true,
  namedCaptures: true,
  captureHistory: false,
  actualTrace: false,
  benchmark: false,
};

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

function base64Value(character: string): number {
  const code = character.charCodeAt(0);
  if (code >= 65 && code <= 90) return code - 65;
  if (code >= 97 && code <= 122) return code - 97 + 26;
  if (code >= 48 && code <= 57) return code - 48 + 52;
  if (character === "+") return 62;
  if (character === "/") return 63;
  return -1;
}

function isCanonicalBase64(encoded: string): boolean {
  if (encoded.length === 0) return true;
  if (encoded.length % 4 !== 0) return false;
  const padding = encoded.endsWith("==") ? 2 : encoded.endsWith("=") ? 1 : 0;
  const dataEnd = encoded.length - padding;
  for (let index = 0; index < dataEnd; index += 1) {
    if (base64Value(encoded[index] ?? "") < 0) return false;
  }
  if (padding === 2) {
    return (base64Value(encoded[dataEnd - 1] ?? "") & 0x0f) === 0;
  }
  if (padding === 1) {
    return (base64Value(encoded[dataEnd - 1] ?? "") & 0x03) === 0;
  }
  return true;
}

function decodeBoundedBase64Utf8(
  encoded: string,
  maximumBytes: number,
): string {
  if (
    encoded.length > Math.ceil(maximumBytes / 3) * 4 ||
    !isCanonicalBase64(encoded)
  ) {
    throw new Error("The .NET bridge returned invalid base64 output.");
  }
  const chunks: Uint8Array[] = [];
  let totalBytes = 0;
  for (let offset = 0; offset < encoded.length; offset += BASE64_CHUNK_UTF16) {
    const binary = atob(encoded.slice(offset, offset + BASE64_CHUNK_UTF16));
    const bytes = new Uint8Array(binary.length);
    for (let index = 0; index < binary.length; index += 1) {
      bytes[index] = binary.charCodeAt(index);
    }
    totalBytes += bytes.byteLength;
    if (totalBytes > maximumBytes) {
      throw new Error("The .NET bridge exceeded the replacement output limit.");
    }
    chunks.push(bytes);
  }
  const combined = new Uint8Array(totalBytes);
  let writeOffset = 0;
  for (const chunk of chunks) {
    combined.set(chunk, writeOffset);
    writeOffset += chunk.byteLength;
  }
  try {
    return new TextDecoder("utf-8", { fatal: true }).decode(combined);
  } catch {
    throw new Error("The .NET bridge returned non-UTF-8 replacement output.");
  }
}

function decodeReplacementEnvelope(
  value: unknown,
  requestedOutputBytes: number,
): unknown {
  if (requestedOutputBytes === 0 || !isRecord(value) || value.ok !== true) {
    return value;
  }
  if (
    typeof value.outputBase64 !== "string" ||
    !Number.isSafeInteger(value.outputBytes) ||
    Number(value.outputBytes) < 0 ||
    Number(value.outputBytes) > requestedOutputBytes ||
    Object.hasOwn(value, "output")
  ) {
    throw new Error("The .NET bridge returned invalid encoded output.");
  }
  const { outputBase64, ...envelope } = value;
  return {
    ...envelope,
    output: decodeBoundedBase64Utf8(outputBase64, Number(value.outputBytes)),
  };
}

class DotNetBridge implements PortableRegexBridge {
  readonly #exports: DotNetRegexExports;

  constructor(exports: DotNetRegexExports) {
    this.#exports = exports;
  }

  load(): Promise<Readonly<Record<string, unknown>>> {
    const identity = this.#exports.RegexBridge.Identity().split("|");
    return Promise.resolve({
      implementation: identity[0],
      runtime: identity[1],
      library: identity[2],
      offsetUnit: identity[3],
      culture: identity[4],
      replacementParity: identity[5],
    });
  }

  run(request: PortableBridgeRequest): Promise<unknown> {
    const encoded = this.#exports.RegexBridge.Run(JSON.stringify(request));
    const requestedOutputBytes =
      request.operation === "replace" && request.maximumOutputBytes
        ? request.maximumOutputBytes
        : 0;
    if (
      encoded.length === 0 ||
      encoded.length >
        MAXIMUM_NON_OUTPUT_RESPONSE_UTF16 +
          Math.ceil(requestedOutputBytes / 3) * 4
    ) {
      throw new Error("The .NET bridge result exceeds its bounded contract.");
    }
    let parsed: unknown;
    try {
      parsed = JSON.parse(encoded) as unknown;
    } catch {
      throw new Error("The .NET bridge returned invalid JSON.");
    }
    return Promise.resolve(
      decodeReplacementEnvelope(parsed, requestedOutputBytes),
    );
  }
}

export class DotNetEngineAdapter extends PortableEngineAdapter {
  constructor(exports: DotNetRegexExports, runtimeIdentity?: string) {
    super(new DotNetBridge(exports), {
      flavour: "dotnet",
      flavourLabel: ".NET Regex",
      expectedFlavourVersion: DOTNET_FLAVOUR_VERSION,
      engineName: "System.Text.RegularExpressions",
      engineVersion: DOTNET_ENGINE_VERSION,
      runtimeVersion: [
        ".NET browser WebAssembly",
        "CultureInvariant",
        runtimeIdentity,
      ]
        .filter(Boolean)
        .join(" · "),
      offsetUnit: "utf16",
      supportedFlags: "gimsnxrcb",
      capabilities: CAPABILITIES,
      rejectLoneSurrogates: true,
      validateIdentity: (identity) => {
        if (
          identity.implementation !== "dotnet-regex" ||
          identity.runtime !== DOTNET_ENGINE_VERSION ||
          identity.library !== "System.Text.RegularExpressions" ||
          identity.offsetUnit !== "utf16" ||
          identity.culture !== "culture-invariant" ||
          identity.replacementParity !== "replacement-parity"
        ) {
          throw new Error(
            "The bundled .NET runtime failed its identity self-test.",
          );
        }
      },
    });
  }
}
