import {
  AVAILABLE_REGEX_FLAVOURS,
  parseRegexFlags,
  parseRegexOptions,
  resolveRegexFlavourVersion,
} from "../flavours/flavour-registry";
import type {
  RegexExecutionRequest,
  RegexReplacementRequest,
} from "../model/match";
import type {
  RegexSyntaxRequest,
  RegexSyntaxResult,
  ReplacementSyntaxResult,
} from "../model/syntax";
import {
  DEFAULT_REGEX_LIMITS,
  utf8ByteLength,
} from "../execution/request-limits";
import { EngineSupervisor } from "../execution/EngineSupervisor";
import { SyntaxSupervisor } from "../execution/SyntaxSupervisor";
import { WorkerRequestError } from "../execution/WorkerSupervisor";
import { compareCompletedResults } from "./compare-results";
import type {
  ComparisonExecutionOutcome,
  ComparisonFlavour,
  ComparisonSideInput,
  ComparisonSideOutcome,
  ComparisonSyntaxOutcome,
  ComparisonTaskFailure,
  RegexComparisonRequest,
  RegexComparisonResult,
} from "./comparison.types";

export interface ComparisonSyntaxRunner {
  parsePattern(
    request: RegexSyntaxRequest,
    timeoutMs?: number,
  ): Promise<RegexSyntaxResult>;
  parseReplacement(
    request: {
      readonly flavour: ComparisonFlavour;
      readonly replacement: string;
      readonly captureMetadata: RegexSyntaxResult["captures"];
    },
    timeoutMs?: number,
  ): Promise<ReplacementSyntaxResult>;
  cancel(): void;
  dispose(): void;
}

export interface ComparisonEngineRunner {
  execute(
    request: RegexExecutionRequest,
    timeoutMs: number,
  ): ReturnType<EngineSupervisor["execute"]>;
  replace(
    request: RegexReplacementRequest,
    timeoutMs: number,
  ): ReturnType<EngineSupervisor["replace"]>;
  cancel(): void;
  dispose(): void;
}

export interface ComparisonOrchestratorDependencies {
  readonly createSyntaxRunner: (
    flavour: ComparisonFlavour,
  ) => ComparisonSyntaxRunner;
  readonly createEngineRunner: () => ComparisonEngineRunner;
  readonly now: () => number;
  readonly isoNow: () => string;
}

const DEFAULT_DEPENDENCIES: ComparisonOrchestratorDependencies = {
  createSyntaxRunner: (flavour) =>
    new SyntaxSupervisor(
      `${flavour} comparison syntax`,
      `regex-tools-comparison-${flavour}-syntax`,
    ),
  createEngineRunner: () => new EngineSupervisor(),
  now: () => performance.now(),
  isoNow: () => new Date().toISOString(),
};

interface PreparedSide {
  readonly input: ComparisonSideInput;
  readonly syntaxRequest: RegexSyntaxRequest;
}

function requireBoundedInteger(
  value: number,
  label: string,
  minimum: number,
  maximum: number,
): number {
  if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
    throw new RangeError(
      `${label} must be an integer from ${minimum.toLocaleString()} to ${maximum.toLocaleString()}.`,
    );
  }
  return value;
}

function validateSide(input: ComparisonSideInput): PreparedSide {
  const definition = AVAILABLE_REGEX_FLAVOURS.require(input.flavour);
  const version = resolveRegexFlavourVersion(
    input.flavourVersion,
    definition,
    `${definition.label} comparison version`,
  ).definition;
  const flags = parseRegexFlags(
    input.flags,
    definition,
    `${definition.label} comparison flags`,
  );
  const options = parseRegexOptions(
    input.options,
    definition,
    `${definition.label} comparison options`,
  );
  if (input.pattern.length > DEFAULT_REGEX_LIMITS.patternHardLengthUtf16) {
    throw new RangeError(
      `${definition.label} comparison pattern exceeds the ${DEFAULT_REGEX_LIMITS.patternHardLengthUtf16.toLocaleString()} UTF-16 unit limit.`,
    );
  }
  if (
    input.replacement !== undefined &&
    input.replacement.length >
      DEFAULT_REGEX_LIMITS.maximumReplacementTemplateUtf16
  ) {
    throw new RangeError(
      `${definition.label} comparison replacement exceeds the ${DEFAULT_REGEX_LIMITS.maximumReplacementTemplateUtf16.toLocaleString()} UTF-16 unit limit.`,
    );
  }
  const normalized: ComparisonSideInput = {
    flavour: input.flavour,
    flavourVersion: version.value,
    pattern: input.pattern,
    flags,
    options,
    ...(input.replacement === undefined
      ? {}
      : { replacement: input.replacement }),
  };
  return {
    input: normalized,
    syntaxRequest: {
      flavour: normalized.flavour,
      flavourVersion: version.syntaxVersion,
      pattern: normalized.pattern,
      flags: normalized.flags,
      options: normalized.options,
    },
  };
}

