type MarkdownContainer = DocumentFragment | HTMLElement;

interface InlineToken {
  end: number;
  kind: "code" | "emphasis" | "link" | "strong";
  href?: string;
  start: number;
  value: string;
}

const UNORDERED_LIST_PATTERN = /^\s*[-+*]\s+(.+)$/;
const ORDERED_LIST_PATTERN = /^\s*\d+[.)]\s+(.+)$/;
const HEADING_PATTERN = /^\s{0,3}(#{1,3})\s+(.+)$/;

/**
 * Renders the limited Markdown emitted by the assistant using DOM nodes only.
 * Model-provided HTML always remains inert text.
 */
export function createMessageMarkdown(markdown: string): DocumentFragment {
  const fragment = document.createDocumentFragment();
  const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
  let lineIndex = 0;

  while (lineIndex < lines.length) {
    const line = lines[lineIndex] ?? "";
    if (line.trim().length === 0) {
      lineIndex += 1;
      continue;
    }

    const heading = HEADING_PATTERN.exec(line);
    if (heading !== null) {
      appendHeading(fragment, heading[1]!.length, heading[2]!);
      lineIndex += 1;
      continue;
    }

    const unorderedItem = UNORDERED_LIST_PATTERN.exec(line);
    if (unorderedItem !== null) {
      lineIndex = appendList(
        fragment,
        lines,
        lineIndex,
        "ul",
        UNORDERED_LIST_PATTERN,
      );
      continue;
    }

    const orderedItem = ORDERED_LIST_PATTERN.exec(line);
    if (orderedItem !== null) {
      lineIndex = appendList(
        fragment,
        lines,
        lineIndex,
        "ol",
        ORDERED_LIST_PATTERN,
      );
      continue;
    }

    lineIndex = appendParagraph(fragment, lines, lineIndex);
  }

  return fragment;
}

function appendHeading(
  container: MarkdownContainer,
  markdownLevel: number,
  content: string,
): void {
  const tagName =
    markdownLevel === 1 ? "h3" : markdownLevel === 2 ? "h4" : "h5";
  const heading = document.createElement(tagName);
  appendInlineMarkdown(heading, content);
  container.append(heading);
}

function appendList(
  container: MarkdownContainer,
  lines: string[],
  startIndex: number,
  tagName: "ol" | "ul",
  itemPattern: RegExp,
): number {
  const list = document.createElement(tagName);
  let lineIndex = startIndex;

  while (lineIndex < lines.length) {
    const item = itemPattern.exec(lines[lineIndex] ?? "");
    if (item === null) break;
    const listItem = document.createElement("li");
    appendInlineMarkdown(listItem, item[1]!);
    list.append(listItem);
    lineIndex += 1;
  }

  container.append(list);
  return lineIndex;
}

function appendParagraph(
  container: MarkdownContainer,
  lines: string[],
  startIndex: number,
): number {
  const paragraph = document.createElement("p");
  let lineIndex = startIndex;

  while (lineIndex < lines.length) {
    const line = lines[lineIndex] ?? "";
    if (
      line.trim().length === 0 ||
      HEADING_PATTERN.test(line) ||
      UNORDERED_LIST_PATTERN.test(line) ||
      ORDERED_LIST_PATTERN.test(line)
    ) {
      break;
    }
    if (paragraph.childNodes.length > 0)
      paragraph.append(document.createElement("br"));
    appendInlineMarkdown(paragraph, line);
    lineIndex += 1;
  }

  container.append(paragraph);
  return lineIndex;
}

function appendInlineMarkdown(container: HTMLElement, content: string): void {
  let cursor = 0;

  while (cursor < content.length) {
    const token = findNextInlineToken(content, cursor);
    if (token === undefined) {
      container.append(document.createTextNode(content.slice(cursor)));
      return;
    }
    if (token.start > cursor) {
      container.append(
        document.createTextNode(content.slice(cursor, token.start)),
      );
    }

    if (token.kind === "link" && token.href !== undefined) {
      const anchor = document.createElement("a");
      anchor.href = token.href;
      anchor.target = "_blank";
      anchor.rel = "noopener noreferrer";
      appendInlineMarkdown(anchor, token.value);
      container.append(anchor);
      cursor = token.end;
      continue;
    }
    const element = document.createElement(
      token.kind === "strong"
        ? "strong"
        : token.kind === "emphasis"
          ? "em"
          : "code",
    );
    if (token.kind === "code") {
      element.textContent = token.value;
    } else {
      appendInlineMarkdown(element, token.value);
    }
    container.append(element);
    cursor = token.end;
  }
}

function findNextInlineToken(
  content: string,
  fromIndex: number,
): InlineToken | undefined {
  const candidates = [
    findLinkToken(content, fromIndex),
    findDelimitedToken(content, fromIndex, "**", "strong"),
    findDelimitedToken(content, fromIndex, "__", "strong"),
    findDelimitedToken(content, fromIndex, "`", "code"),
    findDelimitedToken(content, fromIndex, "*", "emphasis"),
    findDelimitedToken(content, fromIndex, "_", "emphasis"),
  ].filter((token): token is InlineToken => token !== undefined);

  return candidates.sort(
    (left, right) =>
      left.start - right.start ||
      tokenPriority(left.kind) - tokenPriority(right.kind),
  )[0];
}

function findLinkToken(
  content: string,
  fromIndex: number,
): InlineToken | undefined {
  const start = content.indexOf("[", fromIndex);
  if (start === -1) return undefined;
  const labelEnd = content.indexOf("](", start + 1);
  if (labelEnd === -1) return undefined;
  const urlEnd = content.indexOf(")", labelEnd + 2);
  if (urlEnd === -1) return undefined;
  const href = content.slice(labelEnd + 2, urlEnd);
  let url: URL;
  try {
    url = new URL(href);
  } catch {
    return undefined;
  }
  if (url.protocol !== "http:" && url.protocol !== "https:") return undefined;
  return {
    end: urlEnd + 1,
    kind: "link",
    start,
    value: content.slice(start + 1, labelEnd),
    href: url.toString(),
  };
}

function findDelimitedToken(
  content: string,
  fromIndex: number,
  delimiter: string,
  kind: InlineToken["kind"],
): InlineToken | undefined {
  const start = content.indexOf(delimiter, fromIndex);
  if (start === -1) return undefined;
  const valueStart = start + delimiter.length;
  const closingDelimiter = content.indexOf(delimiter, valueStart);
  if (closingDelimiter <= valueStart) return undefined;

  return {
    end: closingDelimiter + delimiter.length,
    kind,
    start,
    value: content.slice(valueStart, closingDelimiter),
  };
}

function tokenPriority(kind: InlineToken["kind"]): number {
  return kind === "link" ? 0 : kind === "code" ? 1 : kind === "strong" ? 2 : 3;
}
