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 {
  AVAILABLE_REGEX_FLAVOURS,
  parseRegexFlags,
  parseRegexOptions,
  resolveRegexFlavourVersion,
} from "../flavours/flavour-registry";
import type {
  RegexExecutionRequest,
  RegexExecutionResult,
  RegexReplacementRequest,
  RegexReplacementResult,
} from "../model/match";
import type {
  CaptureDefinition,
  RegexSyntaxRequest,
  RegexSyntaxResult,
} from "../model/syntax";
import {
  compareSemanticResults,
  engineIdentity,
  evaluateUnitTestFailure,
  type SemanticSideResult,
} from "../minimization/oracles";
import {
  MAXIMUM_REGEX_TESTS,
  type RegexTestCase,
} from "../tests/test-case.types";
import type {
  PatternFormatDifference,
  PatternFormatRuntimeObservation,
  PatternFormatSnapshotCheck,
  PatternFormatTestCheck,
  PatternFormatValidationInput,
  PatternFormatValidationResult,
} from "./formatting.types";

const MAXIMUM_VALIDATION_WALL_TIME_MS = 60_000;
const MAXIMUM_RETAINED_DIFFERENCES = 1_000;

export interface PatternFormatSyntaxRunner {
  parsePattern(
    request: RegexSyntaxRequest,
    timeoutMs?: number,
  ): Promise<RegexSyntaxResult>;
  cancel(): void;
  dispose(): void;
}

export interface PatternFormatEngineRunner {
  execute(
    request: RegexExecutionRequest,
    timeoutMs: number,
  ): Promise<RegexExecutionResult>;
  replace(
    request: RegexReplacementRequest,
    timeoutMs: number,
  ): Promise<RegexReplacementResult>;
  cancel(): void;
  dispose(): void;
}

export interface PatternFormatValidatorDependencies {
  readonly createSyntaxRunner: (
    label: "source" | "formatted",
  ) => PatternFormatSyntaxRunner;
  readonly createEngineRunner: (
    label: "source" | "formatted",
  ) => PatternFormatEngineRunner;
  readonly now: () => number;
}

const DEFAULT_DEPENDENCIES: PatternFormatValidatorDependencies = {
  createSyntaxRunner: (label) =>
    new SyntaxSupervisor(
      `${label} formatter syntax`,
      `regex-tools-formatter-${label}-syntax`,
    ),
  createEngineRunner: () => new EngineSupervisor(),
  now: () => performance.now(),
};

interface CompleteRuntime {
  readonly status: "complete";
  readonly semantic: SemanticSideResult;
  readonly assertionPassed?: boolean;
  readonly assertionMessage?: string;
}

interface FailedRuntime {
  readonly status: "timeout" | "cancelled" | "crash" | "worker-error";
  readonly message: string;
  readonly assertionPassed?: boolean;
}

type Runtime = CompleteRuntime | FailedRuntime;

function stableValue(value: unknown): unknown {
  if (Array.isArray(value)) return value.map(stableValue);
  if (value && typeof value === "object") {
    return Object.fromEntries(
      Object.entries(value)
        .sort(([left], [right]) => left.localeCompare(right))
        .map(([key, entry]) => [key, stableValue(entry)]),
    );
  }
  return value;
}

function stableJson(value: unknown): string {
  return JSON.stringify(stableValue(value));
}

