from __future__ import annotations

import base64
import re
from pathlib import Path

from PySide6.QtGui import (
    QFont,
    QFontDatabase,
    QFontMetrics,
    QImage,
    QTextCharFormat,
    QTextCursor,
    QTextDocument,
    QTextFormat,
    QTextBlockFormat,
    QTextImageFormat,
    QDesktopServices,
)
from PySide6.QtCore import Qt, QRect, QTimer, QUrl
from PySide6.QtWidgets import QTextEdit

from .theme import ThemeManager
from .markdown_highlighter import MarkdownHighlighter
from . import strings


class MarkdownEditor(QTextEdit):
    """A QTextEdit that stores/loads markdown and provides live rendering."""

    _IMAGE_EXTS = (".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp")

    def __init__(self, theme_manager: ThemeManager, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.theme_manager = theme_manager

        # Setup tab width
        tab_w = 4 * self.fontMetrics().horizontalAdvance(" ")
        self.setTabStopDistance(tab_w)

        # We accept plain text, not rich text (markdown is plain text)
        self.setAcceptRichText(False)

        # Load in our preferred fonts
        base_dir = Path(__file__).resolve().parent

        # Load regular text font (primary)
        regular_font_path = base_dir / "fonts" / "NotoSans-Regular.ttf"
        regular_font_id = QFontDatabase.addApplicationFont(str(regular_font_path))
        if regular_font_id == -1:
            print("Failed to load NotoSans-Regular.ttf")

        # Load Symbols font (fallback)
        symbols_font_path = base_dir / "fonts" / "NotoSansSymbols2-Regular.ttf"
        symbols_font_id = QFontDatabase.addApplicationFont(str(symbols_font_path))
        symbols_families = QFontDatabase.applicationFontFamilies(symbols_font_id)
        self.symbols_font_family = symbols_families[0]
        if symbols_font_id == -1:
            print("Failed to load NotoSansSymbols2-Regular.ttf")

        # Use the regular Noto Sans family as the editor font
        regular_families = QFontDatabase.applicationFontFamilies(regular_font_id)
        if regular_families:
            self.text_font_family = regular_families[0]
            self.qfont = QFont(self.text_font_family, 11)
            self.setFont(self.qfont)

        self._apply_line_spacing()  # 1.25× initial spacing

        # Checkbox characters (Unicode for display, markdown for storage)
        self._CHECK_UNCHECKED_DISPLAY = "☐"
        self._CHECK_CHECKED_DISPLAY = "☑"
        self._CHECK_UNCHECKED_STORAGE = "[ ]"
        self._CHECK_CHECKED_STORAGE = "[x]"

        # Bullet character (Unicode for display, "- " for markdown)
        self._BULLET_DISPLAY = "•"
        self._BULLET_STORAGE = "-"

        # Install syntax highlighter
        self.highlighter = MarkdownHighlighter(self.document(), theme_manager, self)

        # Initialize code block metadata
        from .code_highlighter import CodeBlockMetadata

        self._code_metadata = CodeBlockMetadata()

        # Track current list type for smart enter handling
        self._last_enter_was_empty = False

        # Track if we're currently updating text programmatically
        self._updating = False

        # Connect to text changes for smart formatting
        self.textChanged.connect(self._on_text_changed)
        self.textChanged.connect(self._update_code_block_row_backgrounds)
        self.theme_manager.themeChanged.connect(
            lambda *_: self._update_code_block_row_backgrounds()
        )

        # Enable mouse tracking for checkbox clicking
        self.viewport().setMouseTracking(True)
        # Also mark links as mouse-accessible
        flags = self.textInteractionFlags()
        self.setTextInteractionFlags(
            flags | Qt.TextInteractionFlag.LinksAccessibleByMouse
        )

    def setDocument(self, doc):
        super().setDocument(doc)
        # Recreate the highlighter for the new document
        # (the old one gets deleted with the old document)
        if hasattr(self, "highlighter") and hasattr(self, "theme_manager"):
            self.highlighter = MarkdownHighlighter(
                self.document(), self.theme_manager, self
            )
        self._apply_line_spacing()
        self._apply_code_block_spacing()
        QTimer.singleShot(0, self._update_code_block_row_backgrounds)

    def showEvent(self, e):
        super().showEvent(e)
        # First time the widget is shown, Qt may rebuild layout once more.
        QTimer.singleShot(0, self._update_code_block_row_backgrounds)

    def _on_text_changed(self):
        """Handle live formatting updates - convert checkbox markdown to Unicode."""
        if self._updating:
            return

        self._updating = True
        try:
            c = self.textCursor()
            block = c.block()
            line = block.text()
            pos_in_block = c.position() - block.position()

            # Transform markdown checkboxes and 'TODO' to unicode checkboxes
            def transform_line(s: str) -> str:
                s = s.replace(
                    f"- {self._CHECK_CHECKED_STORAGE} ",
                    f"{self._CHECK_CHECKED_DISPLAY} ",
                )
                s = s.replace(
                    f"- {self._CHECK_UNCHECKED_STORAGE} ",
                    f"{self._CHECK_UNCHECKED_DISPLAY} ",
                )
                s = re.sub(
                    r"^([ \t]*)TODO\b[:\-]?\s+",
                    lambda m: f"{m.group(1)}\n{self._CHECK_UNCHECKED_DISPLAY} ",
                    s,
                )
                return s

            new_line = transform_line(line)
            if new_line != line:
                # Replace just the current block
                bc = QTextCursor(block)
                bc.beginEditBlock()
                bc.select(QTextCursor.BlockUnderCursor)
                bc.insertText(new_line)
                bc.endEditBlock()

                # Restore cursor near its original visual position in the edited line
                new_pos = min(
                    block.position() + len(new_line), block.position() + pos_in_block
                )
                c.setPosition(new_pos)
                self.setTextCursor(c)
        finally:
            self._updating = False

    def _is_inside_code_block(self, block):
        """Return True if 'block' is inside a fenced code block (based on fences above)."""
        inside = False
        b = block.previous()
        while b.isValid():
            if b.text().strip().startswith("```"):
                inside = not inside
            b = b.previous()
        return inside

    def _update_code_block_row_backgrounds(self):
        """Paint a full-width background for each line that is in a fenced code block."""
        doc = self.document()
        if doc is None:
            return

        sels = []
        bg_brush = self.highlighter.code_block_format.background()

        inside = False
        block = doc.begin()
        while block.isValid():
            text = block.text()
            stripped = text.strip()
            is_fence = stripped.startswith("```")

            paint_this_line = is_fence or inside
            if paint_this_line:
                sel = QTextEdit.ExtraSelection()
                fmt = QTextCharFormat()
                fmt.setBackground(bg_brush)
                fmt.setProperty(QTextFormat.FullWidthSelection, True)
                fmt.setProperty(QTextFormat.UserProperty, "codeblock_bg")
                sel.format = fmt

                cur = QTextCursor(doc)
                cur.setPosition(block.position())
                sel.cursor = cur
                sels.append(sel)

            if is_fence:
                inside = not inside

            block = block.next()

        others = [
            s
            for s in self.extraSelections()
            if s.format.property(QTextFormat.UserProperty) != "codeblock_bg"
        ]
        self.setExtraSelections(others + sels)

    def _apply_line_spacing(self, height: float = 125.0):
        """Apply proportional line spacing to the whole document."""
        doc = self.document()
        if doc is None:
            return

        cursor = QTextCursor(doc)
        cursor.beginEditBlock()
        cursor.select(QTextCursor.Document)

        fmt = QTextBlockFormat()
        fmt.setLineHeight(
            height,  # 125.0 = 1.25×
            QTextBlockFormat.LineHeightTypes.ProportionalHeight.value,
        )
        cursor.mergeBlockFormat(fmt)
        cursor.endEditBlock()

    def _apply_code_block_spacing(self):
        """
        Make all fenced code-block lines (including ``` fences) single-spaced.
        Call this AFTER _apply_line_spacing().
        """
        doc = self.document()
        if doc is None:
            return

        cursor = QTextCursor(doc)
        cursor.beginEditBlock()

        inside = False
        block = doc.begin()
        while block.isValid():
            text = block.text()
            stripped = text.strip()
            is_fence = stripped.startswith("```")
            is_code_line = is_fence or inside

            if is_code_line:
                fmt = block.blockFormat()
                fmt.setLineHeight(
                    0.0,
                    QTextBlockFormat.LineHeightTypes.SingleHeight.value,
                )
                cursor.setPosition(block.position())
                cursor.setBlockFormat(fmt)

            if is_fence:
                inside = not inside

            block = block.next()

        cursor.endEditBlock()

    def to_markdown(self) -> str:
        """Export current content as markdown."""
        # First, extract any embedded images and convert to markdown
        text = self._extract_images_to_markdown()

        # Convert Unicode checkboxes back to markdown syntax
        text = text.replace(
            f"{self._CHECK_CHECKED_DISPLAY} ", f"- {self._CHECK_CHECKED_STORAGE} "
        )
        text = text.replace(
            f"{self._CHECK_UNCHECKED_DISPLAY} ", f"- {self._CHECK_UNCHECKED_STORAGE} "
        )

        # Convert Unicode bullets back to "- " at the start of a line
        text = re.sub(
            rf"(?m)^(\s*){re.escape(self._BULLET_DISPLAY)}\s+",
            rf"\1{self._BULLET_STORAGE} ",
            text,
        )

        # Append code block metadata if present
        if hasattr(self, "_code_metadata"):
            metadata_str = self._code_metadata.serialize()
            if metadata_str:
                text = text.rstrip() + "\n\n" + metadata_str

        return text

    def _extract_images_to_markdown(self) -> str:
        """Extract embedded images and convert them back to markdown format."""
        doc = self.document()
        cursor = QTextCursor(doc)

        # Build the output text with images as markdown
        result = []
        cursor.movePosition(QTextCursor.MoveOperation.Start)

        block = doc.begin()
        while block.isValid():
            it = block.begin()
            block_text = ""

            while not it.atEnd():
                fragment = it.fragment()
                if fragment.isValid():
                    if fragment.charFormat().isImageFormat():
                        # This is an image - convert to markdown
                        img_format = fragment.charFormat().toImageFormat()
                        img_name = img_format.name()
                        # The name contains the data URI
                        if img_name.startswith("data:image/"):
                            block_text += f"![image]({img_name})"
                    else:
                        # Regular text
                        block_text += fragment.text()
                it += 1

            result.append(block_text)
            block = block.next()

        return "\n".join(result)

    def from_markdown(self, markdown_text: str):
        """Load markdown text into the editor."""
        # Extract and load code block metadata if present
        from .code_highlighter import CodeBlockMetadata

        if not hasattr(self, "_code_metadata"):
            self._code_metadata = CodeBlockMetadata()

        self._code_metadata.deserialize(markdown_text)
        # Remove metadata comment from displayed text
        markdown_text = re.sub(r"\s*<!-- code-langs: [^>]+ -->\s*$", "", markdown_text)

        # Convert markdown checkboxes to Unicode for display
        display_text = markdown_text.replace(
            f"- {self._CHECK_CHECKED_STORAGE} ", f"{self._CHECK_CHECKED_DISPLAY} "
        )
        display_text = display_text.replace(
            f"- {self._CHECK_UNCHECKED_STORAGE} ", f"{self._CHECK_UNCHECKED_DISPLAY} "
        )
        # Also convert any plain 'TODO ' at the start of a line to an unchecked checkbox
        display_text = re.sub(
            r"(?m)^([ \t]*)TODO\s",
            lambda m: f"{m.group(1)}\n{self._CHECK_UNCHECKED_DISPLAY} ",
            display_text,
        )

        # Convert simple markdown bullets ("- ", "* ", "+ ") to Unicode bullets,
        # but skip checkbox lines (- [ ] / - [x])
        display_text = re.sub(
            r"(?m)^([ \t]*)[-*+]\s+(?!\[[ xX]\])",
            rf"\1{self._BULLET_DISPLAY} ",
            display_text,
        )

        self._updating = True
        try:
            self.setPlainText(display_text)
            if hasattr(self, "highlighter") and self.highlighter:
                self.highlighter.rehighlight()
        finally:
            self._updating = False

        self._apply_line_spacing()
        self._apply_code_block_spacing()

        # Render any embedded images
        self._render_images()

        self._update_code_block_row_backgrounds()
        QTimer.singleShot(0, self._update_code_block_row_backgrounds)

    def _render_images(self):
        """Find and render base64 images in the document."""
        text = self.toPlainText()

        # Pattern for markdown images with base64 data
        img_pattern = r"!\[([^\]]*)\]\(data:image/([^;]+);base64,([^\)]+)\)"

        matches = list(re.finditer(img_pattern, text))

        if not matches:
            return

        # Process matches in reverse to preserve positions
        for match in reversed(matches):
            mime_type = match.group(2)
            b64_data = match.group(3)

            # Decode base64 to image
            img_bytes = base64.b64decode(b64_data)
            image = QImage.fromData(img_bytes)

            if image.isNull():
                continue

            # Use original image size - no scaling
            original_width = image.width()
            original_height = image.height()

            # Create image format with original base64
            img_format = QTextImageFormat()
            img_format.setName(f"data:image/{mime_type};base64,{b64_data}")
            img_format.setWidth(original_width)
            img_format.setHeight(original_height)

            # Add image to document resources
            self.document().addResource(
                QTextDocument.ResourceType.ImageResource, img_format.name(), image
            )

            # Replace markdown with rendered image
            cursor = QTextCursor(self.document())
            cursor.setPosition(match.start())
            cursor.setPosition(match.end(), QTextCursor.MoveMode.KeepAnchor)
            cursor.insertImage(img_format)

    def _get_current_line(self) -> str:
        """Get the text of the current line."""
        cursor = self.textCursor()
        cursor.select(QTextCursor.SelectionType.LineUnderCursor)
        return cursor.selectedText()

    def _list_prefix_length_for_block(self, block) -> int:
        """Return the length (in chars) of the visual list prefix for the given
        block (including leading indentation), or 0 if it's not a list item.
        """
        line = block.text()
        stripped = line.lstrip()
        leading_spaces = len(line) - len(stripped)

        # Checkbox (Unicode display)
        if stripped.startswith(
            f"{self._CHECK_UNCHECKED_DISPLAY} "
        ) or stripped.startswith(f"{self._CHECK_CHECKED_DISPLAY} "):
            return leading_spaces + 2  # icon + space

        # Unicode bullet
        if stripped.startswith(f"{self._BULLET_DISPLAY} "):
            return leading_spaces + 2  # bullet + space

        # Markdown bullet list (-, *, +)
        if re.match(r"^[-*+]\s", stripped):
            return leading_spaces + 2  # marker + space

        # Numbered list: e.g. "1. "
        m = re.match(r"^(\d+\.\s)", stripped)
        if m:
            return leading_spaces + leading_spaces + (len(m.group(1)) - leading_spaces)

        return 0

    def _detect_list_type(self, line: str) -> tuple[str | None, str]:
        """
        Detect if line is a list item. Returns (list_type, prefix).
        list_type: 'bullet', 'number', 'checkbox', or None
        prefix: the actual prefix string to use (e.g., '- ', '1. ', '- ☐ ')
        """
        line = line.lstrip()

        # Checkbox list (Unicode display format)
        if line.startswith(f"{self._CHECK_UNCHECKED_DISPLAY} ") or line.startswith(
            f"{self._CHECK_CHECKED_DISPLAY} "
        ):
            return ("checkbox", f"{self._CHECK_UNCHECKED_DISPLAY} ")

        # Bullet list – Unicode bullet
        if line.startswith(f"{self._BULLET_DISPLAY} "):
            return ("bullet", f"{self._BULLET_DISPLAY} ")

        # Bullet list - markdown bullet
        if re.match(r"^[-*+]\s", line):
            match = re.match(r"^([-*+]\s)", line)
            return ("bullet", match.group(1))

        # Numbered list
        if re.match(r"^\d+\.\s", line):
            # Extract the number and increment
            match = re.match(r"^(\d+)\.\s", line)
            num = int(match.group(1))
            return ("number", f"{num + 1}. ")

        return (None, "")

    def _url_at_pos(self, pos) -> str | None:
        """
        Return the URL under the given widget position, or None if there isn't one.
        """
        cursor = self.cursorForPosition(pos)
        block = cursor.block()
        text = block.text()
        if not text:
            return None

        # Position of the cursor inside this block
        pos_in_block = cursor.position() - block.position()

        # Same pattern as in MarkdownHighlighter
        url_pattern = re.compile(r"(https?://[^\s<>()]+)")
        for m in url_pattern.finditer(text):
            start, end = m.span(1)
            if start <= pos_in_block < end:
                return m.group(1)

        return None

    def keyPressEvent(self, event):
        """Handle special key events for markdown editing."""

        # --- Auto-close code fences when typing the 3rd backtick at line start ---
        if event.text() == "`":
            c = self.textCursor()
            block = c.block()
            line = block.text()
            pos_in_block = c.position() - block.position()

            # text before caret on this line
            before = line[:pos_in_block]

            # If we've typed exactly two backticks at line start (or after whitespace),
            # treat this backtick as the "third" and expand to a full fenced block.
            if before.endswith("``") and before.strip() == "``":
                start = (
                    block.position() + pos_in_block - 2
                )  # start of the two backticks

                edit = QTextCursor(self.document())
                edit.beginEditBlock()
                edit.setPosition(start)
                edit.setPosition(start + 2, QTextCursor.KeepAnchor)
                edit.insertText("```\n\n```\n")
                edit.endEditBlock()

                # place caret on the blank line between the fences
                new_pos = start + 4  # after "```\n"
                c.setPosition(new_pos)
                self.setTextCursor(c)
                return

        # Step out of a code block with Down at EOF
        if event.key() == Qt.Key.Key_Down:
            c = self.textCursor()
            b = c.block()
            pos_in_block = c.position() - b.position()
            line = b.text()

            def next_is_closing(bb):
                nb = bb.next()
                return nb.isValid() and nb.text().strip().startswith("```")

            # Case A: caret is on the line BEFORE the closing fence, at EOL → jump after the fence
            if (
                self._is_inside_code_block(b)
                and pos_in_block >= len(line)
                and next_is_closing(b)
            ):
                fence_block = b.next()
                after_fence = fence_block.next()
                if not after_fence.isValid():
                    # make a line after the fence
                    edit = QTextCursor(self.document())
                    endpos = fence_block.position() + len(fence_block.text())
                    edit.setPosition(endpos)
                    edit.insertText("\n")
                    after_fence = fence_block.next()
                c.setPosition(after_fence.position())
                self.setTextCursor(c)
                if hasattr(self, "_update_code_block_row_backgrounds"):
                    self._update_code_block_row_backgrounds()
                return

            # Case B: caret is ON the closing fence, and it's EOF → create a line and move to it
            if (
                b.text().strip().startswith("```")
                and self._is_inside_code_block(b)
                and not b.next().isValid()
            ):
                edit = QTextCursor(self.document())
                edit.setPosition(b.position() + len(b.text()))
                edit.insertText("\n")
                c.setPosition(b.position() + len(b.text()) + 1)
                self.setTextCursor(c)
                if hasattr(self, "_update_code_block_row_backgrounds"):
                    self._update_code_block_row_backgrounds()
                return

        # Handle Backspace on empty list items so the marker itself can be deleted
        if event.key() == Qt.Key.Key_Backspace:
            cursor = self.textCursor()
            # Let Backspace behave normally when deleting a selection.
            if not cursor.hasSelection():
                block = cursor.block()
                prefix_len = self._list_prefix_length_for_block(block)

                if prefix_len > 0:
                    block_start = block.position()
                    line = block.text()
                    pos_in_block = cursor.position() - block_start
                    after_text = line[prefix_len:]

                    # If there is no real content after the marker, treat Backspace
                    # as "remove the list marker".
                    if after_text.strip() == "" and pos_in_block >= prefix_len:
                        cursor.beginEditBlock()
                        cursor.setPosition(block_start)
                        cursor.setPosition(
                            block_start + prefix_len, QTextCursor.KeepAnchor
                        )
                        cursor.removeSelectedText()
                        cursor.endEditBlock()
                        self.setTextCursor(cursor)
                        return

        # Handle Home and Left arrow keys to keep the caret to the *right*
        # of list prefixes (checkboxes / bullets / numbers).
        if event.key() in (Qt.Key.Key_Home, Qt.Key.Key_Left):
            # Let Ctrl+Home / Ctrl+Left keep their usual meaning (start of
            # document / word-left) – we don't interfere with those.
            if event.modifiers() & Qt.ControlModifier:
                pass
            else:
                cursor = self.textCursor()
                block = cursor.block()
                prefix_len = self._list_prefix_length_for_block(block)

                if prefix_len > 0:
                    block_start = block.position()
                    pos_in_block = cursor.position() - block_start
                    target = block_start + prefix_len

                    if event.key() == Qt.Key.Key_Home:
                        # Home should jump to just after the prefix; with Shift
                        # it should *select* back to that position.
                        if event.modifiers() & Qt.ShiftModifier:
                            cursor.setPosition(target, QTextCursor.KeepAnchor)
                        else:
                            cursor.setPosition(target)
                        self.setTextCursor(cursor)
                        return

                    # Left arrow: don't allow the caret to move into the prefix
                    # region; snap it to just after the marker instead.
                    if event.key() == Qt.Key.Key_Left and pos_in_block <= prefix_len:
                        if event.modifiers() & Qt.ShiftModifier:
                            cursor.setPosition(target, QTextCursor.KeepAnchor)
                        else:
                            cursor.setPosition(target)
                        self.setTextCursor(cursor)
                        return

        # After moving vertically, make sure we don't land *inside* a list
        # prefix. We let QTextEdit perform the move first and then adjust.
        if event.key() in (Qt.Key.Key_Up, Qt.Key.Key_Down) and not (
            event.modifiers() & Qt.ControlModifier
        ):
            super().keyPressEvent(event)

            cursor = self.textCursor()
            block = cursor.block()

            # Don't interfere with code blocks (they can contain literal
            # markdown-looking text).
            if self._is_inside_code_block(block):
                return

            prefix_len = self._list_prefix_length_for_block(block)
            if prefix_len > 0:
                block_start = block.position()
                pos_in_block = cursor.position() - block_start
                if pos_in_block < prefix_len:
                    target = block_start + prefix_len
                    if event.modifiers() & Qt.ShiftModifier:
                        # Preserve the current anchor while snapping the visual
                        # caret to just after the marker.
                        anchor = cursor.anchor()
                        cursor.setPosition(anchor)
                        cursor.setPosition(target, QTextCursor.KeepAnchor)
                    else:
                        cursor.setPosition(target)
                    self.setTextCursor(cursor)

            return

        # Handle Enter key for smart list continuation AND code blocks
        if event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
            cursor = self.textCursor()
            current_line = self._get_current_line()

            # Check if we're in a code block
            current_block = cursor.block()
            line_text = current_block.text()
            pos_in_block = cursor.position() - current_block.position()

            moved = False
            i = 0
            patterns = ["**", "__", "~~", "`", "*", "_"]  # bold, italic, strike, code
            # Consume stacked markers like **` if present
            while True:
                matched = False
                for pat in patterns:
                    L = len(pat)
                    if line_text[pos_in_block + i : pos_in_block + i + L] == pat:
                        i += L
                        matched = True
                        moved = True
                        break
                if not matched:
                    break
            if moved:
                cursor.movePosition(
                    QTextCursor.MoveOperation.Right, QTextCursor.MoveMode.MoveAnchor, i
                )
                self.setTextCursor(cursor)

            block_state = current_block.userState()

            stripped = current_line.strip()
            is_fence_line = stripped.startswith("```")

            if is_fence_line:
                # Work out if this fence is closing (inside block before it)
                inside_before = self._is_inside_code_block(current_block.previous())

                # Insert the newline as usual
                super().keyPressEvent(event)

                if inside_before:
                    # We were on the *closing* fence; the new line is outside the block.
                    # Give that new block normal 1.25× spacing.
                    new_block = self.textCursor().block()
                    fmt = new_block.blockFormat()
                    fmt.setLineHeight(
                        125.0,
                        QTextBlockFormat.LineHeightTypes.ProportionalHeight.value,
                    )
                    cur2 = self.textCursor()
                    cur2.setBlockFormat(fmt)
                    self.setTextCursor(cur2)

                return

            # Inside a code block (but not on a fence): newline stays code-style
            if block_state == 1:
                super().keyPressEvent(event)
                return

            # Check for list continuation
            list_type, prefix = self._detect_list_type(current_line)

            if list_type:
                # Check if the line is empty (just the prefix)
                content = current_line.lstrip()
                is_empty = (
                    content == prefix.strip() or not content.replace(prefix, "").strip()
                )

                if is_empty and self._last_enter_was_empty:
                    # Second enter on empty list item - remove the list formatting
                    cursor.select(QTextCursor.SelectionType.LineUnderCursor)
                    cursor.removeSelectedText()
                    cursor.insertText("\n")
                    self._last_enter_was_empty = False
                    return
                elif is_empty:
                    # First enter on empty list item - just insert newline without prefix
                    super().keyPressEvent(event)
                    self._last_enter_was_empty = True
                    return
                else:
                    # Not empty - continue the list
                    self._last_enter_was_empty = False

                # Insert newline and continue the list
                super().keyPressEvent(event)
                cursor = self.textCursor()
                cursor.insertText(prefix)
                return
            else:
                self._last_enter_was_empty = False
        else:
            # Any other key resets the empty enter flag
            self._last_enter_was_empty = False

        # Default handling
        super().keyPressEvent(event)

    def mouseMoveEvent(self, event):
        # Change cursor when hovering a link
        url = self._url_at_pos(event.pos())
        if url:
            self.viewport().setCursor(Qt.PointingHandCursor)
        else:
            self.viewport().setCursor(Qt.IBeamCursor)

        super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        # Let QTextEdit handle caret/selection first
        super().mouseReleaseEvent(event)

        if event.button() != Qt.LeftButton:
            return

        # If the user dragged to select text, don't treat it as a click
        if self.textCursor().hasSelection():
            return

        url_str = self._url_at_pos(event.pos())
        if not url_str:
            return

        url = QUrl(url_str)
        if not url.scheme():
            url.setScheme("https")

        QDesktopServices.openUrl(url)

    def mousePressEvent(self, event):
        """Toggle a checkbox only when the click lands on its icon."""
        if event.button() == Qt.LeftButton:
            pt = event.pos()

            # Cursor and block under the mouse
            cur = self.cursorForPosition(pt)
            block = cur.block()
            text = block.text()

            # The display tokens, e.g. "☐ " / "☑ " (icon + trailing space)
            unchecked = f"{self._CHECK_UNCHECKED_DISPLAY} "
            checked = f"{self._CHECK_CHECKED_DISPLAY} "

            # Helper: rect for a single character at a given doc position
            def char_rect_at(doc_pos, ch):
                c = QTextCursor(self.document())
                c.setPosition(doc_pos)
                # caret rect at char start (viewport coords)
                start_rect = self.cursorRect(c)

                # Use the actual font at this position for an accurate width
                fmt_font = (
                    c.charFormat().font() if c.charFormat().isValid() else self.font()
                )
                fm = QFontMetrics(fmt_font)
                w = max(1, fm.horizontalAdvance(ch))
                return QRect(start_rect.x(), start_rect.y(), w, start_rect.height())

            # Scan the line for any checkbox icons; toggle the one we clicked
            i = 0
            while i < len(text):
                icon = None
                if text.startswith(unchecked, i):
                    icon = self._CHECK_UNCHECKED_DISPLAY
                elif text.startswith(checked, i):
                    icon = self._CHECK_CHECKED_DISPLAY

                if icon:
                    # absolute document position of the icon
                    doc_pos = block.position() + i
                    r = char_rect_at(doc_pos, icon)

                    if r.contains(pt):
                        # Build the replacement: swap ☐ <-> ☑ (keep trailing space)
                        new_icon = (
                            self._CHECK_CHECKED_DISPLAY
                            if icon == self._CHECK_UNCHECKED_DISPLAY
                            else self._CHECK_UNCHECKED_DISPLAY
                        )
                        edit = QTextCursor(self.document())
                        edit.beginEditBlock()
                        edit.setPosition(doc_pos)
                        # icon + space
                        edit.movePosition(
                            QTextCursor.Right, QTextCursor.KeepAnchor, len(icon) + 1
                        )
                        edit.insertText(f"{new_icon} ")
                        edit.endEditBlock()
                        return  # handled

                    # advance past this token
                    i += len(icon) + 1
                else:
                    i += 1

        # Default handling for anything else
        super().mousePressEvent(event)

    # ------------------------ Toolbar action handlers ------------------------

    def apply_weight(self):
        """Toggle bold formatting."""
        cursor = self.textCursor()
        if cursor.hasSelection():
            selected = cursor.selectedText()
            # Check if already bold
            if selected.startswith("**") and selected.endswith("**"):
                # Remove bold
                new_text = selected[2:-2]
            else:
                # Add bold
                new_text = f"**{selected}**"
            cursor.insertText(new_text)
        else:
            # No selection - just insert markers
            cursor.insertText("****")
            cursor.movePosition(
                QTextCursor.MoveOperation.Left, QTextCursor.MoveMode.MoveAnchor, 2
            )
            self.setTextCursor(cursor)

        # Return focus to editor
        self.setFocus()

    def apply_italic(self):
        """Toggle italic formatting."""
        cursor = self.textCursor()
        if cursor.hasSelection():
            selected = cursor.selectedText()
            if (
                selected.startswith("*")
                and selected.endswith("*")
                and not selected.startswith("**")
            ):
                new_text = selected[1:-1]
            else:
                new_text = f"*{selected}*"
            cursor.insertText(new_text)
        else:
            cursor.insertText("**")
            cursor.movePosition(
                QTextCursor.MoveOperation.Left, QTextCursor.MoveMode.MoveAnchor, 1
            )
            self.setTextCursor(cursor)

        # Return focus to editor
        self.setFocus()

    def apply_strikethrough(self):
        """Toggle strikethrough formatting."""
        cursor = self.textCursor()
        if cursor.hasSelection():
            selected = cursor.selectedText()
            if selected.startswith("~~") and selected.endswith("~~"):
                new_text = selected[2:-2]
            else:
                new_text = f"~~{selected}~~"
            cursor.insertText(new_text)
        else:
            cursor.insertText("~~~~")
            cursor.movePosition(
                QTextCursor.MoveOperation.Left, QTextCursor.MoveMode.MoveAnchor, 2
            )
            self.setTextCursor(cursor)

        # Return focus to editor
        self.setFocus()

    def apply_code(self):
        """Insert a fenced code block, or navigate fences without creating inline backticks."""
        c = self.textCursor()
        doc = self.document()

        if c.hasSelection():
            # Wrap selection and ensure exactly one newline after the closing fence
            selected = c.selectedText().replace("\u2029", "\n")
            c.insertText(f"```\n{selected.rstrip()}\n```\n")
            if hasattr(self, "_update_code_block_row_backgrounds"):
                self._update_code_block_row_backgrounds()
            # tighten spacing for the new code block
            self._apply_code_block_spacing()

            self.setFocus()
            return

        block = c.block()
        line = block.text()
        pos_in_block = c.position() - block.position()
        stripped = line.strip()

        # If we're on a fence line, be helpful but never insert inline fences
        if stripped.startswith("```"):
            # Is this fence opening or closing? (look at blocks above)
            inside_before = self._is_inside_code_block(block.previous())
            if inside_before:
                # This fence closes the block → ensure a line after, then move there
                endpos = block.position() + len(line)
                edit = QTextCursor(doc)
                edit.setPosition(endpos)
                if not block.next().isValid():
                    edit.insertText("\n")
                c.setPosition(endpos + 1)
                self.setTextCursor(c)
                if hasattr(self, "_update_code_block_row_backgrounds"):
                    self._update_code_block_row_backgrounds()
                self.setFocus()
                return
            else:
                # Opening fence → move caret to the next line (inside the block)
                nb = block.next()
                if not nb.isValid():
                    e = QTextCursor(doc)
                    e.setPosition(block.position() + len(line))
                    e.insertText("\n")
                    nb = block.next()
                c.setPosition(nb.position())
                self.setTextCursor(c)
                self.setFocus()
                return

        # If we're inside a block (but not on a fence), don't mutate text
        if self._is_inside_code_block(block):
            self.setFocus()
            return

        # Outside any block → create a clean template on its own lines (never inline)
        start_pos = c.position()
        before = line[:pos_in_block]

        edit = QTextCursor(doc)
        edit.beginEditBlock()

        # If there is text before the caret on the line, start the block on a new line
        lead_break = "\n" if before else ""
        # Insert the block; trailing newline guarantees you can Down-arrow out later
        insert = f"{lead_break}```\n\n```\n"
        edit.setPosition(start_pos)
        edit.insertText(insert)
        edit.endEditBlock()

        # Put caret on the blank line inside the block
        c.setPosition(start_pos + len(lead_break) + 4)  # after "```\n"
        self.setTextCursor(c)

        if hasattr(self, "_update_code_block_row_backgrounds"):
            self._update_code_block_row_backgrounds()

        # tighten spacing for the new code block
        self._apply_code_block_spacing()

        self.setFocus()

    def apply_heading(self, size: int):
        """Apply heading formatting to current line."""
        cursor = self.textCursor()

        # Determine heading level from size
        if size >= 24:
            level = 1
        elif size >= 18:
            level = 2
        elif size >= 14:
            level = 3
        else:
            level = 0  # Normal text

        # Get current line
        cursor.movePosition(
            QTextCursor.MoveOperation.StartOfLine, QTextCursor.MoveMode.MoveAnchor
        )
        cursor.movePosition(
            QTextCursor.MoveOperation.EndOfLine, QTextCursor.MoveMode.KeepAnchor
        )
        line = cursor.selectedText()

        # Remove existing heading markers
        line = re.sub(r"^#{1,6}\s+", "", line)

        # Add new heading markers if not normal
        if level > 0:
            new_line = "#" * level + " " + line
        else:
            new_line = line

        cursor.insertText(new_line)

        # Return focus to editor
        self.setFocus()

    def toggle_bullets(self):
        """Toggle bullet list on current line."""
        cursor = self.textCursor()
        cursor.movePosition(
            QTextCursor.MoveOperation.StartOfLine, QTextCursor.MoveMode.MoveAnchor
        )
        cursor.movePosition(
            QTextCursor.MoveOperation.EndOfLine, QTextCursor.MoveMode.KeepAnchor
        )
        line = cursor.selectedText()
        stripped = line.lstrip()

        # Consider existing markdown markers OR our Unicode bullet as "a bullet"
        if (
            stripped.startswith(f"{self._BULLET_DISPLAY} ")
            or stripped.startswith("- ")
            or stripped.startswith("* ")
        ):
            # Remove any of those bullet markers
            pattern = rf"^\s*([{re.escape(self._BULLET_DISPLAY)}\-*])\s+"
            new_line = re.sub(pattern, "", line)
        else:
            new_line = f"{self._BULLET_DISPLAY} " + stripped

        cursor.insertText(new_line)

        # Return focus to editor
        self.setFocus()

    def toggle_numbers(self):
        """Toggle numbered list on current line."""
        cursor = self.textCursor()
        cursor.movePosition(
            QTextCursor.MoveOperation.StartOfLine, QTextCursor.MoveMode.MoveAnchor
        )
        cursor.movePosition(
            QTextCursor.MoveOperation.EndOfLine, QTextCursor.MoveMode.KeepAnchor
        )
        line = cursor.selectedText()

        # Check if already numbered
        if re.match(r"^\s*\d+\.\s", line):
            # Remove number
            new_line = re.sub(r"^\s*\d+\.\s+", "", line)
        else:
            # Add number
            new_line = "1. " + line.lstrip()

        cursor.insertText(new_line)

        # Return focus to editor
        self.setFocus()

    def toggle_checkboxes(self):
        """Toggle checkbox on current line."""
        cursor = self.textCursor()
        cursor.movePosition(
            QTextCursor.MoveOperation.StartOfLine, QTextCursor.MoveMode.MoveAnchor
        )
        cursor.movePosition(
            QTextCursor.MoveOperation.EndOfLine, QTextCursor.MoveMode.KeepAnchor
        )
        line = cursor.selectedText()

        # Check if already has checkbox (Unicode display format)
        if (
            f"{self._CHECK_UNCHECKED_DISPLAY} " in line
            or f"{self._CHECK_CHECKED_DISPLAY} " in line
        ):
            # Remove checkbox - use raw string to avoid escape sequence warning
            new_line = re.sub(
                rf"^\s*[{self._CHECK_UNCHECKED_DISPLAY}{self._CHECK_CHECKED_DISPLAY}]\s+",
                "",
                line,
            )
        else:
            # Add checkbox (Unicode display format)
            new_line = f"{self._CHECK_UNCHECKED_DISPLAY} " + line.lstrip()

        cursor.insertText(new_line)

        # Return focus to editor
        self.setFocus()

    def insert_image_from_path(self, path: Path):
        """Insert an image as rendered image (but save as base64 markdown)."""
        if not path.exists():
            return

        # Read the original image file bytes for base64 encoding
        with open(path, "rb") as f:
            img_data = f.read()

        # Encode ORIGINAL file bytes to base64
        b64_data = base64.b64encode(img_data).decode("ascii")

        # Determine mime type
        ext = path.suffix.lower()
        mime_map = {
            ".png": "image/png",
            ".jpg": "image/jpeg",
            ".jpeg": "image/jpeg",
            ".gif": "image/gif",
            ".bmp": "image/bmp",
            ".webp": "image/webp",
        }
        mime_type = mime_map.get(ext, "image/png")

        # Load the image
        image = QImage(str(path))
        if image.isNull():
            return

        # Create image format with original base64
        img_format = QTextImageFormat()
        img_format.setName(f"data:image/{mime_type};base64,{b64_data}")
        img_format.setWidth(image.width())
        img_format.setHeight(image.height())

        # Add original image to document resources
        self.document().addResource(
            QTextDocument.ResourceType.ImageResource, img_format.name(), image
        )

        # Insert the image at original size
        cursor = self.textCursor()
        cursor.insertImage(img_format)
        cursor.insertText("\n")  # Add newline after image

    # ========== Context Menu Support ==========

    def contextMenuEvent(self, event):
        """Override context menu to add custom actions."""
        from PySide6.QtGui import QAction
        from PySide6.QtWidgets import QMenu

        menu = QMenu(self)
        cursor = self.cursorForPosition(event.pos())

        # Check if we're in a table
        text = self.toPlainText()
        cursor_pos = cursor.position()

        from .table_editor import find_table_at_cursor

        table_info = find_table_at_cursor(text, cursor_pos)

        if table_info:
            # Add table editing action
            edit_table_action = QAction(strings._("edit_table"), self)
            edit_table_action.triggered.connect(
                lambda: self._edit_table_at_cursor(cursor_pos)
            )
            menu.addAction(edit_table_action)
            menu.addSeparator()

        # Check if we're in a code block
        block = cursor.block()
        if self._is_inside_code_block(block):
            # Add language selection submenu
            lang_menu = menu.addMenu(strings._("set_code_language"))

            languages = [
                "python",
                "bash",
                "php",
                "javascript",
                "html",
                "css",
                "sql",
                "java",
                "go",
            ]
            for lang in languages:
                action = QAction(lang.capitalize(), self)
                action.triggered.connect(
                    lambda checked, l=lang: self._set_code_block_language(block, l)
                )
                lang_menu.addAction(action)

            menu.addSeparator()

        # Add standard context menu actions
        if self.textCursor().hasSelection():
            menu.addAction(strings._("cut"), self.cut)
            menu.addAction(strings._("copy"), self.copy)

        menu.addAction(strings._("paste"), self.paste)

        menu.exec(event.globalPos())

    def _edit_table_at_cursor(self, cursor_pos: int):
        """Open table editor dialog for the table at cursor position."""
        from .table_editor import find_table_at_cursor, TableEditorDialog
        from PySide6.QtWidgets import QDialog

        text = self.toPlainText()
        table_info = find_table_at_cursor(text, cursor_pos)

        if not table_info:
            return

        start_pos, end_pos, table_text = table_info

        # Open table editor
        dlg = TableEditorDialog(table_text, self)
        if dlg.exec() == QDialog.Accepted:
            # Replace the table with edited version
            new_table = dlg.get_markdown_table()

            cursor = QTextCursor(self.document())
            cursor.setPosition(start_pos)
            cursor.setPosition(end_pos, QTextCursor.KeepAnchor)
            cursor.insertText(new_table)

    def _set_code_block_language(self, block, language: str):
        """Set the language for a code block and store metadata."""
        if not hasattr(self, "_code_metadata"):
            from .code_highlighter import CodeBlockMetadata

            self._code_metadata = CodeBlockMetadata()

        # Find the opening fence block for this code block
        fence_block = block
        while fence_block.isValid() and not fence_block.text().strip().startswith(
            "```"
        ):
            fence_block = fence_block.previous()

        if fence_block.isValid():
            self._code_metadata.set_language(fence_block.blockNumber(), language)
            # Trigger rehighlight
            self.highlighter.rehighlight()

    def get_current_line_text(self) -> str:
        """Get the text of the current line."""
        cursor = self.textCursor()
        block = cursor.block()
        return block.text()
