import { WorkerRequestError } from "../execution/WorkerSupervisor";
import {
  DEFAULT_REGEX_LIMITS,
  utf8ByteLength,
} from "../execution/request-limits";
import { ANALYSIS_LIMITS } from "./analysis-limits";
import type { AnalysisWorkerClient } from "./AnalysisSupervisor";
import type {
  AnalysisProgress,
  AnalysisRunOptions,
  AnalysisRunStatus,
  BenchmarkWorkerSample,
  GrowthAnalysisRequest,
  GrowthAnalysisResult,
  GrowthSample,
  GrowthStopReason,
  RegexBenchmarkRequest,
  RegexBenchmarkResult,
  RegexRiskFinding,
} from "./analysis.types";
import { summarizeBenchmarkSamples } from "./statistics";

interface Clock {
  now(): number;
}

const SYSTEM_CLOCK: Clock = {
  now: () => performance.now(),
};

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

function assertFiniteRange(
  value: number,
  label: string,
  minimum: number,
  maximum: number,
): void {
  if (!Number.isFinite(value) || value < minimum || value > maximum) {
    throw new RangeError(
      `${label} must be from ${minimum.toLocaleString()} to ${maximum.toLocaleString()}.`,
    );
  }
}

export function validateBenchmarkRequest(request: RegexBenchmarkRequest): void {
  if (request.flavour !== "ecmascript") {
    throw new RangeError("Benchmarking currently supports ECMAScript only.");
  }
  if (request.pattern.length > DEFAULT_REGEX_LIMITS.patternHardLengthUtf16) {
    throw new RangeError("Pattern exceeds the configured hard limit.");
  }
  if (
    utf8ByteLength(request.subject) >
    DEFAULT_REGEX_LIMITS.interactiveSubjectHardBytes
  ) {
    throw new RangeError(
      "Benchmark subject exceeds the configured hard limit.",
    );
  }
  if (
    request.replacement.length >
    DEFAULT_REGEX_LIMITS.maximumReplacementTemplateUtf16
  ) {
    throw new RangeError(
      "Benchmark replacement exceeds the configured hard limit.",
    );
  }
  assertInteger(
    request.maximumMatches,
    "Maximum matches",
    1,
    DEFAULT_REGEX_LIMITS.maximumMatches,
  );
  assertInteger(
    request.settings.warmupIterations,
    "Warm-up iterations",
    0,
    ANALYSIS_LIMITS.maximumWarmupIterations,
  );
  assertInteger(
    request.settings.measuredIterations,
    "Measured iterations",
    1,
    ANALYSIS_LIMITS.maximumMeasuredIterations,
  );
  assertInteger(
    request.settings.sampleTimeoutMs,
    "Sample timeout",
    ANALYSIS_LIMITS.minimumSampleTimeoutMs,
    ANALYSIS_LIMITS.maximumSampleTimeoutMs,
  );
  assertInteger(
    request.settings.maximumWallTimeMs,
    "Benchmark wall time",
    request.settings.sampleTimeoutMs,
    ANALYSIS_LIMITS.maximumAnalysisWallTimeMs,
  );
  if (
    request.settings.warmupIterations +
      request.settings.measuredIterations +
      1 >
    DEFAULT_REGEX_LIMITS.maximumBenchmarkIterations
  ) {
    throw new RangeError(
      `Benchmark exceeds the ${DEFAULT_REGEX_LIMITS.maximumBenchmarkIterations.toLocaleString()}-iteration limit.`,
    );
  }
}

