"""Build TRI diary static HTML page.

Reads:
  meta.yaml          — phases, goals, fears, milestones
  days/YYYY-MM-DD.md — one frontmatter-headed markdown per day

Writes:
  index.html         — single static page (calendar + day cards + pinned goals/fears)

Usage: python build.py
"""

from __future__ import annotations

import calendar
import html
import re
from datetime import date, datetime, timedelta
from pathlib import Path

import yaml

try:
    import markdown as md_lib

    def md_to_html(text: str) -> str:
        return md_lib.markdown(text, extensions=["extra", "sane_lists"])
except ImportError:
    def md_to_html(text: str) -> str:
        return "<pre>" + html.escape(text) + "</pre>"


ROOT = Path(__file__).parent
DAYS_DIR = ROOT / "days"
META = ROOT / "meta.yaml"
OUT = ROOT / "index.html"


FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n(.*)$", re.DOTALL)


def parse_day(path: Path) -> dict:
    text = path.read_text()
    m = FRONTMATTER_RE.match(text)
    if not m:
        raise ValueError(f"{path} missing frontmatter")
    fm = yaml.safe_load(m.group(1))
    body = m.group(2).strip()
    return {
        "date": fm["date"] if isinstance(fm["date"], date) else date.fromisoformat(str(fm["date"])),
        "phase": int(fm.get("phase", 0)),
        "headline": fm.get("headline", ""),
        "approx": bool(fm.get("approx", False)),
        "body_md": body,
        "body_html": md_to_html(body) if body else "",
    }


def phase_for(d: date, phases: list[dict]) -> dict | None:
    for p in phases:
        if p["start"] <= d <= p["end"]:
            return p
    return None


def daterange(start: date, end: date):
    cur = start
    while cur <= end:
        yield cur
        cur += timedelta(days=1)


def month_grid_html(year: int, month: int, days_by_date: dict[date, dict], phases: list[dict], milestones_by_date: dict[date, dict], start: date, end: date) -> str:
    cal = calendar.Calendar(firstweekday=0)  # Monday-first
    weeks = cal.monthdatescalendar(year, month)
    month_name = date(year, month, 1).strftime("%B %Y")
    rows = []
    rows.append('<div class="month">')
    rows.append(f'  <h3 class="month-title">{month_name}</h3>')
    rows.append('  <div class="month-grid">')
    for dow in ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]:
        rows.append(f'    <div class="dow">{dow}</div>')
    for week in weeks:
        for d in week:
            in_internship = start <= d <= end
            if d.month != month:
                rows.append('    <div class="cell other-month"></div>')
                continue
            classes = ["cell"]
            if not in_internship:
                classes.append("oop")
                rows.append(f'    <div class="{ " ".join(classes) }"><span class="date">{d.day}</span></div>')
                continue
            ph = phase_for(d, phases)
            color = ph["color"] if ph else "#444"
            day_info = days_by_date.get(d)
            headline = day_info["headline"] if day_info else ""
            approx = day_info["approx"] if day_info else False
            milestone = milestones_by_date.get(d)
            cls = " ".join(classes)
            anchor = d.isoformat()
            mile_html = f'<span class="milestone" title="{html.escape(milestone["label"])}">{milestone["icon"]}</span>' if milestone else ""
            approx_marker = '<span class="approx" title="approximate, please correct">~</span>' if approx else ""
            esc_headline = html.escape(headline)
            rows.append(
                f'    <a class="{cls}" href="#day-{anchor}" data-date="{anchor}" data-phase="{ph["num"] if ph else 0}" style="border-left:4px solid {color}">'
                f'<span class="date">{d.day}{approx_marker}</span>{mile_html}'
                f'<span class="headline">{esc_headline}</span>'
                f'</a>'
            )
    rows.append('  </div>')
    rows.append('</div>')
    return "\n".join(rows)


