"""
Google Docs to Markdown Converter

Converts Google Docs API JSON responses to clean Markdown, preserving:
- Headings (H1-H6, Title, Subtitle)
- Bold, italic, strikethrough, code, links
- Ordered and unordered lists with nesting
- Checklists with checked/unchecked state
- Tables with header row separators
- Smart chips: person (@mentions), rich links, dates, inline images,
  footnotes, horizontal rules, auto-text (page numbers), equations
- Document tabs (multi-tab and nested child tabs)
"""

from __future__ import annotations

import logging
from datetime import datetime
from typing import Any

logger = logging.getLogger(__name__)

MONO_FONTS = {"Courier New", "Consolas", "Roboto Mono", "Source Code Pro"}

HEADING_MAP = {
    "TITLE": "#",
    "SUBTITLE": "##",
    "HEADING_1": "#",
    "HEADING_2": "##",
    "HEADING_3": "###",
    "HEADING_4": "####",
    "HEADING_5": "#####",
    "HEADING_6": "######",
}


def convert_doc_to_markdown(doc: dict[str, Any]) -> str:
    """Convert a Google Docs API document response to markdown.

    Supports both legacy (top-level body) and tab-aware responses
    (includeTabsContent=True). For multi-tab docs, each tab gets a
    heading separator. Single-tab docs render without a tab heading.

    Args:
        doc: The document JSON from docs.documents.get()

    Returns:
        Markdown string
    """
    tabs = doc.get("tabs", [])

    if tabs:
        return _convert_tabs_to_markdown(tabs)

    # Legacy: no tabs, use top-level body/lists/footnotes/inlineObjects
    return _convert_body_to_markdown(doc)


def _convert_tabs_to_markdown(tabs: list[dict[str, Any]]) -> str:
    """Convert a list of document tabs to markdown, recursing into child tabs."""
    all_tab_docs: list[tuple[str, dict[str, Any]]] = []
    _collect_tabs(tabs, all_tab_docs)

    if len(all_tab_docs) == 1:
        _, tab_doc = all_tab_docs[0]
        return _convert_body_to_markdown(tab_doc)

    sections: list[str] = []
    for title, tab_doc in all_tab_docs:
        tab_md = _convert_body_to_markdown(tab_doc)
        sections.append(f"# {title}\n\n{tab_md}")

    return "\n".join(sections).rstrip("\n") + "\n"


def _collect_tabs(
    tabs: list[dict[str, Any]],
    result: list[tuple[str, dict[str, Any]]],
) -> None:
    """Flatten tab hierarchy into (title, documentTab) pairs."""
    for tab in tabs:
        props = tab.get("tabProperties", {})
        title = props.get("title", "Untitled Tab")
        doc_tab = tab.get("documentTab", {})
        if doc_tab:
            result.append((title, doc_tab))
        for child in tab.get("childTabs", []):
            _collect_tabs([child], result)


