"""
自定义 Markdown → Telegram MarkdownV2 渲染器。

依赖 markdown-it-py 提供的 CommonMark 解析能力，将常见 Markdown
语法（段落、标题、列表、引用、加粗、斜体、代码块、行内代码、链接等）
转换为符合 Telegram MarkdownV2 语法的文本。

Telegram 对 MarkdownV2 的语法要求非常严格，特殊字符必须转义，否则会直接
返回 “can't parse entities” 错误。借助解析树，我们可以准确地区分格式化标记
与普通文本，做到“格式保留 + 无额外反斜杠”。
"""
from __future__ import annotations

import re
from typing import Iterable, List, Optional, Tuple

from markdown_it import MarkdownIt
from markdown_it.token import Token

_TELEGRAM_SPECIAL_CHARS = re.compile(r"([_*\[\]()~`>#+\-=|{}.!\\])")


def _escape_plain_text(value: str) -> str:
    """转义 Telegram MarkdownV2 的特殊字符。"""
    if not value:
        return ""
    return _TELEGRAM_SPECIAL_CHARS.sub(r"\\\1", value)


def _escape_url(value: str) -> str:
    """URL 中统一转义括号、空格等特殊字符。"""
    if not value:
        return ""
    escaped = value.replace("\\", "\\\\")
    escaped = escaped.replace(")", r"\)").replace("(", r"\(")
    return escaped


def _escape_inline_code(value: str) -> str:
    """行内代码仅需要处理反斜杠与反引号。"""
    if not value:
        return "``"
    escaped = value.replace("\\", "\\\\").replace("`", r"\`")
    return f"`{escaped}`"


def _escape_code_block(value: str, language: str = "") -> str:
    """多行代码块同样保留原样，只需保护反引号。"""
    content = (value or "").rstrip("\n")
    content = content.replace("```", r"\`\`\`")
    header = f"```{language.strip()}\n" if language else "```\n"
    return f"{header}{content}\n```"


