import type { RegexMatchResult } from "../model/match";
import { lineAndColumnsAt } from "../execution/offsets/line-offset-map";
import { DEFAULT_REGEX_LIMITS } from "../execution/request-limits";

export type ListTemplateToken =
  | { readonly kind: "literal"; readonly value: string }
  | { readonly kind: "full-match" }
  | { readonly kind: "numbered-capture"; readonly number: number }
  | { readonly kind: "named-capture"; readonly name: string }
  | { readonly kind: "match-number" }
  | { readonly kind: "line-number" }
  | { readonly kind: "source-name" };

export interface ListTemplate {
  readonly source: string;
  readonly tokens: readonly ListTemplateToken[];
  readonly accepted?: boolean;
  readonly diagnostics?: readonly string[];
}

const TOKEN_PATTERN =
  /\$0|\$(?:1000|[1-9]\d{0,2})(?!\d)|\$\{[$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*\}|\{\{(?:match|line|source)\}\}/gu;
export const MAXIMUM_LIST_ROW_UTF16 = 64 * 1024;
export const MAXIMUM_LIST_TOTAL_UTF16 = 8 * 1024 * 1024;
export const MAXIMUM_LIST_RENDER_TOKEN_EVALUATIONS = 250_000;

export function parseListTemplate(source: string): ListTemplate {
  if (source.length > DEFAULT_REGEX_LIMITS.maximumListTemplateUtf16) {
    return {
      source,
      tokens: [],
      accepted: false,
      diagnostics: [
        `List template exceeds the ${DEFAULT_REGEX_LIMITS.maximumListTemplateUtf16.toLocaleString()} UTF-16 unit limit; it was not parsed or rendered.`,
      ],
    };
  }
  const tokens: ListTemplateToken[] = [];
  let cursor = 0;
  for (const match of source.matchAll(TOKEN_PATTERN)) {
    const index = match.index;
    if (index > cursor) {
      tokens.push({ kind: "literal", value: source.slice(cursor, index) });
    }
    const raw = match[0];
    if (raw === "$0") tokens.push({ kind: "full-match" });
    else if (raw === "{{match}}") tokens.push({ kind: "match-number" });
    else if (raw === "{{line}}") tokens.push({ kind: "line-number" });
    else if (raw === "{{source}}") tokens.push({ kind: "source-name" });
    else if (raw.startsWith("${")) {
      tokens.push({ kind: "named-capture", name: raw.slice(2, -1) });
    } else {
      tokens.push({
        kind: "numbered-capture",
        number: Number.parseInt(raw.slice(1), 10),
      });
    }
    cursor = index + raw.length;
  }
  if (cursor < source.length) {
    tokens.push({ kind: "literal", value: source.slice(cursor) });
  }
  return { source, tokens, accepted: true, diagnostics: [] };
}

export interface ListRow {
  readonly matchNumber: number;
  readonly value: string;
  readonly captures: Readonly<Record<string, string | null>>;
  readonly complete: boolean;
  readonly warnings: readonly string[];
}

export interface ListRenderResult {
  readonly rows: readonly ListRow[];
  readonly truncated: boolean;
  readonly sourceResultTruncated: boolean;
  readonly templateAccepted: boolean;
  readonly warnings: readonly string[];
}

export function renderListRows(
  template: ListTemplate,
  matches: readonly RegexMatchResult[],
  subject: string,
  sourceName = "test-text",
  sourceResultTruncated = false,
): ListRenderResult {
  if (
    template.accepted === false ||
    template.source.length > DEFAULT_REGEX_LIMITS.maximumListTemplateUtf16
  ) {
    return {
      rows: [],
      truncated: matches.length > 0 || sourceResultTruncated,
      sourceResultTruncated,
      templateAccepted: false,
      warnings: [
        template.diagnostics?.[0] ??
          `List template exceeds the ${DEFAULT_REGEX_LIMITS.maximumListTemplateUtf16.toLocaleString()} UTF-16 unit limit.`,
      ],
    };
  }
  const matchPositions = lineAndColumnsAt(
    subject,
    matches.map((match) => match.range.startUtf16),
  );
  const rows: ListRow[] = [];
  let remainingTotal = MAXIMUM_LIST_TOTAL_UTF16;
  let tokenEvaluations = 0;
  let stoppedForEvaluationLimit = false;
  for (const match of matches) {
    if (remainingTotal === 0) break;
    const warnings: string[] = [];
    let complete = true;
    const availableCaptureValue = (
      capture: RegexMatchResult["captures"][number],
    ): string | undefined => {
      if (capture.status === "did-not-participate") return undefined;
      if (capture.status === "truncated") {
        complete = false;
        warnings.push(
          `Group ${capture.groupNumber} exceeded the value-preview limit; its clipped preview was not exported.`,
        );
        return undefined;
      }
      return capture.value;
    };
    const availableValues = new Map(
      match.captures.map((capture) => [
        capture.groupNumber,
        availableCaptureValue(capture),
      ]),
    );
    const namedCaptures = new Map<
      string,
      RegexMatchResult["captures"][number]
    >();
    for (const capture of match.captures) {
      if (capture.groupName === undefined) continue;
      const existing = namedCaptures.get(capture.groupName);
      if (
        existing === undefined ||
        (existing.status === "did-not-participate" &&
          capture.status !== "did-not-participate")
      ) {
        namedCaptures.set(capture.groupName, capture);
      }
    }
    const byName = new Map(
      [...namedCaptures].map(([name, capture]) => [
        name,
        availableValues.get(capture.groupNumber),
      ]),
    );
    let remainingRow = Math.min(MAXIMUM_LIST_ROW_UTF16, remainingTotal);
    const parts: string[] = [];
    const append = (part: string) => {
      const accepted = part.slice(0, remainingRow);
      if (accepted.length > 0) parts.push(accepted);
      remainingRow -= accepted.length;
      remainingTotal -= accepted.length;
      if (accepted.length !== part.length) {
        complete = false;
        warnings.push(
          `Generated row exceeded the ${MAXIMUM_LIST_ROW_UTF16.toLocaleString()} UTF-16 unit preview limit.`,
        );
      }
    };
    for (const templateToken of template.tokens) {
      if (tokenEvaluations >= MAXIMUM_LIST_RENDER_TOKEN_EVALUATIONS) {
        complete = false;
        stoppedForEvaluationLimit = true;
        warnings.push(
          `Template evaluation stopped at the ${MAXIMUM_LIST_RENDER_TOKEN_EVALUATIONS.toLocaleString()} token-operation limit.`,
        );
        break;
      }
      tokenEvaluations += 1;
      if (remainingRow === 0) {
        complete = false;
        break;
      }
      switch (templateToken.kind) {
        case "literal":
          append(templateToken.value);
          break;
        case "full-match":
          if (match.valueStatus === "truncated") {
            complete = false;
            warnings.push(
              "The full match exceeded the value-preview limit; its clipped preview was not exported.",
            );
          } else {
            append(match.value);
          }
          break;
        case "numbered-capture":
          append(availableValues.get(templateToken.number) ?? "");
          break;
        case "named-capture":
          append(byName.get(templateToken.name) ?? "");
          break;
        case "match-number":
          append(String(match.matchNumber));
          break;
        case "line-number":
          append(
            String(matchPositions.get(match.range.startUtf16)?.line ?? ""),
          );
          break;
        case "source-name":
          append(sourceName);
          break;
      }
    }
    const captures: Record<string, string | null> = {};
    for (const capture of match.captures) {
      const value = availableValues.get(capture.groupNumber);
      captures[String(capture.groupNumber)] = value ?? null;
    }
    for (const [name, capture] of namedCaptures) {
      captures[name] = availableValues.get(capture.groupNumber) ?? null;
    }
    rows.push({
      matchNumber: match.matchNumber,
      value: parts.join(""),
      captures,
      complete,
      warnings: [...new Set(warnings)],
    });
    if (stoppedForEvaluationLimit) break;
  }
  const truncated =
    sourceResultTruncated ||
    rows.length !== matches.length ||
    stoppedForEvaluationLimit;
  const warnings: string[] = [];
  if (sourceResultTruncated) {
    warnings.push(
      "The engine stopped collecting matches at its configured result limit; generated rows cover only the returned prefix.",
    );
  }
  if (stoppedForEvaluationLimit) {
    warnings.push(
      `List generation stopped at the ${MAXIMUM_LIST_RENDER_TOKEN_EVALUATIONS.toLocaleString()} token-operation limit.`,
    );
  } else if (rows.length !== matches.length) {
    warnings.push(
      `List generation stopped at the ${MAXIMUM_LIST_TOTAL_UTF16.toLocaleString()} UTF-16 unit materialization limit.`,
    );
  }
  return {
    rows,
    truncated,
    sourceResultTruncated,
    templateAccepted: true,
    warnings,
  };
}

function csvCell(value: string | number): string {
  const text = String(value);
  return /[",\r\n]/u.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
}

export function exportListRows(
  rows: readonly ListRow[],
  format: "text" | "csv" | "json" | "ndjson",
): string {
  switch (format) {
    case "text":
      return rows.map((row) => row.value).join("\n");
    case "csv":
      return [
        "match,value",
        ...rows.map(
          (row) => `${csvCell(row.matchNumber)},${csvCell(row.value)}`,
        ),
      ].join("\r\n");
    case "json":
      return `${JSON.stringify(rows, null, 2)}\n`;
    case "ndjson":
      return `${rows.map((row) => JSON.stringify(row)).join("\n")}\n`;
  }
}