def _convert_body_to_markdown(doc: dict[str, Any]) -> str:
    """Convert a single document body (or documentTab) to markdown."""
    body = doc.get("body", {})
    content = body.get("content", [])
    lists_meta = doc.get("lists", {})
    footnotes_meta = doc.get("footnotes", {})
    inline_objects = doc.get("inlineObjects", {})

    lines: list[str] = []
    ordered_counters: dict[tuple[str, int], int] = {}
    prev_was_list = False
    footnote_defs: list[tuple[str, str]] = []

    for element in content:
        if "paragraph" in element:
            para = element["paragraph"]
            text = _convert_paragraph_text(
                para,
                footnotes_meta=footnotes_meta,
                inline_objects=inline_objects,
                footnote_defs=footnote_defs,
            )

            if not text.strip():
                if prev_was_list:
                    prev_was_list = False
                continue

            bullet = para.get("bullet")
            if bullet:
                list_id = bullet["listId"]
                nesting = bullet.get("nestingLevel", 0)

                if _is_checklist(lists_meta, list_id, nesting):
                    checked = _is_checked(para)
                    checkbox = "[x]" if checked else "[ ]"
                    indent = "  " * nesting
                    cb_text = (
                        _convert_paragraph_text(
                            para,
                            skip_strikethrough=True,
                            footnotes_meta=footnotes_meta,
                            inline_objects=inline_objects,
                            footnote_defs=footnote_defs,
                        )
                        if checked
                        else text
                    )
                    lines.append(f"{indent}- {checkbox} {cb_text}")
                elif _is_ordered_list(lists_meta, list_id, nesting):
                    key = (list_id, nesting)
                    ordered_counters[key] = ordered_counters.get(key, 0) + 1
                    counter = ordered_counters[key]
                    indent = "   " * nesting
                    lines.append(f"{indent}{counter}. {text}")
                else:
                    indent = "  " * nesting
                    lines.append(f"{indent}- {text}")
                prev_was_list = True
            else:
                if prev_was_list:
                    ordered_counters.clear()
                    lines.append("")
                    prev_was_list = False

                style = para.get("paragraphStyle", {})
                named_style = style.get("namedStyleType", "NORMAL_TEXT")
                prefix = HEADING_MAP.get(named_style, "")

                if prefix:
                    lines.append(f"{prefix} {text}")
                    lines.append("")
                else:
                    lines.append(text)
                    lines.append("")

        elif "table" in element:
            if prev_was_list:
                ordered_counters.clear()
                lines.append("")
                prev_was_list = False
            table_md = _convert_table(
                element["table"],
                footnotes_meta=footnotes_meta,
                inline_objects=inline_objects,
                footnote_defs=footnote_defs,
            )
            lines.append(table_md)
            lines.append("")

    if footnote_defs:
        lines.append("")
        for fn_id, fn_text in footnote_defs:
            lines.append(f"[^{fn_id}]: {fn_text}")

    result = "\n".join(lines).rstrip("\n") + "\n"
    return result


def _convert_paragraph_text(
    para: dict[str, Any],
    skip_strikethrough: bool = False,
    footnotes_meta: dict[str, Any] | None = None,
    inline_objects: dict[str, Any] | None = None,
    footnote_defs: list[tuple[str, str]] | None = None,
    active_footnotes: set[str] | None = None,
) -> str:
    """Convert paragraph elements to inline markdown text."""
    parts: list[str] = []
    for elem in para.get("elements", []):
        if "textRun" in elem:
            parts.append(_convert_text_run(elem["textRun"], skip_strikethrough))
        elif "person" in elem:
            parts.append(_convert_person_chip(elem["person"]))
        elif "richLink" in elem:
            parts.append(_convert_rich_link_chip(elem["richLink"]))
        elif "dateElement" in elem:
            parts.append(_convert_date_chip(elem["dateElement"]))
        elif "inlineObjectElement" in elem:
            parts.append(
                _convert_inline_object(elem["inlineObjectElement"], inline_objects)
            )
        elif "footnoteReference" in elem:
            parts.append(
                _convert_footnote_reference(
                    elem["footnoteReference"],
                    footnotes_meta,
                    inline_objects,
                    footnote_defs,
                    active_footnotes,
                )
            )
        elif "horizontalRule" in elem:
            parts.append("\n---\n")
        elif "autoText" in elem:
            parts.append(_convert_auto_text(elem["autoText"]))
        elif "pageBreak" in elem or "columnBreak" in elem:
            pass  # No meaningful markdown representation
        elif "equation" in elem:
            parts.append(_convert_equation(elem["equation"]))
    return "".join(parts).strip()


def _convert_text_run(
    text_run: dict[str, Any], skip_strikethrough: bool = False
) -> str:
    """Convert a single text run to markdown."""
    content = text_run.get("content", "")
    style = text_run.get("textStyle", {})

    text = content.rstrip("\n")
    # Replace Google Docs Private Use Area chip placeholders (e.g. \ue907)
    # that appear for unsupported chip types like vote, stopwatch, timer
    text = text.replace("\ue907", "[Smart Chip]")
    if not text:
        return ""

    return _apply_text_style(text, style, skip_strikethrough)