export function validateGrowthRequest(request: GrowthAnalysisRequest): void {
  if (request.flavour !== "ecmascript") {
    throw new RangeError(
      "Dynamic growth analysis currently supports ECMAScript only.",
    );
  }
  if (request.pattern.length > DEFAULT_REGEX_LIMITS.patternHardLengthUtf16) {
    throw new RangeError("Pattern exceeds the configured hard limit.");
  }
  assertInteger(
    request.maximumMatches,
    "Maximum matches",
    1,
    DEFAULT_REGEX_LIMITS.maximumMatches,
  );
  if (request.settings.repeatedFragment.length === 0) {
    throw new RangeError("Repeated fragment must not be empty.");
  }
  if (
    request.settings.repeatedFragment.length >
    ANALYSIS_LIMITS.maximumGrowthFragmentUtf16
  ) {
    throw new RangeError(
      `Repeated fragment exceeds ${ANALYSIS_LIMITS.maximumGrowthFragmentUtf16.toLocaleString()} UTF-16 units.`,
    );
  }
  for (const [label, value] of [
    ["Growth prefix", request.settings.prefix],
    ["Growth suffix", request.settings.suffix],
  ] as const) {
    if (value.length > ANALYSIS_LIMITS.maximumGrowthAffixUtf16) {
      throw new RangeError(
        `${label} exceeds ${ANALYSIS_LIMITS.maximumGrowthAffixUtf16.toLocaleString()} UTF-16 units.`,
      );
    }
  }
  assertInteger(
    request.settings.startingRepetitions,
    "Starting repetitions",
    1,
    ANALYSIS_LIMITS.maximumGrowthRepetitions,
  );
  assertInteger(
    request.settings.maximumRepetitions,
    "Maximum repetitions",
    request.settings.startingRepetitions,
    ANALYSIS_LIMITS.maximumGrowthRepetitions,
  );
  assertFiniteRange(request.settings.multiplier, "Growth multiplier", 1.1, 10);
  assertInteger(
    request.settings.maximumSteps,
    "Maximum growth steps",
    1,
    ANALYSIS_LIMITS.maximumGrowthSteps,
  );
  assertInteger(
    request.settings.sampleTimeoutMs,
    "Sample timeout",
    ANALYSIS_LIMITS.minimumSampleTimeoutMs,
    ANALYSIS_LIMITS.maximumSampleTimeoutMs,
  );
  assertInteger(
    request.settings.maximumWallTimeMs,
    "Growth wall time",
    request.settings.sampleTimeoutMs,
    ANALYSIS_LIMITS.maximumAnalysisWallTimeMs,
  );
  assertInteger(
    request.settings.maximumSubjectBytes,
    "Maximum generated subject bytes",
    1,
    DEFAULT_REGEX_LIMITS.interactiveSubjectHardBytes,
  );
  assertFiniteRange(
    request.settings.normalizedGrowthThreshold,
    "Normalized growth threshold",
    1.1,
    100,
  );
}

function errorKind(
  error: unknown,
): "timeout" | "cancelled" | "crash" | "error" {
  if (error instanceof WorkerRequestError) {
    if (error.kind === "timeout") return "timeout";
    if (error.kind === "cancelled") return "cancelled";
    if (error.kind === "crash") return "crash";
    return "error";
  }
  if (
    error &&
    typeof error === "object" &&
    "kind" in error &&
    typeof error.kind === "string"
  ) {
    if (
      error.kind === "timeout" ||
      error.kind === "cancelled" ||
      error.kind === "crash"
    ) {
      return error.kind;
    }
  }
  return "error";
}

function errorMessage(error: unknown): string {
  return error instanceof Error ? error.message : String(error);
}

function aborted(signal: AbortSignal | undefined): boolean {
  return signal?.aborted === true;
}

function timeoutWithinDeadline(
  configuredTimeoutMs: number,
  deadline: number,
  clock: Clock,
): number {
  return Math.max(
    1,
    Math.min(configuredTimeoutMs, Math.floor(deadline - clock.now())),
  );
}

function benchmarkResult(
  request: RegexBenchmarkRequest,
  started: number,
  clock: Clock,
  values: {
    readonly status: AnalysisRunStatus;
    readonly identity?: RegexBenchmarkResult["identity"];
    readonly coldStartMs?: number;
    readonly coldSample?: BenchmarkWorkerSample;
    readonly warmSamples: readonly BenchmarkWorkerSample[];
    readonly completedWarmups: number;
    readonly stoppedReason?: string;
    readonly warnings: readonly string[];
  },
): RegexBenchmarkResult {
  return {
    status: values.status,
    ...(values.identity ? { identity: values.identity } : {}),
    ...(values.coldStartMs === undefined
      ? {}
      : { coldStartMs: values.coldStartMs }),
    ...(values.coldSample ? { coldSample: values.coldSample } : {}),
    warmSamples: values.warmSamples,
    warmStatistics: summarizeBenchmarkSamples(values.warmSamples),
    completedWarmups: values.completedWarmups,
    completedMeasuredIterations: values.warmSamples.length,
    requestedSettings: request.settings,
    wallTimeMs: Math.max(0, clock.now() - started),
    ...(values.stoppedReason ? { stoppedReason: values.stoppedReason } : {}),
    warnings: values.warnings,
  };
}