export function validateRegexComparisonRequest(
  request: RegexComparisonRequest,
): {
  readonly request: RegexComparisonRequest;
  readonly sides: readonly [PreparedSide, PreparedSide];
} {
  if (request.schemaVersion !== 1) {
    throw new Error("Unsupported comparison request schema.");
  }
  if (
    request.patternModel !== "shared" &&
    request.patternModel !== "variants"
  ) {
    throw new Error("Comparison pattern model is unsupported.");
  }
  if (request.operation !== "match" && request.operation !== "replace") {
    throw new Error("Comparison operation is unsupported.");
  }
  const flavours = request.sides.map((side) => side.flavour);
  if (
    new Set(flavours).size !== 2 ||
    !flavours.includes("ecmascript") ||
    !flavours.includes("pcre2")
  ) {
    throw new Error(
      "This comparison vertical requires one ECMAScript and one PCRE2 side.",
    );
  }
  const preparedInput = request.sides.map(validateSide);
  const ecmascript = preparedInput.find(
    (side) => side.input.flavour === "ecmascript",
  );
  const pcre2 = preparedInput.find((side) => side.input.flavour === "pcre2");
  if (!ecmascript || !pcre2) {
    throw new Error("Comparison sides could not be normalized.");
  }
  const prepared = [ecmascript, pcre2] as const;
  if (
    request.patternModel === "shared" &&
    prepared[0].input.pattern !== prepared[1].input.pattern
  ) {
    throw new Error(
      "Shared-pattern comparison must send the exact same pattern to both engines.",
    );
  }
  if (
    request.operation === "replace" &&
    prepared.some((side) => side.input.replacement === undefined)
  ) {
    throw new Error(
      "Replacement comparison requires an explicit replacement for each flavour.",
    );
  }
  if (
    utf8ByteLength(request.subject) >
    DEFAULT_REGEX_LIMITS.interactiveSubjectHardBytes
  ) {
    throw new RangeError(
      `Comparison subject exceeds the ${DEFAULT_REGEX_LIMITS.interactiveSubjectHardBytes.toLocaleString()} UTF-8 byte limit.`,
    );
  }
  const maximumMatches = requireBoundedInteger(
    request.maximumMatches,
    "Comparison match limit",
    1,
    DEFAULT_REGEX_LIMITS.maximumMatches,
  );
  const maximumCaptureRows = requireBoundedInteger(
    request.maximumCaptureRows,
    "Comparison capture-row limit",
    1,
    DEFAULT_REGEX_LIMITS.maximumCaptureRows,
  );
  const maximumOutputBytes = requireBoundedInteger(
    request.maximumOutputBytes,
    "Comparison replacement-output limit",
    1,
    DEFAULT_REGEX_LIMITS.maximumReplacementOutputBytes,
  );
  const timeoutMs = requireBoundedInteger(
    request.timeoutMs,
    "Comparison side timeout",
    1,
    DEFAULT_REGEX_LIMITS.advancedMaximumTimeoutMs,
  );
  const normalized: RegexComparisonRequest = {
    ...request,
    scanAll: request.scanAll === true,
    maximumMatches,
    maximumCaptureRows,
    maximumOutputBytes,
    timeoutMs,
    sides: [prepared[0].input, prepared[1].input],
  };
  return { request: normalized, sides: prepared };
}

function classifyFailure(error: unknown): ComparisonTaskFailure {
  if (error instanceof WorkerRequestError) {
    if (error.kind === "timeout") {
      return { status: "timeout", message: error.message };
    }
    if (error.kind === "cancelled") {
      return { status: "cancelled", message: error.message };
    }
  }
  return {
    status: "worker-failed",
    message: error instanceof Error ? error.message : String(error),
  };
}