def _convert_person_chip(person: dict[str, Any]) -> str:
    """Convert a person smart chip to a mailto link."""
    props = person.get("personProperties", {})
    name = props.get("name", "")
    email = props.get("email", "")
    if email:
        label = name or email
        return f"[{label}](mailto:{email})"
    return name or ""


def _convert_rich_link_chip(rich_link: dict[str, Any]) -> str:
    """Convert a rich link smart chip to markdown.

    Rich link chips contain a richLinkProperties dict with title and uri.
    """
    props = rich_link.get("richLinkProperties", {})
    title = props.get("title", "")
    uri = props.get("uri", "")
    if title and uri:
        return f"[{title}]({uri})"
    if uri:
        return uri
    return title or ""


def _convert_date_chip(date_elem: dict[str, Any]) -> str:
    """Convert a date smart chip (dateElement) to markdown.

    Date elements contain dateElementProperties with displayText (the
    locale-formatted string) and a timestamp.
    """
    props = date_elem.get("dateElementProperties", {})
    display_text = props.get("displayText", "")
    if display_text:
        return display_text
    # Fallback: parse the timestamp into a date string
    timestamp = props.get("timestamp", "")
    if timestamp:
        try:
            dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
            return dt.strftime("%Y-%m-%d")
        except (ValueError, TypeError):
            return timestamp
    return ""


def _convert_inline_object(
    element: dict[str, Any],
    inline_objects: dict[str, Any] | None,
) -> str:
    """Convert an inline object (image/drawing) to markdown."""
    obj_id = element.get("inlineObjectId", "")
    if not inline_objects or obj_id not in inline_objects:
        return ""
    obj = inline_objects[obj_id]
    props = obj.get("inlineObjectProperties", {}).get("embeddedObject", {})
    title = props.get("title", "") or props.get("description", "")
    uri = props.get("imageProperties", {}).get("contentUri", "")
    if uri:
        return f"![{title}]({uri})"
    return f"[Image: {title}]" if title else "[Image]"


def _convert_footnote_reference(
    ref: dict[str, Any],
    footnotes_meta: dict[str, Any] | None,
    inline_objects: dict[str, Any] | None,
    footnote_defs: list[tuple[str, str]] | None,
    active_footnotes: set[str] | None = None,
) -> str:
    """Convert a footnote reference to a markdown footnote marker.

    Also collects the footnote definition text for appending at the end.
    """
    fn_id = ref.get("footnoteId", "")
    if not fn_id:
        return ""
    if footnotes_meta and footnote_defs is not None and fn_id in footnotes_meta:
        # Avoid duplicates if the same footnote is referenced multiple times.
        existing_ids = {fid for fid, _ in footnote_defs}
        if fn_id not in existing_ids:
            next_active = set(active_footnotes or ())
            if fn_id not in next_active:
                next_active.add(fn_id)
                fn_content = footnotes_meta[fn_id].get("content", [])
                fn_text = _convert_footnote_content(
                    fn_content,
                    footnotes_meta=footnotes_meta,
                    inline_objects=inline_objects,
                    footnote_defs=footnote_defs,
                    active_footnotes=next_active,
                )
                footnote_defs.append((fn_id, fn_text))
    return f"[^{fn_id}]"


def _convert_footnote_content(
    content: list[dict[str, Any]],
    *,
    footnotes_meta: dict[str, Any] | None,
    inline_objects: dict[str, Any] | None,
    footnote_defs: list[tuple[str, str]] | None,
    active_footnotes: set[str] | None = None,
) -> str:
    """Convert footnote content with the same inline handling as body paragraphs."""
    parts: list[str] = []
    for element in content:
        if "paragraph" in element:
            text = _convert_paragraph_text(
                element["paragraph"],
                footnotes_meta=footnotes_meta,
                inline_objects=inline_objects,
                footnote_defs=footnote_defs,
                active_footnotes=active_footnotes,
            )
            if text.strip():
                parts.append(text.strip())
        elif "table" in element:
            table_text = _convert_table(
                element["table"],
                footnotes_meta=footnotes_meta,
                inline_objects=inline_objects,
                footnote_defs=footnote_defs,
                active_footnotes=active_footnotes,
            )
            if table_text.strip():
                parts.append(table_text.replace("\n", " "))
    return " ".join(parts)


