import type { EngineSupervisor } from "../execution/EngineSupervisor";
import {
  DEFAULT_REGEX_LIMITS,
  utf8ByteLength,
} from "../execution/request-limits";
import { WorkerRequestError } from "../execution/WorkerSupervisor";
import type { RegexEngineOptions, RegexFlavourId } from "../model/flavour";
import type { RegexExecutionResult, RegexMatchResult } from "../model/match";
import type { CaptureDefinition } from "../model/syntax";
import { countCorpusLines, splitCorpusLines } from "./corpus-lines";
import type {
  CorpusBatchResult,
  CorpusCaptureSummary,
  CorpusDocument,
  CorpusDocumentResult,
  CorpusOperation,
  CorpusProgress,
  CorpusSubjectMode,
} from "./corpus.types";

const MAXIMUM_DIAGNOSTICS_PER_DOCUMENT = 100;

export interface CorpusRunConfiguration {
  readonly flavour: RegexFlavourId;
  readonly flavourVersion?: string;
  readonly options: RegexEngineOptions;
  readonly pattern: string;
  readonly flags: readonly string[];
  readonly captureMetadata: readonly CaptureDefinition[];
  readonly replacement: string;
  readonly operation: CorpusOperation;
  readonly subjectMode: CorpusSubjectMode;
  readonly timeoutMs: number;
}

export interface CorpusRunDependencies {
  readonly engine: EngineSupervisor;
  readonly signal: AbortSignal;
  readonly onProgress?: (progress: CorpusProgress) => void;
  readonly now?: () => number;
}

interface MutableCaptureSummary {
  groupNumber: number;
  groupName?: string;
  participated: number;
  didNotParticipate: number;
  matchedEmpty: number;
  truncated: number;
  unavailable: number;
  samples: string[];
  samplesClipped: boolean;
}

function emptyCaptureSummary(
  groupNumber: number,
  groupName?: string,
): MutableCaptureSummary {
  return {
    groupNumber,
    ...(groupName === undefined ? {} : { groupName }),
    participated: 0,
    didNotParticipate: 0,
    matchedEmpty: 0,
    truncated: 0,
    unavailable: 0,
    samples: [],
    samplesClipped: false,
  };
}

function retainCaptureSample(
  summary: MutableCaptureSummary,
  value: string | undefined,
  engineClipped: boolean,
): void {
  if (value === undefined) return;
  const clipped =
    engineClipped ||
    value.length > DEFAULT_REGEX_LIMITS.maximumCorpusCaptureSampleUtf16;
  summary.samplesClipped ||= clipped;
  if (
    summary.samples.length <
    DEFAULT_REGEX_LIMITS.maximumCorpusCaptureSamplesPerGroup
  ) {
    summary.samples.push(
      value.slice(0, DEFAULT_REGEX_LIMITS.maximumCorpusCaptureSampleUtf16),
    );
  }
}

function addMatchCaptureSummaries(
  matches: readonly RegexMatchResult[],
  summaries: Map<number, MutableCaptureSummary>,
): boolean {
  let truncated = false;
  const summaryFor = (
    groupNumber: number,
    groupName?: string,
  ): MutableCaptureSummary | undefined => {
    const existing = summaries.get(groupNumber);
    if (existing) return existing;
    if (summaries.size >= DEFAULT_REGEX_LIMITS.maximumCorpusCaptureSummaries) {
      truncated = true;
      return undefined;
    }
    const created = emptyCaptureSummary(groupNumber, groupName);
    summaries.set(groupNumber, created);
    return created;
  };

  for (const match of matches) {
    const fullMatch = summaryFor(0);
    if (fullMatch) {
      if (match.valueStatus === "truncated") {
        fullMatch.truncated += 1;
      } else if (match.value.length === 0) {
        fullMatch.matchedEmpty += 1;
      } else {
        fullMatch.participated += 1;
      }
      retainCaptureSample(
        fullMatch,
        match.value,
        match.valueStatus === "truncated",
      );
    }
    for (const capture of match.captures) {
      const summary = summaryFor(capture.groupNumber, capture.groupName);
      if (!summary) continue;
      switch (capture.status) {
        case "participated":
          summary.participated += 1;
          break;
        case "did-not-participate":
          summary.didNotParticipate += 1;
          break;
        case "matched-empty":
          summary.matchedEmpty += 1;
          break;
        case "truncated":
          summary.truncated += 1;
          break;
        case "unavailable":
          summary.unavailable += 1;
          break;
      }
      retainCaptureSample(
        summary,
        capture.value,
        capture.status === "truncated",
      );
    }
  }
  return truncated;
}

