import type { RegexEngineAdapter } from "../../RegexEngineAdapter";
import { DEFAULT_REGEX_LIMITS, utf8ByteLength } from "../../request-limits";
import type { RegexDiagnostic } from "../../../model/diagnostics";
import type { RegexEngineInfo } from "../../../model/flavour";
import type {
  CaptureResult,
  RegexExecutionFlags,
  RegexExecutionRequest,
  RegexExecutionResult,
  RegexMatchResult,
  RegexReplacementRequest,
  RegexReplacementResult,
} from "../../../model/match";
import type { TeaVmJavaRegexModule } from "./java-module";
import { APPLICATION_VERSION } from "../../../../version";

export const JAVA_ADAPTER_VERSION = APPLICATION_VERSION;
export const JAVA_ENGINE_VERSION = "0.15.0";
export const JAVA_ENGINE_IDENTITY = "TeaVM java.util.regex 0.15.0";
export const JAVA_FLAVOUR_VERSION = JAVA_ENGINE_IDENTITY;
export const JAVA_BRIDGE_ABI_VERSION = 1;
const JAVA_COMPATIBLE_FLAVOUR_VERSIONS = new Set([
  JAVA_FLAVOUR_VERSION,
  "Java Pattern",
  "TeaVM 0.15.0",
]);

const FLAG_ORDER = "gidmsuxU";
const CAPTURE_VALUE_PREVIEW_UTF16 = 64 * 1024;
const TOTAL_VALUE_PREVIEW_UTF16 = 8 * 1024 * 1024;

const FLAG_BITS = {
  i: 1 << 0,
  m: 1 << 1,
  s: 1 << 2,
  u: 1 << 3,
  x: 1 << 4,
  U: 1 << 5,
  d: 1 << 6,
} as const;

interface BridgeFailure {
  readonly status:
    | "bridge-error"
    | "compile-error"
    | "execution-error"
    | "limit-error"
    | "replacement-error";
  readonly name: string;
  readonly message: string;
  readonly index: number;
}

interface RawCapture {
  readonly start: number;
  readonly end: number;
}

interface RawMatch {
  readonly start: number;
  readonly end: number;
  readonly captures: readonly RawCapture[];
}

interface BridgeSuccess {
  readonly status: "ok";
  readonly groupCount: number;
  readonly resultsTruncated: boolean;
  readonly matches: readonly RawMatch[];
  readonly outputHardTruncated?: boolean;
  readonly output?: string;
}

type BridgeResult = BridgeFailure | BridgeSuccess;

class FieldReader {
  readonly #encoded: string;
  #cursor = 0;

  constructor(encoded: string) {
    this.#encoded = encoded;
  }

  field(): string {
    const colon = this.#encoded.indexOf(":", this.#cursor);
    if (colon < 0) {
      throw new Error("TeaVM Java bridge returned a malformed field length.");
    }
    const lengthText = this.#encoded.slice(this.#cursor, colon);
    if (!/^(?:0|[1-9][0-9]*)$/u.test(lengthText)) {
      throw new Error("TeaVM Java bridge returned an invalid field length.");
    }
    const length = Number.parseInt(lengthText, 10);
    const start = colon + 1;
    const end = start + length;
    if (!Number.isSafeInteger(length) || end > this.#encoded.length) {
      throw new Error(
        "TeaVM Java bridge returned a field outside its payload.",
      );
    }
    this.#cursor = end;
    return this.#encoded.slice(start, end);
  }

  integer(minimum: number, maximum: number): number {
    const value = this.field();
    if (!/^-?(?:0|[1-9][0-9]*)$/u.test(value)) {
      throw new Error("TeaVM Java bridge returned an invalid integer.");
    }
    const parsed = Number.parseInt(value, 10);
    if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum) {
      throw new Error("TeaVM Java bridge returned an out-of-range integer.");
    }
    return parsed;
  }

  boolean(): boolean {
    const value = this.field();
    if (value !== "0" && value !== "1") {
      throw new Error("TeaVM Java bridge returned an invalid boolean.");
    }
    return value === "1";
  }

  finish(): void {
    if (this.#cursor !== this.#encoded.length) {
      throw new Error("TeaVM Java bridge returned trailing payload data.");
    }
  }
}

