"""
Google Docs API client for g-gremlin.

Provides document creation, reading, and updating capabilities using the
existing Google OAuth authentication from google_client.py.

Supports markdown-to-Docs formatting conversion for:
- Headers: # ## ### (HEADING_1, HEADING_2, HEADING_3)
- Bold: **text**
- Italic: *text* or _text_
- Bullet lists: - item (with nested indentation support)
- Numbered lists: 1. item (with nested indentation support)
- Inline code: `code` (Consolas font + gray background)
- Horizontal rules: ---
- Code blocks: ```python ... ``` (Consolas font + gray background)
- Tables: |col1|col2| with header styling (light blue background)
- Screenshot placeholders: [Screenshot: description] (yellow callout box)

API Documentation: https://developers.google.com/docs/api
"""

from __future__ import annotations

import logging
import re
import time
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, List, Optional, Tuple

from googleapiclient.discovery import Resource, build
from googleapiclient.errors import HttpError

from g_gremlin.google_client import load_credentials

logger = logging.getLogger(__name__)


class ParagraphStyle(Enum):
    """Google Docs paragraph styles."""

    NORMAL = "NORMAL_TEXT"
    HEADING_1 = "HEADING_1"
    HEADING_2 = "HEADING_2"
    HEADING_3 = "HEADING_3"
    HEADING_4 = "HEADING_4"
    HEADING_5 = "HEADING_5"
    HEADING_6 = "HEADING_6"


@dataclass
class TextSegment:
    """A segment of text with formatting info."""

    text: str
    bold: bool = False
    italic: bool = False
    code: bool = False
    start_index: int = 0  # Filled in during insertion
    end_index: int = 0


@dataclass
class TableCell:
    """A cell in a markdown table."""

    content: str
    segments: List[TextSegment] = field(default_factory=list)


@dataclass
class TableRow:
    """A row in a markdown table."""

    cells: List[TableCell]
    is_header: bool = False


@dataclass
class MarkdownTable:
    """A parsed markdown table."""

    rows: List[TableRow]
    column_count: int


@dataclass
class CodeBlock:
    """A fenced code block."""

    content: str
    language: Optional[str] = None


@dataclass
class ScreenshotPlaceholder:
    """A screenshot placeholder."""

    description: str


@dataclass
class FormattedParagraph:
    """A paragraph with style and text segments."""

    segments: List[TextSegment]
    style: ParagraphStyle = ParagraphStyle.NORMAL
    is_bullet: bool = False
    is_numbered: bool = False
    is_hr: bool = False
    indent_level: int = 0
    table: Optional[MarkdownTable] = None
    code_block: Optional[CodeBlock] = None
    placeholder: Optional[ScreenshotPlaceholder] = None