def _convert_auto_text(auto_text: dict[str, Any]) -> str:
    """Convert an autoText element (e.g. page number) to a placeholder."""
    text_type = auto_text.get("type", "")
    if text_type == "PAGE_NUMBER":
        return "[Page #]"
    if text_type == "PAGE_COUNT":
        return "[Page Count]"
    return ""


def _convert_equation(equation: dict[str, Any]) -> str:
    """Convert an equation element to markdown."""
    # The Docs API does not expose equation content as text — only the
    # suggestedInsertionIds / suggestedDeletionIds are available.
    return "[Equation]"


def _apply_text_style(
    text: str, style: dict[str, Any], skip_strikethrough: bool = False
) -> str:
    """Apply markdown formatting based on text style."""
    link = style.get("link", {})
    url = link.get("url")

    font_family = style.get("weightedFontFamily", {}).get("fontFamily", "")
    if font_family in MONO_FONTS:
        return f"`{text}`"

    bold = style.get("bold", False)
    italic = style.get("italic", False)
    strikethrough = style.get("strikethrough", False)

    if bold and italic:
        text = f"***{text}***"
    elif bold:
        text = f"**{text}**"
    elif italic:
        text = f"*{text}*"

    if strikethrough and not skip_strikethrough:
        text = f"~~{text}~~"

    if url:
        text = f"[{text}]({url})"

    return text


def _is_ordered_list(lists_meta: dict[str, Any], list_id: str, nesting: int) -> bool:
    """Check if a list at a given nesting level is ordered."""
    list_info = lists_meta.get(list_id, {})
    nesting_levels = list_info.get("listProperties", {}).get("nestingLevels", [])
    if nesting < len(nesting_levels):
        level = nesting_levels[nesting]
        glyph = level.get("glyphType", "")
        return glyph not in ("", "GLYPH_TYPE_UNSPECIFIED")
    return False


def _is_checklist(lists_meta: dict[str, Any], list_id: str, nesting: int) -> bool:
    """Check if a list at a given nesting level is a checklist.

    Google Docs checklists are distinguished from regular bullet lists by having
    GLYPH_TYPE_UNSPECIFIED with no glyphSymbol — the Docs UI renders interactive
    checkboxes rather than a static glyph character.
    """
    list_info = lists_meta.get(list_id, {})
    nesting_levels = list_info.get("listProperties", {}).get("nestingLevels", [])
    if nesting < len(nesting_levels):
        level = nesting_levels[nesting]
        glyph_type = level.get("glyphType", "")
        has_glyph_symbol = "glyphSymbol" in level
        return glyph_type in ("", "GLYPH_TYPE_UNSPECIFIED") and not has_glyph_symbol
    return False


def _is_checked(para: dict[str, Any]) -> bool:
    """Check if a checklist item is checked.

    Google Docs marks checked checklist items by applying strikethrough
    formatting to the paragraph text.
    """
    for elem in para.get("elements", []):
        if "textRun" in elem:
            content = elem["textRun"].get("content", "").strip()
            if content:
                return elem["textRun"].get("textStyle", {}).get("strikethrough", False)
    return False


def _convert_table(
    table: dict[str, Any],
    *,
    footnotes_meta: dict[str, Any] | None = None,
    inline_objects: dict[str, Any] | None = None,
    footnote_defs: list[tuple[str, str]] | None = None,
    active_footnotes: set[str] | None = None,
) -> str:
    """Convert a table element to markdown."""
    rows = table.get("tableRows", [])
    if not rows:
        return ""

    md_rows: list[str] = []
    for i, row in enumerate(rows):
        cells: list[str] = []
        for cell in row.get("tableCells", []):
            cell_text = _extract_cell_text(
                cell,
                footnotes_meta=footnotes_meta,
                inline_objects=inline_objects,
                footnote_defs=footnote_defs,
                active_footnotes=active_footnotes,
            )
            cells.append(cell_text)
        md_rows.append("| " + " | ".join(cells) + " |")

        if i == 0:
            sep = "| " + " | ".join("---" for _ in cells) + " |"
            md_rows.append(sep)

    return "\n".join(md_rows)