def gantt_html(phases: list[dict], start: date, end: date) -> str:
    total = (end - start).days + 1
    parts = ['<div class="gantt">']
    parts.append('  <div class="gantt-bar">')
    for p in phases:
        left = (p["start"] - start).days / total * 100
        width = ((p["end"] - p["start"]).days + 1) / total * 100
        label = html.escape(f"P{p['num']}: {p['name']}")
        parts.append(
            f'    <a class="gantt-phase" href="#phase-{p["num"]}" '
            f'style="left:{left:.2f}%;width:{width:.2f}%;background:{p["color"]}" '
            f'title="{label} ({p["start"]} → {p["end"]})">'
            f'<span class="gantt-label">P{p["num"]}</span></a>'
        )
    parts.append('    <div class="today-marker" id="today-marker"></div>')
    parts.append('  </div>')
    parts.append('  <div class="gantt-axis">')
    parts.append(f'    <span class="axis-start">{start.strftime("%b %d")}</span>')
    parts.append(f'    <span class="axis-end">{end.strftime("%b %d")}</span>')
    parts.append('  </div>')
    parts.append('</div>')
    return "\n".join(parts)


def day_card_html(d: date, day_info: dict | None, phase: dict | None, milestone: dict | None) -> str:
    anchor = d.isoformat()
    phase_color = phase["color"] if phase else "#444"
    phase_label = f"P{phase['num']}: {phase['name']}" if phase else "—"
    dow = d.strftime("%a")
    pretty_date = d.strftime("%a, %b %d %Y")
    approx_html = '<span class="approx-badge" title="approximate, please correct">approx</span>' if (day_info and day_info["approx"]) else ""
    milestone_html = f'<span class="milestone-badge">{milestone["icon"]} {html.escape(milestone["label"])}</span>' if milestone else ""
    if day_info and day_info["body_html"]:
        body = day_info["body_html"]
        headline = html.escape(day_info["headline"])
    elif day_info:
        body = '<p class="empty">—</p>'
        headline = html.escape(day_info["headline"])
    else:
        body = '<p class="empty">No entry yet.</p>'
        headline = ""
    return (
        f'<article class="day-card" id="day-{anchor}" data-date="{anchor}" style="border-left:6px solid {phase_color}">\n'
        f'  <header class="day-header">\n'
        f'    <span class="day-date">{pretty_date}</span>\n'
        f'    <span class="day-phase" style="color:{phase_color}">{phase_label}</span>\n'
        f'    {milestone_html}\n'
        f'    {approx_html}\n'
        f'  </header>\n'
        f'  <h2 class="day-headline">{headline}</h2>\n'
        f'  <div class="day-body">{body}</div>\n'
        f'</article>'
    )


def phase_banner_html(p: dict) -> str:
    return (
        f'<section class="phase-banner" id="phase-{p["num"]}" style="background:{p["color"]}22;border-left:6px solid {p["color"]}">\n'
        f'  <div class="phase-title">Phase {p["num"]}: {html.escape(p["name"])}</div>\n'
        f'  <div class="phase-dates">{p["start"]} → {p["end"]}</div>\n'
        f'  <div class="phase-gate"><strong>Soft gate:</strong> {html.escape(p["gate"])}</div>\n'
        f'</section>'
    )


def goals_fears_html(goals: list[dict], fears: list[dict]) -> str:
    parts = ['<div class="goals-fears">']
    parts.append('  <div class="gf-col goals-col">')
    parts.append('    <h3>Goals</h3>')
    parts.append('    <ul>')
    for g in goals:
        parts.append(f'      <li><strong>{html.escape(g["title"])}</strong><br><span class="gf-detail">{html.escape(g["detail"])}</span></li>')
    parts.append('    </ul>')
    parts.append('  </div>')
    parts.append('  <div class="gf-col fears-col">')
    parts.append('    <h3>Fears</h3>')
    parts.append('    <ul>')
    for f in fears:
        parts.append(f'      <li><strong>{html.escape(f["title"])}</strong><br><span class="gf-detail">{html.escape(f["detail"])}</span></li>')
    parts.append('    </ul>')
    parts.append('  </div>')
    parts.append('</div>')
    return "\n".join(parts)