class MarkdownParser:
    """Parse markdown into formatted paragraphs for Google Docs."""

    # Patterns
    HEADER_PATTERN = re.compile(r"^(#{1,6})\s+(.+)$")
    HR_PATTERN = re.compile(r"^-{3,}$|^\*{3,}$|^_{3,}$")

    # List patterns with indentation support
    NESTED_BULLET_PATTERN = re.compile(r"^(\s*)[-*]\s+(.+)$")
    NESTED_NUMBERED_PATTERN = re.compile(r"^(\s*)(\d+)\.\s+(.+)$")
    # Legacy patterns for backwards compatibility
    BULLET_PATTERN = re.compile(r"^[-*]\s+(.+)$")
    NUMBERED_PATTERN = re.compile(r"^\d+\.\s+(.+)$")

    # Code block patterns
    CODE_BLOCK_START = re.compile(r"^```(\w*)$")
    CODE_BLOCK_END = re.compile(r"^```$")

    # Table patterns
    TABLE_ROW_PATTERN = re.compile(r"^\|(.+)\|$")
    TABLE_SEPARATOR_PATTERN = re.compile(r"^\|[\s\-:|]+\|$")

    # Screenshot placeholder pattern
    SCREENSHOT_PATTERN = re.compile(r"^\[Screenshot:\s*(.+?)\]$")

    # Inline patterns - order matters for processing
    BOLD_PATTERN = re.compile(r"\*\*(.+?)\*\*|__(.+?)__")
    ITALIC_PATTERN = re.compile(r"(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)|(?<!_)_(?!_)(.+?)(?<!_)_(?!_)")
    CODE_PATTERN = re.compile(r"`([^`]+)`")

    def parse(self, markdown: str) -> List[FormattedParagraph]:
        """Parse markdown text into formatted paragraphs."""
        lines = markdown.split("\n")
        paragraphs: List[FormattedParagraph] = []
        idx = 0

        while idx < len(lines):
            line = lines[idx]

            # Check for code block (``` fence)
            if self.CODE_BLOCK_START.match(line):
                code_block, idx = self._parse_code_block(lines, idx)
                paragraphs.append(FormattedParagraph(
                    segments=[],
                    code_block=code_block
                ))
                continue

            # Check for table (|...|)
            if self.TABLE_ROW_PATTERN.match(line.strip()):
                table, idx = self._parse_table(lines, idx)
                paragraphs.append(FormattedParagraph(
                    segments=[],
                    table=table
                ))
                continue

            # Check for screenshot placeholder ([Screenshot: ...])
            placeholder_match = self.SCREENSHOT_PATTERN.match(line.strip())
            if placeholder_match:
                paragraphs.append(FormattedParagraph(
                    segments=[],
                    placeholder=ScreenshotPlaceholder(placeholder_match.group(1))
                ))
                idx += 1
                continue

            # Regular line parsing (existing logic)
            para = self._parse_line(line)
            paragraphs.append(para)
            idx += 1

        return paragraphs

    def _parse_line(self, line: str) -> FormattedParagraph:
        """Parse a single line into a formatted paragraph."""
        # Check for horizontal rule
        if self.HR_PATTERN.match(line.strip()):
            return FormattedParagraph(
                segments=[TextSegment("─" * 50)],  # Unicode horizontal line
                is_hr=True,
            )

        # Check for header
        header_match = self.HEADER_PATTERN.match(line)
        if header_match:
            level = len(header_match.group(1))
            content = header_match.group(2)
            style = ParagraphStyle[f"HEADING_{min(level, 6)}"]
            segments = self._parse_inline_formatting(content)
            return FormattedParagraph(segments=segments, style=style)

        # Check for bullet list (with nested support)
        bullet_match = self.NESTED_BULLET_PATTERN.match(line)
        if bullet_match:
            leading_ws = bullet_match.group(1)
            content = bullet_match.group(2)
            indent_level = self._get_indent_level(leading_ws)
            segments = self._parse_inline_formatting(content)
            return FormattedParagraph(
                segments=segments, is_bullet=True, indent_level=indent_level
            )

        # Check for numbered list (with nested support)
        numbered_match = self.NESTED_NUMBERED_PATTERN.match(line)
        if numbered_match:
            leading_ws = numbered_match.group(1)
            content = numbered_match.group(3)  # group(2) is the number
            indent_level = self._get_indent_level(leading_ws)
            segments = self._parse_inline_formatting(content)
            return FormattedParagraph(
                segments=segments, is_numbered=True, indent_level=indent_level
            )

        # Regular paragraph
        segments = self._parse_inline_formatting(line)
        return FormattedParagraph(segments=segments)

    def _parse_inline_formatting(self, text: str) -> List[TextSegment]:
        """Parse inline formatting (bold, italic, code) from text."""
        if not text:
            return [TextSegment("")]

        segments: List[TextSegment] = []
        # Track formatting ranges
        ranges: List[Tuple[int, int, str]] = []  # (start, end, type)

        # Find all code spans first (they take precedence)
        for match in self.CODE_PATTERN.finditer(text):
            ranges.append((match.start(), match.end(), "code"))

        # Find bold
        for match in self.BOLD_PATTERN.finditer(text):
            # Check if overlaps with code
            if not self._overlaps_any(match.start(), match.end(), ranges):
                ranges.append((match.start(), match.end(), "bold"))

        # Find italic
        for match in self.ITALIC_PATTERN.finditer(text):
            if not self._overlaps_any(match.start(), match.end(), ranges):
                ranges.append((match.start(), match.end(), "italic"))

        # Sort by start position
        ranges.sort(key=lambda x: x[0])

        # Build segments
        pos = 0
        for start, end, fmt_type in ranges:
            # Add plain text before this range
            if start > pos:
                segments.append(TextSegment(text[pos:start]))

            # Extract the content (remove markdown syntax)
            if fmt_type == "code":
                content = text[start + 1 : end - 1]  # Remove ` markers
                segments.append(TextSegment(content, code=True))
            elif fmt_type == "bold":
                content = text[start + 2 : end - 2]  # Remove ** markers
                segments.append(TextSegment(content, bold=True))
            elif fmt_type == "italic":
                content = text[start + 1 : end - 1]  # Remove * markers
                segments.append(TextSegment(content, italic=True))

            pos = end

        # Add remaining text
        if pos < len(text):
            segments.append(TextSegment(text[pos:]))

        return segments if segments else [TextSegment("")]

    def _overlaps_any(
        self, start: int, end: int, ranges: List[Tuple[int, int, str]]
    ) -> bool:
        """Check if a range overlaps with any existing ranges."""
        for r_start, r_end, _ in ranges:
            if start < r_end and end > r_start:
                return True
        return False

    def _get_indent_level(self, leading_whitespace: str) -> int:
        """Convert leading whitespace to indent level (2 spaces = 1 level)."""
        spaces = len(leading_whitespace.replace("\t", "    "))
        return spaces // 2

    def _parse_code_block(
        self, lines: List[str], start_idx: int
    ) -> Tuple[CodeBlock, int]:
        """Parse a fenced code block starting at start_idx.

        Args:
            lines: All lines in the document
            start_idx: Index of the opening ``` line

        Returns:
            Tuple of (CodeBlock, next_line_index)
        """
        # Extract language from opening fence
        opening_match = self.CODE_BLOCK_START.match(lines[start_idx])
        language = opening_match.group(1) if opening_match and opening_match.group(1) else None

        # Collect content lines until closing fence
        content_lines: List[str] = []
        idx = start_idx + 1

        while idx < len(lines):
            if self.CODE_BLOCK_END.match(lines[idx]):
                idx += 1  # Move past closing fence
                break
            content_lines.append(lines[idx])
            idx += 1
        # If no closing fence found, treat rest as code (graceful degradation)

        return CodeBlock(content="\n".join(content_lines), language=language), idx

    def _parse_table(
        self, lines: List[str], start_idx: int
    ) -> Tuple[MarkdownTable, int]:
        """Parse a markdown table starting at start_idx.

        Args:
            lines: All lines in the document
            start_idx: Index of the first table row

        Returns:
            Tuple of (MarkdownTable, next_line_index)
        """
        rows: List[TableRow] = []
        idx = start_idx
        separator_found = False

        while idx < len(lines) and self.TABLE_ROW_PATTERN.match(lines[idx].strip()):
            line = lines[idx].strip()

            # Check for separator row (|---|---|)
            if self.TABLE_SEPARATOR_PATTERN.match(line):
                separator_found = True
                idx += 1
                continue

            # Parse cells - remove outer pipes and split
            inner = line[1:-1]  # Remove leading/trailing |
            cells_text = inner.split("|")
            cells = [
                TableCell(
                    content=c.strip(),
                    segments=self._parse_inline_formatting(c.strip())
                )
                for c in cells_text
            ]

            # Rows before separator are headers
            is_header = not separator_found
            rows.append(TableRow(cells=cells, is_header=is_header))
            idx += 1

        column_count = len(rows[0].cells) if rows else 0
        table = MarkdownTable(rows=rows, column_count=column_count)

        # Validate table
        if not self._is_valid_table(table):
            logger.warning("Invalid table detected: inconsistent column counts")

        return table, idx

    def _is_valid_table(self, table: MarkdownTable) -> bool:
        """Validate table has consistent column count."""
        if not table.rows:
            return False
        col_count = table.column_count
        return all(len(row.cells) == col_count for row in table.rows)


# Docs API scope - should be added to google_client.py SCOPES
DOCS_SCOPE = "https://www.googleapis.com/auth/documents"


@dataclass
class GoogleDoc:
    """Representation of a Google Doc."""

    id: str
    title: str
    url: Optional[str] = None
    revision_id: Optional[str] = None
    body_content: str = ""

    def to_dict(self) -> Dict[str, Any]:
        return {
            "id": self.id,
            "title": self.title,
            "url": self.url or f"https://docs.google.com/document/d/{self.id}/edit",
            "revision_id": self.revision_id,
            "body_content": self.body_content,
        }


class GoogleDocsError(RuntimeError):
    """Raised when Google Docs operations fail."""