class ValuePreviewBudget {
  #remaining = TOTAL_VALUE_PREVIEW_UTF16;
  truncated = false;

  take(value: string): { readonly value: string; readonly truncated: boolean } {
    const maximum = Math.min(CAPTURE_VALUE_PREVIEW_UTF16, this.#remaining);
    const preview = value.slice(0, maximum);
    this.#remaining -= preview.length;
    const truncated = preview.length !== value.length;
    this.truncated ||= truncated;
    return { value: preview, truncated };
  }
}

export function javaEngineInfo(runtimeVersion?: string): RegexEngineInfo {
  return {
    flavour: "java",
    adapterVersion: JAVA_ADAPTER_VERSION,
    engineName: "TeaVM java.util.regex",
    engineVersion: JAVA_ENGINE_VERSION,
    runtimeVersion: runtimeVersion ?? "TeaVM JavaScript · not OpenJDK",
    offsetUnit: "utf16",
    capabilities: {
      compilation: true,
      matching: true,
      replacement: true,
      namedCaptures: true,
      captureHistory: false,
      actualTrace: false,
      benchmark: false,
    },
  };
}

export function normalizedJavaFlags(
  userFlags: readonly string[],
  scanAll: boolean,
): RegexExecutionFlags {
  if (
    new Set(userFlags).size !== userFlags.length ||
    userFlags.some((flag) => flag.length !== 1 || !FLAG_ORDER.includes(flag))
  ) {
    throw new RangeError("The request contains unsupported Java flags.");
  }
  const selected = new Set(userFlags);
  const user = [...FLAG_ORDER].filter((flag) => selected.has(flag)).join("");
  const internallyAddedGlobalFlag = scanAll && !selected.has("g");
  if (internallyAddedGlobalFlag) selected.add("g");
  if (selected.has("U")) selected.add("u");
  return {
    userFlags: user,
    effectiveFlags: [...FLAG_ORDER]
      .filter((flag) => selected.has(flag))
      .join(""),
    internallyAddedIndicesFlag: false,
    internallyAddedGlobalFlag,
  };
}

export function javaApplicationFlags(flags: RegexExecutionFlags): number {
  let result = 0;
  for (const flag of flags.effectiveFlags) {
    result |= FLAG_BITS[flag as keyof typeof FLAG_BITS] ?? 0;
  }
  return result;
}

function validateRequest(
  request: RegexExecutionRequest,
  replacement?: RegexReplacementRequest,
): void {
  if (request.flavour !== "java") {
    throw new Error(`Java adapter cannot execute ${request.flavour}.`);
  }
  if (
    request.flavourVersion !== undefined &&
    !JAVA_COMPATIBLE_FLAVOUR_VERSIONS.has(request.flavourVersion)
  ) {
    throw new RangeError(
      `Java version ${JSON.stringify(request.flavourVersion)} is unsupported by this engine.`,
    );
  }
  if (request.options && Object.keys(request.options).length > 0) {
    throw new RangeError("The Java engine does not accept engine options.");
  }
  if (request.pattern.length > DEFAULT_REGEX_LIMITS.patternHardLengthUtf16) {
    throw new RangeError(
      `Java patterns are limited to ${DEFAULT_REGEX_LIMITS.patternHardLengthUtf16.toLocaleString()} UTF-16 units.`,
    );
  }
  if (
    utf8ByteLength(request.subject) >
    DEFAULT_REGEX_LIMITS.interactiveSubjectHardBytes
  ) {
    throw new RangeError("Java subjects are limited to 16 MiB of UTF-8.");
  }
  if (
    !Number.isSafeInteger(request.maximumMatches) ||
    request.maximumMatches < 1 ||
    request.maximumMatches > DEFAULT_REGEX_LIMITS.maximumMatches ||
    !Number.isSafeInteger(request.maximumCaptureRows) ||
    request.maximumCaptureRows < 1 ||
    request.maximumCaptureRows > DEFAULT_REGEX_LIMITS.maximumCaptureRows
  ) {
    throw new RangeError(
      "Java result limits are outside the bounded contract.",
    );
  }
  if (
    replacement &&
    (replacement.replacement.length >
      DEFAULT_REGEX_LIMITS.maximumReplacementTemplateUtf16 ||
      !Number.isSafeInteger(replacement.maximumOutputBytes) ||
      replacement.maximumOutputBytes < 1 ||
      replacement.maximumOutputBytes >
        DEFAULT_REGEX_LIMITS.maximumReplacementOutputBytes)
  ) {
    throw new RangeError(
      "Java replacement or output limits are outside the bounded contract.",
    );
  }
}

function decodeBridgeResult(
  encoded: string,
  patternLength: number,
  subjectLength: number,
  maximumMatches: number,
  replacement: boolean,
): BridgeResult {
  const reader = new FieldReader(encoded);
  if (reader.field() !== "1") {
    throw new Error("TeaVM Java bridge ABI payload version is unsupported.");
  }
  const status = reader.field();
  if (status !== "ok") {
    if (
      status !== "bridge-error" &&
      status !== "compile-error" &&
      status !== "execution-error" &&
      status !== "limit-error" &&
      status !== "replacement-error"
    ) {
      throw new Error("TeaVM Java bridge returned an unknown failure status.");
    }
    const failure: BridgeFailure = {
      status,
      name: reader.field(),
      message: reader.field(),
      index: reader.integer(-1, patternLength),
    };
    reader.finish();
    return failure;
  }

  const groupCount = reader.integer(
    0,
    DEFAULT_REGEX_LIMITS.maximumCaptureGroups,
  );
  const resultsTruncated = reader.boolean();
  const matchCount = reader.integer(0, maximumMatches);
  const matches: RawMatch[] = [];
  for (let matchNumber = 0; matchNumber < matchCount; matchNumber += 1) {
    const start = reader.integer(0, subjectLength);
    const end = reader.integer(start, subjectLength);
    const captures: RawCapture[] = [];
    for (let group = 1; group <= groupCount; group += 1) {
      const captureStart = reader.integer(-1, subjectLength);
      const captureEnd = reader.integer(-1, subjectLength);
      if (
        (captureStart === -1) !== (captureEnd === -1) ||
        (captureStart >= 0 && captureEnd < captureStart)
      ) {
        throw new Error(
          "TeaVM Java bridge returned inconsistent capture offsets.",
        );
      }
      captures.push({ start: captureStart, end: captureEnd });
    }
    matches.push({ start, end, captures });
  }

  const result: BridgeSuccess = replacement
    ? {
        status: "ok",
        groupCount,
        resultsTruncated,
        matches,
        outputHardTruncated: reader.boolean(),
        output: reader.field(),
      }
    : {
        status: "ok",
        groupCount,
        resultsTruncated,
        matches,
      };
  reader.finish();
  return result;
}

function bridgeFailureDiagnostic(
  failure: BridgeFailure,
  request: RegexExecutionRequest,
): RegexDiagnostic {
  const code =
    failure.status === "compile-error"
      ? "compile-error"
      : failure.status === "replacement-error"
        ? "replacement-error"
        : failure.status === "limit-error"
          ? "resource-limit"
          : "execution-error";
  const index = failure.index;
  return {
    id: `java-engine-${failure.status}`,
    source: "execution-engine",
    severity: "error",
    code,
    message: failure.message
      ? `${failure.name}: ${failure.message}`
      : failure.name,
    ...(index >= 0 && index <= request.pattern.length
      ? {
          range: {
            startUtf16: index,
            endUtf16: Math.min(request.pattern.length, index + 1),
          },
        }
      : {}),
    flavour: "java",
    provenance: "reported",
  };
}

function emptyExecution(
  info: RegexEngineInfo,
  flags: RegexExecutionFlags,
  start: number,
  diagnostic: RegexDiagnostic,
): RegexExecutionResult {
  return {
    accepted: false,
    engine: info,
    flags,
    matches: [],
    diagnostics: [diagnostic],
    elapsedMs: performance.now() - start,
    truncated: false,
  };
}

function materializeMatches(
  raw: BridgeSuccess,
  request: RegexExecutionRequest,
): {
  readonly matches: readonly RegexMatchResult[];
  readonly valuesTruncated: boolean;
} {
  const names = new Map(
    request.captureMetadata
      .filter(
        (capture) => capture.number >= 1 && capture.number <= raw.groupCount,
      )
      .map((capture) => [capture.number, capture.name]),
  );
  const previews = new ValuePreviewBudget();
  const matches = raw.matches.map((match, matchIndex) => {
    const value = request.subject.slice(match.start, match.end);
    const matchPreview = previews.take(value);
    const captures: CaptureResult[] = match.captures.map(
      (capture, captureIndex) => {
        const groupNumber = captureIndex + 1;
        const groupName = names.get(groupNumber);
        if (capture.start < 0) {
          return {
            groupNumber,
            ...(groupName ? { groupName } : {}),
            status: "did-not-participate" as const,
          };
        }
        const captureValue = request.subject.slice(capture.start, capture.end);
        const preview = previews.take(captureValue);
        return {
          groupNumber,
          ...(groupName ? { groupName } : {}),
          value: preview.value,
          status: preview.truncated
            ? ("truncated" as const)
            : capture.start === capture.end
              ? ("matched-empty" as const)
              : ("participated" as const),
          range: {
            startUtf16: capture.start,
            endUtf16: capture.end,
          },
          nativeRange: {
            start: capture.start,
            end: capture.end,
            unit: "utf16" as const,
          },
        };
      },
    );
    return {
      matchNumber: matchIndex + 1,
      value: matchPreview.value,
      valueStatus: matchPreview.truncated
        ? ("truncated" as const)
        : ("complete" as const),
      range: { startUtf16: match.start, endUtf16: match.end },
      nativeRange: {
        start: match.start,
        end: match.end,
        unit: "utf16" as const,
      },
      captures,
    };
  });
  return { matches, valuesTruncated: previews.truncated };
}

function compatibilityDiagnostics(
  flags: RegexExecutionFlags,
): RegexDiagnostic[] {
  return flags.effectiveFlags.includes("U")
    ? [
        {
          id: "java-teavm-unicode-character-class-compatibility",
          source: "execution-engine",
          severity: "warning",
          code: "engine-compatibility",
          message:
            "TeaVM 0.15.0 does not implement OpenJDK Pattern.UNICODE_CHARACTER_CLASS. U implies Unicode case folding here, while predefined classes and word boundaries retain TeaVM class-library behaviour.",
          flavour: "java",
          provenance: "reported",
        },
      ]
    : [];
}

function utf8Prefix(
  value: string,
  maximumBytes: number,
): {
  readonly value: string;
  readonly bytes: number;
  readonly complete: boolean;
} {
  let bytes = 0;
  let index = 0;
  while (index < value.length) {
    const first = value.charCodeAt(index);
    let width: number;
    let units = 1;
    if (first <= 0x7f) {
      width = 1;
    } else if (first <= 0x7ff) {
      width = 2;
    } else if (
      first >= 0xd800 &&
      first <= 0xdbff &&
      index + 1 < value.length &&
      value.charCodeAt(index + 1) >= 0xdc00 &&
      value.charCodeAt(index + 1) <= 0xdfff
    ) {
      width = 4;
      units = 2;
    } else {
      width = 3;
    }
    if (bytes + width > maximumBytes) break;
    bytes += width;
    index += units;
  }
  return {
    value: value.slice(0, index),
    bytes,
    complete: index === value.length,
  };
}

export class JavaEngineAdapter implements RegexEngineAdapter {
  readonly flavour = "java" as const;
  readonly #module: TeaVmJavaRegexModule;
  readonly #info: RegexEngineInfo;