def build() -> None:
    meta = yaml.safe_load(META.read_text())
    start = meta["internship"]["start"]
    end = meta["internship"]["end"]
    if isinstance(start, str):
        start = date.fromisoformat(start)
    if isinstance(end, str):
        end = date.fromisoformat(end)

    phases = []
    for p in meta["phases"]:
        p2 = dict(p)
        p2["start"] = p["start"] if isinstance(p["start"], date) else date.fromisoformat(str(p["start"]))
        p2["end"] = p["end"] if isinstance(p["end"], date) else date.fromisoformat(str(p["end"]))
        phases.append(p2)

    milestones_by_date: dict[date, dict] = {}
    for m in meta.get("milestones", []):
        md = m["date"] if isinstance(m["date"], date) else date.fromisoformat(str(m["date"]))
        milestones_by_date[md] = m

    day_paths = sorted(DAYS_DIR.glob("*.md"))
    days_by_date: dict[date, dict] = {}
    for p in day_paths:
        d = parse_day(p)
        days_by_date[d["date"]] = d

    months = []
    seen = set()
    for d in daterange(start, end):
        key = (d.year, d.month)
        if key in seen:
            continue
        seen.add(key)
        months.append(key)

    gantt = gantt_html(phases, start, end)
    calendars = "\n".join(month_grid_html(y, m, days_by_date, phases, milestones_by_date, start, end) for y, m in months)

    # Day cards: only render days from start → today (no empty future entries clutter the stream),
    # but phase banners get inserted at phase starts.
    today = date.today()
    cards_until = min(today, end)
    cards_parts = []
    phase_inserted = set()
    for d in daterange(start, cards_until):
        ph = phase_for(d, phases)
        if ph and ph["num"] not in phase_inserted:
            cards_parts.append(phase_banner_html(ph))
            phase_inserted.add(ph["num"])
        info = days_by_date.get(d)
        cards_parts.append(day_card_html(d, info, ph, milestones_by_date.get(d)))
    # Future phases also get banners so user can navigate ahead
    for ph in phases:
        if ph["num"] not in phase_inserted:
            cards_parts.append(phase_banner_html(ph))
            phase_inserted.add(ph["num"])
    cards = "\n".join(cards_parts)

    goals = meta.get("goals", [])
    fears = meta.get("fears", [])
    gf = goals_fears_html(goals, fears)

    total_days = (end - start).days + 1
    days_in = (today - start).days + 1
    days_remaining = max(0, (end - today).days)

    today_pct = max(0.0, min(100.0, (today - start).days / total_days * 100))

    html_out = HTML_TEMPLATE.format(
        gantt=gantt,
        calendars=calendars,
        cards=cards,
        goals_fears=gf,
        today=today.isoformat(),
        today_pretty=today.strftime("%a, %b %d %Y"),
        days_in=days_in,
        days_remaining=days_remaining,
        total_days=total_days,
        today_pct=f"{today_pct:.2f}",
        start_iso=start.isoformat(),
        end_iso=end.isoformat(),
    )
    OUT.write_text(html_out)
    print(f"wrote {OUT} ({len(html_out):,} bytes)")
    print(f"  days: {days_in}/{total_days}  remaining: {days_remaining}")