function frozenCaptureSummaries(
  summaries: ReadonlyMap<number, MutableCaptureSummary>,
): readonly CorpusCaptureSummary[] {
  return [...summaries.values()]
    .sort((left, right) => left.groupNumber - right.groupNumber)
    .map((summary) => ({
      ...summary,
      samples: [...summary.samples],
    }));
}

function pendingResult(
  document: CorpusDocument,
  lineCount: number,
  status: "cancelled" | "not-run",
  message: string,
): CorpusDocumentResult {
  return {
    documentId: document.id,
    name: document.name,
    inputBytes: document.bytes,
    lineCount,
    status,
    matchCount: 0,
    elapsedMs: 0,
    captureSummaries: [],
    captureSummariesTruncated: false,
    diagnostics: [],
    message,
  };
}

function aggregateResult(
  configuration: CorpusRunConfiguration,
  documents: readonly CorpusDocument[],
  results: readonly CorpusDocumentResult[],
  cancelled: boolean,
  message?: string,
): CorpusBatchResult {
  const processed = results.filter(
    (result) =>
      result.status === "complete" ||
      result.status === "partial" ||
      result.status === "error",
  ).length;
  const partial =
    results.some((result) => result.status !== "complete") ||
    processed !== documents.length;
  return {
    operation: configuration.operation,
    subjectMode: configuration.subjectMode,
    status: cancelled ? "cancelled" : partial ? "partial" : "complete",
    documents: results,
    processedDocuments: processed,
    totalDocuments: documents.length,
    totalMatches: results.reduce((sum, result) => sum + result.matchCount, 0),
    totalElapsedMs: results.reduce((sum, result) => sum + result.elapsedMs, 0),
    retainedOutputBytes: results.reduce(
      (sum, result) => sum + (result.outputBytes ?? 0),
      0,
    ),
    message:
      message ??
      (partial
        ? "The batch completed with incomplete documents. Review each status before exporting."
        : `Processed ${documents.length.toLocaleString()} documents locally.`),
  };
}

function wallTimeAbort(signal: AbortSignal): boolean {
  return signal.reason === "corpus-wall-time";
}

function errorMessage(error: unknown): string {
  if (error instanceof WorkerRequestError && error.kind === "timeout") {
    return `${error.message}. The line/document worker was terminated and recreated for subsequent work.`;
  }
  return error instanceof Error ? error.message : String(error);
}

function addDiagnostics(
  execution: RegexExecutionResult,
  diagnostics: RegexExecutionResult["diagnostics"][number][],
  keys: Set<string>,
): void {
  for (const diagnostic of execution.diagnostics) {
    const key = `${diagnostic.severity}\0${diagnostic.code}\0${diagnostic.message}`;
    if (keys.has(key)) continue;
    keys.add(key);
    if (diagnostics.length < MAXIMUM_DIAGNOSTICS_PER_DOCUMENT) {
      diagnostics.push(diagnostic);
    }
  }
}

function preflightLineCounts(
  documents: readonly CorpusDocument[],
  subjectMode: CorpusSubjectMode,
): readonly number[] {
  const counts = documents.map((document) => countCorpusLines(document.text));
  if (subjectMode === "line") {
    const total = counts.reduce((sum, count) => sum + count, 0);
    if (total > DEFAULT_REGEX_LIMITS.maximumCorpusLines) {
      throw new Error(
        `Independent-line mode is limited to ${DEFAULT_REGEX_LIMITS.maximumCorpusLines.toLocaleString()} logical lines per batch.`,
      );
    }
  }
  return counts;
}