function progress(options: AnalysisRunOptions, value: AnalysisProgress): void {
  options.onProgress?.(value);
}

export async function runRegexBenchmark(
  request: RegexBenchmarkRequest,
  client: AnalysisWorkerClient,
  options: AnalysisRunOptions = {},
  clock: Clock = SYSTEM_CLOCK,
): Promise<RegexBenchmarkResult> {
  validateBenchmarkRequest(request);
  const started = clock.now();
  const deadline = started + request.settings.maximumWallTimeMs;
  let identity: RegexBenchmarkResult["identity"];
  let coldStartMs: number | undefined;
  let coldSample: BenchmarkWorkerSample | undefined;
  let completedWarmups = 0;
  const warmSamples: BenchmarkWorkerSample[] = [];
  let requestWasWallLimited = false;
  const nextRequestTimeout = () => {
    requestWasWallLimited =
      deadline - clock.now() < request.settings.sampleTimeoutMs;
    return timeoutWithinDeadline(
      request.settings.sampleTimeoutMs,
      deadline,
      clock,
    );
  };
  const warnings = [
    "One browser, pattern and subject are not a complete performance characterization.",
    "Native browser and WebAssembly engine timings are not directly comparable.",
    "Displayed precision does not imply nanosecond measurement accuracy.",
  ];

  const partial = (
    status: AnalysisRunStatus,
    stoppedReason: string,
  ): RegexBenchmarkResult =>
    benchmarkResult(request, started, clock, {
      status,
      identity,
      coldStartMs,
      coldSample,
      warmSamples,
      completedWarmups,
      stoppedReason,
      warnings,
    });

  try {
    if (aborted(options.signal)) {
      return partial("cancelled", "Benchmark cancelled before it started.");
    }
    progress(options, {
      phase: "starting-worker",
      completed: 0,
      total: 1,
      message: "Starting a fresh ECMAScript analysis worker…",
    });
    const coldStart = clock.now();
    identity = await client.identity(nextRequestTimeout());
    coldStartMs = Math.max(0, clock.now() - coldStart);

    if (clock.now() >= deadline) {
      return partial(
        "wall-time-limit",
        "Benchmark stopped at its aggregate wall-time limit.",
      );
    }
    progress(options, {
      phase: "cold-sample",
      completed: 0,
      total: 1,
      message: "Measuring the first engine use separately…",
    });
    coldSample = await client.benchmarkSample(request, nextRequestTimeout());
    if (!coldSample.accepted) {
      return partial(
        "error",
        coldSample.compileError ??
          "The native ECMAScript engine rejected the benchmark pattern.",
      );
    }

    for (let index = 0; index < request.settings.warmupIterations; index += 1) {
      if (aborted(options.signal)) {
        return partial("cancelled", "Benchmark cancelled during warm-up.");
      }
      if (clock.now() >= deadline) {
        return partial(
          "wall-time-limit",
          "Benchmark stopped at its aggregate wall-time limit.",
        );
      }
      progress(options, {
        phase: "warming-up",
        completed: index,
        total: request.settings.warmupIterations,
        message: `Warm-up ${index + 1} of ${request.settings.warmupIterations}…`,
      });
      const sample = await client.benchmarkSample(
        request,
        nextRequestTimeout(),
      );
      if (!sample.accepted) {
        return partial(
          "error",
          sample.compileError ??
            "The native ECMAScript engine rejected a warm-up sample.",
        );
      }
      completedWarmups += 1;
    }

    for (
      let index = 0;
      index < request.settings.measuredIterations;
      index += 1
    ) {
      if (aborted(options.signal)) {
        return partial("cancelled", "Benchmark cancelled while measuring.");
      }
      if (clock.now() >= deadline) {
        return partial(
          "wall-time-limit",
          "Benchmark stopped at its aggregate wall-time limit.",
        );
      }
      progress(options, {
        phase: "measuring",
        completed: index,
        total: request.settings.measuredIterations,
        message: `Measured sample ${index + 1} of ${request.settings.measuredIterations}…`,
      });
      const sample = await client.benchmarkSample(
        request,
        nextRequestTimeout(),
      );
      if (!sample.accepted) {
        return partial(
          "error",
          sample.compileError ??
            "The native ECMAScript engine rejected a measured sample.",
        );
      }
      warmSamples.push(sample);
    }
  } catch (error) {
    const kind = errorKind(error);
    if (kind === "timeout") {
      if (requestWasWallLimited || clock.now() >= deadline) {
        return partial(
          "wall-time-limit",
          "Benchmark stopped at its aggregate wall-time limit.",
        );
      }
      return partial(
        "timeout",
        "A benchmark sample timed out; its worker was terminated.",
      );
    }
    if (kind === "cancelled" || aborted(options.signal)) {
      return partial(
        "cancelled",
        "Benchmark cancelled; its worker was terminated.",
      );
    }
    return partial("error", `Benchmark worker failed: ${errorMessage(error)}`);
  }

  for (const sample of [coldSample, ...warmSamples]) {
    if (sample?.matchCollectionTruncated) {
      warnings.push(
        `Match collection reached the ${request.maximumMatches.toLocaleString()}-match benchmark limit.`,
      );
      break;
    }
  }
  const skippedReplacement = [coldSample, ...warmSamples].find(
    (sample) => sample?.replacementSkippedReason,
  )?.replacementSkippedReason;
  if (skippedReplacement) {
    warnings.push(`Replacement timing skipped: ${skippedReplacement}`);
  }
  return benchmarkResult(request, started, clock, {
    status: "complete",
    identity,
    coldStartMs,
    coldSample,
    warmSamples,
    completedWarmups,
    warnings,
  });
}

