/*
 * Nextra <Tabs> blocks mix JSX tags with Markdown children. That boundary is
 * fragile: MDX may accept a Tabs block where the tags, fenced code blocks, and
 * following prose are adjacent, but Prettier will not necessarily recover the
 * safer block structure for us.
 *
 * For example, this input has the unsafe adjacency we want to reject. The
 * fenced JavaScript is intentionally unformatted so the Prettier behavior is
 * visible:
 *
 * <Tabs items={["Code"]}>
 *   <Tabs.Tab>
 * ```js
 * const value={a:1};
 * ```
 *   </Tabs.Tab></Tabs>
 * Next paragraph.
 *
 * Running `prettier --parser mdx` on that input produces this:
 *
 * <Tabs items={["Code"]}>
 *   <Tabs.Tab>
 * ```js
 * const value={a:1};
 * ```
 *   </Tabs.Tab></Tabs>
 * Next paragraph.
 *
 * The formatting problem is that Prettier does not treat the fenced block as a
 * normal Markdown code fence in a Tabs panel: the JavaScript stays unformatted,
 * the fence remains glued to <Tabs.Tab>, the closing tags remain collapsed, and
 * the next paragraph remains glued to </Tabs>.
 *
 * With clear JSX/Markdown boundaries, Prettier formats the fenced JavaScript and
 * keeps the Tabs block readable:
 *
 * <Tabs items={["Code"]}>
 *   <Tabs.Tab>
 *
 * ```js
 * const value = { a: 1 };
 * ```
 *
 *   </Tabs.Tab>
 * </Tabs>
 *
 * Next paragraph.
 *
 * This is intentionally not a general Markdown style rule. It only protects the
 * Nextra Tabs shapes that can survive Prettier in a bad form.
 */

const mdxSourceByFilename = new Map();
const mdxTabsSpacingProcessor = {
  meta: {
    name: 'internal-rules/mdx-tabs-spacing',
  },
  preprocess(text, filename) {
    mdxSourceByFilename.set(filename, text);
    return [''];
  },
  postprocess(messageLists, filename) {
    const source = mdxSourceByFilename.get(filename);
    mdxSourceByFilename.delete(filename);

    return [
      ...messageLists.flat(),
      ...(source == null ? [] : checkMdxTabsSpacing(source)),
    ];
  },
  supportsAutofix: false,
};

function checkMdxTabsSpacing(source) {
  const lines = source.split(/\r?\n/u);
  const messages = [];

  for (let index = 0; index < lines.length; index++) {
    const line = lines[index];
    const trimmed = line.trim();

    // Prettier can collapse or preserve this one-line close in a way that makes
    // later conflict resolution hard to read and easy to break again. Keeping the
    // component close and container close on separate lines gives Markdown a
    // clear block boundary after the final tab panel.
    if (/<\/Tabs\.Tab>\s*<\/Tabs>/.test(line)) {
      report(
        messages,
        lines,
        index,
        line.indexOf('</Tabs.Tab>'),
        'Close </Tabs.Tab> and </Tabs> on separate lines.',
      );
    }

    // A <Tabs> block should start as its own Markdown block. If it is glued to
    // the previous paragraph/list item, MDX still accepts the file, but Prettier
    // can treat the JSX and surrounding Markdown as one construct and preserve
    // surprising layout.
    if (isTabsOpen(trimmed) && index > 0 && !isBlank(lines[index - 1])) {
      report(
        messages,
        lines,
        index,
        line.indexOf('<Tabs'),
        'Add a blank line before <Tabs> blocks.',
      );
    }

    // The closing </Tabs> needs the same protection in the other direction. The
    // bug this guard was added for was exactly a closing Tabs block followed by
    // prose with no blank line; that made the following paragraph part of the
    // same MDX flow and led Prettier to keep an unsafe shape.
    if (
      isTabsClose(trimmed) &&
      index < lines.length - 1 &&
      !isBlank(lines[index + 1])
    ) {
      report(
        messages,
        lines,
        index,
        line.indexOf('</Tabs>'),
        'Add a blank line after </Tabs> blocks.',
      );
    }

    // Fenced code inside a JSX child is only unambiguously Markdown when there is
    // a blank line after <Tabs.Tab>. Without it, MDX/Prettier can handle the
    // fence as adjacent JSX text, and later formatting may not restore the tab
    // panel structure we expect.
    if (
      isTabsTabOpen(trimmed) &&
      index < lines.length - 1 &&
      isCodeFence(lines[index + 1])
    ) {
      report(
        messages,
        lines,
        index,
        line.indexOf('<Tabs.Tab>'),
        'Add a blank line between <Tabs.Tab> and fenced code blocks.',
      );
    }

    // Likewise, the end of a fenced code block should be separated from the
    // closing tab panel tag. This keeps the fence close from being visually and
    // syntactically glued to JSX during conflict resolution and Prettier passes.
    if (
      isCodeFence(line) &&
      index < lines.length - 1 &&
      isTabsTabClose(lines[index + 1].trim())
    ) {
      report(
        messages,
        lines,
        index,
        firstNonWhitespaceIndex(line),
        'Add a blank line between fenced code blocks and </Tabs.Tab>.',
      );
    }
  }

  return messages;
}

function report(messages, lines, lineIndex, columnIndex, message) {
  messages.push({
    ruleId: 'internal-rules/mdx-tabs-spacing',
    severity: 2,
    message,
    line: lineIndex + 1,
    column: columnIndex + 1,
    endLine: lineIndex + 1,
    endColumn: lines[lineIndex].length + 1,
  });
}

function isTabsOpen(trimmedLine) {
  return /^<Tabs(?:\s|>)/u.test(trimmedLine);
}

function isTabsClose(trimmedLine) {
  return trimmedLine === '</Tabs>';
}

function isTabsTabOpen(trimmedLine) {
  return trimmedLine === '<Tabs.Tab>';
}

function isTabsTabClose(trimmedLine) {
  return trimmedLine === '</Tabs.Tab>';
}

function isCodeFence(line) {
  return /^`{3,}/u.test(line.trim());
}

function isBlank(line) {
  return line.trim() === '';
}

function firstNonWhitespaceIndex(line) {
  const match = /\S/u.exec(line);
  return match == null ? 0 : match.index;
}

export { mdxTabsSpacingProcessor };