class GoogleDocsClient:
    """Google Docs API client."""

    def __init__(self, credentials=None) -> None:
        """
        Initialize the Google Docs client.

        Args:
            credentials: Optional Google credentials. If not provided,
                         will use load_credentials() from google_client.
        """
        self._credentials = credentials
        self._service: Optional[Resource] = None

    def _get_service(self) -> Resource:
        """Get or create the Docs API service."""
        if self._service is None:
            creds = self._credentials or load_credentials()
            self._service = build("docs", "v1", credentials=creds, cache_discovery=False)
        return self._service

    def create_document(
        self,
        title: str,
        content: Optional[str] = None,
        folder_id: Optional[str] = None,
        format_markdown: bool = True,
    ) -> GoogleDoc:
        """
        Create a new Google Doc.

        Args:
            title: Document title
            content: Optional initial content (plain text or markdown)
            folder_id: Optional Drive folder ID to create document in
                       (requires Drive API, not implemented yet)
            format_markdown: If True, detect and apply markdown formatting.

        Returns:
            GoogleDoc with the created document info
        """
        service = self._get_service()

        try:
            # Create the document
            doc = service.documents().create(body={"title": title}).execute()
            doc_id = doc.get("documentId")

            if not doc_id:
                raise GoogleDocsError("Failed to get document ID from response")

            # Add content if provided
            if content:
                use_formatting = format_markdown and self._is_markdown(content)
                if use_formatting:
                    self._insert_formatted_text(doc_id, content)
                else:
                    self._insert_text(doc_id, content)

            return GoogleDoc(
                id=doc_id,
                title=title,
                url=f"https://docs.google.com/document/d/{doc_id}/edit",
                revision_id=doc.get("revisionId"),
                body_content=content or "",
            )

        except HttpError as exc:
            raise GoogleDocsError(f"Failed to create document: {exc}") from exc

    def get_document(self, doc_id: str) -> GoogleDoc:
        """
        Get a document by ID.

        Args:
            doc_id: Document ID or URL

        Returns:
            GoogleDoc with document info and content
        """
        doc_id = self._extract_doc_id(doc_id)
        service = self._get_service()

        try:
            doc = service.documents().get(documentId=doc_id).execute()

            # Extract text content from the document body
            body_content = self._extract_text_content(doc)

            return GoogleDoc(
                id=doc_id,
                title=doc.get("title", ""),
                url=f"https://docs.google.com/document/d/{doc_id}/edit",
                revision_id=doc.get("revisionId"),
                body_content=body_content,
            )

        except HttpError as exc:
            if exc.resp.status == 404:
                raise GoogleDocsError(f"Document not found: {doc_id}") from exc
            raise GoogleDocsError(f"Failed to get document: {exc}") from exc

    def update_document(
        self,
        doc_id: str,
        content: str,
        replace: bool = True,
        format_markdown: bool = True,
    ) -> GoogleDoc:
        """
        Update a document's content.

        Args:
            doc_id: Document ID or URL
            content: New content (plain text or markdown)
            replace: If True, replace all content. If False, append.
            format_markdown: If True, detect and apply markdown formatting.

        Returns:
            GoogleDoc with updated document info
        """
        doc_id = self._extract_doc_id(doc_id)

        # Determine if we should apply markdown formatting
        use_formatting = format_markdown and self._is_markdown(content)

        if replace:
            # Get current document to find content range
            doc = self.get_document(doc_id)
            if doc.body_content:
                self._clear_content(doc_id)

            if use_formatting:
                self._insert_formatted_text(doc_id, content)
            else:
                self._insert_text(doc_id, content)
        else:
            if use_formatting:
                self._append_formatted_text(doc_id, content)
            else:
                self._append_text(doc_id, content)

        return self.get_document(doc_id)

    def append_content(
        self,
        doc_id: str,
        content: str,
        add_newline: bool = True,
        format_markdown: bool = True,
    ) -> GoogleDoc:
        """
        Append content to a document.

        Args:
            doc_id: Document ID or URL
            content: Content to append (plain text or markdown)
            add_newline: If True, add newline before content
            format_markdown: If True, detect and apply markdown formatting.

        Returns:
            GoogleDoc with updated document info
        """
        doc_id = self._extract_doc_id(doc_id)

        if add_newline:
            content = "\n" + content

        use_formatting = format_markdown and self._is_markdown(content)
        if use_formatting:
            self._append_formatted_text(doc_id, content)
        else:
            self._append_text(doc_id, content)

        return self.get_document(doc_id)

    def insert_text(
        self,
        doc_id: str,
        text: str,
        index: int = 1,
    ) -> GoogleDoc:
        """
        Insert text at a specific index.

        Args:
            doc_id: Document ID or URL
            text: Text to insert
            index: Character index (1 is start of document)

        Returns:
            GoogleDoc with updated document info
        """
        doc_id = self._extract_doc_id(doc_id)
        self._insert_text(doc_id, text, index)
        return self.get_document(doc_id)

    def _insert_text(self, doc_id: str, text: str, index: int = 1) -> None:
        """Insert text at specified index using batchUpdate."""
        service = self._get_service()

        requests = [
            {
                "insertText": {
                    "location": {"index": index},
                    "text": text,
                }
            }
        ]

        try:
            service.documents().batchUpdate(
                documentId=doc_id,
                body={"requests": requests},
            ).execute()
        except HttpError as exc:
            raise GoogleDocsError(f"Failed to insert text: {exc}") from exc

    def _insert_formatted_text(
        self, doc_id: str, content: str, index: int = 1
    ) -> None:
        """
        Insert text with markdown formatting applied.

        Parses markdown and applies appropriate Google Docs styles.
        Handles special elements (tables, code blocks, placeholders) individually.
        """
        parser = MarkdownParser()
        paragraphs = parser.parse(content)

        # Check if we have any special elements that need individual handling
        has_special = any(
            para.table or para.code_block or para.placeholder
            for para in paragraphs
        )

        if has_special:
            # Process paragraphs sequentially to handle special elements
            self._insert_formatted_text_sequential(doc_id, paragraphs, index)
        else:
            # Use optimized batch insertion for regular text
            self._insert_formatted_text_batch(doc_id, paragraphs, index)

    def _insert_formatted_text_sequential(
        self, doc_id: str, paragraphs: List[FormattedParagraph], index: int
    ) -> None:
        """Insert paragraphs with batching for regular text and sequential for special elements.

        Strategy: Batch consecutive regular paragraphs together (2 API calls per batch),
        only process special elements (tables, code blocks, placeholders) individually.
        This dramatically reduces API calls vs inserting each paragraph separately.

        Google Docs API quota: 60 writes/minute.
        """
        current_pos = index
        api_calls = 0
        start_time = time.time()

        def wait_for_rate_limit():
            """Wait if we're approaching the rate limit."""
            nonlocal api_calls, start_time
            elapsed = time.time() - start_time
            # Target: 45 calls/min (75% of limit) = 0.75 calls/sec
            if api_calls > 0 and elapsed > 0:
                rate = api_calls / elapsed
                if rate > 0.7:  # Getting close to limit
                    # Wait enough to get back under target rate
                    target_elapsed = api_calls / 0.6  # 36 calls/min target
                    sleep_time = max(0, target_elapsed - elapsed)
                    if sleep_time > 0:
                        logger.debug(f"Rate limiting: {api_calls} calls in {elapsed:.1f}s, sleeping {sleep_time:.1f}s")
                        time.sleep(min(sleep_time, 5.0))  # Cap at 5s

        def handle_rate_limit_error():
            """Reset tracking and wait for quota reset."""
            nonlocal api_calls, start_time
            logger.warning("Rate limited (429), waiting 65s for quota reset")
            time.sleep(65)
            api_calls = 0
            start_time = time.time()

        # Group paragraphs: collect consecutive regular paragraphs between special elements
        i = 0
        while i < len(paragraphs):
            para = paragraphs[i]

            # Check for special element
            if para.table or para.code_block or para.placeholder:
                wait_for_rate_limit()
                time.sleep(1.0)  # Extra delay before complex operations

                try:
                    if para.table:
                        current_pos = self._insert_table(doc_id, para.table, current_pos)
                        api_calls += 4  # Table uses 4 API calls (optimized)
                    elif para.code_block:
                        current_pos = self._insert_code_block(doc_id, para.code_block, current_pos)
                        api_calls += 2
                    elif para.placeholder:
                        current_pos = self._insert_placeholder(
                            doc_id, para.placeholder.description, current_pos
                        )
                        api_calls += 2
                except HttpError as exc:
                    if exc.resp.status == 429:
                        handle_rate_limit_error()
                        # Retry
                        if para.table:
                            current_pos = self._insert_table(doc_id, para.table, current_pos)
                        elif para.code_block:
                            current_pos = self._insert_code_block(doc_id, para.code_block, current_pos)
                        elif para.placeholder:
                            current_pos = self._insert_placeholder(
                                doc_id, para.placeholder.description, current_pos
                            )
                    else:
                        raise
                i += 1
            else:
                # Collect consecutive regular paragraphs
                regular_batch: List[FormattedParagraph] = []
                while i < len(paragraphs):
                    p = paragraphs[i]
                    if p.table or p.code_block or p.placeholder:
                        break
                    regular_batch.append(p)
                    i += 1

                # Batch insert regular paragraphs (2 API calls total for any batch size)
                if regular_batch:
                    wait_for_rate_limit()
                    try:
                        batch_len = self._insert_paragraph_batch(doc_id, regular_batch, current_pos)
                        current_pos += batch_len
                        api_calls += 2  # One batch = 2 API calls (insert + format)
                    except HttpError as exc:
                        if exc.resp.status == 429:
                            handle_rate_limit_error()
                            batch_len = self._insert_paragraph_batch(doc_id, regular_batch, current_pos)
                            current_pos += batch_len
                        else:
                            raise

    def _insert_paragraph_batch(
        self, doc_id: str, paragraphs: List[FormattedParagraph], index: int
    ) -> int:
        """Insert a batch of regular paragraphs efficiently.

        Combines all paragraphs into a single text insertion, then applies
        formatting in a single batch update. This uses only 2 API calls
        regardless of how many paragraphs are in the batch.

        Args:
            doc_id: Document ID
            paragraphs: List of regular FormattedParagraphs (no tables/code blocks/placeholders)
            index: Document index to insert at

        Returns:
            Total length of inserted text
        """
        if not paragraphs:
            return 0

        service = self._get_service()

        # Build combined text and track segment positions
        combined_text = ""
        para_info: List[Dict[str, Any]] = []  # Track para start, end, and formatting

        for para in paragraphs:
            para_start = index + len(combined_text)
            para_text = "".join(seg.text for seg in para.segments) + "\n"
            para_end = para_start + len(para_text) - 1  # Exclude newline for formatting

            # Track segment positions within this paragraph
            seg_infos = []
            seg_pos = para_start
            for seg in para.segments:
                if seg.text:
                    seg_infos.append({
                        "start": seg_pos,
                        "end": seg_pos + len(seg.text),
                        "bold": seg.bold,
                        "italic": seg.italic,
                        "code": seg.code,
                    })
                    seg_pos += len(seg.text)

            para_info.append({
                "start": para_start,
                "end": para_end,
                "text_end": para_start + len(para_text),
                "style": para.style,
                "is_bullet": para.is_bullet,
                "is_numbered": para.is_numbered,
                "is_hr": para.is_hr,
                "indent_level": para.indent_level,
                "segments": seg_infos,
            })
            combined_text += para_text

        if not combined_text:
            return 0

        try:
            # API Call 1: Insert all text at once
            service.documents().batchUpdate(
                documentId=doc_id,
                body={"requests": [{
                    "insertText": {
                        "location": {"index": index},
                        "text": combined_text,
                    }
                }]},
            ).execute()

            # Build all formatting requests
            formatting_requests: List[Dict[str, Any]] = []

            for pinfo in para_info:
                # Horizontal rule - special handling
                if pinfo["is_hr"]:
                    continue  # HR text is already inserted, no extra formatting needed

                # Text segment formatting (bold, italic, code)
                for seg in pinfo["segments"]:
                    if seg["bold"] or seg["italic"] or seg["code"]:
                        text_style: Dict[str, Any] = {}
                        fields = []

                        if seg["bold"]:
                            text_style["bold"] = True
                            fields.append("bold")
                        if seg["italic"]:
                            text_style["italic"] = True
                            fields.append("italic")
                        if seg["code"]:
                            text_style["weightedFontFamily"] = {
                                "fontFamily": "Consolas",
                                "weight": 400,
                            }
                            text_style["backgroundColor"] = {
                                "color": {"rgbColor": {"red": 0.93, "green": 0.93, "blue": 0.93}}
                            }
                            fields.extend(["weightedFontFamily", "backgroundColor"])

                        formatting_requests.append({
                            "updateTextStyle": {
                                "range": {"startIndex": seg["start"], "endIndex": seg["end"]},
                                "textStyle": text_style,
                                "fields": ",".join(fields),
                            }
                        })

                # Paragraph style (headers)
                if pinfo["style"] != ParagraphStyle.NORMAL:
                    formatting_requests.append({
                        "updateParagraphStyle": {
                            "range": {"startIndex": pinfo["start"], "endIndex": pinfo["end"]},
                            "paragraphStyle": {"namedStyleType": pinfo["style"].value},
                            "fields": "namedStyleType",
                        }
                    })

                # Bullet lists
                if pinfo["is_bullet"]:
                    formatting_requests.append({
                        "createParagraphBullets": {
                            "range": {"startIndex": pinfo["start"], "endIndex": pinfo["end"]},
                            "bulletPreset": "BULLET_DISC_CIRCLE_SQUARE",
                        }
                    })
                    if pinfo["indent_level"] > 0:
                        formatting_requests.append({
                            "updateParagraphStyle": {
                                "range": {"startIndex": pinfo["start"], "endIndex": pinfo["end"]},
                                "paragraphStyle": {
                                    "indentFirstLine": {"magnitude": 18 * pinfo["indent_level"], "unit": "PT"},
                                    "indentStart": {"magnitude": 18 * pinfo["indent_level"], "unit": "PT"},
                                },
                                "fields": "indentFirstLine,indentStart",
                            }
                        })

                # Numbered lists
                if pinfo["is_numbered"]:
                    formatting_requests.append({
                        "createParagraphBullets": {
                            "range": {"startIndex": pinfo["start"], "endIndex": pinfo["end"]},
                            "bulletPreset": "NUMBERED_DECIMAL_NESTED",
                        }
                    })
                    if pinfo["indent_level"] > 0:
                        formatting_requests.append({
                            "updateParagraphStyle": {
                                "range": {"startIndex": pinfo["start"], "endIndex": pinfo["end"]},
                                "paragraphStyle": {
                                    "indentFirstLine": {"magnitude": 18 * pinfo["indent_level"], "unit": "PT"},
                                    "indentStart": {"magnitude": 18 * pinfo["indent_level"], "unit": "PT"},
                                },
                                "fields": "indentFirstLine,indentStart",
                            }
                        })

            # API Call 2: Apply all formatting at once
            if formatting_requests:
                service.documents().batchUpdate(
                    documentId=doc_id,
                    body={"requests": formatting_requests},
                ).execute()

            return len(combined_text)

        except HttpError as exc:
            raise GoogleDocsError(f"Failed to insert paragraph batch: {exc}") from exc

    def _insert_single_paragraph(
        self, doc_id: str, para: FormattedParagraph, index: int
    ) -> int:
        """Insert a single formatted paragraph.

        Args:
            doc_id: Document ID
            para: FormattedParagraph to insert
            index: Document index to insert at

        Returns:
            New index position after insertion
        """
        service = self._get_service()

        # Build paragraph text
        para_text = "".join(seg.text for seg in para.segments) + "\n"
        text_len = len(para_text)

        try:
            # Insert text
            service.documents().batchUpdate(
                documentId=doc_id,
                body={"requests": [{
                    "insertText": {
                        "location": {"index": index},
                        "text": para_text,
                    }
                }]},
            ).execute()

            # Build formatting requests
            formatting_requests: List[Dict[str, Any]] = []
            current_pos = index

            for seg in para.segments:
                seg_len = len(seg.text)
                if seg_len == 0:
                    continue

                seg.start_index = current_pos
                seg.end_index = current_pos + seg_len

                # Apply text styles (bold, italic, code)
                if seg.bold or seg.italic or seg.code:
                    text_style: Dict[str, Any] = {}
                    fields = []

                    if seg.bold:
                        text_style["bold"] = True
                        fields.append("bold")
                    if seg.italic:
                        text_style["italic"] = True
                        fields.append("italic")
                    if seg.code:
                        # Use monospace font for inline code
                        text_style["weightedFontFamily"] = {
                            "fontFamily": "Consolas",
                            "weight": 400,
                        }
                        text_style["backgroundColor"] = {
                            "color": {"rgbColor": {"red": 0.93, "green": 0.93, "blue": 0.93}}
                        }
                        fields.extend(["weightedFontFamily", "backgroundColor"])

                    formatting_requests.append({
                        "updateTextStyle": {
                            "range": {
                                "startIndex": seg.start_index,
                                "endIndex": seg.end_index,
                            },
                            "textStyle": text_style,
                            "fields": ",".join(fields),
                        }
                    })

                current_pos += seg_len

            para_end = current_pos  # Before newline

            # Apply paragraph style (headers)
            if para.style != ParagraphStyle.NORMAL:
                formatting_requests.append({
                    "updateParagraphStyle": {
                        "range": {
                            "startIndex": index,
                            "endIndex": para_end,
                        },
                        "paragraphStyle": {
                            "namedStyleType": para.style.value,
                        },
                        "fields": "namedStyleType",
                    }
                })

            # Apply bullet list with indentation
            if para.is_bullet:
                formatting_requests.append({
                    "createParagraphBullets": {
                        "range": {
                            "startIndex": index,
                            "endIndex": para_end,
                        },
                        "bulletPreset": "BULLET_DISC_CIRCLE_SQUARE",
                    }
                })
                # Apply indentation for nested lists
                if para.indent_level > 0:
                    formatting_requests.append({
                        "updateParagraphStyle": {
                            "range": {
                                "startIndex": index,
                                "endIndex": para_end,
                            },
                            "paragraphStyle": {
                                "indentFirstLine": {
                                    "magnitude": 18 * para.indent_level,
                                    "unit": "PT"
                                },
                                "indentStart": {
                                    "magnitude": 18 * para.indent_level,
                                    "unit": "PT"
                                }
                            },
                            "fields": "indentFirstLine,indentStart",
                        }
                    })

            # Apply numbered list with indentation
            if para.is_numbered:
                formatting_requests.append({
                    "createParagraphBullets": {
                        "range": {
                            "startIndex": index,
                            "endIndex": para_end,
                        },
                        "bulletPreset": "NUMBERED_DECIMAL_NESTED",
                    }
                })
                # Apply indentation for nested lists
                if para.indent_level > 0:
                    formatting_requests.append({
                        "updateParagraphStyle": {
                            "range": {
                                "startIndex": index,
                                "endIndex": para_end,
                            },
                            "paragraphStyle": {
                                "indentFirstLine": {
                                    "magnitude": 18 * para.indent_level,
                                    "unit": "PT"
                                },
                                "indentStart": {
                                    "magnitude": 18 * para.indent_level,
                                    "unit": "PT"
                                }
                            },
                            "fields": "indentFirstLine,indentStart",
                        }
                    })

            # Apply formatting
            if formatting_requests:
                service.documents().batchUpdate(
                    documentId=doc_id,
                    body={"requests": formatting_requests},
                ).execute()

            return index + text_len

        except HttpError as exc:
            raise GoogleDocsError(f"Failed to insert paragraph: {exc}") from exc

    def _insert_formatted_text_batch(
        self, doc_id: str, paragraphs: List[FormattedParagraph], index: int
    ) -> None:
        """
        Batch insert regular paragraphs (optimized path when no special elements).

        This is the original implementation for better performance.
        """
        service = self._get_service()

        # Build the full plain text first (with newlines)
        all_requests: List[Dict[str, Any]] = []
        formatting_requests: List[Dict[str, Any]] = []

        # Build plain text and track positions
        plain_text_parts: List[str] = []
        for para in paragraphs:
            para_text = "".join(seg.text for seg in para.segments)
            plain_text_parts.append(para_text)

        plain_text = "\n".join(plain_text_parts)

        # Insert all text first
        all_requests.append({
            "insertText": {
                "location": {"index": index},
                "text": plain_text,
            }
        })

        # Now build formatting requests
        # Track current position in the document
        current_pos = index
        for para_idx, para in enumerate(paragraphs):
            para_start = current_pos

            # Process each segment
            for seg in para.segments:
                seg_len = len(seg.text)
                if seg_len == 0:
                    continue

                seg.start_index = current_pos
                seg.end_index = current_pos + seg_len

                # Apply text styles (bold, italic, code)
                if seg.bold or seg.italic or seg.code:
                    text_style: Dict[str, Any] = {}
                    fields = []

                    if seg.bold:
                        text_style["bold"] = True
                        fields.append("bold")
                    if seg.italic:
                        text_style["italic"] = True
                        fields.append("italic")
                    if seg.code:
                        # Use monospace font for inline code
                        text_style["weightedFontFamily"] = {
                            "fontFamily": "Consolas",
                            "weight": 400,
                        }
                        text_style["backgroundColor"] = {
                            "color": {"rgbColor": {"red": 0.93, "green": 0.93, "blue": 0.93}}
                        }
                        fields.extend(["weightedFontFamily", "backgroundColor"])

                    formatting_requests.append({
                        "updateTextStyle": {
                            "range": {
                                "startIndex": seg.start_index,
                                "endIndex": seg.end_index,
                            },
                            "textStyle": text_style,
                            "fields": ",".join(fields),
                        }
                    })

                current_pos += seg_len

            # Calculate paragraph end (include the newline if not last paragraph)
            para_end = current_pos
            if para_idx < len(paragraphs) - 1:
                current_pos += 1  # Account for newline

            # Apply paragraph style (headers)
            if para.style != ParagraphStyle.NORMAL:
                formatting_requests.append({
                    "updateParagraphStyle": {
                        "range": {
                            "startIndex": para_start,
                            "endIndex": para_end,
                        },
                        "paragraphStyle": {
                            "namedStyleType": para.style.value,
                        },
                        "fields": "namedStyleType",
                    }
                })

            # Apply bullet list
            if para.is_bullet:
                formatting_requests.append({
                    "createParagraphBullets": {
                        "range": {
                            "startIndex": para_start,
                            "endIndex": para_end,
                        },
                        "bulletPreset": "BULLET_DISC_CIRCLE_SQUARE",
                    }
                })
                # Apply indentation for nested lists
                if para.indent_level > 0:
                    formatting_requests.append({
                        "updateParagraphStyle": {
                            "range": {
                                "startIndex": para_start,
                                "endIndex": para_end,
                            },
                            "paragraphStyle": {
                                "indentFirstLine": {
                                    "magnitude": 18 * para.indent_level,
                                    "unit": "PT"
                                },
                                "indentStart": {
                                    "magnitude": 18 * para.indent_level,
                                    "unit": "PT"
                                }
                            },
                            "fields": "indentFirstLine,indentStart",
                        }
                    })

            # Apply numbered list
            if para.is_numbered:
                formatting_requests.append({
                    "createParagraphBullets": {
                        "range": {
                            "startIndex": para_start,
                            "endIndex": para_end,
                        },
                        "bulletPreset": "NUMBERED_DECIMAL_NESTED",
                    }
                })
                # Apply indentation for nested lists
                if para.indent_level > 0:
                    formatting_requests.append({
                        "updateParagraphStyle": {
                            "range": {
                                "startIndex": para_start,
                                "endIndex": para_end,
                            },
                            "paragraphStyle": {
                                "indentFirstLine": {
                                    "magnitude": 18 * para.indent_level,
                                    "unit": "PT"
                                },
                                "indentStart": {
                                    "magnitude": 18 * para.indent_level,
                                    "unit": "PT"
                                }
                            },
                            "fields": "indentFirstLine,indentStart",
                        }
                    })

        # Execute all requests
        try:
            # First insert the text
            service.documents().batchUpdate(
                documentId=doc_id,
                body={"requests": all_requests},
            ).execute()

            # Then apply formatting (if any)
            if formatting_requests:
                service.documents().batchUpdate(
                    documentId=doc_id,
                    body={"requests": formatting_requests},
                ).execute()

        except HttpError as exc:
            raise GoogleDocsError(f"Failed to insert formatted text: {exc}") from exc

    def _insert_code_block(self, doc_id: str, code_block: CodeBlock, index: int) -> int:
        """Insert a code block with monospace font and gray background.

        Args:
            doc_id: Document ID
            code_block: CodeBlock to insert
            index: Document index to insert at

        Returns:
            New index position after insertion
        """
        service = self._get_service()

        # Add newline before and after for separation
        text = code_block.content + "\n"
        text_len = len(text)

        try:
            # Insert the text
            service.documents().batchUpdate(
                documentId=doc_id,
                body={"requests": [{
                    "insertText": {
                        "location": {"index": index},
                        "text": text,
                    }
                }]},
            ).execute()

            # Apply code block formatting (Consolas + gray background)
            formatting_requests = [
                {
                    "updateTextStyle": {
                        "range": {
                            "startIndex": index,
                            "endIndex": index + text_len - 1,  # Exclude trailing newline
                        },
                        "textStyle": {
                            "weightedFontFamily": {
                                "fontFamily": "Consolas",
                                "weight": 400,
                            },
                            "backgroundColor": {
                                "color": {"rgbColor": {"red": 0.95, "green": 0.95, "blue": 0.95}}
                            }
                        },
                        "fields": "weightedFontFamily,backgroundColor",
                    }
                }
            ]

            service.documents().batchUpdate(
                documentId=doc_id,
                body={"requests": formatting_requests},
            ).execute()

            return index + text_len

        except HttpError as exc:
            raise GoogleDocsError(f"Failed to insert code block: {exc}") from exc

    def _get_table_cell_indices(
        self, doc: Dict[str, Any], table_start: int
    ) -> List[List[int]]:
        """Walk the document body to find cell content start indices.

        Tables appear as structural elements in doc['body']['content'].
        Each cell has a 'content' array with paragraph elements.
        Extract startIndex from each cell's first paragraph.

        Returns:
            [[row0_col0_idx, row0_col1_idx], [row1_col0_idx, ...], ...]
        """
        body = doc.get("body", {})
        content = body.get("content", [])

        cell_indices: List[List[int]] = []
        for element in content:
            if "table" in element:
                # Check if this table starts at or after our expected position
                elem_start = element.get("startIndex", 0)
                if elem_start >= table_start:
                    table_elem = element["table"]
                    for row in table_elem.get("tableRows", []):
                        row_indices: List[int] = []
                        for cell in row.get("tableCells", []):
                            cell_content = cell.get("content", [])
                            if cell_content:
                                # First paragraph's startIndex is where we insert
                                para = cell_content[0]
                                row_indices.append(para.get("startIndex", 0))
                        cell_indices.append(row_indices)
                    return cell_indices
        return cell_indices

    def _insert_table(self, doc_id: str, table: MarkdownTable, index: int) -> int:
        """Insert a markdown table as a Google Docs table.

        Optimized 4-call execution:
        1. InsertTableRequest (creates structure)
        2. Re-fetch document (discover cell indices + table end)
        3. InsertText for all cells (batched)
        4. Apply header formatting (batched)

        Args:
            doc_id: Document ID
            table: MarkdownTable to insert
            index: Document index to insert at

        Returns:
            New index position after insertion
        """
        if not table.rows:
            return index

        service = self._get_service()

        try:
            # API Call 1: Create table structure
            service.documents().batchUpdate(
                documentId=doc_id,
                body={"requests": [{
                    "insertTable": {
                        "rows": len(table.rows),
                        "columns": table.column_count,
                        "location": {"index": index}
                    }
                }]},
            ).execute()

            # API Call 2: Re-fetch to discover cell indices and table position
            doc = service.documents().get(documentId=doc_id).execute()
            cell_indices = self._get_table_cell_indices(doc, index)

            # Also find table start/end from this same fetch
            table_start_idx = None
            table_end_idx = index
            body = doc.get("body", {})
            content = body.get("content", [])
            for element in content:
                if "table" in element:
                    elem_start = element.get("startIndex", 0)
                    if elem_start >= index:
                        table_start_idx = elem_start
                        table_end_idx = element.get("endIndex", index)
                        break

            if not cell_indices:
                logger.warning("Could not find table cell indices")
                # Estimate: each cell is ~2 chars (content + newline), plus table overhead
                return index + len(table.rows) * table.column_count * 2 + 4

            # Calculate total text being inserted for end position adjustment
            total_text_len = sum(
                len(cell.content)
                for row in table.rows
                for cell in row.cells
                if cell.content
            )

            # API Call 3: Populate all cells (in reverse order to preserve indices)
            text_requests: List[Dict[str, Any]] = []
            for row_idx in range(len(table.rows) - 1, -1, -1):
                row = table.rows[row_idx]
                for col_idx in range(len(row.cells) - 1, -1, -1):
                    if row_idx >= len(cell_indices) or col_idx >= len(cell_indices[row_idx]):
                        continue

                    cell = row.cells[col_idx]
                    cell_index = cell_indices[row_idx][col_idx]

                    if cell.content:
                        text_requests.append({
                            "insertText": {
                                "location": {"index": cell_index},
                                "text": cell.content
                            }
                        })

            if text_requests:
                service.documents().batchUpdate(
                    documentId=doc_id,
                    body={"requests": text_requests},
                ).execute()

            # API Call 4: Apply header formatting (use table_start_idx from earlier fetch)
            # The table's position in the document doesn't change, only cell contents shift
            if table_start_idx is not None:
                formatting_requests: List[Dict[str, Any]] = []

                # Header cell background (light blue #D0E4F5)
                for col_idx in range(table.column_count):
                    formatting_requests.append({
                        "updateTableCellStyle": {
                            "tableCellStyle": {
                                "backgroundColor": {
                                    "color": {"rgbColor": {
                                        "red": 0.816,
                                        "green": 0.894,
                                        "blue": 0.961
                                    }}
                                }
                            },
                            "tableRange": {
                                "tableCellLocation": {
                                    "tableStartLocation": {"index": table_start_idx},
                                    "rowIndex": 0,
                                    "columnIndex": col_idx
                                },
                                "rowSpan": 1,
                                "columnSpan": 1
                            },
                            "fields": "backgroundColor"
                        }
                    })

                if formatting_requests:
                    try:
                        service.documents().batchUpdate(
                            documentId=doc_id,
                            body={"requests": formatting_requests},
                        ).execute()
                    except HttpError:
                        # Header formatting is best-effort
                        logger.warning("Could not apply table header formatting")

            # Return estimated end position (table_end + text inserted)
            return table_end_idx + total_text_len

        except HttpError as exc:
            raise GoogleDocsError(f"Failed to insert table: {exc}") from exc

    def _insert_placeholder(self, doc_id: str, description: str, index: int) -> int:
        """Insert a screenshot placeholder with styled callout box.

        Args:
            doc_id: Document ID
            description: Placeholder description
            index: Document index to insert at

        Returns:
            New index position after insertion
        """
        service = self._get_service()
        placeholder_text = f"[Screenshot: {description}]\n"
        text_len = len(placeholder_text)

        try:
            # Insert text
            service.documents().batchUpdate(
                documentId=doc_id,
                body={"requests": [{
                    "insertText": {
                        "location": {"index": index},
                        "text": placeholder_text,
                    }
                }]},
            ).execute()

            # Apply styling - yellow callout box with gold left border
            # Note: Explicit padding required to avoid UNIT_UNSPECIFIED error
            formatting_requests = [
                {
                    "updateParagraphStyle": {
                        "range": {
                            "startIndex": index,
                            "endIndex": index + text_len,
                        },
                        "paragraphStyle": {
                            "borderLeft": {
                                "color": {"color": {"rgbColor": {
                                    "red": 0.9, "green": 0.8, "blue": 0.4
                                }}},
                                "width": {"magnitude": 3, "unit": "PT"},
                                "dashStyle": "SOLID",
                                "padding": {"magnitude": 6, "unit": "PT"}
                            },
                            "borderTop": {
                                "color": {"color": {"rgbColor": {
                                    "red": 0.7, "green": 0.7, "blue": 0.7
                                }}},
                                "width": {"magnitude": 1, "unit": "PT"},
                                "dashStyle": "SOLID",
                                "padding": {"magnitude": 6, "unit": "PT"}
                            },
                            "borderBottom": {
                                "color": {"color": {"rgbColor": {
                                    "red": 0.7, "green": 0.7, "blue": 0.7
                                }}},
                                "width": {"magnitude": 1, "unit": "PT"},
                                "dashStyle": "SOLID",
                                "padding": {"magnitude": 6, "unit": "PT"}
                            },
                            "shading": {
                                "backgroundColor": {"color": {"rgbColor": {
                                    "red": 1.0, "green": 0.98, "blue": 0.9
                                }}}
                            }
                        },
                        "fields": "borderLeft,borderTop,borderBottom,shading"
                    }
                },
                {
                    "updateTextStyle": {
                        "range": {
                            "startIndex": index,
                            "endIndex": index + text_len - 1,  # Exclude trailing newline
                        },
                        "textStyle": {"italic": True},
                        "fields": "italic"
                    }
                }
            ]

            service.documents().batchUpdate(
                documentId=doc_id,
                body={"requests": formatting_requests},
            ).execute()

            return index + text_len

        except HttpError as exc:
            raise GoogleDocsError(f"Failed to insert placeholder: {exc}") from exc

    def _is_markdown(self, content: str) -> bool:
        """Detect if content appears to be markdown."""
        markdown_indicators = [
            r"^#{1,6}\s",         # Headers
            r"\*\*.+\*\*",        # Bold
            r"^\s*[-*]\s",        # Bullet lists
            r"^\s*\d+\.\s",       # Numbered lists
            r"^-{3,}$",           # Horizontal rules
            r"`.+`",              # Inline code
            r"^```",              # Code blocks
            r"^\|.+\|$",          # Tables
            r"^\[Screenshot:",    # Screenshot placeholders
        ]
        for pattern in markdown_indicators:
            if re.search(pattern, content, re.MULTILINE):
                return True
        return False

    def _append_text(self, doc_id: str, text: str) -> None:
        """Append text to end of document."""
        service = self._get_service()

        # Get document to find end index
        try:
            doc = service.documents().get(documentId=doc_id).execute()
            body = doc.get("body", {})
            content = body.get("content", [])

            # Find the end index
            end_index = 1
            if content:
                last_element = content[-1]
                end_index = last_element.get("endIndex", 1) - 1

            requests = [
                {
                    "insertText": {
                        "location": {"index": end_index},
                        "text": text,
                    }
                }
            ]

            service.documents().batchUpdate(
                documentId=doc_id,
                body={"requests": requests},
            ).execute()

        except HttpError as exc:
            raise GoogleDocsError(f"Failed to append text: {exc}") from exc

    def _append_formatted_text(self, doc_id: str, content: str) -> None:
        """Append formatted text to end of document."""
        service = self._get_service()

        try:
            doc = service.documents().get(documentId=doc_id).execute()
            body = doc.get("body", {})
            doc_content = body.get("content", [])

            # Find the end index
            end_index = 1
            if doc_content:
                last_element = doc_content[-1]
                end_index = last_element.get("endIndex", 1) - 1

            # Use formatted insert at end position
            self._insert_formatted_text(doc_id, content, end_index)

        except HttpError as exc:
            raise GoogleDocsError(f"Failed to append formatted text: {exc}") from exc

    def _clear_content(self, doc_id: str) -> None:
        """Clear all content from document."""
        service = self._get_service()

        try:
            doc = service.documents().get(documentId=doc_id).execute()
            body = doc.get("body", {})
            content = body.get("content", [])

            if not content or len(content) <= 1:
                return

            # Find content range (skip structural elements)
            start_index = 1
            end_index = 1

            for element in content:
                end_idx = element.get("endIndex", 1)
                if end_idx > end_index:
                    end_index = end_idx

            # Account for trailing newline
            if end_index > 1:
                end_index -= 1

            if end_index <= start_index:
                return

            requests = [
                {
                    "deleteContentRange": {
                        "range": {
                            "startIndex": start_index,
                            "endIndex": end_index,
                        }
                    }
                }
            ]

            service.documents().batchUpdate(
                documentId=doc_id,
                body={"requests": requests},
            ).execute()

        except HttpError as exc:
            raise GoogleDocsError(f"Failed to clear content: {exc}") from exc

    def _extract_text_content(self, doc: Dict[str, Any]) -> str:
        """Extract plain text content from document structure."""
        body = doc.get("body", {})
        content = body.get("content", [])
        text_parts: List[str] = []

        for element in content:
            if "paragraph" in element:
                paragraph = element["paragraph"]
                for text_run in paragraph.get("elements", []):
                    if "textRun" in text_run:
                        text_parts.append(text_run["textRun"].get("content", ""))

        return "".join(text_parts)

    def _extract_doc_id(self, doc_id_or_url: str) -> str:
        """Extract document ID from URL or return ID as-is."""
        # Handle full URL
        if "docs.google.com" in doc_id_or_url:
            # Extract ID from URL like:
            # https://docs.google.com/document/d/DOC_ID/edit
            match = re.search(r"/document/d/([a-zA-Z0-9_-]+)", doc_id_or_url)
            if match:
                return match.group(1)
            raise GoogleDocsError(f"Could not extract document ID from URL: {doc_id_or_url}")

        return doc_id_or_url


