// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

/**
 * Shared frontmatter parsing utilities.
 *
 * Provides a single, canonical YAML-subset frontmatter parser used by both
 * the stash open logic and the metadata generator.
 */

/**
 * Parse YAML-subset frontmatter from a Markdown (or similar) string.
 *
 * Returns the parsed key-value data and the remaining body content.
 *
 * **Limitations**: This is a hand-rolled YAML-subset parser with intentional
 * constraints for simplicity and safety:
 * - **Top-level values**: string, boolean, and number scalars are supported,
 *   as well as top-level list-valued keys using YAML block sequences
 *   (`- item`) or flow arrays (`[a, b, c]`).
 * - **List item types**: list items must be scalar values and may be strings,
 *   booleans, or numbers.
 * - **No nested objects beyond one level**: Only a single level of indented
 *   key-value pairs is supported.
 * - **Block scalars**: `|` (literal), `|-` (strip), and `|+` (keep) block
 *   scalars are supported for multi-line string values as emitted by the
 *   `yaml` library's `stringify`.
 */
export function parseFrontmatter(raw: string): {
  data: Record<string, unknown>;
  content: string;
  frontmatter: string | null;
  bodyStartLine: number;
} {
  const parsedBlock = parseFrontmatterBlock(raw);
  if (!parsedBlock) {
    return { data: {}, content: raw, frontmatter: null, bodyStartLine: 1 };
  }

  const data: Record<string, unknown> = {};
  let currentKey: string | null = null;
  /**
   * "scalar" | "list" | "object" | "pending" | "block" —
   * "pending" means empty value, mode determined by next line.
   * "block" means we are accumulating lines for a `|`-block scalar.
   */
  let mode: "scalar" | "list" | "object" | "pending" | "block" = "scalar";
  let nested: Record<string, unknown> | null = null;
  let currentList: unknown[] | null = null;
  /** Lines collected while in "block" mode. */
  let blockLines: string[] | null = null;
  /** Block scalar chomping: "clip" (|), "strip" (|-), "keep" (|+). */
  let blockChomping: "clip" | "strip" | "keep" = "clip";

  const flushPending = () => {
    // Called when we start a new top-level key and the previous key was still "pending".
    // An empty-value key followed by another top-level key means it was an empty scalar.
    if (mode === "pending" && currentKey !== null) {
      data[currentKey] = "";
    }
  };

  const flushBlock = () => {
    // Commit the accumulated block-scalar lines to `data[currentKey]`.
    if (mode !== "block" || currentKey === null || blockLines === null) return;
    // De-indent: strip the common 2-space prefix `yaml.stringify` emits.
    const deindented = blockLines.map((l) => (l.startsWith("  ") ? l.slice(2) : l));
    // Chomping: apply trailing-newline policy.
    // "clip" (|): single trailing newline.
    // "strip" (|-): no trailing newline.
    // "keep" (|+): keep all trailing newlines as-is.
    if (blockChomping === "keep") {
      data[currentKey] = deindented.join("\n");
    } else if (blockChomping === "strip") {
      data[currentKey] = deindented.join("\n").replace(/\n+$/, "");
    } else {
      // "clip": exactly one trailing newline
      data[currentKey] = `${deindented.join("\n").replace(/\n+$/, "")}\n`;
    }
  };

  for (const line of parsedBlock.frontmatter.split(/\r?\n/)) {
    // If we are in block-scalar mode, collect indented lines or end the block.
    if (mode === "block") {
      if (line.startsWith("  ") || line === "") {
        // Continuation of the block scalar (indented content or blank line).
        (blockLines as string[]).push(line);
        continue;
      }
      // Non-indented line ends the block scalar — flush and fall through to
      // parse the new line as a top-level key.
      flushBlock();
      mode = "scalar";
      blockLines = null;
    }

    // Block-sequence item: "- value" or "  - value" (optional 2-space indent)
    // Only match when the current key is in list or pending mode.
    const seqItem = line.match(/^(?: {2})?- (.*)$/);
    if (seqItem && currentKey !== null && (mode === "list" || mode === "pending")) {
      if (mode === "pending") {
        // First block-sequence item after an empty-value key — switch to list mode
        currentList = [];
        data[currentKey] = currentList;
        mode = "list";
      }
      (currentList as unknown[]).push(parseYamlScalar(seqItem[1].trim()));
      continue;
    }

    // Plain-style multi-line scalar continuation: a 2-space-indented line that
    // is not a sequence item or nested key. YAML plain scalars fold newlines
    // into a single space, so we append with a space. This handles LLM-emitted
    // descriptions like:
    //   description: Use 4-colon outer containers when mixing
    //     nesting depths in markdown-it-container plugins.
    // Without this, only the first line is captured and the truncation
    // heuristic wrongly flags it as cut off mid-sentence.
    if (mode === "scalar" && currentKey !== null && /^ {2}\S/.test(line)) {
      data[currentKey] = `${String(data[currentKey])} ${line.trim()}`;
      continue;
    }

    // Indented nested key-value (object under a key with empty value)
    const indented = line.match(/^ {2}(\w[\w-]*):\s*(.+)$/);
    if (indented && currentKey !== null && (mode === "object" || mode === "pending")) {
      if (mode === "pending") {
        // First indented k-v after an empty-value key — switch to object mode
        nested = {};
        data[currentKey] = nested;
        mode = "object";
      }
      (nested as Record<string, unknown>)[indented[1]] = parseYamlScalar(indented[2].trim());
      continue;
    }

    // Top-level key (possibly with inline value)
    const top = line.match(/^(\w[\w-]*):\s*(.*)$/);
    if (!top) {
      continue;
    }

    // Starting a new top-level key — flush any pending empty-value key
    flushPending();

    currentKey = top[1];
    const value = top[2].trim();

    if (value === "|" || value === "|-" || value === "|+") {
      // Block scalar header — collect subsequent indented lines.
      mode = "block";
      blockLines = [];
      blockChomping = value === "|-" ? "strip" : value === "|+" ? "keep" : "clip";
      nested = null;
      currentList = null;
    } else if (value === "") {
      // Defer mode decision until we see the next line
      mode = "pending";
      nested = null;
      currentList = null;
      // Don't store anything yet — flushPending will set "" if no continuation
    } else if (value.startsWith("[") && value.endsWith("]")) {
      // Inline flow array: tags: [ops, networking]
      mode = "list";
      nested = null;
      currentList = null;
      currentList = parseFlowArray(value);
      data[currentKey] = currentList;
    } else {
      mode = "scalar";
      nested = null;
      currentList = null;
      data[currentKey] = parseYamlScalar(value);
    }
  }

  // Flush any in-progress block scalar at end of frontmatter.
  if (mode === "block") {
    flushBlock();
  }

  // Flush the last key if it was still pending (empty value, no continuation)
  flushPending();

  return {
    data,
    content: parsedBlock.content,
    frontmatter: parsedBlock.frontmatter,
    bodyStartLine: parsedBlock.bodyStartLine,
  };
}

