import { DEFAULT_REGEX_LIMITS } from "../execution/request-limits";
import { EngineSupervisor } from "../execution/EngineSupervisor";
import { WorkerRequestError } from "../execution/WorkerSupervisor";
import type {
  RegexExecutionRequest,
  RegexExecutionResult,
} from "../model/match";
import { GENERATION_LIMITS } from "./generation-limits";
import {
  GENERATED_CASE_GENERATOR_ID,
  GENERATED_CASE_GENERATOR_VERSION,
  type CaseGenerationInput,
  type CaseGenerationProgress,
  type CaseGenerationResult,
  type DiscardedGeneratedCandidate,
  type GeneratedSubjectCandidate,
  type GenerationDiscardReason,
  type VerifiedGeneratedCase,
} from "./generation.types";
import {
  GenerationSupervisor,
  type GenerationWorkerClient,
} from "./GenerationSupervisor";
import { generationSeedHash } from "./seeded-random";

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

export interface CaseGenerationDependencies {
  readonly generator?: GenerationWorkerClient;
  readonly engine?: GenerationEngineClient;
  readonly now?: () => number;
}

export interface CaseGenerationClient {
  run(
    input: CaseGenerationInput,
    onProgress?: (progress: CaseGenerationProgress) => void,
  ): Promise<CaseGenerationResult>;
  cancel(): void;
  dispose(): void;
}

function resultPreview(value: string): string {
  const maximum = GENERATION_LIMITS.maximumDiagnosticPreviewUtf16;
  return value.length <= maximum ? value : `${value.slice(0, maximum)}…`;
}

function executionDetail(result: RegexExecutionResult): string {
  return (
    result.diagnostics.find((diagnostic) => diagnostic.severity === "error")
      ?.message ?? "The selected engine rejected the pattern."
  );
}

function discardedReason(error: WorkerRequestError): {
  readonly reason: GenerationDiscardReason;
  readonly detail: string;
} {
  switch (error.kind) {
    case "timeout":
      return {
        reason: "timeout",
        detail:
          "The actual engine did not complete within the configured per-case timeout.",
      };
    case "crash":
      return {
        reason: "worker-crash",
        detail: "The actual engine worker crashed while verifying this case.",
      };
    case "worker-error":
      return { reason: "execution-error", detail: error.message };
    case "cancelled":
      throw error;
  }
}

function generatedName(
  candidate: GeneratedSubjectCandidate,
  index: number,
): string {
  const outcome =
    candidate.intendedOutcome === "match" ? "positive" : "negative";
  return `Generated ${outcome} ${index + 1} · ${candidate.label}`;
}

function assertInput(input: CaseGenerationInput): void {
  if (input.pattern.length > DEFAULT_REGEX_LIMITS.patternHardLengthUtf16) {
    throw new RangeError(
      `Pattern exceeds the ${DEFAULT_REGEX_LIMITS.patternHardLengthUtf16.toLocaleString()} UTF-16 unit limit.`,
    );
  }
  if (
    input.captureMetadata.length > DEFAULT_REGEX_LIMITS.maximumCaptureGroups
  ) {
    throw new RangeError(
      `Pattern defines more than ${DEFAULT_REGEX_LIMITS.maximumCaptureGroups.toLocaleString()} capture groups.`,
    );
  }
  if (input.root.support.flavour !== input.flavour) {
    throw new Error(
      `The normalized AST belongs to ${input.root.support.flavour}, not ${input.flavour}.`,
    );
  }
}

export class CaseGenerationOrchestrator implements CaseGenerationClient {
  readonly #generator: GenerationWorkerClient;
  readonly #engine: GenerationEngineClient;
  readonly #now: () => number;
  #revision = 0;
  #disposed = false;

  constructor(dependencies: CaseGenerationDependencies = {}) {
    this.#generator = dependencies.generator ?? new GenerationSupervisor();
    this.#engine = dependencies.engine ?? new EngineSupervisor();
    this.#now = dependencies.now ?? (() => performance.now());
  }