def _extract_cell_text(
    cell: dict[str, Any],
    *,
    footnotes_meta: dict[str, Any] | None = None,
    inline_objects: dict[str, Any] | None = None,
    footnote_defs: list[tuple[str, str]] | None = None,
    active_footnotes: set[str] | None = None,
) -> str:
    """Extract text from a table cell."""
    parts: list[str] = []
    for content_elem in cell.get("content", []):
        if "paragraph" in content_elem:
            text = _convert_paragraph_text(
                content_elem["paragraph"],
                footnotes_meta=footnotes_meta,
                inline_objects=inline_objects,
                footnote_defs=footnote_defs,
                active_footnotes=active_footnotes,
            )
            if text.strip():
                parts.append(text.strip())
    cell_text = " ".join(parts)
    return cell_text.replace("|", "\\|")


def format_comments_inline(markdown: str, comments: list[dict[str, Any]]) -> str:
    """Insert footnote-style comment annotations inline in markdown.

    For each comment, finds the anchor text in the markdown and inserts
    a footnote reference. Unmatched comments go to an appendix at the bottom.
    """
    if not comments:
        return markdown

    footnotes: list[str] = []
    unmatched: list[dict[str, Any]] = []

    for i, comment in enumerate(comments, 1):
        ref = f"[^c{i}]"
        anchor = comment.get("anchor_text", "")

        if anchor and anchor in markdown:
            markdown = markdown.replace(anchor, anchor + ref, 1)
            footnotes.append(_format_footnote(i, comment))
        else:
            unmatched.append(comment)

    if footnotes:
        markdown = markdown.rstrip("\n") + "\n\n" + "\n".join(footnotes) + "\n"

    if unmatched:
        appendix = format_comments_appendix(unmatched)
        if appendix.strip():
            markdown = markdown.rstrip("\n") + "\n\n" + appendix

    return markdown


def _format_footnote(num: int, comment: dict[str, Any]) -> str:
    """Format a single footnote."""
    lines = [f"[^c{num}]: **{comment['author']}**: {comment['content']}"]
    for reply in comment.get("replies", []):
        lines.append(f"    - **{reply['author']}**: {reply['content']}")
    return "\n".join(lines)


def format_comments_appendix(comments: list[dict[str, Any]]) -> str:
    """Format comments as an appendix section with blockquoted anchors."""
    if not comments:
        return ""

    lines = ["## Comments", ""]
    for comment in comments:
        resolved_tag = " *(Resolved)*" if comment.get("resolved") else ""
        anchor = comment.get("anchor_text", "")
        if anchor:
            lines.append(f"> {anchor}")
            lines.append("")
        lines.append(f"- **{comment['author']}**: {comment['content']}{resolved_tag}")
        for reply in comment.get("replies", []):
            lines.append(f"  - **{reply['author']}**: {reply['content']}")
        lines.append("")

    return "\n".join(lines)


def parse_drive_comments(
    response: dict[str, Any], include_resolved: bool = False
) -> list[dict[str, Any]]:
    """Parse Drive API comments response into structured dicts.

    Args:
        response: Raw JSON from drive.comments.list()
        include_resolved: Whether to include resolved comments

    Returns:
        List of comment dicts with keys: author, content, anchor_text,
        replies, resolved
    """
    results = []
    for comment in response.get("comments", []):
        if not include_resolved and comment.get("resolved", False):
            continue

        anchor_text = comment.get("quotedFileContent", {}).get("value", "")
        replies = [
            {
                "author": r.get("author", {}).get("displayName", "Unknown"),
                "content": r.get("content", ""),
            }
            for r in comment.get("replies", [])
        ]
        results.append(
            {
                "author": comment.get("author", {}).get("displayName", "Unknown"),
                "content": comment.get("content", ""),
                "anchor_text": anchor_text,
                "replies": replies,
                "resolved": comment.get("resolved", False),
            }
        )
    return results