/**
 * Parse a YAML flow array string like `[a, b, c]` into an array of scalars.
 */
function parseFlowArray(value: string): unknown[] {
  const inner = value.slice(1, -1).trim();
  if (!inner) return [];
  return inner.split(",").map((item) => parseYamlScalar(item.trim()));
}

export function parseFrontmatterBlock(
  raw: string,
): { frontmatter: string; content: string; bodyStartLine: number } | null {
  // Handle both LF and CRLF line endings throughout.
  // The closing --- may be preceded by \r\n; capture and strip trailing \r
  // from the frontmatter block so key parsing sees clean LF-terminated lines.
  const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r\n|\r|\n|$)([\s\S]*)$/);
  if (!match) return null;
  // Strip any \r characters from the frontmatter block to normalise CRLF → LF
  const frontmatter = match[1].replace(/\r/g, "");
  const content = match[2];
  return {
    frontmatter,
    content,
    bodyStartLine: countLines(raw.slice(0, match[0].length - match[2].length)) + 1,
  };
}

function countLines(text: string): number {
  if (text.length === 0) return 0;
  return text.split(/\r?\n/).length - 1;
}

/**
 * Parse a simple YAML scalar value (string, boolean, or number).
 */
export function parseYamlScalar(value: string): unknown {
  if (value === "") return "";
  if (value === "true") return true;
  if (value === "false") return false;
  const asNumber = Number(value);
  if (!Number.isNaN(asNumber)) return asNumber;
  if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
    return value.slice(1, -1);
  }
  return value;
}