class _TelegramMarkdownRenderer:
    """对 markdown-it 解析出的 Token 序列进行 Telegram MarkdownV2 序列化。"""

    def __init__(self) -> None:
        self._parser = MarkdownIt("commonmark", {"linkify": True})

    # ---- 外部接口 ---------------------------------------------------------
    def render(self, markdown_text: str) -> str:
        tokens = self._parser.parse(markdown_text or "")
        return self._render_block_tokens(tokens, 0, len(tokens)).strip()

    # ---- Block 级渲染 -----------------------------------------------------
    def _render_block_tokens(self, tokens: List[Token], start: int, end: int) -> str:
        blocks: List[str] = []
        index = start
        while index < end:
            token = tokens[index]
            token_type = token.type

            if token_type == "paragraph_open":
                inline = tokens[index + 1]
                blocks.append(self._render_inline(inline.children or []))
                index += 3  # 跳过 inline + paragraph_close
                continue

            if token_type == "heading_open":
                inline = tokens[index + 1]
                content = self._render_inline(inline.children or [])
                blocks.append(f"*{content}*")
                index += 3
                continue

            if token_type == "bullet_list_open":
                close_idx = self._find_matching(tokens, index, "bullet_list_close")
                blocks.append(self._render_list(tokens, index + 1, close_idx, ordered=False))
                index = close_idx + 1
                continue

            if token_type == "ordered_list_open":
                close_idx = self._find_matching(tokens, index, "ordered_list_close")
                start_number = int(token.attrGet("start") or "1")
                blocks.append(
                    self._render_list(tokens, index + 1, close_idx, ordered=True, start_number=start_number)
                )
                index = close_idx + 1
                continue

            if token_type == "blockquote_open":
                close_idx = self._find_matching(tokens, index, "blockquote_close")
                quoted = self._render_block_tokens(tokens, index + 1, close_idx)
                lines = quoted.splitlines()
                blocks.append("\n".join(f"> {line}" if line else ">" for line in lines))
                index = close_idx + 1
                continue

            if token_type in {"fence", "code_block"}:
                language = token.info or "" if token_type == "fence" else ""
                blocks.append(_escape_code_block(token.content, language))
                index += 1
                continue

            if token_type == "inline":
                blocks.append(self._render_inline(token.children or []))
                index += 1
                continue

            if token_type == "hr":
                blocks.append("―" * 3)
                index += 1
                continue

            # 其他 Block token（如 html_block）直接忽略或转换为原始文本
            if token_type == "html_block":
                blocks.append(_escape_plain_text(token.content))
                index += 1
                continue

            index += 1

        return "\n\n".join(part for part in blocks if part)

    def _render_list(
        self,
        tokens: List[Token],
        start: int,
        end: int,
        *,
        ordered: bool,
        start_number: int = 1,
    ) -> str:
        parts: List[str] = []
        index = start
        counter = start_number
        while index < end:
            token = tokens[index]
            if token.type != "list_item_open":
                index += 1
                continue

            close_idx = self._find_matching(tokens, index, "list_item_close")
            body = self._render_block_tokens(tokens, index + 1, close_idx)
            lines = body.splitlines()
            prefix = f"{counter}. " if ordered else "- "
            if lines:
                rendered = [prefix + lines[0]]
                rendered.extend(("  " if ordered else "  ") + line for line in lines[1:])
                parts.append("\n".join(rendered))
            else:
                parts.append(prefix.rstrip())

            if ordered:
                counter += 1
            index = close_idx + 1

        return "\n".join(parts)

    # ---- Inline 渲染 ------------------------------------------------------
    def _render_inline(self, tokens: Iterable[Token]) -> str:
        rendered, _ = self._render_inline_segment(list(tokens), 0, None)
        return rendered

    def _render_inline_segment(
        self,
        tokens: List[Token],
        start: int,
        closing_type: Optional[str],
    ) -> Tuple[str, int]:
        parts: List[str] = []
        index = start

        while index < len(tokens):
            token = tokens[index]
            token_type = token.type

            if closing_type and token_type == closing_type:
                return "".join(parts), index + 1

            if token_type == "text":
                parts.append(_escape_plain_text(token.content))
                index += 1
                continue

            if token_type in {"softbreak", "hardbreak"}:
                parts.append("\n")
                index += 1
                continue

            if token_type == "code_inline":
                parts.append(_escape_inline_code(token.content))
                index += 1
                continue

            if token_type == "strong_open":
                inner, next_index = self._render_inline_segment(tokens, index + 1, "strong_close")
                parts.append(f"*{inner}*")
                index = next_index
                continue

            if token_type == "em_open":
                inner, next_index = self._render_inline_segment(tokens, index + 1, "em_close")
                parts.append(f"_{inner}_")
                index = next_index
                continue

            if token_type == "link_open":
                href = token.attrGet("href") or ""
                inner, next_index = self._render_inline_segment(tokens, index + 1, "link_close")
                parts.append(f"[{inner}]({_escape_url(href)})")
                index = next_index
                continue

            if token_type == "image":
                alt_text = self._render_inline(token.children or [])
                parts.append(f"[{alt_text}]")
                index += 1
                continue

            if token_type == "html_inline":
                parts.append(_escape_plain_text(token.content))
                index += 1
                continue

            if token_type == "strikethrough_open":
                inner, next_index = self._render_inline_segment(tokens, index + 1, "strikethrough_close")
                parts.append(f"~{inner}~")
                index = next_index
                continue

            index += 1

        return "".join(parts), len(tokens)

    # ---- 工具方法 ---------------------------------------------------------
    @staticmethod
    def _find_matching(tokens: List[Token], start_idx: int, closing_type: str) -> int:
        """使用 nesting 深度寻找匹配的关闭 token。"""
        open_type = tokens[start_idx].type
        depth = 0
        for idx in range(start_idx, len(tokens)):
            token = tokens[idx]
            if token.type == open_type and token.nesting == 1:
                depth += 1
            elif token.type == closing_type and token.nesting == -1:
                depth -= 1
                if depth == 0:
                    return idx
        raise ValueError(f"未找到匹配的 {closing_type}")


_RENDERER = _TelegramMarkdownRenderer()


def render_markdown_to_telegram(markdown_text: str) -> str:
    """将 Markdown 文本转换为 Telegram MarkdownV2。"""
    return _RENDERER.render(markdown_text)