export async function runCorpusBatch(
  documents: readonly CorpusDocument[],
  configuration: CorpusRunConfiguration,
  dependencies: CorpusRunDependencies,
): Promise<CorpusBatchResult> {
  if (documents.length === 0) {
    throw new Error("Add at least one corpus document before running.");
  }
  if (documents.length > DEFAULT_REGEX_LIMITS.maximumCorpusDocuments) {
    throw new Error("The corpus document limit was exceeded.");
  }
  const lineCounts = preflightLineCounts(documents, configuration.subjectMode);
  const now = dependencies.now ?? (() => performance.now());
  const startedAt = now();
  let retainedOutputBytes = 0;
  let retainedMatches = 0;
  const results: CorpusDocumentResult[] = [];
  dependencies.onProgress?.({
    completedDocuments: 0,
    totalDocuments: documents.length,
    currentDocumentName: documents[0]?.name,
    totalMatches: 0,
  });

  const finishStoppedBatch = (
    fromIndex: number,
    kind: "cancelled" | "wall-time" | "match-limit",
    currentResult?: CorpusDocumentResult,
  ): CorpusBatchResult => {
    const beginning = currentResult
      ? [...results, currentResult]
      : [...results];
    const remainingStart = currentResult ? fromIndex + 1 : fromIndex;
    const status = kind === "cancelled" ? "cancelled" : "not-run";
    const reason =
      kind === "cancelled"
        ? "Not run because the batch was cancelled."
        : kind === "wall-time"
          ? "Not run because the aggregate corpus wall-time limit was reached."
          : "Not run because the aggregate corpus match limit was reached.";
    const remaining = documents
      .slice(remainingStart)
      .map((document, offset) =>
        pendingResult(
          document,
          lineCounts[remainingStart + offset] ?? 1,
          status,
          reason,
        ),
      );
    const message =
      kind === "cancelled"
        ? "Corpus processing was cancelled. Completed rows remain labelled and no incomplete applied output can be exported."
        : kind === "wall-time"
          ? `Corpus processing stopped at the ${DEFAULT_REGEX_LIMITS.maximumCorpusWallTimeMs / 1_000} second aggregate wall-time limit.`
          : `Corpus processing stopped at the ${DEFAULT_REGEX_LIMITS.maximumCorpusMatches.toLocaleString()} retained-match limit.`;
    return aggregateResult(
      configuration,
      documents,
      [...beginning, ...remaining],
      kind === "cancelled",
      message,
    );
  };

  for (const [documentIndex, document] of documents.entries()) {
    if (dependencies.signal.aborted) {
      return finishStoppedBatch(
        documentIndex,
        wallTimeAbort(dependencies.signal) ? "wall-time" : "cancelled",
      );
    }
    if (now() - startedAt >= DEFAULT_REGEX_LIMITS.maximumCorpusWallTimeMs) {
      dependencies.engine.cancel();
      return finishStoppedBatch(documentIndex, "wall-time");
    }
    if (retainedMatches >= DEFAULT_REGEX_LIMITS.maximumCorpusMatches) {
      return finishStoppedBatch(documentIndex, "match-limit");
    }

    const segments =
      configuration.subjectMode === "line"
        ? splitCorpusLines(document.text)
        : [{ text: document.text, ending: "" }];
    const captureSummaries = new Map<number, MutableCaptureSummary>();
    const diagnostics: RegexExecutionResult["diagnostics"][number][] = [];
    const diagnosticKeys = new Set<string>();
    const outputChunks: string[] = [];
    const errors: string[] = [];
    let matchCount = 0;
    let elapsedMs = 0;
    let outputBytes = 0;
    let completedSegments = 0;
    let successfulSegments = 0;
    let partial = false;
    let fatal = false;
    let captureSummariesTruncated = false;
    let stoppedByMatchLimit = false;

    for (const segment of segments) {
      if (dependencies.signal.aborted) {
        const stopKind = wallTimeAbort(dependencies.signal)
          ? "wall-time"
          : "cancelled";
        const currentResult: CorpusDocumentResult = {
          documentId: document.id,
          name: document.name,
          inputBytes: document.bytes,
          lineCount: lineCounts[documentIndex] ?? 1,
          status: stopKind === "cancelled" ? "cancelled" : "partial",
          matchCount,
          elapsedMs,
          ...(configuration.operation === "replace"
            ? {
                output: outputChunks.join(""),
                outputBytes,
              }
            : {}),
          captureSummaries: frozenCaptureSummaries(captureSummaries),
          captureSummariesTruncated,
          diagnostics,
          message:
            stopKind === "cancelled"
              ? "Document processing was cancelled before completion."
              : "Document processing stopped at the aggregate wall-time limit.",
        };
        return finishStoppedBatch(documentIndex, stopKind, currentResult);
      }
      if (now() - startedAt >= DEFAULT_REGEX_LIMITS.maximumCorpusWallTimeMs) {
        dependencies.engine.cancel();
        const currentResult: CorpusDocumentResult = {
          documentId: document.id,
          name: document.name,
          inputBytes: document.bytes,
          lineCount: lineCounts[documentIndex] ?? 1,
          status: "partial",
          matchCount,
          elapsedMs,
          ...(configuration.operation === "replace"
            ? { output: outputChunks.join(""), outputBytes }
            : {}),
          captureSummaries: frozenCaptureSummaries(captureSummaries),
          captureSummariesTruncated,
          diagnostics,
          message:
            "Document processing stopped at the aggregate wall-time limit.",
        };
        return finishStoppedBatch(documentIndex, "wall-time", currentResult);
      }

      const remainingMatches =
        DEFAULT_REGEX_LIMITS.maximumCorpusMatches - retainedMatches;
      if (remainingMatches <= 0) {
        stoppedByMatchLimit = true;
        partial = true;
        break;
      }
      const commonRequest = {
        flavour: configuration.flavour,
        ...(configuration.flavourVersion === undefined
          ? {}
          : { flavourVersion: configuration.flavourVersion }),
        pattern: configuration.pattern,
        flags: configuration.flags,
        options: configuration.options,
        subject: segment.text,
        captureMetadata: configuration.captureMetadata,
        // Corpus processing is exhaustive by definition. Each adapter maps
        // scanAll to its own iteration semantics without changing saved flags.
        scanAll: true,
        maximumMatches: Math.min(
          DEFAULT_REGEX_LIMITS.maximumMatches,
          remainingMatches,
        ),
        maximumCaptureRows: DEFAULT_REGEX_LIMITS.maximumCaptureRows,
      } as const;

      try {
        let execution: RegexExecutionResult;
        if (configuration.operation === "replace") {
          const endingBytes = utf8ByteLength(segment.ending);
          const remainingOutput =
            DEFAULT_REGEX_LIMITS.maximumCorpusOutputBytes -
            retainedOutputBytes -
            endingBytes;
          const replacementResult = await dependencies.engine.replace(
            {
              ...commonRequest,
              replacement: configuration.replacement,
              maximumOutputBytes: Math.max(0, remainingOutput),
            },
            configuration.timeoutMs,
          );
          execution = replacementResult.execution;
          outputChunks.push(replacementResult.output);
          outputBytes += replacementResult.outputBytes;
          retainedOutputBytes += replacementResult.outputBytes;
          if (
            !replacementResult.outputTruncated &&
            retainedOutputBytes + endingBytes <=
              DEFAULT_REGEX_LIMITS.maximumCorpusOutputBytes
          ) {
            outputChunks.push(segment.ending);
            outputBytes += endingBytes;
            retainedOutputBytes += endingBytes;
          } else if (segment.ending.length > 0) {
            partial = true;
          }
          partial ||= replacementResult.truncated;
        } else {
          execution = await dependencies.engine.execute(
            commonRequest,
            configuration.timeoutMs,
          );
        }

        addDiagnostics(execution, diagnostics, diagnosticKeys);
        elapsedMs += execution.elapsedMs;
        if (!execution.accepted) {
          fatal = true;
          errors.push(
            execution.diagnostics[0]?.message ??
              "The execution engine rejected the pattern.",
          );
          break;
        }
        successfulSegments += 1;
        completedSegments += 1;
        matchCount += execution.matches.length;
        retainedMatches += execution.matches.length;
        partial ||= execution.truncated;
        captureSummariesTruncated ||= addMatchCaptureSummaries(
          execution.matches,
          captureSummaries,
        );
        if (
          retainedMatches >= DEFAULT_REGEX_LIMITS.maximumCorpusMatches &&
          (completedSegments < segments.length ||
            documentIndex + 1 < documents.length)
        ) {
          stoppedByMatchLimit = true;
          partial ||= completedSegments < segments.length;
          break;
        }
      } catch (error) {
        if (
          dependencies.signal.aborted ||
          (error instanceof WorkerRequestError && error.kind === "cancelled")
        ) {
          const stopKind = wallTimeAbort(dependencies.signal)
            ? "wall-time"
            : "cancelled";
          const currentResult: CorpusDocumentResult = {
            documentId: document.id,
            name: document.name,
            inputBytes: document.bytes,
            lineCount: lineCounts[documentIndex] ?? 1,
            status: stopKind === "cancelled" ? "cancelled" : "partial",
            matchCount,
            elapsedMs,
            ...(configuration.operation === "replace"
              ? { output: outputChunks.join(""), outputBytes }
              : {}),
            captureSummaries: frozenCaptureSummaries(captureSummaries),
            captureSummariesTruncated,
            diagnostics,
            message:
              stopKind === "cancelled"
                ? "Document processing was cancelled before completion."
                : "Document processing stopped at the aggregate wall-time limit.",
          };
          return finishStoppedBatch(documentIndex, stopKind, currentResult);
        }
        errors.push(errorMessage(error));
        partial = true;
        completedSegments += 1;
        if (configuration.operation === "replace") {
          const original = `${segment.text}${segment.ending}`;
          const originalBytes = utf8ByteLength(original);
          if (
            retainedOutputBytes + originalBytes <=
            DEFAULT_REGEX_LIMITS.maximumCorpusOutputBytes
          ) {
            outputChunks.push(original);
            outputBytes += originalBytes;
            retainedOutputBytes += originalBytes;
          }
        }
      }
    }

    const output =
      configuration.operation === "replace" ? outputChunks.join("") : undefined;
    const status =
      (fatal || errors.length > 0) && successfulSegments === 0
        ? "error"
        : partial || completedSegments < segments.length || errors.length > 0
          ? "partial"
          : "complete";
    const messages = [
      ...errors,
      ...(captureSummariesTruncated
        ? [
            `Capture summaries are limited to ${DEFAULT_REGEX_LIMITS.maximumCorpusCaptureSummaries.toLocaleString()} groups per document.`,
          ]
        : []),
      ...(stoppedByMatchLimit
        ? ["Processing stopped at the aggregate corpus match limit."]
        : []),
    ];
    results.push({
      documentId: document.id,
      name: document.name,
      inputBytes: document.bytes,
      lineCount: lineCounts[documentIndex] ?? 1,
      status,
      matchCount,
      elapsedMs,
      ...(output === undefined
        ? {}
        : {
            output,
            outputBytes,
            changed: status === "complete" && output !== document.text,
          }),
      captureSummaries: frozenCaptureSummaries(captureSummaries),
      captureSummariesTruncated,
      diagnostics,
      ...(messages.length === 0
        ? {}
        : {
            message: messages.join(" "),
          }),
    });
    dependencies.onProgress?.({
      completedDocuments: documentIndex + 1,
      totalDocuments: documents.length,
      ...(documents[documentIndex + 1]
        ? { currentDocumentName: documents[documentIndex + 1]?.name }
        : {}),
      totalMatches: retainedMatches,
    });
    if (stoppedByMatchLimit && documentIndex + 1 < documents.length) {
      return finishStoppedBatch(documentIndex + 1, "match-limit");
    }
  }

  return aggregateResult(configuration, documents, results, false);
}
