defmodule Microwaveprop.Markdown do
  @moduledoc """
  Minimal Markdown-to-HTML converter. Handles the subset used by algo.md:
  headings, paragraphs, fenced code blocks, tables, lists, inline formatting,
  links, and horizontal rules. Derived from Earmark's approach but without
  the parser combinator dependency.
  """

  @doc "Converts a Markdown string to HTML."
  @spec to_html!(String.t()) :: String.t()
  def to_html!(markdown) do
    markdown
    |> String.replace("\r\n", "\n")
    |> String.split("\n")
    |> parse_blocks([])
    |> Enum.map_join("\n", &render_block/1)
  end

  # ── Block parsing ────────────────────────────────────────────────

  defp parse_blocks([], acc), do: Enum.reverse(acc)

  # Fenced code block
  defp parse_blocks(["```" <> lang | rest], acc) do
    {code_lines, remaining} = take_until_fence(rest, [])
    lang = String.trim(lang)
    lang_attr = if lang == "", do: "", else: " class=\"language-#{lang}\""
    parse_blocks(remaining, [{:code, lang_attr, code_lines} | acc])
  end

  # Horizontal rule
  defp parse_blocks([line | rest], acc) when line in ["---", "***", "___"] do
    parse_blocks(rest, [{:hr} | acc])
  end

  # Heading
  defp parse_blocks(["#" <> _ = line | rest], acc) do
    {level, text} = parse_heading(line)
    parse_blocks(rest, [{:heading, level, text} | acc])
  end

  # Table (starts with |)
  defp parse_blocks(["|" <> _ = line | rest], acc) do
    {table_lines, remaining} = take_table([line | rest], [])
    parse_blocks(remaining, [{:table, table_lines} | acc])
  end

  # Unordered list
  defp parse_blocks(["- " <> item | rest], acc) do
    {items, remaining} = take_list(rest, [item])
    parse_blocks(remaining, [{:ul, items} | acc])
  end

  # Ordered list
  defp parse_blocks([line | rest], acc) do
    if Regex.match?(~r/^\d+\.\s/, line) do
      item = Regex.replace(~r/^\d+\.\s+/, line, "")
      {items, remaining} = take_ordered_list(rest, [item])
      parse_blocks(remaining, [{:ol, items} | acc])
    else
      case String.trim(line) do
        "" ->
          parse_blocks(rest, acc)

        trimmed ->
          {para_lines, remaining} = take_paragraph(rest, [trimmed])
          parse_blocks(remaining, [{:paragraph, para_lines} | acc])
      end
    end
  end

  defp take_until_fence([], acc), do: {Enum.reverse(acc), []}

  defp take_until_fence(["```" <> _ | rest], acc), do: {Enum.reverse(acc), rest}

  defp take_until_fence([line | rest], acc), do: take_until_fence(rest, [line | acc])

  defp parse_heading(line) do
    {hashes, text} = split_heading(line, 0)
    {min(hashes, 6), String.trim(text)}
  end

  defp split_heading("#" <> rest, n), do: split_heading(rest, n + 1)
  defp split_heading(rest, n), do: {n, rest}

  defp take_table(["|" <> _ = line | rest], acc), do: take_table(rest, [line | acc])
  defp take_table(rest, acc), do: {Enum.reverse(acc), rest}

  defp take_list(["- " <> item | rest], acc), do: take_list(rest, [item | acc])
  defp take_list(["  " <> cont | rest], [prev | items]), do: take_list(rest, [prev <> " " <> String.trim(cont) | items])
  defp take_list(rest, acc), do: {Enum.reverse(acc), rest}

  defp take_ordered_list([line | rest], acc) do
    if Regex.match?(~r/^\d+\.\s/, line) do
      item = Regex.replace(~r/^\d+\.\s+/, line, "")
      take_ordered_list(rest, [item | acc])
    else
      {Enum.reverse(acc), [line | rest]}
    end
  end

  defp take_ordered_list([], acc), do: {Enum.reverse(acc), []}

  defp take_paragraph([], acc), do: {Enum.reverse(acc), []}
  defp take_paragraph(["" | rest], acc), do: {Enum.reverse(acc), rest}
  defp take_paragraph(["#" <> _ | _] = rest, acc), do: {Enum.reverse(acc), rest}
  defp take_paragraph(["|" <> _ | _] = rest, acc), do: {Enum.reverse(acc), rest}
  defp take_paragraph(["- " <> _ | _] = rest, acc), do: {Enum.reverse(acc), rest}
  defp take_paragraph(["```" <> _ | _] = rest, acc), do: {Enum.reverse(acc), rest}
  defp take_paragraph(["---" | _] = rest, acc), do: {Enum.reverse(acc), rest}

  defp take_paragraph([line | rest], acc) do
    if Regex.match?(~r/^\d+\.\s/, line) do
      {Enum.reverse(acc), [line | rest]}
    else
      take_paragraph(rest, [String.trim(line) | acc])
    end
  end

  # ── Block rendering ──────────────────────────────────────────────

  defp render_block({:heading, level, text}) do
    "<h#{level}>#{inline(text)}</h#{level}>"
  end

  defp render_block({:paragraph, lines}) do
    "<p>#{Enum.map_join(lines, " ", &inline/1)}</p>"
  end

  defp render_block({:code, lang_attr, lines}) do
    escaped = Enum.map_join(lines, "\n", &escape_html/1)
    "<pre><code#{lang_attr}>#{escaped}</code></pre>"
  end

  defp render_block({:hr}) do
    "<hr />"
  end

  defp render_block({:ul, items}) do
    lis = Enum.map_join(items, "\n", fn item -> "<li>#{inline(item)}</li>" end)
    "<ul>\n#{lis}\n</ul>"
  end

  defp render_block({:ol, items}) do
    lis = Enum.map_join(items, "\n", fn item -> "<li>#{inline(item)}</li>" end)
    "<ol>\n#{lis}\n</ol>"
  end

  defp render_block({:table, lines}) do
    rows = Enum.map(lines, &parse_table_row/1)

    case rows do
      [header, _separator | body] ->
        thead = "<thead><tr>#{cells(header, "th")}</tr></thead>"
        tbody_rows = Enum.map_join(body, "\n", fn row -> "<tr>#{cells(row, "td")}</tr>" end)
        "<table>\n#{thead}\n<tbody>\n#{tbody_rows}\n</tbody>\n</table>"

      _ ->
        tbody_rows = Enum.map_join(rows, "\n", fn row -> "<tr>#{cells(row, "td")}</tr>" end)
        "<table>\n<tbody>\n#{tbody_rows}\n</tbody>\n</table>"
    end
  end

  defp parse_table_row(line) do
    line
    |> String.trim()
    |> String.trim("|")
    |> String.split("|")
    |> Enum.map(&String.trim/1)
  end

  defp cells(row, tag) do
    Enum.map_join(row, "", fn cell -> "<#{tag}>#{inline(cell)}</#{tag}>" end)
  end

  # ── Inline formatting ────────────────────────────────────────────

  defp inline(text) do
    text
    |> escape_html()
    |> replace_inline_code()
    |> replace_bold()
    |> replace_italic()
    |> replace_links()
  end

  defp escape_html(text) do
    text
    |> String.replace("&", "&amp;")
    |> String.replace("<", "&lt;")
    |> String.replace(">", "&gt;")
    |> String.replace("\"", "&quot;")
  end

  defp replace_inline_code(text) do
    Regex.replace(~r/`([^`]+)`/, text, "<code>\\1</code>")
  end

  defp replace_bold(text) do
    text
    |> then(&Regex.replace(~r/\*\*(.+?)\*\*/, &1, "<strong>\\1</strong>"))
    |> then(&Regex.replace(~r/__(.+?)__/, &1, "<strong>\\1</strong>"))
  end

  defp replace_italic(text) do
    text
    |> then(&Regex.replace(~r/\*(.+?)\*/, &1, "<em>\\1</em>"))
    |> then(&Regex.replace(~r/_(.+?)_/, &1, "<em>\\1</em>"))
  end

  defp replace_links(text) do
    Regex.replace(~r/\[([^\]]+)\]\(([^)]+)\)/, text, "<a href=\"\\2\">\\1</a>")
  end
end
