"""Markdown → NSAttributedString renderer for the Lirelle bank window.

Built specifically for the markdown shape that comes out of Lirelle's two
content types:

  1. summary.md — written by the LLM summary endpoint in the user's
     Notion-style format. Uses `###` headings, `**bold**` subsection
     labels, `*italic*` for term emphasis, `- bullet` lists,
     `- [ ]` checkboxes for action items, and `---` separators.

  2. transcript.md — written by meeting_live_transcribe. Just
     `[MM:SS] Speaker: text` lines with a small markdown header.

The output is an NSAttributedString that can drop straight into an
NSTextView (the scrollable text widget) inside the meetings detail pane.
We do NOT use AppKit's built-in `initWithMarkdownData_` because:

  - It lands on macOS 12+ only and the rendering choices are not
    configurable (heading sizes, paragraph spacing, code font all baked
    in to Apple's defaults that don't match Lirelle's brand).
  - It doesn't handle GitHub-style checkboxes `- [ ]`, which we use
    heavily in summaries.
  - Lirelle's voice is calmer and warmer than the system defaults; we want
    Source Code Pro for code, Dela-Gothic-style display weights for
    headings, and our own paragraph spacing.

The grammar we support is a strict subset — enough for our two file
types, no more. If summary.md grows tables or images later, this module
gets extended; until then keep it small.

Supported constructs:

  Block-level:
    `### heading`    (also ##, #)
    `- item`         (also `* item`)
    `- [ ] task`     (open checkbox)
    `- [x] task`     (closed checkbox)
    `> blockquote`   (single-line)
    `---`            (horizontal rule)
    blank line       (paragraph break)

  Inline (within block content):
    `**bold**`
    `*italic*`
    `` `code` ``

Anti-features (intentionally NOT supported, won't crash but render as
literal text):
  - Tables
  - Images
  - Nested lists (rendered flat with single-level indent)
  - Reference-style links
  - HTML pass-through
"""

from __future__ import annotations

import re
from typing import Optional

from AppKit import (  # type: ignore
    NSAttributedString,
    NSColor,
    NSFont,
    NSFontAttributeName,
    NSForegroundColorAttributeName,
    NSMutableAttributedString,
    NSMutableParagraphStyle,
    NSParagraphStyleAttributeName,
    NSUnderlineStyleAttributeName,
    NSUnderlineStyleSingle,
)
from Foundation import NSMakeRange  # type: ignore


# ── Brand-aligned typography ──────────────────────────────────────────
# Matches the bank window's existing font usage. Sizes tuned for
# in-window reading at the bank's typical 760pt width.

BODY_SIZE = 15.0          # paragraph text — bumped 13→15 per redesign spec
H1_SIZE = 28.0            # detail page title (rendered separately, but
                          # included for completeness)
H2_SIZE = 21.0            # primary section heading in summaries
H3_SIZE = 17.0            # subsection
SMALL_SIZE = 11.5         # metadata, separators

# Line-height: 1.75 body / 1.25 heading per spec. NSParagraphStyle
# expects a "line height multiple" relative to default 1.0.
BODY_LINE_HEIGHT = 1.75
HEADING_LINE_HEIGHT = 1.25

# Spacing (in points, between blocks). Tuned to match the airy CSS
# rhythm of the prototype.
PARAGRAPH_SPACING = 14.0
HEADING_SPACING_BEFORE = 28.0    # H2 in spec
HEADING_H3_SPACING_BEFORE = 22.0
HEADING_SPACING_AFTER = 8.0
LIST_ITEM_SPACING = 5.0
HR_SPACING = 20.0
QUOTE_SPACING = 18.0

# Bullet indent — left side of bullet glyph.
BULLET_INDENT = 6.0
# Text indent after the bullet glyph.
BULLET_TEXT_INDENT = 22.0

# Blockquote indent.
QUOTE_INDENT = 18.0