  constructor(module: TeaVmJavaRegexModule, runtimeVersion?: string) {
    this.#module = module;
    this.#info = javaEngineInfo(runtimeVersion);
  }

  async load(): Promise<RegexEngineInfo> {
    if (
      this.#module.bridgeAbiVersion() !== JAVA_BRIDGE_ABI_VERSION ||
      this.#module.engineIdentity() !== JAVA_ENGINE_IDENTITY ||
      this.#module.selfTest() !== 0
    ) {
      throw new Error(
        "The bundled TeaVM Java regex engine failed its identity or bridge self-test.",
      );
    }
    return this.#info;
  }

  async execute(request: RegexExecutionRequest): Promise<RegexExecutionResult> {
    const result = this.#run(request);
    if (!("execution" in result)) return result;
    throw new Error("Java execution returned a replacement result.");
  }

  async replace(
    request: RegexReplacementRequest,
  ): Promise<RegexReplacementResult> {
    const result = this.#run(request, request);
    if ("execution" in result) return result;
    throw new Error("Java replacement returned an execution-only result.");
  }

  #run(
    request: RegexExecutionRequest,
    replacementRequest?: RegexReplacementRequest,
  ): RegexExecutionResult | RegexReplacementResult {
    const start = performance.now();
    validateRequest(request, replacementRequest);
    const flags = normalizedJavaFlags(request.flags, request.scanAll);
    const applicationFlags = javaApplicationFlags(flags);
    const iterate = flags.effectiveFlags.includes("g");
    const encoded = replacementRequest
      ? this.#module.replace(
          request.pattern,
          request.subject,
          replacementRequest.replacement,
          applicationFlags,
          iterate,
          request.maximumMatches,
          request.maximumCaptureRows,
        )
      : this.#module.execute(
          request.pattern,
          request.subject,
          applicationFlags,
          iterate,
          request.maximumMatches,
          request.maximumCaptureRows,
        );
    const bridge = decodeBridgeResult(
      encoded,
      request.pattern.length,
      request.subject.length,
      request.maximumMatches,
      replacementRequest !== undefined,
    );
    if (bridge.status !== "ok") {
      const execution = emptyExecution(
        this.#info,
        flags,
        start,
        bridgeFailureDiagnostic(bridge, request),
      );
      return replacementRequest
        ? {
            execution,
            output: "",
            outputBytes: 0,
            outputTruncated: false,
            truncated: false,
          }
        : execution;
    }

    const materialized = materializeMatches(bridge, request);
    const diagnostics = compatibilityDiagnostics(flags);
    if (bridge.resultsTruncated) {
      diagnostics.push({
        id: "java-results-truncated",
        source: "execution-engine",
        severity: "warning",
        code: "result-limit",
        message: `Results reached the configured limit (${request.maximumMatches.toLocaleString()} matches or ${request.maximumCaptureRows.toLocaleString()} capture rows).`,
        flavour: "java",
        provenance: "derived",
      });
    }
    if (materialized.valuesTruncated) {
      diagnostics.push({
        id: "java-value-previews-truncated",
        source: "execution-engine",
        severity: "warning",
        code: "result-value-preview-limit",
        message:
          "Displayed TeaVM Java result values were clipped to bounded previews; exact UTF-16 ranges are retained.",
        flavour: "java",
        provenance: "derived",
      });
    }
    const execution: RegexExecutionResult = {
      accepted: true,
      engine: this.#info,
      flags,
      matches: materialized.matches,
      diagnostics,
      elapsedMs: performance.now() - start,
      truncated: bridge.resultsTruncated,
    };
    if (!replacementRequest) return execution;

    const bounded = utf8Prefix(
      bridge.output ?? "",
      replacementRequest.maximumOutputBytes,
    );
    const outputTruncated =
      bridge.outputHardTruncated === true || !bounded.complete;
    const executionWithOutputDiagnostic: RegexExecutionResult = outputTruncated
      ? {
          ...execution,
          diagnostics: [
            ...execution.diagnostics,
            {
              id: "java-output-truncated",
              source: "execution-engine",
              severity: "warning",
              code: "replacement-output-limit",
              message: `Replacement preview was truncated at ${replacementRequest.maximumOutputBytes.toLocaleString()} UTF-8 bytes or the bounded TeaVM bridge cap.`,
              flavour: "java",
              provenance: "derived",
            },
          ],
        }
      : execution;
    return {
      execution: executionWithOutputDiagnostic,
      output: bounded.value,
      outputBytes: bounded.bytes,
      outputTruncated,
      truncated: outputTruncated || bridge.resultsTruncated,
    };
  }

  terminate(): void {
    // Lifecycle termination is performed by killing the containing worker.
  }
}
