"""
PlannerRenderer - Generate beautiful PDF planners using Typst.

This module handles:
- Generating front matter (charts, ZR timeline, graphic ephemeris)
- Rendering daily pages with events
- Typst compilation to PDF
"""

from __future__ import annotations

import os
import tempfile
from dataclasses import dataclass
from datetime import date
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from stellium.planner.builder import PlannerConfig

# Check if typst is available
try:
    import typst as typst_lib

    TYPST_AVAILABLE = True
except ImportError:
    TYPST_AVAILABLE = False


@dataclass
class ChartPaths:
    """Paths to generated chart SVG files."""

    natal: str | None = None
    progressed: str | None = None
    solar_return: str | None = None
    graphic_ephemeris: str | None = None
    zr_overview: str | None = None
    zr_timeline: str | None = None
    profection_wheel: str | None = None
    profection_table: str | None = None


class PlannerRenderer:
    """
    Renders PDF planners using Typst typesetting.

    Generates:
    - Title page with planner info
    - Front matter section with charts
    - Month overview pages
    - Daily pages with transit events
    """

    def __init__(self, config: PlannerConfig) -> None:
        """
        Initialize renderer with configuration.

        Args:
            config: PlannerConfig from PlannerBuilder
        """
        if not TYPST_AVAILABLE:
            raise ImportError(
                "Typst library not available. Install with: pip install typst"
            )

        self.config = config
        self._temp_dir: str | None = None
        self._chart_paths = ChartPaths()

    def render(self) -> bytes:
        """
        Render the complete planner to PDF.

        Returns:
            PDF as bytes
        """

        # Create temp directory for chart files
        self._temp_dir = tempfile.mkdtemp(prefix="stellium_planner_")

        try:
            # Generate charts
            self._generate_charts()

            # Collect events
            events_by_date = self._collect_events()

            # Generate Typst document
            typst_content = self._generate_typst_document(events_by_date)

            # Write to temp file and compile
            typst_path = os.path.join(self._temp_dir, "planner.typ")
            with open(typst_path, "w", encoding="utf-8") as f:
                f.write(typst_content)

            # Get font directories
            font_dirs = self._get_font_dirs()

            # Compile to PDF
            pdf_bytes = typst_lib.compile(
                typst_path,
                root="/",
                font_paths=font_dirs,
            )

            return pdf_bytes

        finally:
            # Clean up temp files
            self._cleanup_temp_files()

    def _get_font_dirs(self) -> list[str]:
        """Get font directories for Typst compilation."""
        base_font_dir = os.path.join(
            os.path.dirname(
                os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
            ),
            "assets",
            "fonts",
        )
        return [
            base_font_dir,
            os.path.join(base_font_dir, "Cinzel_Decorative"),
            os.path.join(base_font_dir, "Crimson_Pro"),
            os.path.join(base_font_dir, "Crimson_Pro", "static"),
        ]

    def _cleanup_temp_files(self) -> None:
        """Clean up temporary files."""
        import shutil

        if self._temp_dir and os.path.exists(self._temp_dir):
            shutil.rmtree(self._temp_dir)

    def _generate_charts(self) -> None:
        """Generate chart SVG files for front matter."""
        from stellium.core.builder import ChartBuilder

        # Generate natal chart
        natal_chart = ChartBuilder.from_native(self.config.native).calculate()

        if self.config.include_natal_chart:
            natal_path = os.path.join(self._temp_dir, "natal.svg")
            natal_chart.draw(natal_path).with_zodiac_palette("rainbow").save()
            self._chart_paths.natal = natal_path

        # Generate progressed chart
        if self.config.include_progressed_chart:
            from stellium.core.multichart import MultiChartBuilder

            # Calculate age at start of planner
            birth_date = self.config.native.datetime.local_datetime.date()
            planner_start = self.config.start_date
            age = planner_start.year - birth_date.year
            if (planner_start.month, planner_start.day) < (
                birth_date.month,
                birth_date.day,
            ):
                age -= 1

            try:
                mc = MultiChartBuilder.progression(natal_chart, age=age).calculate()
                progressed_path = os.path.join(self._temp_dir, "progressed.svg")
                mc.draw(progressed_path).with_zodiac_palette("rainbow").save()
                self._chart_paths.progressed = progressed_path
            except Exception:
                pass  # Skip if progression fails

        # Generate solar return
        if self.config.include_solar_return:
            from stellium.returns import ReturnBuilder

            try:
                sr_year = self.config.year or self.config.start_date.year
                sr_chart = ReturnBuilder.solar(natal_chart, sr_year).calculate()
                sr_path = os.path.join(self._temp_dir, "solar_return.svg")
                sr_chart.draw(sr_path).with_zodiac_palette("rainbow").save()
                self._chart_paths.solar_return = sr_path
            except Exception:
                pass

        # Generate graphic ephemeris with natal chart overlay
        if self.config.include_graphic_ephemeris:
            from stellium.visualization import GraphicEphemeris

            try:
                start_str = self.config.start_date.isoformat()
                end_str = self.config.end_date.isoformat()
                eph = GraphicEphemeris(
                    start_date=start_str,
                    end_date=end_str,
                    harmonic=self.config.graphic_ephemeris_harmonic,
                    natal_chart=natal_chart,  # Include natal positions for transit aspects
                )
                eph_path = os.path.join(self._temp_dir, "ephemeris.svg")
                eph.draw(eph_path)
                self._chart_paths.graphic_ephemeris = eph_path
            except Exception:
                pass

        # Generate ZR visualization
        if self.config.include_zr_timeline:
            self._generate_zr_charts(natal_chart)

        # Generate profection wheel visualization
        if self.config.include_profections:
            self._generate_profection_charts(natal_chart)

    def _generate_zr_charts(self, natal_chart) -> None:
        """Generate Zodiacal Releasing visualization SVGs."""
        from stellium.core.builder import ChartBuilder
        from stellium.engines.releasing import ZodiacalReleasingAnalyzer
        from stellium.presentation.sections.zr_visualization import (
            ZRVisualizationSection,
        )

        try:
            # Re-calculate chart with ZR analyzer if not present
            if "zodiacal_releasing" not in natal_chart.metadata:
                natal_chart = (
                    ChartBuilder.from_native(self.config.native)
                    .add_analyzer(ZodiacalReleasingAnalyzer([self.config.zr_lot]))
                    .calculate()
                )

            # Generate overview SVG
            overview_section = ZRVisualizationSection(
                lot=self.config.zr_lot,
                year=self.config.year or self.config.start_date.year,
                output="overview",
            )
            overview_data = overview_section.generate_data(natal_chart)
            if overview_data.get("type") == "svg":
                overview_path = os.path.join(self._temp_dir, "zr_overview.svg")
                with open(overview_path, "w", encoding="utf-8") as f:
                    f.write(overview_data["content"])
                self._chart_paths.zr_overview = overview_path

            # Generate timeline SVG
            timeline_section = ZRVisualizationSection(
                lot=self.config.zr_lot,
                year=self.config.year or self.config.start_date.year,
                levels=(1, 2, 3),
                output="timeline",
            )
            timeline_data = timeline_section.generate_data(natal_chart)
            if timeline_data.get("type") == "svg":
                timeline_path = os.path.join(self._temp_dir, "zr_timeline.svg")
                with open(timeline_path, "w", encoding="utf-8") as f:
                    f.write(timeline_data["content"])
                self._chart_paths.zr_timeline = timeline_path

        except Exception:
            pass  # Skip if ZR generation fails

    def _generate_profection_charts(self, natal_chart) -> None:
        """Generate profection wheel visualization SVGs."""
        from stellium.presentation.sections.profection_visualization import (
            ProfectionVisualizationSection,
        )

        try:
            # Calculate age at planner start
            birth_date = self.config.native.datetime.local_datetime.date()
            planner_start = self.config.start_date
            age = planner_start.year - birth_date.year
            if (planner_start.month, planner_start.day) < (
                birth_date.month,
                birth_date.day,
            ):
                age -= 1

            # Generate wheel SVG
            wheel_section = ProfectionVisualizationSection(
                age=age,
                compare_ages=[age, age + 1],
                show_wheel=True,
                show_table=False,
            )
            wheel_data = wheel_section.generate_data(natal_chart)

            # Handle compound or direct SVG response
            if wheel_data.get("type") == "compound":
                for name, subdata in wheel_data.get("sections", []):
                    if "Wheel" in name and subdata.get("type") == "svg":
                        wheel_path = os.path.join(
                            self._temp_dir, "profection_wheel.svg"
                        )
                        with open(wheel_path, "w", encoding="utf-8") as f:
                            f.write(subdata["content"])
                        self._chart_paths.profection_wheel = wheel_path
                        break
            elif wheel_data.get("type") == "svg":
                wheel_path = os.path.join(self._temp_dir, "profection_wheel.svg")
                with open(wheel_path, "w", encoding="utf-8") as f:
                    f.write(wheel_data["content"])
                self._chart_paths.profection_wheel = wheel_path

            # Generate table SVG
            table_section = ProfectionVisualizationSection(
                age=age,
                compare_ages=[age, age + 1],
                show_wheel=False,
                show_table=True,
            )
            table_data = table_section.generate_data(natal_chart)

            # Handle compound or direct SVG response
            if table_data.get("type") == "compound":
                for name, subdata in table_data.get("sections", []):
                    if "Details" in name and subdata.get("type") == "svg":
                        table_path = os.path.join(
                            self._temp_dir, "profection_table.svg"
                        )
                        with open(table_path, "w", encoding="utf-8") as f:
                            f.write(subdata["content"])
                        self._chart_paths.profection_table = table_path
                        break
            elif table_data.get("type") == "svg":
                table_path = os.path.join(self._temp_dir, "profection_table.svg")
                with open(table_path, "w", encoding="utf-8") as f:
                    f.write(table_data["content"])
                self._chart_paths.profection_table = table_path

        except Exception:
            pass  # Skip if profection generation fails

    def _collect_events(self) -> dict[date, list]:
        """Collect all events for the planner period."""
        from stellium.core.builder import ChartBuilder
        from stellium.planner.events import DailyEventCollector

        natal_chart = ChartBuilder.from_native(self.config.native).calculate()

        collector = DailyEventCollector(
            natal_chart=natal_chart,
            start=self.config.start_date,
            end=self.config.end_date,
            timezone=self.config.timezone,
        )

        collector.collect_all(
            natal_transits=self.config.natal_transit_planets is not None
            or self.config.include_mundane_transits,
            transit_planets=self.config.natal_transit_planets,
            ingresses=self.config.ingress_planets is not None,
            ingress_planets=self.config.ingress_planets,
            stations=self.config.station_planets is not None,
            station_planets=self.config.station_planets,
            moon_phases=self.config.include_moon_phases,
            voc=self.config.include_voc,
            voc_mode=self.config.voc_mode,
            eclipses=True,
        )

        return collector._events_by_date

    def _generate_typst_document(self, events_by_date: dict[date, list]) -> str:
        """Generate complete Typst document."""
        parts = []

        # Document preamble
        parts.append(self._get_preamble())

        # Title page
        parts.append(self._render_title_page())

        # Front matter
        parts.append(self._render_front_matter())

        # Monthly pages
        parts.append(self._render_monthly_pages(events_by_date))

        return "\n".join(parts)

    def _get_preamble(self) -> str:
        """Get Typst document preamble with styling."""
        paper_name = {
            "a4": "a4",
            "a5": "a5",
            "letter": "us-letter",
            "half-letter": "a5",  # Use A5 as closest standard size
        }.get(self.config.page_size, "a4")
        page_setup = f'paper: "{paper_name}"'

        binding = self.config.binding_margin

        return f"""// Stellium Astrological Planner
// Generated with Typst

// ============================================================================
// COLOR PALETTE - Warm mystical purple theme
// ============================================================================
#let primary = rgb("#4a3353")
#let secondary = rgb("#6b4d6e")
#let accent = rgb("#8e6b8a")
#let gold = rgb("#b8953d")
#let cream = rgb("#faf8f5")
#let text-dark = rgb("#2d2330")

// ============================================================================
// PAGE SETUP
// ============================================================================
#set page(
  {page_setup},
  margin: (top: 0.6in, bottom: 0.6in, left: {0.7 + binding}in, right: 0.7in),
  fill: cream,
  header: context {{
    if counter(page).get().first() > 2 [
      #set text(font: "Cinzel Decorative", size: 7pt, fill: accent, tracking: 0.5pt)
      #h(1fr)
      Astrological Planner
      #h(1fr)
    ]
  }},
  footer: context {{
    set text(size: 7pt, fill: accent)
    h(1fr)
    counter(page).display("1")
    h(1fr)
  }},
)

// ============================================================================
// TYPOGRAPHY
// ============================================================================
#set text(
  font: ("Crimson Pro", "Noto Sans Symbols 2", "Noto Sans Symbols", "Georgia", "serif"),
  size: 9pt,
  fill: text-dark,
)

#set par(justify: true, leading: 0.7em)

// Heading styles
#show heading.where(level: 1): it => {{
  set text(font: "Cinzel Decorative", size: 22pt, weight: "regular", fill: primary, tracking: 1.5pt)
  set par(justify: false)
  align(center)[#it.body]
  v(0.4em)
}}

#show heading.where(level: 2): it => {{
  v(0.8em)
  block(
    width: 100%,
    fill: primary,
    inset: (x: 10pt, y: 6pt),
    radius: 2pt,
  )[
    #set text(font: "Cinzel Decorative", size: 9pt, weight: "regular", fill: white, tracking: 0.5pt)
    #sym.star.stroked #it.body
  ]
  v(0.4em)
}}

#show heading.where(level: 3): it => {{
  set text(font: "Cinzel Decorative", size: 9pt, weight: "regular", fill: secondary)
  v(0.3em)
  it.body
  v(0.2em)
}}

// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
#let star-divider = {{
  set align(center)
  v(0.1in)
  box(width: 60%)[
    #grid(
      columns: (1fr, auto, 1fr),
      align: (right, center, left),
      column-gutter: 8pt,
      line(length: 100%, stroke: 0.6pt + gold),
      text(fill: gold, size: 8pt)[#sym.star.stroked],
      line(length: 100%, stroke: 0.6pt + gold),
    )
  ]
  v(0.1in)
}}

#let day-header(day-name, date-str, moon-info) = {{
  block(
    width: 100%,
    fill: primary,
    inset: (x: 8pt, y: 5pt),
    radius: 2pt,
  )[
    #set text(fill: white, size: 8pt)
    #text(font: "Cinzel Decorative", weight: "regular")[#day-name, #date-str]
    #h(1fr)
    #text(fill: cream.lighten(20%), size: 7pt)[#moon-info]
  ]
}}

#let event-row(time-str, event-text) = {{
  grid(
    columns: (50pt, 1fr),
    gutter: 6pt,
    text(fill: secondary, size: 8pt)[#time-str],
    text(size: 8pt)[#event-text],
  )
}}
"""

    def _render_title_page(self) -> str:
        """Render the title page."""
        native = self.config.native
        name = getattr(native, "name", "Personal") or "Personal"

        year_str = ""
        if self.config.year:
            year_str = str(self.config.year)
        else:
            year_str = f"{self.config.start_date} - {self.config.end_date}"

        return f"""
// ============================================================================
// TITLE PAGE
// ============================================================================
#v(1.5in)
#star-divider

= Astrological Planner

#star-divider
#v(0.3in)

#align(center)[
  #text(font: "Cinzel Decorative", size: 14pt, fill: secondary)[
    {self._escape(name)}
  ]
  #v(0.2in)
  #text(size: 11pt, fill: accent)[
    {year_str}
  ]
]

#v(1fr)

#align(center)[
  #text(font: "Cinzel Decorative", size: 8pt, fill: accent, style: "italic")[
    Generated with Stellium
  ]
]

#pagebreak()
"""

    def _render_front_matter(self) -> str:
        """Render front matter pages with charts."""
        parts = []

        parts.append("""
// ============================================================================
// FRONT MATTER
// ============================================================================
""")

        # Natal chart
        if self._chart_paths.natal:
            parts.append(f"""
== Natal Chart

#align(center)[
  #box(
    stroke: 1pt + gold,
    radius: 4pt,
    clip: true,
    inset: 8pt,
    fill: white,
    image("{self._chart_paths.natal}", width: 85%)
  )
]

#pagebreak()
""")

        # Progressed chart
        if self._chart_paths.progressed:
            parts.append(f"""
== Progressed Chart

#align(center)[
  #box(
    stroke: 1pt + gold,
    radius: 4pt,
    clip: true,
    inset: 8pt,
    fill: white,
    image("{self._chart_paths.progressed}", width: 85%)
  )
]

#pagebreak()
""")

        # Solar Return
        if self._chart_paths.solar_return:
            year = self.config.year or self.config.start_date.year
            parts.append(f"""
== Solar Return {year}

#align(center)[
  #box(
    stroke: 1pt + gold,
    radius: 4pt,
    clip: true,
    inset: 8pt,
    fill: white,
    image("{self._chart_paths.solar_return}", width: 85%)
  )
]

#pagebreak()
""")

        # Profections
        if self.config.include_profections:
            parts.append(self._render_profections())

        # Zodiacal Releasing
        if self._chart_paths.zr_overview:
            lot_name = self.config.zr_lot.replace("Part of ", "")
            parts.append(f"""
== Zodiacal Releasing Overview

#align(center)[
  #box(
    stroke: 1pt + gold,
    radius: 4pt,
    clip: true,
    inset: 8pt,
    fill: white,
    image("{self._chart_paths.zr_overview}", width: 95%)
  )
]

#pagebreak()
""")

        if self._chart_paths.zr_timeline:
            lot_name = self.config.zr_lot.replace("Part of ", "")
            parts.append(f"""
== Zodiacal Releasing from {lot_name}

#align(center)[
  #box(
    stroke: 1pt + gold,
    radius: 4pt,
    clip: true,
    inset: 8pt,
    fill: white,
    image("{self._chart_paths.zr_timeline}", width: 95%)
  )
]

#pagebreak()
""")

        # Graphic Ephemeris
        if self._chart_paths.graphic_ephemeris:
            parts.append(f"""
== Graphic Ephemeris

#align(center)[
  #box(
    stroke: 1pt + gold,
    radius: 4pt,
    clip: true,
    inset: 8pt,
    fill: white,
    image("{self._chart_paths.graphic_ephemeris}", width: 95%)
  )
]

#pagebreak()
""")

        return "\n".join(parts)

    def _render_profections(self) -> str:
        """Render profection information with wheel visualization."""
        parts = []

        # If we have the wheel visualization, use it
        if self._chart_paths.profection_wheel:
            parts.append(f"""
== Annual Profections

#align(center)[
  #box(
    stroke: 1pt + gold,
    radius: 4pt,
    clip: true,
    inset: 8pt,
    fill: white,
    image("{self._chart_paths.profection_wheel}", width: 95%)
  )
]
""")

        # If we have the table visualization, add it
        if self._chart_paths.profection_table:
            parts.append(f"""
#v(0.3in)

#align(center)[
  #box(
    stroke: 1pt + gold,
    radius: 4pt,
    clip: true,
    inset: 8pt,
    fill: white,
    image("{self._chart_paths.profection_table}", width: 95%)
  )
]
""")

        # If we have visualizations, add explanation and page break
        if parts:
            parts.append("""
#v(0.3in)

#text(size: 8pt, fill: accent)[
  Annual profections advance the Ascendant one sign per year of life.
  The planet ruling that sign becomes the Lord of the Year,
  indicating themes and areas of focus for this annual period.
  The wheel shows ages 0-95 spiraling through the 12 houses.
]

#pagebreak()
""")
            return "\n".join(parts)

        # Fallback to text-only version if visualizations aren't available
        from stellium.core.builder import ChartBuilder
        from stellium.engines.profections import ProfectionEngine

        natal_chart = ChartBuilder.from_native(self.config.native).calculate()
        engine = ProfectionEngine(natal_chart)

        # Calculate age at planner start
        birth_date = self.config.native.datetime.local_datetime.date()
        planner_start = self.config.start_date
        age = planner_start.year - birth_date.year
        if (planner_start.month, planner_start.day) < (
            birth_date.month,
            birth_date.day,
        ):
            age -= 1

        try:
            result = engine.annual(age)
            profected_sign = result.profected_sign
            lord = result.ruler
        except Exception:
            profected_sign = "Unknown"
            lord = "Unknown"

        return f"""
== Annual Profection

#block(
  fill: rgb("#f9f6f7"),
  inset: 12pt,
  radius: 4pt,
  width: 100%,
)[
  #grid(
    columns: (120pt, 1fr),
    gutter: 8pt,
    row-gutter: 10pt,
    [#text(fill: secondary, weight: "semibold")[Age:]], [{age}],
    [#text(fill: secondary, weight: "semibold")[Profected Sign:]], [{profected_sign}],
    [#text(fill: secondary, weight: "semibold")[Lord of the Year:]], [{lord}],
  )
]

#v(0.5em)
#text(size: 8pt, fill: accent)[
  The Lord of the Year indicates themes and areas of focus for this annual period.
  Transits to and from {lord} may be especially significant.
]

#pagebreak()
"""

    def _render_monthly_pages(self, events_by_date: dict[date, list]) -> str:
        """Render month calendar grids and weekly detail pages."""
        parts = []

        parts.append("""
// ============================================================================
// CALENDAR PAGES
// ============================================================================
""")

        # Process each month in the range
        current_month_start = self.config.start_date.replace(day=1)

        while current_month_start <= self.config.end_date:
            # Month calendar grid
            parts.append(
                self._render_month_calendar(current_month_start, events_by_date)
            )

            # Weekly detail pages for this month
            parts.append(self._render_month_weeks(current_month_start, events_by_date))

            # Move to next month
            if current_month_start.month == 12:
                current_month_start = current_month_start.replace(
                    year=current_month_start.year + 1, month=1
                )
            else:
                current_month_start = current_month_start.replace(
                    month=current_month_start.month + 1
                )

        return "\n".join(parts)

    def _get_week_start_day(self) -> int:
        """Get Python calendar firstweekday from config."""
        # Python calendar: 0=Monday, 6=Sunday
        return 6 if self.config.week_starts_on == "sunday" else 0

    def _get_day_headers(self) -> list[str]:
        """Get day header names based on week start."""
        if self.config.week_starts_on == "sunday":
            return ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
        else:
            return ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]

    def _render_month_calendar(
        self, month_start: date, events_by_date: dict[date, list]
    ) -> str:
        """Render a full-page month calendar grid with all events."""
        import calendar

        month_name = month_start.strftime("%B %Y")
        cal = calendar.Calendar(firstweekday=self._get_week_start_day())

        # Get all days in month (including padding from prev/next months)
        month_days = list(cal.itermonthdates(month_start.year, month_start.month))

        # Build week rows
        weeks = []
        for i in range(0, len(month_days), 7):
            week = month_days[i : i + 7]
            weeks.append(week)

        num_weeks = len(weeks)
        day_headers = self._get_day_headers()

        # Build day cells with full event listings
        day_cells = []
        for week in weeks:
            for day in week:
                is_current_month = day.month == month_start.month
                events = events_by_date.get(day, []) if is_current_month else []

                # Build cell content with ALL events (single column, may clip)
                if is_current_month:
                    # Format each event line
                    event_lines = []
                    for evt in events:
                        time_str = evt.time.strftime("%I:%M").lstrip("0")
                        am_pm = evt.time.strftime("%p").lower()[0]  # 'a' or 'p'
                        event_lines.append(
                            f"#text(size: 5.5pt, fill: secondary)[{time_str}{am_pm}] "
                            f"#text(size: 5.5pt)[{self._escape(evt.symbol)}]"
                        )

                    # Single column for month grid (space is limited)
                    events_display = (
                        " #linebreak() ".join(event_lines) if event_lines else ""
                    )

                    # Day number prominent at top, events below
                    cell = f"""table.cell(
      fill: cream,
      inset: 4pt,
    )[
      #align(left)[
        #text(size: 11pt, weight: "bold", fill: primary)[{day.day}]
      ]
      #v(2pt)
      {events_display}
    ]"""
                else:
                    # Gray out days from other months
                    cell = f"""table.cell(
      fill: rgb("#f5f3f0"),
      inset: 4pt,
    )[
      #align(left)[
        #text(size: 11pt, fill: accent)[{day.day}]
      ]
    ]"""

                day_cells.append(cell)

        rows_str = ",\n  ".join(day_cells)

        # Build header row with correct day order
        header_cells = ",\n  ".join(
            f'table.cell(fill: primary, inset: 6pt)[#align(center)[#text(fill: white, size: 8pt, weight: "bold")[{d}]]]'
            for d in day_headers
        )

        return f"""
#pagebreak()

// Month: {month_name}
#block(height: 100%)[
  #align(center)[
    #text(font: "Cinzel Decorative", size: 18pt, fill: primary, tracking: 1pt)[{month_name}]
  ]
  #v(0.12in)

  // Full-page calendar grid
  #block(
    width: 100%,
    height: 1fr,
    radius: 6pt,
    clip: true,
    stroke: 1pt + primary,
  )[
    #table(
      columns: (1fr, 1fr, 1fr, 1fr, 1fr, 1fr, 1fr),
      rows: (auto, 1fr, 1fr, 1fr, 1fr, 1fr, {"1fr, " if num_weeks > 5 else ""}),
      stroke: 0.5pt + accent,
      align: left + top,

      // Header row
      {header_cells},

      // Day cells
      {rows_str}
    )
  ]
]
"""

    def _render_month_weeks(
        self, month_start: date, events_by_date: dict[date, list]
    ) -> str:
        """Render weekly detail pages for a month."""
        import calendar

        parts = []
        cal = calendar.Calendar(firstweekday=self._get_week_start_day())

        # Get weeks that contain days from this month
        month_days = list(cal.itermonthdates(month_start.year, month_start.month))

        for i in range(0, len(month_days), 7):
            week = month_days[i : i + 7]

            # Only render week if it contains days from this month
            if any(d.month == month_start.month for d in week):
                parts.append(self._render_week_page(week, month_start, events_by_date))

        return "\n".join(parts)

    def _render_week_page(
        self, week: list[date], month_start: date, events_by_date: dict[date, list]
    ) -> str:
        """Render a single week as a full page with 7 day boxes."""
        # Week date range for header
        week_start = week[0]
        week_end = week[-1]
        week_header = (
            f"{week_start.strftime('%b %d')} - {week_end.strftime('%b %d, %Y')}"
        )

        # Build day boxes
        day_boxes = []
        for day in week:
            is_current_month = day.month == month_start.month
            events = events_by_date.get(day, [])

            # Day header
            day_name = day.strftime("%A")
            day_num = day.day

            # Format events - single column, tight spacing
            event_lines = []
            for event in events:
                time_str = event.time.strftime("%I:%M%p").lower().lstrip("0")
                event_lines.append(
                    f"#text(fill: secondary, size: 6pt)[{time_str}] #text(size: 6pt)[{self._escape(event.description)}]"
                )

            if event_lines:
                # Use tight spacing - set leading to minimal and join with linebreaks
                events_str = "#set par(leading: 0.3em)\n    " + " #linebreak() ".join(
                    event_lines
                )
            else:
                events_str = (
                    '#text(fill: accent, size: 6pt, style: "italic")[No events]'
                )

            if is_current_month:
                fill_color = "cream"
                text_color = "text-dark"
            else:
                fill_color = 'rgb("#f0ede8")'
                text_color = "accent"

            day_boxes.append(f"""  box(
    width: 100%,
    height: 100%,
    stroke: 0.5pt + accent,
    radius: 2pt,
    fill: {fill_color},
    inset: 5pt,
    clip: true,
  )[
    #grid(
      columns: (1fr, auto),
      [#text(font: "Cinzel Decorative", size: 8pt, fill: {text_color})[{day_name}]],
      [#text(size: 12pt, weight: "bold", fill: {text_color})[{day_num}]],
    )
    #line(length: 100%, stroke: 0.3pt + accent)
    #v(2pt)
    {events_str}
  ],""")

        days_content = "\n".join(day_boxes)

        return f"""
#pagebreak()

// Week: {week_header}
#align(center)[
  #text(font: "Cinzel Decorative", size: 11pt, fill: secondary)[{week_header}]
]
#v(0.05in)

#block(height: 1fr)[
#grid(
  columns: (1fr,),
  rows: (1fr, 1fr, 1fr, 1fr, 1fr, 1fr, 1fr),
  row-gutter: 3pt,
{days_content}
)
]
"""

    def _render_day(self, day: date, events: list) -> str:
        """Render a single day entry (legacy - kept for reference)."""
        day_name = day.strftime("%A")
        date_str = day.strftime("%B %d")

        # Get moon info (simplified for now)
        moon_info = ""  # TODO: Add moon sign/phase

        # Format events
        event_rows = []
        for event in events:
            time_str = event.time.strftime("%I:%M %p").lstrip("0")
            event_rows.append(
                f'  #event-row("{time_str}", "{self._escape(event.description)}")'
            )

        events_content = (
            "\n".join(event_rows)
            if event_rows
            else "  #text(fill: accent, size: 7pt)[No major transits]"
        )

        return f"""
#day-header("{day_name}", "{date_str}", "{moon_info}")
#v(3pt)
{events_content}
#v(6pt)
"""

    def _escape(self, text: str) -> str:
        """Escape text for Typst."""
        if not text:
            return ""
        # Escape special Typst characters
        text = text.replace("\\", "\\\\")
        text = text.replace("#", "\\#")
        text = text.replace("$", "\\$")
        text = text.replace("@", "\\@")
        text = text.replace("<", "\\<")
        text = text.replace(">", "\\>")
        text = text.replace("[", "\\[")
        text = text.replace("]", "\\]")
        text = text.replace("{", "\\{")
        text = text.replace("}", "\\}")
        text = text.replace("_", "\\_")
        text = text.replace("*", "\\*")
        text = text.replace('"', '\\"')
        return text