def _color(rgb: tuple[float, float, float], alpha: float = 1.0) -> NSColor:
    r, g, b = rgb
    return NSColor.colorWithCalibratedRed_green_blue_alpha_(r, g, b, alpha)


# Brand colour tokens (mirrored from bank.py — must match).
TEXT_RGB = (0xEF / 255, 0xEA / 255, 0xE4 / 255)       # cream
SECONDARY_RGB = (0x88 / 255, 0x8F / 255, 0x8A / 255)  # ink
ACCENT_RGB = (0xEB / 255, 0xB6 / 255, 0x38 / 255)     # gold
CODE_BG_RGB = (0x1A / 255, 0x16 / 255, 0x12 / 255)    # warm coal


# ── Inline pattern compilation ────────────────────────────────────────
# Order matters: code spans first (so ** inside `code` isn't bold), then
# bold (`**`), then italic (`*`). The regex captures everything between
# the markers so a single span() call gives us the text to inline.

_INLINE_CODE = re.compile(r"`([^`\n]+?)`")
_INLINE_BOLD = re.compile(r"\*\*([^*\n]+?)\*\*")
_INLINE_ITALIC_AST = re.compile(r"(?<![\*\w])\*([^*\n]+?)\*(?!\w)")
_INLINE_ITALIC_UND = re.compile(r"(?<!\w)_([^_\n]+?)_(?!\w)")
_INLINE_LINK = re.compile(r"\[([^\]\n]+)\]\(([^)\n]+)\)")


def _body_font(size: float = BODY_SIZE, *,
               bold: bool = False, italic: bool = False) -> NSFont:
    # Use the system font for body — it ships everywhere and gets
    # weight/italic variants without extra installs.
    # AppKit weights live on a -1.0..1.0 scale (NSFontWeightRegular = 0.0,
    # NSFontWeightBold = 0.4). Passing CSS-style 400/600 clamps to the
    # heaviest end — that was the "everything looks too bold" bug.
    font = NSFont.systemFontOfSize_weight_(
        size, 0.4 if bold else 0.0,
    )
    if italic:
        # systemFontOfSize doesn't have italic variants; use the
        # italic system font via the font descriptor trait.
        from AppKit import NSFontDescriptorTraitItalic
        descriptor = font.fontDescriptor().fontDescriptorWithSymbolicTraits_(
            NSFontDescriptorTraitItalic,
        )
        italic_font = NSFont.fontWithDescriptor_size_(descriptor, size)
        if italic_font is not None:
            font = italic_font
    return font


def _heading_font(size: float) -> NSFont:
    # Semibold weight stands out from body without needing display fonts.
    # AppKit weight scale is -1.0..1.0 (NSFontWeightSemibold = 0.3).
    return NSFont.systemFontOfSize_weight_(size, 0.3)


def _code_font(size: float = BODY_SIZE - 1) -> NSFont:
    # Monospaced for inline code.
    return NSFont.monospacedSystemFontOfSize_weight_(size, 0.0)


def _make_paragraph_style(*, head_indent: float = 0.0,
                          first_line_head_indent: float = 0.0,
                          paragraph_spacing: float = PARAGRAPH_SPACING,
                          paragraph_spacing_before: float = 0.0,
                          line_height_multiple: float = BODY_LINE_HEIGHT,
                          ) -> NSMutableParagraphStyle:
    style = NSMutableParagraphStyle.alloc().init()
    style.setHeadIndent_(head_indent)
    style.setFirstLineHeadIndent_(first_line_head_indent)
    style.setParagraphSpacing_(paragraph_spacing)
    style.setParagraphSpacingBefore_(paragraph_spacing_before)
    style.setLineHeightMultiple_(line_height_multiple)
    return style