function boundedInteger(
  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 normalizedVersion(value: string | undefined): string {
  const definition = AVAILABLE_REGEX_FLAVOURS.require("ecmascript");
  return resolveRegexFlavourVersion(
    value,
    definition,
    "ECMAScript formatter version",
  ).definition.value;
}

function validateInput(
  input: PatternFormatValidationInput,
): PatternFormatValidationInput {
  const definition = AVAILABLE_REGEX_FLAVOURS.require("ecmascript");
  if (input.flavour !== "ecmascript") {
    throw new Error("Only ECMAScript formatting is implemented.");
  }
  if (input.sourcePattern === input.formattedPattern) {
    throw new Error("The formatter candidate does not change the pattern.");
  }
  if (
    input.sourcePattern.length > DEFAULT_REGEX_LIMITS.patternHardLengthUtf16 ||
    input.formattedPattern.length > DEFAULT_REGEX_LIMITS.patternHardLengthUtf16
  ) {
    throw new RangeError(
      `Both patterns must fit the ${DEFAULT_REGEX_LIMITS.patternHardLengthUtf16.toLocaleString()} UTF-16 unit limit.`,
    );
  }
  if (
    utf8ByteLength(input.subject) >
    DEFAULT_REGEX_LIMITS.interactiveSubjectHardBytes
  ) {
    throw new RangeError(
      `Formatter validation subject exceeds the ${DEFAULT_REGEX_LIMITS.interactiveSubjectHardBytes.toLocaleString()} UTF-8 byte limit.`,
    );
  }
  if (
    input.replacement.length >
    DEFAULT_REGEX_LIMITS.maximumReplacementTemplateUtf16
  ) {
    throw new RangeError(
      `Replacement exceeds the ${DEFAULT_REGEX_LIMITS.maximumReplacementTemplateUtf16.toLocaleString()} UTF-16 unit limit.`,
    );
  }
  if (input.tests.length > MAXIMUM_REGEX_TESTS) {
    throw new RangeError(
      `Formatter validation accepts at most ${MAXIMUM_REGEX_TESTS.toLocaleString()} unit tests.`,
    );
  }
  const flags = parseRegexFlags(
    input.flags,
    definition,
    "ECMAScript formatter flags",
  );
  const options = parseRegexOptions(
    input.options,
    definition,
    "ECMAScript formatter options",
  );
  const timeoutMs = boundedInteger(
    input.timeoutMs,
    "Formatter worker timeout",
    1,
    DEFAULT_REGEX_LIMITS.advancedMaximumTimeoutMs,
  );
  return {
    ...input,
    flavourVersion: normalizedVersion(input.flavourVersion),
    flags,
    options,
    timeoutMs,
  };
}

function syntaxRequest(
  input: PatternFormatValidationInput,
  pattern: string,
): RegexSyntaxRequest {
  const definition = AVAILABLE_REGEX_FLAVOURS.require("ecmascript");
  const version = resolveRegexFlavourVersion(
    input.flavourVersion,
    definition,
    "ECMAScript formatter version",
  ).definition;
  return {
    flavour: "ecmascript",
    flavourVersion: version.syntaxVersion,
    pattern,
    flags: input.flags,
    options: input.options,
  };
}

function captureShape(
  captures: readonly CaptureDefinition[],
): readonly string[] {
  return captures.map((capture) =>
    [
      capture.number,
      capture.name ?? "",
      capture.repeated ? "repeated" : "single",
      capture.parentCaptureNumber ?? "",
    ].join(":"),
  );
}

function sameCaptureShape(
  left: readonly CaptureDefinition[],
  right: readonly CaptureDefinition[],
): boolean {
  const leftShape = captureShape(left);
  const rightShape = captureShape(right);
  return (
    leftShape.length === rightShape.length &&
    leftShape.every((value, index) => value === rightShape[index])
  );
}

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

function runtimeObservation(runtime: Runtime): PatternFormatRuntimeObservation {
  if (runtime.status !== "complete") {
    return {
      status: runtime.status,
      ...(runtime.assertionPassed === undefined
        ? {}
        : { assertionPassed: runtime.assertionPassed }),
      message: runtime.message,
    };
  }
  return {
    status: "complete",
    engineIdentity: engineIdentity(runtime.semantic.execution),
    ...(runtime.assertionPassed === undefined
      ? {}
      : { assertionPassed: runtime.assertionPassed }),
    message:
      runtime.assertionMessage ??
      "The actual ECMAScript engine completed with a bounded result.",
  };
}

function executionRequest(
  input: PatternFormatValidationInput,
  pattern: string,
  subject: string,
  captures: readonly CaptureDefinition[],
  scanAll: boolean,
): RegexExecutionRequest {
  return {
    flavour: "ecmascript",
    ...(input.flavourVersion === undefined
      ? {}
      : { flavourVersion: input.flavourVersion }),
    pattern,
    flags: input.flags,
    options: input.options,
    subject,
    captureMetadata: captures,
    scanAll,
    maximumMatches: DEFAULT_REGEX_LIMITS.maximumMatches,
    maximumCaptureRows: DEFAULT_REGEX_LIMITS.maximumCaptureRows,
  };
}

async function runCurrent(
  runner: PatternFormatEngineRunner,
  input: PatternFormatValidationInput,
  pattern: string,
  captures: readonly CaptureDefinition[],
): Promise<Runtime> {
  try {
    const replacement = await runner.replace(
      {
        ...executionRequest(
          input,
          pattern,
          input.subject,
          captures,
          input.scanAll,
        ),
        replacement: input.replacement,
        maximumOutputBytes: DEFAULT_REGEX_LIMITS.maximumReplacementOutputBytes,
      },
      input.timeoutMs,
    );
    return {
      status: "complete",
      semantic: { execution: replacement.execution, replacement },
    };
  } catch (error) {
    return classifyFailure(error);
  }
}

async function runTest(
  runner: PatternFormatEngineRunner,
  input: PatternFormatValidationInput,
  pattern: string,
  captures: readonly CaptureDefinition[],
  test: RegexTestCase,
  timeoutMs: number,
): Promise<Runtime> {
  const request = executionRequest(
    input,
    pattern,
    test.subject,
    captures,
    test.scanAll ?? false,
  );
  try {
    let semantic: SemanticSideResult;
    if (test.expectation.kind === "replacement") {
      const replacement = await runner.replace(
        {
          ...request,
          replacement: test.replacement ?? "",
          maximumOutputBytes:
            DEFAULT_REGEX_LIMITS.maximumReplacementOutputBytes,
        },
        timeoutMs,
      );
      semantic = { execution: replacement.execution, replacement };
    } else {
      semantic = {
        execution: await runner.execute(request, timeoutMs),
      };
    }
    const assertion = evaluateUnitTestFailure(
      test.expectation,
      semantic.execution,
      semantic.replacement,
      test.subject,
    );
    return {
      status: "complete",
      semantic,
      ...(assertion.kind === "passes"
        ? { assertionPassed: true }
        : assertion.kind === "failure"
          ? { assertionPassed: false }
          : {}),
      assertionMessage: assertion.summary,
    };
  } catch (error) {
    const failure = classifyFailure(error);
    return {
      ...failure,
      ...(failure.status === "timeout"
        ? { assertionPassed: test.expectation.kind === "must-time-out" }
        : {}),
    };
  }
}

function compareRuntimes(
  before: Runtime,
  after: Runtime,
  allowMatchingTimeout: boolean,
): PatternFormatSnapshotCheck {
  const observations = {
    before: runtimeObservation(before),
    after: runtimeObservation(after),
  };
  if (before.status === "complete" && after.status === "complete") {
    const beforeIdentity = engineIdentity(before.semantic.execution);
    const afterIdentity = engineIdentity(after.semantic.execution);
    if (beforeIdentity !== afterIdentity) {
      return {
        comparison: "inconclusive",
        ...observations,
        summary: `Engine identity changed from ${beforeIdentity} to ${afterIdentity}.`,
        mismatchKinds: ["engine-identity"],
      };
    }
    const semantic = compareSemanticResults(before.semantic, after.semantic);
    if (semantic.kind !== "same") {
      return {
        comparison: semantic.kind === "mismatch" ? "different" : "inconclusive",
        ...observations,
        summary: semantic.summary,
        mismatchKinds: semantic.mismatchKinds,
      };
    }
    if (before.assertionPassed !== after.assertionPassed) {
      return {
        comparison: "different",
        ...observations,
        summary: "The unit-test assertion outcome changed after formatting.",
        mismatchKinds: ["unit-test-outcome"],
      };
    }
    return {
      comparison: "same",
      ...observations,
      summary:
        before.assertionPassed === undefined
          ? semantic.summary
          : `Exact semantic output is unchanged; the assertion ${
              before.assertionPassed ? "passes" : "fails"
            } on both snapshots.`,
      mismatchKinds: [],
    };
  }
  if (
    allowMatchingTimeout &&
    before.status === "timeout" &&
    after.status === "timeout" &&
    before.assertionPassed === after.assertionPassed
  ) {
    return {
      comparison: "same",
      ...observations,
      summary:
        "Both exact test snapshots reached the same fixed worker timeout and retained the same assertion outcome.",
      mismatchKinds: [],
    };
  }
  if (
    (before.status === "complete" && after.status === "timeout") ||
    (before.status === "timeout" && after.status === "complete")
  ) {
    return {
      comparison: "different",
      ...observations,
      summary:
        "One exact snapshot completed while the other reached the fixed worker timeout.",
      mismatchKinds: ["timeout-behavior"],
    };
  }
  return {
    comparison: "inconclusive",
    ...observations,
    summary: `Validation did not produce two authoritative results (${before.status} versus ${after.status}).`,
    mismatchKinds: ["runtime-inconclusive"],
  };
}

function testIsApplicable(
  input: PatternFormatValidationInput,
  test: RegexTestCase,
): boolean {
  return (
    test.enabled &&
    test.flavour === "ecmascript" &&
    normalizedVersion(test.flavourVersion) ===
      normalizedVersion(input.flavourVersion) &&
    test.pattern === input.sourcePattern &&
    test.flags.join("") === input.flags.join("") &&
    stableJson(test.options) === stableJson(input.options)
  );
}

function unavailableCheck(summary: string): PatternFormatSnapshotCheck {
  const observation: PatternFormatRuntimeObservation = {
    status: "worker-error",
    message: "Engine validation was not started.",
  };
  return {
    comparison: "inconclusive",
    before: observation,
    after: observation,
    summary,
    mismatchKinds: ["validation-not-run"],
  };
}

export class PatternFormatValidator {
  readonly #dependencies: PatternFormatValidatorDependencies;
  readonly #sourceSyntax: PatternFormatSyntaxRunner;
  readonly #formattedSyntax: PatternFormatSyntaxRunner;
  readonly #sourceEngine: PatternFormatEngineRunner;
  readonly #formattedEngine: PatternFormatEngineRunner;
  #generation = 0;
  #disposed = false;

  constructor(
    dependencies: PatternFormatValidatorDependencies = DEFAULT_DEPENDENCIES,
  ) {
    this.#dependencies = dependencies;
    this.#sourceSyntax = dependencies.createSyntaxRunner("source");
    this.#formattedSyntax = dependencies.createSyntaxRunner("formatted");
    this.#sourceEngine = dependencies.createEngineRunner("source");
    this.#formattedEngine = dependencies.createEngineRunner("formatted");
  }

  async validate(
    rawInput: PatternFormatValidationInput,
    onProgress?: (completed: number, total: number) => void,
  ): Promise<PatternFormatValidationResult> {
    if (this.#disposed) {
      throw new Error("Pattern formatter validator has been disposed.");
    }
    this.cancel();
    const generation = this.#generation;
    const input = validateInput(rawInput);
    const started = this.#dependencies.now();
    const elapsed = () => Math.max(0, this.#dependencies.now() - started);
    const assertCurrent = () => {
      if (generation !== this.#generation || this.#disposed) {
        throw new WorkerRequestError(
          "cancelled",
          "Pattern formatter validation was cancelled.",
        );
      }
    };
    const differences: PatternFormatDifference[] = [];
    const notices: string[] = [
      "Validation executes exact source and formatted snapshots in separate actual-engine workers. It is bounded evidence for the selected subject and applicable tests, not a proof over every possible input.",
    ];
    const addDifference = (difference: PatternFormatDifference) => {
      if (differences.length < MAXIMUM_RETAINED_DIFFERENCES) {
        differences.push(difference);
      }
    };

    const syntaxTimeout = Math.min(1_000, input.timeoutMs);
    const [sourceSyntax, formattedSyntax] = await Promise.all([
      this.#sourceSyntax.parsePattern(
        syntaxRequest(input, input.sourcePattern),
        syntaxTimeout,
      ),
      this.#formattedSyntax.parsePattern(
        syntaxRequest(input, input.formattedPattern),
        syntaxTimeout,
      ),
    ]);
    assertCurrent();
    const captureShapePreserved =
      sourceSyntax.accepted &&
      formattedSyntax.accepted &&
      sameCaptureShape(sourceSyntax.captures, formattedSyntax.captures);
    if (!sourceSyntax.accepted) {
      addDifference({
        scope: "syntax",
        code: "source-syntax-rejected",
        summary:
          sourceSyntax.diagnostics[0]?.message ??
          "The syntax provider rejected the source pattern.",
      });
    }
    if (!formattedSyntax.accepted) {
      addDifference({
        scope: "syntax",
        code: "formatted-syntax-rejected",
        summary:
          formattedSyntax.diagnostics[0]?.message ??
          "The syntax provider rejected the formatted pattern.",
      });
    }
    if (
      sourceSyntax.accepted &&
      formattedSyntax.accepted &&
      !captureShapePreserved
    ) {
      addDifference({
        scope: "syntax",
        code: "capture-shape-changed",
        summary:
          "Capture numbering, naming, repetition or parent structure changed after formatting.",
      });
    }
    if (
      !sourceSyntax.accepted ||
      !formattedSyntax.accepted ||
      !captureShapePreserved
    ) {
      const current = unavailableCheck(
        "Engine validation was not started because the syntax gate did not preserve an accepted capture structure.",
      );
      return {
        status:
          sourceSyntax.accepted !== formattedSyntax.accepted ||
          !captureShapePreserved
            ? "different"
            : "inconclusive",
        canApply: false,
        sourceSyntaxAccepted: sourceSyntax.accepted,
        formattedSyntaxAccepted: formattedSyntax.accepted,
        captureShapePreserved,
        current,
        tests: [],
        applicableTestCount: 0,
        skippedTestCount: input.tests.length,
        completedTestCount: 0,
        differences,
        elapsedMs: elapsed(),
        notices,
      };
    }

    const applicable = input.tests.filter((test) =>
      testIsApplicable(input, test),
    );
    const skippedTestCount = input.tests.length - applicable.length;
    onProgress?.(0, applicable.length);
    const [sourceCurrent, formattedCurrent] = await Promise.all([
      runCurrent(
        this.#sourceEngine,
        input,
        input.sourcePattern,
        sourceSyntax.captures,
      ),
      runCurrent(
        this.#formattedEngine,
        input,
        input.formattedPattern,
        formattedSyntax.captures,
      ),
    ]);
    assertCurrent();
    const current = compareRuntimes(sourceCurrent, formattedCurrent, false);
    if (current.comparison !== "same") {
      addDifference({
        scope: "current-subject",
        code:
          current.comparison === "different"
            ? "current-behavior-changed"
            : "current-behavior-inconclusive",
        summary: current.summary,
      });
    }

    const testChecks: PatternFormatTestCheck[] = [];
    let suiteIncomplete = false;
    for (const test of applicable) {
      assertCurrent();
      const testTimeout = boundedInteger(
        test.resourceLimits?.manualExecutionTimeoutMs ?? input.timeoutMs,
        `Unit-test timeout for ${test.name}`,
        1,
        DEFAULT_REGEX_LIMITS.advancedMaximumTimeoutMs,
      );
      if (elapsed() + testTimeout > MAXIMUM_VALIDATION_WALL_TIME_MS) {
        suiteIncomplete = true;
        notices.push(
          `Unit-test validation stopped before ${test.name} because its full ${testTimeout.toLocaleString()} ms worker timeout no longer fit the ${MAXIMUM_VALIDATION_WALL_TIME_MS / 1_000} second aggregate wall budget.`,
        );
        break;
      }
      const [sourceTest, formattedTest] = await Promise.all([
        runTest(
          this.#sourceEngine,
          input,
          input.sourcePattern,
          sourceSyntax.captures,
          test,
          testTimeout,
        ),
        runTest(
          this.#formattedEngine,
          input,
          input.formattedPattern,
          formattedSyntax.captures,
          test,
          testTimeout,
        ),
      ]);
      assertCurrent();
      const comparison = compareRuntimes(sourceTest, formattedTest, true);
      const check: PatternFormatTestCheck = {
        testId: test.id,
        name: test.name,
        ...comparison,
      };
      testChecks.push(check);
      if (check.comparison !== "same") {
        addDifference({
          scope: "unit-test",
          code:
            check.comparison === "different"
              ? "test-behavior-changed"
              : "test-behavior-inconclusive",
          summary: `${test.name}: ${check.summary}`,
          testId: test.id,
        });
      }
      onProgress?.(testChecks.length, applicable.length);
    }
    if (testChecks.length < applicable.length) suiteIncomplete = true;
    if (applicable.length === 0) {
      notices.push(
        "No enabled unit test has the exact active pattern, version, flags and options; only the current subject/replacement snapshot was compared.",
      );
    } else {
      notices.push(
        `${testChecks.length.toLocaleString()} of ${applicable.length.toLocaleString()} applicable enabled unit tests completed on both exact snapshots.`,
      );
    }

    const hasDifferent =
      current.comparison === "different" ||
      testChecks.some((test) => test.comparison === "different");
    const hasInconclusive =
      current.comparison === "inconclusive" ||
      suiteIncomplete ||
      testChecks.some((test) => test.comparison === "inconclusive");
    const status = hasDifferent
      ? "different"
      : hasInconclusive
        ? "inconclusive"
        : "equivalent";
    return {
      status,
      canApply: status === "equivalent",
      sourceSyntaxAccepted: true,
      formattedSyntaxAccepted: true,
      captureShapePreserved: true,
      current,
      tests: testChecks,
      applicableTestCount: applicable.length,
      skippedTestCount,
      completedTestCount: testChecks.length,
      differences,
      elapsedMs: elapsed(),
      notices,
    };
  }

  cancel(): void {
    this.#generation += 1;
    this.#sourceSyntax.cancel();
    this.#formattedSyntax.cancel();
    this.#sourceEngine.cancel();
    this.#formattedEngine.cancel();
  }

  dispose(): void {
    if (this.#disposed) return;
    this.cancel();
    this.#disposed = true;
    this.#sourceSyntax.dispose();
    this.#formattedSyntax.dispose();
    this.#sourceEngine.dispose();
    this.#formattedEngine.dispose();
  }
}