def get_docs_service() -> Resource:
    """
    Build and return the Google Docs API service client.

    Uses the same credentials as Sheets/Drive from google_client.py.
    """
    credentials = load_credentials()
    return build("docs", "v1", credentials=credentials, cache_discovery=False)


# Convenience functions
def create_doc(title: str, content: Optional[str] = None) -> GoogleDoc:
    """Create a new Google Doc."""
    client = GoogleDocsClient()
    return client.create_document(title, content)


def read_doc(doc_id: str) -> GoogleDoc:
    """Read a Google Doc by ID or URL."""
    client = GoogleDocsClient()
    return client.get_document(doc_id)


def update_doc(doc_id: str, content: str, replace: bool = True) -> GoogleDoc:
    """Update a Google Doc's content."""
    client = GoogleDocsClient()
    return client.update_document(doc_id, content, replace)


def append_to_doc(doc_id: str, content: str) -> GoogleDoc:
    """Append content to a Google Doc."""
    client = GoogleDocsClient()
    return client.append_content(doc_id, content)


__all__ = [
    # Data classes
    "GoogleDoc",
    "TableCell",
    "TableRow",
    "MarkdownTable",
    "CodeBlock",
    "ScreenshotPlaceholder",
    "FormattedParagraph",
    "TextSegment",
    # Client and errors
    "GoogleDocsError",
    "GoogleDocsClient",
    "MarkdownParser",
    # Convenience functions
    "get_docs_service",
    "create_doc",
    "read_doc",
    "update_doc",
    "append_to_doc",
    "DOCS_SCOPE",
]