function generatedSubjectSize(
  request: GrowthAnalysisRequest,
  repetitions: number,
): { readonly utf16: number; readonly bytes: number } {
  return {
    utf16:
      request.settings.prefix.length +
      request.settings.repeatedFragment.length * repetitions +
      request.settings.suffix.length,
    bytes:
      utf8ByteLength(request.settings.prefix) +
      utf8ByteLength(request.settings.repeatedFragment) * repetitions +
      utf8ByteLength(request.settings.suffix),
  };
}

function dynamicFinding(
  request: GrowthAnalysisRequest,
  rule: "observed-growth" | "observed-timeout",
  details: {
    readonly confidence: "medium" | "high";
    readonly severity: "warning" | "high";
    readonly explanation: string;
    readonly exampleRisk: string;
  },
): RegexRiskFinding {
  return {
    id: `risk-${rule}-0-${request.pattern.length}`,
    flavour: "ecmascript",
    range: { startUtf16: 0, endUtf16: request.pattern.length },
    rule,
    title:
      rule === "observed-timeout"
        ? "Observed timeout under selected limits"
        : "Observed disproportionate growth",
    explanation: details.explanation,
    exampleRisk: details.exampleRisk,
    confidence: details.confidence,
    severity: details.severity,
    limitations:
      "This observation applies only to the generated subjects, browser runtime and limits shown in this result.",
    suggestedInvestigation:
      "Repeat with representative production boundaries, retain strict worker limits, and simplify ambiguous repetition where possible.",
    evidence: "dynamically-observed",
  };
}

function growthResult(
  request: GrowthAnalysisRequest,
  started: number,
  clock: Clock,
  values: {
    readonly status: AnalysisRunStatus;
    readonly identity?: GrowthAnalysisResult["identity"];
    readonly samples: readonly GrowthSample[];
    readonly stopReason: GrowthStopReason;
    readonly stoppedReason: string;
    readonly dynamicFindings: readonly RegexRiskFinding[];
  },
): GrowthAnalysisResult {
  return {
    status: values.status,
    ...(values.identity ? { identity: values.identity } : {}),
    samples: values.samples,
    stopReason: values.stopReason,
    stoppedReason: values.stoppedReason,
    wallTimeMs: Math.max(0, clock.now() - started),
    dynamicFindings: values.dynamicFindings,
    settings: request.settings,
  };
}