  async run(
    input: CaseGenerationInput,
    onProgress?: (progress: CaseGenerationProgress) => void,
  ): Promise<CaseGenerationResult> {
    if (this.#disposed) {
      throw new Error("Generated-case orchestrator has been disposed.");
    }
    assertInput(input);
    const revision = ++this.#revision;
    const startedAt = this.#now();
    const elapsed = () => Math.max(0, this.#now() - startedAt);
    const remaining = () =>
      Math.max(0, input.settings.maximumWallTimeMs - elapsed());
    const assertCurrent = () => {
      if (revision !== this.#revision) {
        throw new WorkerRequestError(
          "cancelled",
          "Generated-case run was cancelled.",
        );
      }
    };

    onProgress?.({
      phase: "synthesizing",
      completed: 0,
      total: 0,
      retained: 0,
      message: "Synthesizing bounded candidates from the normalized AST.",
    });

    if (!input.syntaxAccepted) {
      const reason =
        "The syntax provider rejected the pattern; candidate generation was not started.";
      return {
        status: "unsupported",
        seed: input.seed,
        seedHash: generationSeedHash(input.seed),
        flavour: input.flavour,
        cases: [],
        discarded: [],
        coverage: [],
        unsupportedConstructs: [],
        candidateAttempts: 0,
        engineExecutions: 0,
        generatedCandidateBytes: 0,
        verifiedSubjectBytes: 0,
        wallTimeMs: elapsed(),
        stoppedReason: reason,
        warnings: [reason],
      };
    }

    const generation = await this.#generator.generate(
      {
        flavour: input.flavour,
        root: input.root,
        flags: input.flags,
        seed: input.seed,
        settings: input.settings,
      },
      Math.max(1, remaining()),
    );
    assertCurrent();

    if (generation.candidates.length === 0) {
      return {
        status: "unsupported",
        seed: input.seed,
        seedHash: generation.seedHash,
        flavour: input.flavour,
        cases: [],
        discarded: [],
        coverage: generation.coverage,
        unsupportedConstructs: generation.unsupportedConstructs,
        candidateAttempts: generation.candidateAttempts,
        engineExecutions: 0,
        generatedCandidateBytes: generation.candidateBytes,
        verifiedSubjectBytes: 0,
        wallTimeMs: elapsed(),
        ...(generation.stoppedReason
          ? { stoppedReason: generation.stoppedReason }
          : {}),
        warnings: generation.warnings,
      };
    }

    const cases: VerifiedGeneratedCase[] = [];
    const discarded: DiscardedGeneratedCandidate[] = [];
    const warnings = [...generation.warnings];
    let engine: RegexExecutionResult["engine"] | undefined;
    let engineExecutions = 0;
    let status: CaseGenerationResult["status"] = "complete";
    let stoppedReason = generation.stoppedReason;

    const discard = (
      candidate: GeneratedSubjectCandidate,
      reason: GenerationDiscardReason,
      detail: string,
    ) => {
      if (discarded.length < GENERATION_LIMITS.maximumDiscardedDiagnostics) {
        discarded.push({
          candidateId: candidate.id,
          intendedOutcome: candidate.intendedOutcome,
          reason,
          detail,
          subjectBytes: candidate.subjectBytes,
          subjectPreview: resultPreview(candidate.subject),
        });
      } else if (
        !warnings.some((warning) => warning.includes("discard diagnostics"))
      ) {
        warnings.push(
          `Advanced discard diagnostics stopped at ${GENERATION_LIMITS.maximumDiscardedDiagnostics.toLocaleString()} entries.`,
        );
      }
    };

    for (let index = 0; index < generation.candidates.length; index += 1) {
      assertCurrent();
      if (cases.length >= input.settings.maximumCases) {
        status = "partial";
        stoppedReason = `Verification retained the configured maximum of ${input.settings.maximumCases.toLocaleString()} cases.`;
        break;
      }
      const wallTimeRemaining = remaining();
      if (wallTimeRemaining <= 0) {
        status = "wall-time-limit";
        stoppedReason = `Generation stopped at the ${input.settings.maximumWallTimeMs.toLocaleString()} ms aggregate wall-time limit.`;
        const candidate = generation.candidates[
          index
        ] as GeneratedSubjectCandidate;
        discard(candidate, "wall-time-limit", stoppedReason);
        break;
      }
      const candidate = generation.candidates[
        index
      ] as GeneratedSubjectCandidate;
      onProgress?.({
        phase: "verifying",
        completed: index,
        total: generation.candidates.length,
        retained: cases.length,
        message: `Verifying candidate ${index + 1} of ${generation.candidates.length} through the selected actual engine.`,
      });
      const request: RegexExecutionRequest = {
        flavour: input.flavour,
        ...(input.flavourVersion === undefined
          ? {}
          : { flavourVersion: input.flavourVersion }),
        pattern: input.pattern,
        flags: input.flags,
        options: input.options,
        subject: candidate.subject,
        captureMetadata: input.captureMetadata,
        scanAll: input.scanAll,
        maximumMatches: 1,
        maximumCaptureRows: Math.max(
          1,
          Math.min(
            DEFAULT_REGEX_LIMITS.maximumCaptureRows,
            input.captureMetadata.length,
          ),
        ),
      };
      let execution: RegexExecutionResult;
      try {
        engineExecutions += 1;
        execution = await this.#engine.execute(
          request,
          Math.max(
            1,
            Math.min(input.settings.perCaseTimeoutMs, wallTimeRemaining),
          ),
        );
      } catch (error) {
        assertCurrent();
        if (error instanceof WorkerRequestError) {
          const failure = discardedReason(error);
          discard(candidate, failure.reason, failure.detail);
          if (remaining() <= 0) {
            status = "wall-time-limit";
            stoppedReason = `Generation stopped at the ${input.settings.maximumWallTimeMs.toLocaleString()} ms aggregate wall-time limit.`;
            break;
          }
          continue;
        }
        discard(
          candidate,
          "execution-error",
          error instanceof Error ? error.message : String(error),
        );
        continue;
      }
      assertCurrent();
      engine ??= execution.engine;
      if (!execution.accepted) {
        status = "compile-rejected";
        stoppedReason = `The selected actual engine rejected the pattern: ${executionDetail(execution)}`;
        discard(candidate, "compile-rejected", executionDetail(execution));
        break;
      }
      const matched = execution.matches.length > 0;
      const expectedMatch = candidate.intendedOutcome === "match";
      if (matched !== expectedMatch) {
        discard(
          candidate,
          matched ? "unexpected-match" : "unexpected-no-match",
          matched
            ? "The actual engine matched this intended negative candidate, so it was not presented as a negative test."
            : "The actual engine did not match this intended positive candidate, so it was not presented as a positive test.",
        );
        continue;
      }
      cases.push({
        id: candidate.id,
        name: generatedName(candidate, cases.length),
        subject: candidate.subject,
        subjectBytes: candidate.subjectBytes,
        expectation: matched ? "should-match" : "should-not-match",
        features: candidate.features,
        notes: candidate.notes,
        elapsedMs: execution.elapsedMs,
        matched,
        matchCount: execution.matches.length,
        provenance: {
          kind: "generated",
          generatorId: GENERATED_CASE_GENERATOR_ID,
          generatorVersion: GENERATED_CASE_GENERATOR_VERSION,
          seed: input.seed,
          candidateId: candidate.id,
          intendedOutcome: candidate.intendedOutcome,
        },
      });
    }