HTML_TEMPLATE = """<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>TRI diary</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root {{
  --bg: #0f1216;
  --bg-panel: #161a21;
  --bg-card: #1b2129;
  --fg: #e2e6ec;
  --fg-dim: #8d97a3;
  --accent: #4ecdc4;
  --border: #2a313b;
}}
* {{ box-sizing: border-box; }}
html, body {{ background: var(--bg); color: var(--fg); margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; line-height: 1.5; }}
a {{ color: inherit; text-decoration: none; }}

.shell {{ max-width: 1200px; margin: 0 auto; padding: 24px 24px 240px 24px; }}
.top {{ margin-bottom: 24px; }}
.top h1 {{ margin: 0 0 6px 0; font-weight: 600; letter-spacing: -0.01em; }}
.top .sub {{ color: var(--fg-dim); font-size: 14px; }}
.top .counter {{ display: inline-block; padding: 4px 10px; background: var(--accent); color: #0f1216; border-radius: 4px; font-weight: 600; font-size: 13px; margin-right: 8px; }}

/* Gantt */
.gantt {{ margin: 20px 0 16px 0; }}
.gantt-bar {{ position: relative; height: 28px; background: var(--bg-panel); border-radius: 6px; overflow: hidden; }}
.gantt-phase {{ position: absolute; top: 0; bottom: 0; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 700; color: rgba(15,18,22,0.85); transition: opacity 0.15s; }}
.gantt-phase:hover {{ opacity: 0.8; }}
.gantt-label {{ pointer-events: none; }}
.today-marker {{ position: absolute; top: -4px; bottom: -4px; width: 2px; background: #fff; box-shadow: 0 0 4px #fff; }}
.gantt-axis {{ display: flex; justify-content: space-between; font-size: 11px; color: var(--fg-dim); margin-top: 4px; }}

/* Calendars */
.calendars {{ display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; margin: 20px 0; }}
@media (max-width: 800px) {{ .calendars {{ grid-template-columns: 1fr; }} }}
.month {{ background: var(--bg-panel); border-radius: 8px; padding: 12px; }}
.month-title {{ margin: 0 0 10px 0; font-size: 14px; font-weight: 600; color: var(--fg-dim); text-transform: uppercase; letter-spacing: 0.05em; }}
.month-grid {{ display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }}
.dow {{ font-size: 10px; color: var(--fg-dim); text-align: center; padding: 4px 0; font-weight: 600; text-transform: uppercase; }}
.cell {{ background: var(--bg-card); border-radius: 4px; padding: 4px 5px; min-height: 56px; font-size: 11px; display: flex; flex-direction: column; gap: 2px; color: var(--fg); cursor: pointer; transition: background 0.1s, transform 0.05s; position: relative; }}
.cell:hover {{ background: #232a35; }}
.cell:active {{ transform: scale(0.97); }}
.cell.oop {{ background: transparent; color: var(--fg-dim); cursor: default; min-height: 56px; opacity: 0.4; }}
.cell.other-month {{ background: transparent; min-height: 56px; }}
.cell .date {{ font-weight: 700; font-size: 12px; color: var(--fg); }}
.cell .approx {{ color: var(--fg-dim); font-size: 10px; margin-left: 1px; }}
.cell .headline {{ font-size: 10px; line-height: 1.25; color: var(--fg-dim); overflow: hidden; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; }}
.cell.is-today {{ outline: 2px solid #fff; }}
.cell .milestone {{ position: absolute; top: 3px; right: 4px; font-size: 11px; }}

/* Day stream */
.stream {{ margin-top: 32px; }}
.phase-banner {{ padding: 14px 16px; border-radius: 6px; margin: 28px 0 12px 0; }}
.phase-title {{ font-size: 16px; font-weight: 700; }}
.phase-dates {{ font-size: 12px; color: var(--fg-dim); margin-top: 2px; }}
.phase-gate {{ font-size: 13px; margin-top: 6px; }}

.day-card {{ background: var(--bg-card); border-radius: 6px; padding: 14px 16px; margin: 8px 0; transition: background 0.15s; }}
.day-card:target {{ background: #232d39; outline: 1px solid var(--accent); }}
.day-header {{ display: flex; flex-wrap: wrap; align-items: center; gap: 10px; font-size: 12px; color: var(--fg-dim); margin-bottom: 4px; }}
.day-date {{ font-weight: 600; }}
.day-phase {{ font-weight: 600; }}
.milestone-badge {{ background: var(--accent); color: #0f1216; padding: 2px 8px; border-radius: 3px; font-size: 11px; font-weight: 600; }}
.approx-badge {{ background: #3a3a4a; color: var(--fg-dim); padding: 2px 6px; border-radius: 3px; font-size: 10px; }}
.day-headline {{ margin: 4px 0 8px 0; font-size: 17px; font-weight: 600; letter-spacing: -0.01em; }}
.day-body h2 {{ font-size: 14px; margin: 12px 0 4px 0; color: var(--fg-dim); text-transform: uppercase; letter-spacing: 0.05em; }}
.day-body p {{ margin: 4px 0; }}
.day-body ul {{ margin: 4px 0; padding-left: 22px; }}
.day-body li {{ margin: 2px 0; }}
.day-body code {{ background: #2a313b; padding: 1px 4px; border-radius: 3px; font-size: 0.92em; }}
.day-body .empty {{ color: var(--fg-dim); font-style: italic; }}

/* Goals + fears (pinned footer) */
.goals-fears {{ position: fixed; left: 0; right: 0; bottom: 0; background: var(--bg-panel); border-top: 1px solid var(--border); padding: 14px 24px; display: grid; grid-template-columns: 1fr 1fr; gap: 24px; max-height: 220px; overflow-y: auto; z-index: 50; box-shadow: 0 -8px 24px rgba(0,0,0,0.4); }}
.goals-fears h3 {{ margin: 0 0 6px 0; font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--fg-dim); }}
.goals-col h3 {{ color: var(--accent); }}
.fears-col h3 {{ color: #ff9b8c; }}
.goals-fears ul {{ margin: 0; padding-left: 18px; font-size: 12px; }}
.goals-fears li {{ margin: 4px 0; }}
.gf-detail {{ color: var(--fg-dim); font-size: 11px; }}
.gf-collapse {{ position: fixed; right: 16px; bottom: 222px; background: var(--bg-panel); border: 1px solid var(--border); color: var(--fg-dim); padding: 4px 10px; border-radius: 4px; cursor: pointer; font-size: 11px; z-index: 51; }}
body.gf-collapsed .goals-fears {{ display: none; }}
body.gf-collapsed .gf-collapse {{ bottom: 16px; }}

</style>
</head>
<body>
<div class="shell">

  <div class="top">
    <h1>TRI diary</h1>
    <div class="sub">
      <span class="counter">Day {days_in}/{total_days}</span>
      <span class="counter" style="background:#f38181">{days_remaining} days remaining</span>
      <span>{start_iso} → {end_iso} · today: {today_pretty}</span>
    </div>
  </div>

  {gantt}

  <div class="calendars">
    {calendars}
  </div>

  <section class="stream">
    {cards}
  </section>

</div>

<button class="gf-collapse" onclick="document.body.classList.toggle('gf-collapsed')">toggle goals/fears</button>
{goals_fears}

<script>
(function() {{
  // Place today marker on the gantt bar
  var pct = {today_pct};
  var marker = document.getElementById('today-marker');
  if (marker) marker.style.left = pct + '%';

  // Highlight today's cell
  var today = '{today}';
  document.querySelectorAll('.cell[data-date="' + today + '"]').forEach(function(c) {{
    c.classList.add('is-today');
  }});

  // Smooth scroll to anchor on click
  document.querySelectorAll('.cell[href], .gantt-phase[href]').forEach(function(a) {{
    a.addEventListener('click', function(ev) {{
      var href = a.getAttribute('href');
      if (href && href.startsWith('#')) {{
        var t = document.querySelector(href);
        if (t) {{
          ev.preventDefault();
          t.scrollIntoView({{behavior: 'smooth', block: 'start'}});
          history.replaceState(null, '', href);
        }}
      }}
    }});
  }});
}})();
</script>

</body>
</html>
"""


if __name__ == "__main__":
    build()