function stableOptions(options: ComparisonSideInput["options"]): string {
  return JSON.stringify(
    Object.fromEntries(
      Object.entries(options).sort(([left], [right]) =>
        left.localeCompare(right),
      ),
    ),
  );
}

function requestIdentity(
  request: RegexExecutionRequest | RegexReplacementRequest,
): string {
  const replacement =
    "replacement" in request ? `\u0000${request.replacement}` : "";
  const value = [
    request.flavour,
    request.flavourVersion ?? "",
    request.pattern,
    request.flags.join(""),
    stableOptions(request.options ?? {}),
    request.subject,
    String(request.scanAll),
    String(request.maximumMatches),
    String(request.maximumCaptureRows),
    JSON.stringify(request.captureMetadata),
    "maximumOutputBytes" in request ? String(request.maximumOutputBytes) : "",
    replacement,
  ].join("\u0001");
  let hash = 0x811c9dc5;
  for (let index = 0; index < value.length; index += 1) {
    hash ^= value.charCodeAt(index);
    hash = Math.imul(hash, 0x01000193);
  }
  return `request-${(hash >>> 0).toString(16).padStart(8, "0")}`;
}

function executionRequest(
  request: RegexComparisonRequest,
  prepared: PreparedSide,
  captures: RegexSyntaxResult["captures"],
): RegexExecutionRequest {
  return {
    flavour: prepared.input.flavour,
    flavourVersion: prepared.input.flavourVersion,
    pattern: prepared.input.pattern,
    flags: prepared.input.flags,
    options: prepared.input.options,
    subject: request.subject,
    captureMetadata: captures,
    scanAll: request.scanAll,
    maximumMatches: request.maximumMatches,
    maximumCaptureRows: request.maximumCaptureRows,
  };
}

export class ComparisonOrchestrator {
  readonly #dependencies: ComparisonOrchestratorDependencies;
  readonly #syntax: ReadonlyMap<ComparisonFlavour, ComparisonSyntaxRunner>;
  readonly #engine: ComparisonEngineRunner;
  #generation = 0;
  #disposed = false;