export async function runGrowthAnalysis(
  request: GrowthAnalysisRequest,
  client: AnalysisWorkerClient,
  options: AnalysisRunOptions = {},
  clock: Clock = SYSTEM_CLOCK,
): Promise<GrowthAnalysisResult> {
  validateGrowthRequest(request);
  const started = clock.now();
  const deadline = started + request.settings.maximumWallTimeMs;
  const samples: GrowthSample[] = [];
  const dynamicFindings: RegexRiskFinding[] = [];
  let identity: GrowthAnalysisResult["identity"];
  let currentRepetitions = request.settings.startingRepetitions;
  let requestWasWallLimited = false;
  const nextRequestTimeout = () => {
    requestWasWallLimited =
      deadline - clock.now() < request.settings.sampleTimeoutMs;
    return timeoutWithinDeadline(
      request.settings.sampleTimeoutMs,
      deadline,
      clock,
    );
  };

  const finish = (
    status: AnalysisRunStatus,
    stopReason: GrowthStopReason,
    stoppedReason: string,
  ) =>
    growthResult(request, started, clock, {
      status,
      identity,
      samples,
      stopReason,
      stoppedReason,
      dynamicFindings,
    });

  try {
    if (aborted(options.signal)) {
      return finish(
        "cancelled",
        "cancelled",
        "Growth analysis cancelled before it started.",
      );
    }
    identity = await client.identity(nextRequestTimeout());

    while (samples.length < request.settings.maximumSteps) {
      if (aborted(options.signal)) {
        return finish(
          "cancelled",
          "cancelled",
          "Growth analysis cancelled; its worker was terminated.",
        );
      }
      if (clock.now() >= deadline) {
        return finish(
          "wall-time-limit",
          "wall-time-limit",
          "Growth analysis stopped at its aggregate wall-time limit.",
        );
      }
      if (currentRepetitions > request.settings.maximumRepetitions) {
        return finish(
          "complete",
          "maximum-repetitions",
          "Growth analysis reached the configured repetition bound.",
        );
      }
      const size = generatedSubjectSize(request, currentRepetitions);
      if (
        !Number.isSafeInteger(size.bytes) ||
        !Number.isSafeInteger(size.utf16) ||
        size.bytes > request.settings.maximumSubjectBytes
      ) {
        return finish(
          "complete",
          "maximum-subject-size",
          "Growth analysis stopped before exceeding the generated-subject byte limit.",
        );
      }

      progress(options, {
        phase: "growth-probe",
        completed: samples.length,
        total: request.settings.maximumSteps,
        message: `Probing ${currentRepetitions.toLocaleString()} fragment repetitions…`,
      });
      const subject =
        request.settings.prefix +
        request.settings.repeatedFragment.repeat(currentRepetitions) +
        request.settings.suffix;
      let workerSample;
      try {
        workerSample = await client.growthProbe(
          {
            flavour: "ecmascript",
            pattern: request.pattern,
            flags: request.flags,
            subject,
            scanAll: request.scanAll,
            maximumMatches: request.maximumMatches,
          },
          nextRequestTimeout(),
        );
      } catch (error) {
        const kind = errorKind(error);
        if (kind === "timeout") {
          if (requestWasWallLimited || clock.now() >= deadline) {
            return finish(
              "wall-time-limit",
              "wall-time-limit",
              "Growth analysis stopped at its aggregate wall-time limit.",
            );
          }
          samples.push({
            repetitions: currentRepetitions,
            inputUtf16: size.utf16,
            inputBytes: size.bytes,
            status: "timeout",
            message: "Worker timed out and was terminated.",
          });
          dynamicFindings.push(
            dynamicFinding(request, "observed-timeout", {
              confidence: "high",
              severity: "high",
              explanation: `Execution did not finish within ${request.settings.sampleTimeoutMs.toLocaleString()} ms at ${currentRepetitions.toLocaleString()} generated repetitions.`,
              exampleRisk:
                "The selected generated near-miss exhausted the configured per-sample wall time.",
            }),
          );
          return finish(
            "timeout",
            "timeout",
            "Observed timeout under selected limits; the worker was terminated.",
          );
        }
        if (kind === "cancelled" || aborted(options.signal)) {
          return finish(
            "cancelled",
            "cancelled",
            "Growth analysis cancelled; its worker was terminated.",
          );
        }
        samples.push({
          repetitions: currentRepetitions,
          inputUtf16: size.utf16,
          inputBytes: size.bytes,
          status: "crash",
          message: errorMessage(error),
        });
        return finish(
          "error",
          "crash",
          `Growth worker failed: ${errorMessage(error)}`,
        );
      }

      if (!workerSample.accepted) {
        samples.push({
          repetitions: currentRepetitions,
          inputUtf16: size.utf16,
          inputBytes: size.bytes,
          status: "compile-error",
          message: workerSample.compileError,
        });
        return finish(
          "error",
          "compile-error",
          workerSample.compileError ??
            "The native ECMAScript engine rejected the pattern.",
        );
      }

      const previous = samples.at(-1);
      const executionMs = workerSample.executionMs ?? 0;
      let normalizedGrowth: number | undefined;
      if (
        previous?.status === "complete" &&
        previous.executionMs !== undefined &&
        previous.executionMs >= 0.25 &&
        executionMs >= 1 &&
        previous.inputBytes > 0
      ) {
        const inputRatio = size.bytes / previous.inputBytes;
        const timeRatio = executionMs / previous.executionMs;
        if (inputRatio > 0) normalizedGrowth = timeRatio / inputRatio;
      }
      samples.push({
        repetitions: currentRepetitions,
        inputUtf16: size.utf16,
        inputBytes: size.bytes,
        status: "complete",
        executionMs,
        matched: workerSample.matched,
        matchCount: workerSample.matchCount,
        matchCollectionTruncated: workerSample.matchCollectionTruncated,
        ...(normalizedGrowth === undefined ? {} : { normalizedGrowth }),
      });

      if (
        normalizedGrowth !== undefined &&
        normalizedGrowth >= request.settings.normalizedGrowthThreshold
      ) {
        dynamicFindings.push(
          dynamicFinding(request, "observed-growth", {
            confidence: "medium",
            severity: "warning",
            explanation: `Observed time growth was ${normalizedGrowth.toFixed(2)}× the input growth between the last two generated samples.`,
            exampleRisk:
              "Execution time increased disproportionately for the selected generated subject family.",
          }),
        );
        return finish(
          "complete",
          "growth-threshold",
          "Growth analysis stopped at the configured normalized-growth threshold.",
        );
      }
      if (currentRepetitions >= request.settings.maximumRepetitions) {
        return finish(
          "complete",
          "maximum-repetitions",
          "Growth analysis reached the configured repetition bound.",
        );
      }
      const next = Math.min(
        request.settings.maximumRepetitions,
        Math.max(
          currentRepetitions + 1,
          Math.ceil(currentRepetitions * request.settings.multiplier),
        ),
      );
      currentRepetitions = next;
    }
  } catch (error) {
    const kind = errorKind(error);
    if (kind === "timeout") {
      if (requestWasWallLimited || clock.now() >= deadline) {
        return finish(
          "wall-time-limit",
          "wall-time-limit",
          "Growth analysis stopped at its aggregate wall-time limit.",
        );
      }
      return finish(
        "timeout",
        "timeout",
        "The analysis worker timed out while starting and was terminated.",
      );
    }
    if (kind === "cancelled" || aborted(options.signal)) {
      return finish(
        "cancelled",
        "cancelled",
        "Growth analysis cancelled; its worker was terminated.",
      );
    }
    return finish(
      "error",
      "crash",
      `Growth worker failed: ${errorMessage(error)}`,
    );
  }

  return finish(
    "complete",
    "maximum-steps",
    "Growth analysis reached the configured step limit.",
  );
}