def _append_inline(out: NSMutableAttributedString, text: str, *,
                   base_font: NSFont, base_color: NSColor) -> None:
    """Walk `text` looking for inline markers; append styled runs to `out`.

    Single-pass with sequential pattern matching — fast enough for our
    typical paragraph size (under 1 kB)."""
    # Sequentially process the text, peeling off the next match of any
    # inline pattern. The cost of running multiple regex scans on every
    # paragraph is negligible compared to the AppKit work that follows.
    i = 0
    while i < len(text):
        # Find the EARLIEST inline marker.
        candidates = []
        for kind, pattern in (
            ("code", _INLINE_CODE),
            ("bold", _INLINE_BOLD),
            ("italic", _INLINE_ITALIC_AST),
            ("italic", _INLINE_ITALIC_UND),
            ("link", _INLINE_LINK),
        ):
            m = pattern.search(text, i)
            if m is not None:
                candidates.append((m.start(), kind, m))
        if not candidates:
            # Plain text remainder.
            _append_run(out, text[i:], font=base_font, color=base_color)
            break
        candidates.sort(key=lambda c: c[0])
        start, kind, m = candidates[0]
        # Plain text before the marker.
        if start > i:
            _append_run(out, text[i:start], font=base_font, color=base_color)
        # Styled marker.
        if kind == "code":
            # Design spec: inline code = orange text on card bg + 1px border
            # + 5 radius. NSAttributedString can do colour + background but
            # not a per-glyph rounded border; the bg colour alone hints at
            # "code-as-pill" enough for our purposes.
            from AppKit import NSBackgroundColorAttributeName as _BG_NAME
            attrs = {
                NSFontAttributeName: _code_font(),
                NSForegroundColorAttributeName: _color((0xFF / 255, 0x9B / 255, 0x50 / 255)),
                _BG_NAME: NSColor.colorWithCalibratedRed_green_blue_alpha_(
                    0x14 / 255, 0x12 / 255, 0x10 / 255, 1.0,
                ),
            }
            out.appendAttributedString_(
                NSAttributedString.alloc().initWithString_attributes_(m.group(1), attrs),
            )
        elif kind == "bold":
            _append_run(
                out, m.group(1),
                font=_body_font(base_font.pointSize(), bold=True),
                color=base_color,
            )
        elif kind == "italic":
            _append_run(
                out, m.group(1),
                font=_body_font(base_font.pointSize(), italic=True),
                color=base_color,
            )
        elif kind == "link":
            label, _href = m.group(1), m.group(2)
            _append_run(
                out, label,
                font=base_font,
                color=_color(ACCENT_RGB),
                underline=True,
            )
        i = m.end()


def _append_run(out: NSMutableAttributedString, text: str, *,
                font: NSFont, color: NSColor,
                paragraph_style: Optional[NSMutableParagraphStyle] = None,
                underline: bool = False) -> None:
    if not text:
        return
    attrs = {
        NSFontAttributeName: font,
        NSForegroundColorAttributeName: color,
    }
    if paragraph_style is not None:
        attrs[NSParagraphStyleAttributeName] = paragraph_style
    if underline:
        attrs[NSUnderlineStyleAttributeName] = NSUnderlineStyleSingle
    out.appendAttributedString_(
        NSAttributedString.alloc().initWithString_attributes_(text, attrs),
    )


# ── Block parser ──────────────────────────────────────────────────────

_HEADING_RE = re.compile(r"^(#{1,3})\s+(.+?)\s*#*\s*$")
_BULLET_RE = re.compile(r"^[-*]\s+(.+)$")
_CHECKBOX_RE = re.compile(r"^[-*]\s+\[([ xX])\]\s+(.+)$")
_BLOCKQUOTE_RE = re.compile(r"^>\s?(.*)$")
_HR_RE = re.compile(r"^[-_*]{3,}\s*$")