  constructor(
    dependencies: ComparisonOrchestratorDependencies = DEFAULT_DEPENDENCIES,
  ) {
    this.#dependencies = dependencies;
    this.#syntax = new Map(
      (["ecmascript", "pcre2"] as const).map((flavour) => [
        flavour,
        dependencies.createSyntaxRunner(flavour),
      ]),
    );
    this.#engine = dependencies.createEngineRunner();
  }

  async compare(input: RegexComparisonRequest): Promise<RegexComparisonResult> {
    if (this.#disposed) {
      throw new Error("Comparison orchestrator has been disposed.");
    }
    this.cancel();
    const generation = this.#generation;
    const validated = validateRegexComparisonRequest(input);
    const startedAt = this.#dependencies.isoNow();
    const start = this.#dependencies.now();
    const settled = await Promise.allSettled(
      validated.sides.map((side) =>
        this.#runSide(validated.request, side, generation),
      ),
    );
    const outcomes = settled.map((entry, index) => {
      if (entry.status === "fulfilled") return entry.value;
      const prepared = validated.sides[index];
      if (!prepared) throw entry.reason;
      return this.#failedSide(
        validated.request,
        prepared,
        classifyFailure(entry.reason),
      );
    }) as unknown as readonly [ComparisonSideOutcome, ComparisonSideOutcome];
    return compareCompletedResults(
      validated.request,
      startedAt,
      this.#dependencies.now() - start,
      outcomes,
    );
  }

  cancel(): void {
    this.#generation += 1;
    for (const syntax of this.#syntax.values()) syntax.cancel();
    this.#engine.cancel();
  }

  dispose(): void {
    if (this.#disposed) return;
    this.cancel();
    this.#disposed = true;
    for (const syntax of this.#syntax.values()) syntax.dispose();
    this.#engine.dispose();
  }

  async #runSide(
    request: RegexComparisonRequest,
    prepared: PreparedSide,
    generation: number,
  ): Promise<ComparisonSideOutcome> {
    const syntaxRunner = this.#syntax.get(prepared.input.flavour);
    if (!syntaxRunner) {
      throw new Error(`Missing ${prepared.input.flavour} syntax runner.`);
    }
    const syntaxStart = this.#dependencies.now();
    let patternSyntax: RegexSyntaxResult | undefined;
    let replacementSyntax: ReplacementSyntaxResult | undefined;
    let syntaxFailure: ComparisonTaskFailure | undefined;
    try {
      patternSyntax = await syntaxRunner.parsePattern(
        prepared.syntaxRequest,
        Math.min(1_000, request.timeoutMs),
      );
      if (request.operation === "replace") {
        replacementSyntax = await syntaxRunner.parseReplacement(
          {
            flavour: prepared.input.flavour,
            replacement: prepared.input.replacement ?? "",
            captureMetadata: patternSyntax.captures,
          },
          Math.min(1_000, request.timeoutMs),
        );
      }
    } catch (error) {
      syntaxFailure = classifyFailure(error);
    }
    const syntax: ComparisonSyntaxOutcome = {
      status: syntaxFailure?.status ?? "complete",
      elapsedMs: this.#dependencies.now() - syntaxStart,
      ...(patternSyntax ? { pattern: patternSyntax } : {}),
      ...(replacementSyntax ? { replacement: replacementSyntax } : {}),
      ...(syntaxFailure ? { error: syntaxFailure.message } : {}),
    };
    const exactExecution = executionRequest(
      request,
      prepared,
      patternSyntax?.captures ?? [],
    );
    const exactReplacement: RegexReplacementRequest | undefined =
      request.operation === "replace"
        ? {
            ...exactExecution,
            replacement: prepared.input.replacement ?? "",
            maximumOutputBytes: request.maximumOutputBytes,
          }
        : undefined;
    if (generation !== this.#generation) {
      const runtime: ComparisonExecutionOutcome = {
        status: "cancelled",
        elapsedMs: 0,
        error: "Comparison was cancelled before engine execution.",
      };
      return {
        requestIdentity: requestIdentity(exactReplacement ?? exactExecution),
        input: prepared.input,
        syntaxRequest: prepared.syntaxRequest,
        executionRequest: exactExecution,
        ...(exactReplacement ? { replacementRequest: exactReplacement } : {}),
        syntax,
        runtime,
      };
    }

    const runtimeStart = this.#dependencies.now();
    let runtime: ComparisonExecutionOutcome;
    try {
      if (exactReplacement) {
        const replacement = await this.#engine.replace(
          exactReplacement,
          request.timeoutMs,
        );
        runtime = {
          status: "complete",
          elapsedMs: this.#dependencies.now() - runtimeStart,
          replacement,
        };
      } else {
        const execution = await this.#engine.execute(
          exactExecution,
          request.timeoutMs,
        );
        runtime = {
          status: "complete",
          elapsedMs: this.#dependencies.now() - runtimeStart,
          execution,
        };
      }
    } catch (error) {
      const failure = classifyFailure(error);
      runtime = {
        status: failure.status,
        elapsedMs: this.#dependencies.now() - runtimeStart,
        error: failure.message,
      };
    }
    const execution = runtime.replacement?.execution ?? runtime.execution;
    return {
      requestIdentity: requestIdentity(exactReplacement ?? exactExecution),
      input: prepared.input,
      syntaxRequest: prepared.syntaxRequest,
      executionRequest: exactExecution,
      ...(exactReplacement ? { replacementRequest: exactReplacement } : {}),
      syntax,
      runtime,
      ...(execution ? { engine: execution.engine } : {}),
    };
  }

  #failedSide(
    request: RegexComparisonRequest,
    prepared: PreparedSide,
    failure: ComparisonTaskFailure,
  ): ComparisonSideOutcome {
    const exactExecution = executionRequest(request, prepared, []);
    const exactReplacement =
      request.operation === "replace"
        ? {
            ...exactExecution,
            replacement: prepared.input.replacement ?? "",
            maximumOutputBytes: request.maximumOutputBytes,
          }
        : undefined;
    return {
      requestIdentity: requestIdentity(exactReplacement ?? exactExecution),
      input: prepared.input,
      syntaxRequest: prepared.syntaxRequest,
      executionRequest: exactExecution,
      ...(exactReplacement ? { replacementRequest: exactReplacement } : {}),
      syntax: {
        status: "worker-failed",
        elapsedMs: 0,
        error: "Comparison side failed before syntax completion.",
      },
      runtime: {
        status: failure.status,
        elapsedMs: 0,
        error: failure.message,
      },
    };
  }
}