    onProgress?.({
      phase: "verifying",
      completed: Math.min(engineExecutions, generation.candidates.length),
      total: generation.candidates.length,
      retained: cases.length,
      message: `Retained ${cases.length.toLocaleString()} actual-engine-verified cases.`,
    });

    const positives = cases.filter(
      (candidate) => candidate.expectation === "should-match",
    ).length;
    const negatives = cases.length - positives;
    if (positives === 0) {
      warnings.push(
        "No intended positive candidate was confirmed by the selected actual engine.",
      );
    }
    if (negatives === 0) {
      warnings.push(
        "No intended negative candidate was confirmed by the selected actual engine.",
      );
    }
    if (
      status === "complete" &&
      (generation.candidatesTruncated ||
        generation.unsupportedConstructs.length > 0 ||
        generation.coverage.some((entry) => entry.status === "partial"))
    ) {
      status = "partial";
    }

    return {
      status,
      seed: input.seed,
      seedHash: generation.seedHash,
      flavour: input.flavour,
      cases,
      discarded,
      coverage: generation.coverage,
      unsupportedConstructs: generation.unsupportedConstructs,
      ...(engine ? { engine } : {}),
      candidateAttempts: generation.candidateAttempts,
      engineExecutions,
      generatedCandidateBytes: generation.candidateBytes,
      verifiedSubjectBytes: cases.reduce(
        (total, candidate) => total + candidate.subjectBytes,
        0,
      ),
      wallTimeMs: elapsed(),
      ...(stoppedReason ? { stoppedReason } : {}),
      warnings,
    };
  }

  cancel(): void {
    this.#revision += 1;
    this.#generator.cancel();
    this.#engine.cancel();
  }

  dispose(): void {
    if (this.#disposed) return;
    this.#disposed = true;
    this.#revision += 1;
    this.#generator.dispose();
    this.#engine.dispose();
  }
}