def markdown_to_attributed(text: str) -> NSAttributedString:
    """Convert markdown `text` into an NSAttributedString styled for the
    bank window's content area.

    The output uses Lirelle's brand colours (cream text on dark warm coal
    background), system font for body, Source Code Pro for inline code,
    semibold for headings + bold spans, and italic system font for
    italics."""
    out = NSMutableAttributedString.alloc().init()
    if not text:
        return out

    # Normalise line endings + strip trailing whitespace per line so the
    # block matchers don't have to deal with CRLF or trailing spaces.
    lines = [line.rstrip() for line in text.replace("\r\n", "\n").split("\n")]

    body_color = _color(TEXT_RGB)
    secondary_color = _color(SECONDARY_RGB)
    accent_color = _color(ACCENT_RGB)

    i = 0
    is_first_block = True

    def add_paragraph_break() -> None:
        # Append a newline to act as a paragraph terminator. We use
        # paragraph spacing on the prior block's NSParagraphStyle to
        # control the gap, so a single \n is enough.
        out.appendAttributedString_(
            NSAttributedString.alloc().initWithString_("\n"),
        )

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

        # ── Skip blank lines (treated as paragraph breaks elsewhere) ──
        if not line.strip():
            i += 1
            continue

        # ── Heading ──────────────────────────────────────────────────
        m = _HEADING_RE.match(line)
        if m:
            level = len(m.group(1))
            text_content = m.group(2)
            size = {1: H1_SIZE, 2: H2_SIZE, 3: H3_SIZE}.get(level, H3_SIZE)
            font = _heading_font(size)
            # H3 sits closer to surrounding content than H2 per the
            # prototype's rhythm.
            space_before = HEADING_H3_SPACING_BEFORE if level == 3 else HEADING_SPACING_BEFORE
            style = _make_paragraph_style(
                paragraph_spacing=HEADING_SPACING_AFTER,
                paragraph_spacing_before=(
                    0 if is_first_block else space_before
                ),
                line_height_multiple=HEADING_LINE_HEIGHT,
            )
            run = NSMutableAttributedString.alloc().init()
            _append_inline(run, text_content,
                           base_font=font, base_color=body_color)
            # Apply paragraph style to the whole heading line.
            full_range = NSMakeRange(0, run.length())
            run.addAttribute_value_range_(
                NSParagraphStyleAttributeName, style, full_range,
            )
            out.appendAttributedString_(run)
            add_paragraph_break()
            is_first_block = False
            i += 1
            continue

        # ── Horizontal rule ──────────────────────────────────────────
        if _HR_RE.match(line):
            # Render as a row of em-dashes in the secondary colour.
            style = _make_paragraph_style(
                paragraph_spacing=HR_SPACING,
                paragraph_spacing_before=HR_SPACING,
            )
            _append_run(
                out, "—" * 28 + "\n",
                font=_body_font(SMALL_SIZE),
                color=secondary_color,
                paragraph_style=style,
            )
            is_first_block = False
            i += 1
            continue

        # ── Checkbox list item (must precede plain bullet match) ─────
        m = _CHECKBOX_RE.match(line)
        if m:
            checked = m.group(1).lower() == "x"
            content = m.group(2)
            box = "☑︎ " if checked else "☐ "
            box_color = accent_color if checked else body_color
            style = _make_paragraph_style(
                head_indent=BULLET_TEXT_INDENT,
                first_line_head_indent=BULLET_INDENT,
                paragraph_spacing=LIST_ITEM_SPACING,
            )
            run = NSMutableAttributedString.alloc().init()
            _append_run(
                run, box,
                font=_body_font(BODY_SIZE),
                color=box_color,
                paragraph_style=style,
            )
            _append_inline(run, content,
                           base_font=_body_font(BODY_SIZE),
                           base_color=body_color)
            # Re-apply paragraph style to the appended inline content so
            # the head indent works for wrapped lines too.
            full_range = NSMakeRange(0, run.length())
            run.addAttribute_value_range_(
                NSParagraphStyleAttributeName, style, full_range,
            )
            out.appendAttributedString_(run)
            add_paragraph_break()
            is_first_block = False
            i += 1
            continue

        # ── Bullet list item ─────────────────────────────────────────
        # Design spec: 5px gold dot at left, 22 indent, 9 row gap.
        # Render as a gold ● glyph + 2 spaces, with paragraph head-indent
        # so wrapped lines align under the text (not under the bullet).
        m = _BULLET_RE.match(line)
        if m:
            content = m.group(1)
            style = _make_paragraph_style(
                head_indent=BULLET_TEXT_INDENT,
                first_line_head_indent=BULLET_INDENT,
                paragraph_spacing=LIST_ITEM_SPACING,
            )
            run = NSMutableAttributedString.alloc().init()
            _append_run(
                run, "●  ",
                font=_body_font(BODY_SIZE - 4),
                color=accent_color,
                paragraph_style=style,
            )
            _append_inline(run, content,
                           base_font=_body_font(BODY_SIZE),
                           base_color=body_color)
            full_range = NSMakeRange(0, run.length())
            run.addAttribute_value_range_(
                NSParagraphStyleAttributeName, style, full_range,
            )
            out.appendAttributedString_(run)
            add_paragraph_break()
            is_first_block = False
            i += 1
            continue

        # ── Blockquote ───────────────────────────────────────────────
        # Design spec: 3px gold left bar + gold-tint fill + italic text.
        # We approximate in NSAttributedString with:
        #   • full-line gold-tint background colour
        #   • indent the text (head indent) so it doesn't hug the bg edge
        #   • italic cream-coloured text
        # AppKit attributed strings can't draw an inline coloured bar on
        # the leading edge — a true bar would need a custom NSTextView
        # subclass overriding drawBackgroundForGlyphRange. For now the
        # tinted background block + italic communicates "blockquote".
        m = _BLOCKQUOTE_RE.match(line)
        if m:
            content = m.group(1)
            style = _make_paragraph_style(
                head_indent=QUOTE_INDENT,
                first_line_head_indent=QUOTE_INDENT,
                paragraph_spacing=PARAGRAPH_SPACING,
                paragraph_spacing_before=PARAGRAPH_SPACING,
            )
            run = NSMutableAttributedString.alloc().init()
            _append_inline(run, content,
                           base_font=_body_font(BODY_SIZE, italic=True),
                           base_color=body_color)
            full_range = NSMakeRange(0, run.length())
            run.addAttribute_value_range_(
                NSParagraphStyleAttributeName, style, full_range,
            )
            # Gold-tint background across the blockquote paragraph.
            from AppKit import NSBackgroundColorAttributeName
            quote_bg = NSColor.colorWithCalibratedRed_green_blue_alpha_(
                0xEB / 255, 0xB6 / 255, 0x38 / 255, 0.10,
            )
            run.addAttribute_value_range_(
                NSBackgroundColorAttributeName, quote_bg, full_range,
            )
            out.appendAttributedString_(run)
            add_paragraph_break()
            is_first_block = False
            i += 1
            continue

        # ── Paragraph (default) ──────────────────────────────────────
        # Gather all consecutive non-special lines into one paragraph
        # so soft-wrapped lines from the LLM render as one block.
        block_lines = [line]
        j = i + 1
        while j < len(lines):
            nxt = lines[j]
            if not nxt.strip():
                break
            if (_HEADING_RE.match(nxt) or _HR_RE.match(nxt)
                    or _CHECKBOX_RE.match(nxt) or _BULLET_RE.match(nxt)
                    or _BLOCKQUOTE_RE.match(nxt)):
                break
            block_lines.append(nxt)
            j += 1
        paragraph_text = " ".join(block_lines)
        style = _make_paragraph_style(paragraph_spacing=PARAGRAPH_SPACING)
        run = NSMutableAttributedString.alloc().init()
        _append_inline(run, paragraph_text,
                       base_font=_body_font(BODY_SIZE),
                       base_color=body_color)
        full_range = NSMakeRange(0, run.length())
        run.addAttribute_value_range_(
            NSParagraphStyleAttributeName, style, full_range,
        )
        out.appendAttributedString_(run)
        add_paragraph_break()
        is_first_block = False
        i = j

    return out
