"""
Render the metrics dict into a self-contained, A TIME-branded HTML report.

No external JS/CDN (only the Inter web font). All charts are inline SVG /
CSS so the file prints cleanly and emails/opens anywhere.
"""

import html
import json

import config

C = config.BRAND


_LOGO_SVG = """<svg role="img" aria-label="A TIME" viewBox="0 0 128 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_40000125_17500)">
<path d="M116.296 6.8395C118.726 2.62183 123.045 0.0397893 127.94 0C127.98 0 128 0.0203695 128 0.0611061V3.24709C128 3.29351 127.977 3.3172 127.93 3.31814C122.32 3.45172 118.037 7.74613 117.907 13.3465C117.907 13.3579 117.902 13.3687 117.894 13.3767C117.886 13.3847 117.875 13.3891 117.863 13.3891L114.627 13.3905C114.619 13.3905 114.612 13.389 114.605 13.3862C114.598 13.3833 114.591 13.3792 114.586 13.3739C114.58 13.3686 114.576 13.3623 114.573 13.3554C114.57 13.3485 114.569 13.3411 114.569 13.3337C114.539 10.8866 113.686 8.6286 112.08 6.78834C110.158 4.59046 107.648 3.42993 104.547 3.30678C104.507 3.30488 104.487 3.28404 104.487 3.24425V0.0611061C104.487 0.0222642 104.507 0.0028427 104.546 0.0028427C108.883 0.0198953 112.753 2.05199 115.3 5.52077C115.615 5.95087 115.916 6.39092 116.202 6.84092C116.235 6.89208 116.266 6.8916 116.296 6.8395Z" fill="#1A242A"/>
<path d="M50.3884 2.78662H45.769C45.7399 2.78662 45.7163 2.81016 45.7163 2.8392V7.11087C45.7163 7.1399 45.7399 7.16344 45.769 7.16344H50.3884C50.4175 7.16344 50.4411 7.1399 50.4411 7.11087V2.8392C50.4411 2.81016 50.4175 2.78662 50.3884 2.78662Z" fill="#1A242A"/>
<path d="M37.9975 14.8798C37.9843 17.7948 37.9833 20.7103 37.9947 23.6263C37.9985 24.5613 38.0734 25.2269 38.2195 25.6229C38.9923 27.7217 41.1128 27.5299 43.0012 27.5072C43.0534 27.5072 43.0795 27.5337 43.0795 27.5868L43.0809 31.4563C43.0809 31.4717 43.0748 31.4865 43.0638 31.4975C43.0529 31.5084 43.0381 31.5145 43.0226 31.5145C41.6934 31.5273 40.2973 31.5998 38.9895 31.3895C35.6807 30.8552 33.6926 28.2674 33.6912 24.9933C33.6902 21.6226 33.6874 18.2519 33.6826 14.8812C33.6826 14.8357 33.6599 14.813 33.6143 14.813L30.3397 14.8144C30.3254 14.8144 30.3116 14.8085 30.3015 14.7982C30.2913 14.7878 30.2856 14.7737 30.2856 14.759V10.9321C30.2856 10.8762 30.3136 10.8482 30.3696 10.8482L33.6172 10.8497C33.6627 10.8497 33.6855 10.8269 33.6855 10.7815L33.6869 5.45538C33.6869 5.40896 33.7101 5.38574 33.7566 5.38574H37.9221C37.9677 5.38574 37.9904 5.40848 37.9904 5.45395V10.78C37.9904 10.8265 38.0137 10.8497 38.0602 10.8497H43.0069C43.0553 10.8497 43.0795 10.8743 43.0795 10.9236L43.0809 14.7334C43.0809 14.7874 43.0539 14.8144 42.9998 14.8144L38.063 14.813C38.0194 14.813 37.9975 14.8352 37.9975 14.8798Z" fill="#1A242A"/>
<path d="M90.8222 22.832C91.168 25.6315 92.9981 27.6707 95.769 28.1908C96.3192 28.2931 97.0773 28.285 98.0431 28.1666C99.9292 27.9355 101.362 27.0051 102.342 25.3757C102.378 25.316 102.432 25.2862 102.502 25.2862H106.797C106.804 25.2861 106.811 25.2878 106.818 25.2912C106.824 25.2946 106.83 25.2995 106.834 25.3056C106.838 25.3116 106.841 25.3186 106.841 25.3259C106.842 25.3332 106.841 25.3405 106.838 25.3473C105.496 28.7578 102.647 31.1934 99.0094 31.8201C91.1395 33.1758 85.0258 26.6745 86.4916 18.9199C87.8849 11.5475 96.065 8.24781 102.379 11.8203C106.205 13.9846 108.055 18.3301 107.329 22.6316C107.317 22.7103 107.271 22.7496 107.191 22.7496H90.8962C90.8402 22.7496 90.8155 22.7771 90.8222 22.832ZM91.0613 18.4481C90.9873 18.6991 90.9375 18.917 90.9118 19.1018C90.9062 19.1453 90.9256 19.1671 90.9702 19.1671H102.889C102.897 19.167 102.906 19.1651 102.913 19.1616C102.921 19.158 102.928 19.1529 102.933 19.1465C102.939 19.1401 102.942 19.1327 102.945 19.1248C102.947 19.1168 102.947 19.1085 102.946 19.1003C102.401 15.704 99.489 13.7075 96.1247 14.1309C93.5043 14.4597 91.8165 15.8987 91.0613 18.4481Z" fill="#1A242A"/>
<path d="M17.6141 13.3734L17.6127 10.9193C17.6127 10.8719 17.6359 10.8482 17.6824 10.8482L21.7298 10.8497C21.7772 10.8497 21.8009 10.8738 21.8009 10.9221L21.8023 31.4676C21.8023 31.4745 21.801 31.4812 21.7984 31.4875C21.7958 31.4938 21.792 31.4995 21.7872 31.5043C21.7825 31.5091 21.7768 31.5128 21.7706 31.5153C21.7644 31.5178 21.7578 31.519 21.7511 31.5188H17.6482C17.6392 31.5188 17.6305 31.5152 17.6241 31.5088C17.6177 31.5024 17.6141 31.4937 17.6141 31.4847L17.6127 29.0092C17.6127 28.9041 17.578 28.8908 17.5088 28.9694C16.1141 30.5681 14.3153 31.4918 12.1977 31.8357C7.61807 32.5789 3.07546 30.1389 1.07882 25.9668C-1.68488 20.1902 1.04324 13.0608 7.18544 10.9548C10.534 9.80519 14.5046 10.3637 17.0861 12.9699C17.2341 13.1195 17.3888 13.2635 17.55 13.4019C17.5927 13.4388 17.6141 13.4293 17.6141 13.3734ZM15.6459 26.1557C18.1833 23.7712 18.3527 19.5465 16.2678 16.8934C13.043 12.7894 6.33157 13.9305 4.71348 18.9169C3.36151 23.0806 5.91032 27.5427 10.4188 27.9377C12.45 28.1168 14.1924 27.5228 15.6459 26.1557Z" fill="#1A242A"/>
<path d="M57.5653 13.2669C60.8029 8.88585 67.2739 9.71858 69.854 14.2787C69.8834 14.3289 69.9142 14.3299 69.9465 14.2816C71.7325 11.6157 74.1575 10.1818 77.4093 10.4206C81.266 10.7048 83.9045 13.6194 84.4438 17.4221C84.5197 17.9517 84.5582 18.8564 84.5591 20.1363C84.5639 23.921 84.5643 27.7015 84.5605 31.4776C84.5605 31.4886 84.5562 31.4991 84.5485 31.5068C84.5407 31.5145 84.5302 31.5189 84.5193 31.5189H80.3296C80.2822 31.5189 80.2584 31.4952 80.2584 31.4478C80.2546 27.4594 80.2537 23.4724 80.2556 19.4869C80.2556 17.8583 79.6508 16.1374 78.3059 15.1427C77.2528 14.364 75.703 14.2148 74.4635 14.6198C72.3502 15.3104 71.1049 17.3993 71.1049 19.5934C71.1059 23.5496 71.1059 27.5044 71.1049 31.4577C71.1049 31.474 71.0983 31.4895 71.0866 31.501C71.0748 31.5124 71.0589 31.5189 71.0423 31.5189H66.8455C66.8323 31.5189 66.8196 31.5138 66.8103 31.5047C66.8009 31.4956 66.7957 31.4833 66.7957 31.4705C66.7976 27.5551 66.7995 23.6434 66.8014 19.7355C66.8028 18.2221 66.457 16.7499 65.4153 15.6216C63.9452 14.0286 61.3394 14.0542 59.6815 15.3346C58.3552 16.3606 57.691 17.8039 57.6891 19.6645C57.6853 23.5951 57.6849 27.5276 57.6877 31.462C57.6877 31.4771 57.6817 31.4915 57.671 31.5022C57.6604 31.5129 57.6459 31.5189 57.6308 31.5189H53.4724C53.4117 31.5189 53.3813 31.4885 53.3813 31.4279V10.9322C53.3813 10.8763 53.4093 10.8483 53.4653 10.8483L57.413 10.8497C57.4605 10.8497 57.4842 10.8734 57.4842 10.9208L57.4828 13.2399C57.4828 13.3328 57.5103 13.3418 57.5653 13.2669Z" fill="#1A242A"/>
<path d="M50.2233 10.8483H46.0137C45.987 10.8483 45.9653 10.8699 45.9653 10.8966V31.4705C45.9653 31.4972 45.987 31.5188 46.0137 31.5188H50.2233C50.25 31.5188 50.2717 31.4972 50.2717 31.4705V10.8966C50.2717 10.8699 50.25 10.8483 50.2233 10.8483Z" fill="#1A242A"/>
</g>
<defs>
<clipPath id="clip0_40000125_17500">
<rect width="128" height="32" fill="white"/>
</clipPath>
</defs>
</svg>"""


def _rgb(hexcolor):
    h = hexcolor.lstrip("#")
    return ",".join(str(int(h[i:i + 2], 16)) for i in (0, 2, 4))


def fmt(n):
    try:
        return f"{int(round(float(n))):,}"
    except (TypeError, ValueError):
        return str(n)


def fmt1(n):
    """One-decimal formatter for small hour figures — fmt()'s int rounding
    turns e.g. 0.3h into '0h', which reads as no time at all on the PDF."""
    try:
        return f"{float(n):.1f}"
    except (TypeError, ValueError):
        return str(n)


def esc(s):
    return html.escape(str(s))


# ---------------------------------------------------------------------------
# Small chart primitives (inline SVG)
# ---------------------------------------------------------------------------
def _grouped_month_chart(monthly):
    """Answered vs missed grouped bars per month, with answer-rate line."""
    if not monthly:
        return ""
    w, h = 840, 260
    pad_l, pad_b, pad_t = 38, 34, 14
    n = len(monthly)
    gw = (w - pad_l - 8) / n
    maxv = max(max(m["inbound_answered"], m["inbound_missed"]) for m in monthly) or 1
    plot_h = h - pad_b - pad_t

    def y(v):
        return pad_t + plot_h * (1 - v / maxv)

    bars, line_pts, labels = [], [], []
    bw = gw * 0.30
    for i, m in enumerate(monthly):
        cx = pad_l + gw * i + gw / 2
        ax = cx - bw - 1
        mx = cx + 1
        bars.append(
            f'<rect x="{ax:.1f}" y="{y(m["inbound_answered"]):.1f}" width="{bw:.1f}" '
            f'height="{(h-pad_b-y(m["inbound_answered"])):.1f}" fill="{C["purple_deep"]}" rx="1"/>'
            f'<rect x="{mx:.1f}" y="{y(m["inbound_missed"]):.1f}" width="{bw:.1f}" '
            f'height="{(h-pad_b-y(m["inbound_missed"])):.1f}" fill="{C["orange"]}" rx="1"/>'
        )
        line_pts.append(f"{cx:.1f},{pad_t + plot_h*(1-m['answer_rate']/100):.1f}")
        labels.append(
            f'<text x="{cx:.1f}" y="{h-pad_b+15:.1f}" text-anchor="middle" '
            f'font-size="10" fill="{C["purple_med"]}">{esc(m["month"][5:])}</text>'
        )
    # answer-rate polyline (0..100 scaled to plot)
    line = (f'<polyline points="{" ".join(line_pts)}" fill="none" '
            f'stroke="{C["gold"]}" stroke-width="2" stroke-dasharray="1 0"/>')
    dots = "".join(f'<circle cx="{p.split(",")[0]}" cy="{p.split(",")[1]}" r="2.5" '
                   f'fill="{C["gold"]}"/>' for p in line_pts)
    gridlines = ""
    for frac in (0, 0.5, 1):
        gv = maxv * (1 - frac)
        yy = pad_t + plot_h * frac
        gridlines += (f'<line x1="{pad_l}" y1="{yy:.1f}" x2="{w-4}" y2="{yy:.1f}" '
                      f'stroke="{C["border"]}" stroke-width="1"/>'
                      f'<text x="0" y="{yy+3:.1f}" font-size="9" fill="{C["faint"]}">{fmt(gv)}</text>')
    return (f'<svg viewBox="0 0 {w} {h}" width="100%" role="img">'
            f'{gridlines}{"".join(bars)}{line}{dots}{"".join(labels)}</svg>')


def _hour_chart(hourly):
    w, h = 840, 170
    pad_l, pad_b, pad_t = 30, 26, 10
    maxv = max(hourly) or 1
    gw = (w - pad_l - 6) / 24
    plot_h = h - pad_b - pad_t
    bars, labels = [], []
    for hr, v in enumerate(hourly):
        x = pad_l + gw * hr + gw * 0.12
        bh = plot_h * v / maxv
        # shade after-hours bars orange-ish
        after = hr < config.BUSINESS_START_HOUR or hr >= config.BUSINESS_END_HOUR
        color = C["gold"] if after else C["purple_med"]
        bars.append(f'<rect x="{x:.1f}" y="{pad_t+plot_h-bh:.1f}" width="{gw*0.76:.1f}" '
                    f'height="{bh:.1f}" fill="{color}" rx="1"/>')
        if hr % 3 == 0:
            labels.append(f'<text x="{x+gw*0.38:.1f}" y="{h-pad_b+14:.1f}" text-anchor="middle" '
                          f'font-size="9" fill="{C["purple_med"]}">{hr}:00</text>')
    return (f'<svg viewBox="0 0 {w} {h}" width="100%" role="img">'
            f'{"".join(bars)}{"".join(labels)}</svg>')


def _dow_bars(dow):
    names = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
    maxv = max(dow) or 1
    rows = ""
    for i, v in enumerate(dow):
        pct = 100 * v / maxv
        rows += (f'<div class="reg-row"><div class="reg-name">{names[i]}</div>'
                 f'<div class="reg-bar-wrap"><div class="reg-bar" style="width:{pct:.1f}%"></div></div>'
                 f'<div class="reg-val">{fmt(v)}</div></div>')
    return rows


def _heatmap(heat):
    """7x24 grid; cell alpha scales to volume. Sat row reads near-empty (Shabbos)."""
    names = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
    maxv = max(max(r) for r in heat) or 1
    cells = ""
    # hour header
    head = '<div class="hm-cell hm-corner"></div>'
    for hr in range(24):
        lab = f"{hr}" if hr % 2 == 0 else ""
        head += f'<div class="hm-h">{lab}</div>'
    cells += head
    for d in range(7):
        cells += f'<div class="hm-day">{names[d]}</div>'
        for hr in range(24):
            v = heat[d][hr]
            a = 0.06 + 0.94 * (v / maxv) if v else 0.0
            bg = f"rgba({_rgb(C['purple_deep'])},{a:.3f})" if v else C["bg"]
            cells += f'<div class="hm-cell" title="{names[d]} {hr}:00 — {v} calls" style="background:{bg}"></div>'
    return f'<div class="heatmap">{cells}</div>'


# ---------------------------------------------------------------------------
# Sections
# ---------------------------------------------------------------------------
def _hero(s):
    cells = [
        (fmt(s["inbound_answered"]), "Inbound calls answered",
         f'{s["answer_rate"]}% of attempts answered live', ""),
        (f'{fmt(s["talk_hours_inbound"])}<small> hrs</small>', "On the phone with callers",
         f'inbound talk time ({fmt(s["talk_minutes_inbound"])} minutes)', ""),
        (fmt(s["unique_callers"]), "Unique people who called", f'{s["reach_rate"]}% reached a live person', ""),
        (fmt(s["total_calls"]), "Overall interactions (in + out)",
         f'{fmt(s["inbound_attempts"])} in &middot; {fmt(s["outbound"])} out', ""),
        (fmt(s["outbound"]), "Outbound follow-up calls",
         f'staff calling families back · {fmt(s["talk_hours_outbound"])} hrs of talk time', "orange"),
        (fmt(s["overnight_count"]), "Inbound calls overnight (10p–6a)",
         f'{s["overnight_pct"]}% of inbound &middot; {s["after_hours_pct"]}% outside 8a–7p', "orange"),
    ]
    out = '<div class="hero-grid">'
    for num, label, cap, cls in cells:
        out += (f'<div class="hero-cell"><div class="hero-num {cls}">{num}</div>'
                f'<div class="hero-label">{label}</div><div class="hero-caption">{esc(cap)}</div></div>')
    return out + "</div>"


def _division_totals_table(rows):
    """Quick-glance Total In (=inbound attempts) / Total Out / Grand Total per division."""
    body = ""
    for r in rows:
        body += (f'<tr><td class="dept-name">{esc(r["division"])}</td>'
                 f'<td class="num strong">{fmt(r["inbound_attempts"])}</td>'
                 f'<td class="num">{fmt(r["outbound"])}</td>'
                 f'<td class="num strong">{fmt(r["total"])}</td></tr>')
    return (
        f'<h3>Totals by division</h3>'
        f'<table class="tbl" style="max-width:560px"><thead><tr>'
        f'<th>Division</th><th class="num">Total&nbsp;In</th>'
        f'<th class="num">Total&nbsp;Out</th><th class="num">Grand&nbsp;Total</th>'
        f'</tr></thead><tbody>{body}</tbody></table>')


def _division_section(rows):
    """Two tables: inbound (routing columns sum to attempts) and outbound."""
    body = ""
    for r in rows:
        body += (f'<tr><td class="dept-name">{esc(r["division"])}</td>'
                 f'<td class="num strong">{fmt(r["inbound_answered"])}</td>'
                 f'<td class="num">{fmt(r["inbound_missed"])}</td>'
                 f'<td class="num">{fmt(r["inbound_attempts"])}</td>'
                 f'<td class="num">{r["answer_rate"]}%</td>'
                 f'<td class="num">{fmt(r["talk_hours_in"])}</td>'
                 f'<td class="num muted">{fmt(r["unique_callers"])}</td>'
                 f'<td class="num muted">{fmt(r["voicemail"])}</td>'
                 f'<td class="num muted">{fmt(r["queue"])}</td>'
                 f'<td class="num muted">{fmt(r["direct"])}</td>'
                 f'<td class="num muted">{fmt(r["dept"])}</td></tr>')
    inbound_tbl = (
        f'<h3>Inbound</h3>'
        f'<table class="tbl"><thead><tr>'
        f'<th>Division</th><th class="num">Answered</th><th class="num">Missed</th>'
        f'<th class="num">Attempts</th><th class="num">Answer&nbsp;%</th>'
        f'<th class="num">Talk&nbsp;hrs</th><th class="num">Callers</th>'
        f'<th class="num">Voicemail</th>'
        f'<th class="num">Queue</th><th class="num">Direct</th><th class="num">Dept</th>'
        f'</tr></thead><tbody>{body}</tbody></table>'
        f'<p class="hero-caption">Queue, Direct, and Dept show how each inbound attempt was '
        f'matched to its division (routing queue, staff extension, or the CDR department '
        f'label) &mdash; the three always add up to the attempts column.</p>')
    out_rows = sorted([r for r in rows if r["outbound"]],
                      key=lambda r: r["outbound"], reverse=True)
    if not out_rows:
        return inbound_tbl
    obody = ""
    for r in out_rows:
        obody += (f'<tr><td class="dept-name">{esc(r["division"])}</td>'
                  f'<td class="num strong">{fmt(r["outbound"])}</td>'
                  f'<td class="num">{fmt(r["talk_hours_out"])}</td></tr>')
    outbound_tbl = (
        f'<h3>Outbound (staff follow-up calls)</h3>'
        f'<table class="tbl" style="max-width:420px"><thead><tr>'
        f'<th>Division</th><th class="num">Calls</th><th class="num">Talk&nbsp;hrs</th>'
        f'</tr></thead><tbody>{obody}</tbody></table>')
    return inbound_tbl + outbound_tbl


def _cm_section(people, limit=14):
    if not people:
        return ""
    maxv = max(p["total"] for p in people[:limit]) or 1
    rows = ""
    for p in people[:limit]:
        pct = 100 * p["total"] / maxv
        rows += (f'<div class="cm-row"><div class="cm-name">{esc(p["name"])}</div>'
                 f'<div class="cm-bar-wrap"><div class="cm-bar" style="width:{pct:.1f}%"></div>'
                 f'<div class="cm-val">{fmt(p["inbound_answered"])} answered · '
                 f'{p["answer_rate"]}% · {fmt1(p.get("talk_hours_in", p["talk_hours"]))}h in / '
                 f'{fmt1(p.get("talk_hours_out", 0))}h out</div></div></div>')
    return f'<div class="cm-table">{rows}</div>'


def _regional_section(regional, limit=10):
    maxv = max(r["calls"] for r in regional[:limit]) or 1
    rows = ""
    for r in regional[:limit]:
        pct = 100 * r["calls"] / maxv
        rows += (f'<div class="reg-row"><div class="reg-name">{esc(r["region"])}</div>'
                 f'<div class="reg-bar-wrap"><div class="reg-bar" style="width:{pct:.1f}%"></div></div>'
                 f'<div class="reg-val">{fmt(r["calls"])}</div></div>')
    return rows


def _area_table(area_codes, limit=15):
    body = ""
    for a in area_codes[:limit]:
        body += (f'<tr><td class="strong">{esc(a["area_code"])}</td>'
                 f'<td class="muted">{esc(a["region"]) or "&mdash;"}</td>'
                 f'<td class="num">{fmt(a["calls"])}</td>'
                 f'<td class="num muted">{fmt(a["unique_callers"])}</td></tr>')
    return (f'<table class="tbl"><thead><tr><th>Area&nbsp;code</th><th>Region</th>'
            f'<th class="num">Inbound&nbsp;calls</th><th class="num">Unique&nbsp;callers</th>'
            f'</tr></thead><tbody>{body}</tbody></table>')


def _region_section(reg):
    s = reg["summary"]
    meta = reg["meta"]
    accent = meta.get("accent") or C["orange"]
    name = meta["display"]
    sn = meta["key"][:3].upper()
    codes_join = " and ".join(a["area_code"] for a in reg["area_codes"])
    split = (
        f'<div class="split">'
        f'<div class="split-item"><div class="stat-big">{fmt(s["inbound_attempts"])}</div>'
        f'<div class="stat-label">inbound calls from {esc(name)} '
        f'({s["answer_rate"]}% answered live)</div></div>'
        f'<div class="split-item"><div class="stat-big">{fmt(s["unique_callers"])}</div>'
        f'<div class="stat-label">unique {esc(name)}-area callers reached out '
        f'&mdash; {s["reach_rate"]}% connected with a person</div></div>'
        f'<div class="split-item"><div class="stat-big" style="color:{accent}">{fmt(s["talk_hours"])}<span style="font-size:18px"> hrs</span></div>'
        f'<div class="stat-label">of phone support to {esc(name)} families '
        f'({fmt(s["talk_minutes"])} minutes)</div></div>'
        f'</div>'
    )
    ac_body = ""
    for a in reg["area_codes"]:
        ac_body += (f'<tr><td class="strong">{esc(a["area_code"])}</td>'
                    f'<td class="muted">{esc(a["label"])}</td>'
                    f'<td class="num">{fmt(a["calls"])}</td>'
                    f'<td class="num">{fmt(a["answered"])}</td>'
                    f'<td class="num muted">{fmt(a["unique_callers"])}</td></tr>')
    ac = (f'<table class="tbl"><thead><tr><th>Area&nbsp;code</th><th>Coverage</th>'
          f'<th class="num">Inbound</th><th class="num">Answered</th>'
          f'<th class="num">Callers</th></tr></thead><tbody>{ac_body}</tbody></table>')
    div_body = ""
    for r in reg["by_division"]:
        if r["inbound_attempts"] == 0:
            continue
        div_body += (f'<tr><td class="dept-name">{esc(r["division"])}</td>'
                     f'<td class="num strong">{fmt(r["inbound_answered"])}</td>'
                     f'<td class="num">{fmt(r["inbound_missed"])}</td>'
                     f'<td class="num muted">{fmt(r["unique_callers"])}</td>'
                     f'<td class="num muted">{fmt(r["talk_hours"])}</td></tr>')
    divtbl = (f'<table class="tbl"><thead><tr><th>Division</th><th class="num">Answered</th>'
              f'<th class="num">Missed</th><th class="num">Callers</th>'
              f'<th class="num">Talk&nbsp;hrs</th></tr></thead><tbody>{div_body}</tbody></table>')
    note = (f'<p class="hero-caption" style="margin-top:10px">{esc(meta["excluded_note"])}</p>'
            if meta.get("excluded_note") else "")
    return (
        f'<section class="region" style="border-color:{accent};'
        f'background:linear-gradient(180deg,{accent}0d,transparent 60%)">'
        f'<h2 style="color:{accent}"><span class="sn" style="color:{accent}">{sn}</span>'
        f'{esc(name)} &mdash; a closer look</h2>'
        f'<p class="lede" style="border-color:{accent};color:{C["purple_deep"]}">'
        f'For our {esc(name)}-area supporters: here is exactly how A&nbsp;TIME showed up for '
        f'families across {esc(meta["msa_name"])} this year. Area codes {esc(codes_join)} '
        f'cover {esc(meta.get("counties",""))}.</p>'
        f'{split}'
        f'<h3 style="color:{accent}">By area code</h3>{ac}'
        f'<h3 style="color:{accent}">Which divisions {esc(name)} families reached</h3>{divtbl}'
        f'{note}'
        f'</section>'
    )


def _talking_points(m, region_key=None):
    s = m["summary"]
    period = m["meta"].get("period_label") or m["meta"]["period_end"][:4]
    points = [
        f'A&nbsp;TIME answered <strong>{fmt(s["inbound_answered"])}</strong> calls live in {esc(period)} and '
        f'spent <strong>{fmt(s["talk_hours"])} hours</strong> &mdash; the equivalent of '
        f'<strong>{fmt(s["talk_hours"]/24)} full days</strong> &mdash; on the phone with families '
        f'({fmt(s["talk_hours_inbound"])} hrs with inbound callers, '
        f'{fmt(s["talk_hours_outbound"])} hrs of outbound follow-up).',
        f'<strong>{fmt(s["unique_callers"])}</strong> different people reached out across '
        f'{len(m["by_division"])} reporting divisions; <strong>{s["reach_rate"]}%</strong> '
        f'connected with a real person.',
        f'<strong>{s["overnight_pct"]}%</strong> of inbound calls came overnight (10pm&ndash;6am) and '
        f'<strong>{s["after_hours_pct"]}%</strong> outside business hours &mdash; the helplines are '
        f'there when families have nowhere else to turn.',
        f'When no one could pick up, <strong>{fmt(s["voicemail_count"])}</strong> callers were able to '
        f'leave a voicemail so their call was never simply lost.',
    ]
    reg = m["regions"].get(region_key) if region_key else None
    if reg:
        c = reg["summary"]
        name = reg["meta"]["display"]
        points.append(
            f'{esc(name)}-area families placed <strong>{fmt(c["inbound_attempts"])}</strong> calls and '
            f'received <strong>{fmt(c["talk_hours"])} hours</strong> of dedicated phone support.')
    return "".join(f'<div class="callout">{p}</div>' for p in points)


def _methodology(m):
    v = m["validation"]
    return (
        '<div class="method">'
        '<h4>Data source</h4>'
        f'<p>Every figure is computed from {fmt(v["total_records"])} call detail records (CDRs) pulled '
        'directly from the NetSapiens platform API (<code>v2 /domains/ATIME/cdrs</code>) for the '
        f'reporting period {esc(m["meta"]["period_start"])} to {esc(m["meta"]["period_end"])}. '
        'Records are pulled in pages of 100 to preserve the full 99-field record (larger page sizes '
        'silently strip caller-ID and department fields).</p>'
        '<h4>How calls are assigned to a Reporting Division</h4>'
        '<p>Every department is mapped to exactly one Reporting Division by an authoritative '
        'department&nbsp;&rarr;&nbsp;division matrix, so each user (including anyone added later) '
        'inherits a division through their department. Calls are matched in priority order: (1) the '
        'call&rsquo;s queue / auto-attendant / bridge extension, (2) the direct user extension and its '
        'department, (3) the raw CDR department text &mdash; all through the same matrix. This assignment '
        'agrees with the CDR&rsquo;s own department labels '
        f'<strong>{v["classification_agreement_pct"]}%</strong> of the '
        f'time ({fmt(v["classification_disagreements"])} of {fmt(v["classification_compared"])} compared). '
        'Internal calls and the main auto-attendant IVR greeting leg '
        f'(ext&nbsp;998, {fmt(v["main_aa_998_legs"])} legs) are excluded from division metrics; '
        f'{v["unmapped_pct"]}% of all records remain unmapped and are excluded to avoid double-counting.</p>'
        '<h4>Voicemail detection</h4>'
        '<p>A call is counted as reaching voicemail when its terminating URI contains <code>VMail</code> '
        'or its match URI contains <code>record-vmail</code>. Voicemails occur on unanswered (missed) '
        'inbound calls.</p>'
        '<h4>Caller geography</h4>'
        '<p>Caller phone numbers are extracted from the originating SIP URI '
        '(<code>call-orig-from-uri</code>), which yields a usable 10-digit number for '
        f'<strong>{v["phone_extracted_pct_inbound"]}%</strong> of inbound calls. The area code is the '
        'first three digits. The Cleveland&ndash;Elyria metro is defined as area codes 216 and 440; '
        'Akron&ndash;Canton (330 / 234) is a separate MSA and is excluded.</p>'
        '<h4>Metric definitions</h4>'
        '<p><strong>Answer rate</strong> = answered &divide; (answered + missed). '
        '<strong>Reach rate</strong> = unique callers who reached a live person at least once &divide; '
        'all unique callers. <strong>Talk time</strong> uses <code>call-talking-duration-seconds</code> '
        '(connected time only, not ring time), reported separately for inbound and outbound calls. '
        '<strong>Queue / Direct / Dept</strong> counts cover inbound attempts only and describe how each '
        'attempt was matched to its division; they sum to the attempts column. '
        'Hour-of-day and day-of-week are in US/Eastern.</p>'
        '<h4>Known limitations</h4>'
        '<p>A share of calls are classified from the CDR department text rather than a '
        'direct extension match (the handling extension is not always recorded on every call leg); these '
        'agree with the extension-based logic where both exist. A small number of callers place calls '
        'from numbers that cannot be parsed (~3% of inbound) and are excluded from geography only.</p>'
        '</div>'
    )


# ---------------------------------------------------------------------------
# CSS (A TIME design system — colors/fonts pulled from atime.org; see BRAND
# in config.py for provenance of each token)
# ---------------------------------------------------------------------------
def _css():
    pd, pm, org, gd = C["purple_deep"], C["purple_med"], C["orange"], C["gold"]
    return f"""
*{{box-sizing:border-box}}html,body{{margin:0;padding:0}}
body{{font-family:'{C["font"]}',system-ui,sans-serif;background:{C["bg"]};color:{C["text"]};line-height:1.55;font-size:16px}}
.page{{max-width:900px;margin:0 auto;padding:56px 48px 80px;background:linear-gradient(135deg,rgba({_rgb(pd)},.04) 0%,transparent 40%),linear-gradient(225deg,rgba({_rgb(gd)},.05) 0%,transparent 50%),#fff;box-shadow:0 2px 40px rgba({_rgb(pd)},.06);border-radius:16px;min-height:100vh}}
.logo{{width:168px;margin-bottom:28px}}.logo svg{{display:block;width:100%;height:auto}}
.eyebrow{{font-size:11px;font-weight:700;letter-spacing:.2em;text-transform:uppercase;color:{org};margin-bottom:16px}}
h1{{font-weight:800;font-size:42px;line-height:1.08;letter-spacing:-.02em;margin:0 0 12px;color:{pd}}}
h1 em{{font-style:normal;font-weight:300;color:{org}}}
.dateline{{font-size:14px;color:{pm};margin-bottom:40px;padding-bottom:28px;border-bottom:3px solid {gd}}}
h2{{font-weight:700;font-size:26px;margin:48px 0 16px;color:{pd}}}
h2 .sn{{font-size:12px;font-weight:700;color:{org};vertical-align:top;margin-right:8px;letter-spacing:.08em}}
h3{{font-weight:600;font-size:18px;margin:24px 0 10px;color:{pm}}}
p{{margin:0 0 14px}}
.lede{{font-size:19px;font-weight:300;line-height:1.5;color:{pd};margin-bottom:28px;padding-left:18px;border-left:4px solid {gd}}}
.hero-grid{{display:grid;grid-template-columns:repeat(3,1fr);gap:3px;margin:20px 0 8px;background:{C["border_soft"]};border:1px solid {C["border_soft"]};border-radius:12px;overflow:hidden}}
.hero-cell{{background:#fff;padding:22px 18px}}
.hero-num{{font-weight:800;font-size:40px;line-height:1;color:{pd};letter-spacing:-.03em}}
.hero-num small{{font-size:15px;color:{pm};font-weight:400}}
.hero-num.orange{{color:{org}}}
.hero-label{{font-size:12px;font-weight:600;color:{C["text"]};margin-top:8px}}
.hero-caption{{font-size:11px;color:{pm};margin-top:4px;line-height:1.4}}
.tbl{{width:100%;border-collapse:collapse;margin:14px 0 8px;font-size:12px}}
.tbl th{{text-align:left;font-weight:700;font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:{org};padding:8px 6px 8px 0;border-bottom:2px solid {pd}}}
.tbl th.num{{text-align:right}}.tbl td{{padding:9px 6px 9px 0;border-bottom:1px solid {C["border"]}}}
.tbl td.num{{text-align:right;font-variant-numeric:tabular-nums}}.tbl td.muted{{color:{pm}}}
.tbl td.strong{{font-weight:700;color:{pd}}}.tbl td.dept-name{{font-weight:600;color:{C["text"]}}}
.tbl tbody tr:last-child td{{border-bottom:2px solid {pd}}}
.cm-table{{margin:14px 0}}.cm-row{{display:grid;grid-template-columns:170px 1fr;gap:14px;align-items:center;padding:6px 0;border-bottom:1px solid {C["border"]};font-size:13px}}
.cm-name{{font-weight:500;color:{C["text"]}}}.cm-bar-wrap{{display:flex;align-items:center;gap:10px}}
.cm-bar{{height:8px;background:linear-gradient(90deg,{pd},{pm});border-radius:4px;min-width:2px}}
.cm-val{{font-size:11px;color:{pm};font-variant-numeric:tabular-nums;white-space:nowrap}}
.chart{{margin:16px 0 8px;padding:16px 4px 4px;background:{C["bg"]};border:1px solid {C["border"]};border-radius:10px}}
.chart-cap{{font-size:12px;color:{pm};margin-top:10px;padding:0 14px 10px;font-style:italic}}
.legend{{display:flex;gap:20px;font-size:12px;color:{C["muted"]};margin:6px 0 14px;padding-left:4px}}
.legend-swatch{{display:inline-block;width:14px;height:10px;vertical-align:middle;margin-right:5px;border-radius:2px}}
.callout{{background:{C["bg_warm"]};border-left:4px solid {gd};padding:16px 22px;margin:14px 0;font-size:15px;line-height:1.5;border-radius:0 10px 10px 0}}
.callout strong{{color:{pd}}}
.split{{display:grid;grid-template-columns:repeat(3,1fr);gap:24px;margin:14px 0}}
.split-item .stat-big{{font-weight:800;font-size:36px;line-height:1;color:{pd};margin-bottom:6px}}
.split-item .stat-big.orange{{color:{org}}}
.split-item .stat-label{{font-size:13px;color:{C["muted"]};line-height:1.4}}
.reg-row{{display:grid;grid-template-columns:170px 1fr 60px;align-items:center;gap:12px;padding:5px 0;font-size:14px}}
.reg-name{{color:{C["text"]}}}.reg-bar-wrap{{height:6px;background:{C["border"]};border-radius:3px;overflow:hidden}}
.reg-bar{{height:100%;background:{pm};border-radius:3px}}.reg-val{{text-align:right;color:{pm};font-variant-numeric:tabular-nums;font-size:13px}}
.heatmap{{display:grid;grid-template-columns:34px repeat(24,1fr);gap:2px;margin:10px 0;font-size:9px}}
.hm-cell{{aspect-ratio:1/1;border-radius:2px}}.hm-corner{{background:transparent}}
.hm-h{{color:{C["faint"]};text-align:center;align-self:end;font-size:8px}}
.hm-day{{color:{pm};font-size:10px;display:flex;align-items:center;font-weight:600}}
.region{{border:2px solid {org};border-radius:14px;padding:8px 28px 24px;margin:40px 0}}
footer{{margin-top:56px;padding-top:20px;border-top:3px solid {gd};font-size:12px;color:{pm};line-height:1.6}}
footer strong{{color:{pd}}}
.method{{background:{C["bg"]};border:1px solid {C["border"]};border-radius:10px;padding:24px 28px;margin:16px 0;font-size:13px;line-height:1.6;color:{C["text"]}}}
.method h4{{color:{pd};font-size:16px;margin:20px 0 8px;font-weight:700}}.method h4:first-child{{margin-top:0}}
.method code{{background:{C["border"]};padding:1px 5px;border-radius:3px;font-size:12px;color:{pd}}}
@media(max-width:640px){{.page{{padding:32px 18px 56px}}h1{{font-size:30px}}.hero-grid{{grid-template-columns:1fr 1fr}}.hero-num{{font-size:30px}}.split{{grid-template-columns:1fr}}.cm-row{{grid-template-columns:120px 1fr;font-size:12px}}.tbl{{font-size:11px}}.heatmap{{font-size:7px}}}}
@media print{{body{{background:white}}.page{{box-shadow:none;padding:0}}h2{{page-break-after:avoid}}.chart,.callout,.hero-grid,.cle{{page-break-inside:avoid}}}}
""".strip()


def _legend(items):
    sw = "".join(f'<span><span class="legend-swatch" style="background:{c}"></span>{esc(l)}</span>'
                 for l, c in items)
    return f'<div class="legend">{sw}</div>'


# ---------------------------------------------------------------------------
# Top level
# ---------------------------------------------------------------------------
def render(m, region_key=None):
    """region_key=None renders the generic org-wide report (no regional section)."""
    s = m["summary"]
    meta = m["meta"]
    year = meta["period_end"][:4]
    period_label = meta.get("period_label") or year
    region = m["regions"].get(region_key) if region_key else None
    rname = region["meta"]["display"] if region else ""
    title_region = f' · {esc(rname)}' if rname else ""
    eyebrow_tail = f' &middot; Prepared for {esc(rname)} supporters' if rname else ""
    lede_tail = f' &mdash; including those across {esc(rname)}' if rname else ""
    parts = []
    parts.append(f'<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">'
                 f'<meta name="viewport" content="width=device-width,initial-scale=1">'
                 f'<title>{esc(meta["org_name"])} · Call Activity Report{title_region} · {esc(period_label)}</title>'
                 f'<link rel="preconnect" href="https://fonts.googleapis.com">'
                 f'<link href="{C["font_url"]}" rel="stylesheet">'
                 f'<style>{_css()}</style></head><body><div class="page">')

    # Header
    parts.append(f'<div class="logo">{_LOGO_SVG}</div>')
    parts.append(
        f'<div class="eyebrow">{esc(meta["org_name"])} &middot; Call Activity Report{eyebrow_tail}</div>'
        f'<h1><em>Showing up</em> for families.</h1>'
        f'<div class="dateline">{esc(meta["period_start"])} &ndash; {esc(meta["period_end"])} '
        f'({esc(period_label)}) &nbsp;·&nbsp; All reporting divisions &nbsp;·&nbsp; Prepared for A&nbsp;TIME by PressONE</div>'
        f'<p class="lede">Behind every number on this page is a person who picked up the phone, and a '
        f'family who needed someone on the other end. In {esc(period_label)}, A&nbsp;TIME&rsquo;s helplines and programs '
        f'answered {fmt(s["inbound_answered"])} calls and spent {fmt(s["talk_hours"])} hours listening, '
        f'guiding, and walking alongside the families who reached out{lede_tail}.</p>'
    )
    parts.append(_hero(s))

    _sec = iter(range(1, 20))

    def _h2(title):
        return f'<h2><span class="sn">{next(_sec):02d}</span>{title}</h2>'

    # monthly trend — only when the period actually spans multiple months
    # (a single-month report has nothing to trend)
    if len(m["monthly"]) > 1:
        parts.append(_h2("The year, month by month"))
        parts.append(_legend([("Answered live", C["purple_deep"]), ("Missed", C["orange"]),
                              ("Answer rate", C["gold"])]))
        parts.append(f'<div class="chart">{_grouped_month_chart(m["monthly"])}'
                     f'<div class="chart-cap">Inbound calls answered vs. missed each month, with the '
                     f'live-answer rate overlaid.</div></div>')

    # divisions
    parts.append(_h2("Call activity by Reporting Division"))
    parts.append('<p>Every A&nbsp;TIME department rolls up to a Reporting Division &mdash; the nine '
                 'classic helplines plus the organization&rsquo;s other programs &mdash; each with its own '
                 'callers, rhythms, and specialists. Inbound calls (families reaching out) and outbound '
                 'calls (staff following up) are shown separately.</p>')
    parts.append(_division_totals_table(m["by_division"]))
    parts.append(_division_section(m["by_division"]))

    # MHL case managers
    parts.append(_h2("The Medical Helpline team"))
    parts.append('<p>The Medical Helpline (MHL) is A&nbsp;TIME&rsquo;s busiest division. Here is the '
                 'call volume carried by each case manager this year.</p>')
    parts.append(_cm_section(m["mhl_by_person"]))

    # when people call
    parts.append(_h2("When families call"))
    parts.append(_legend([("Business hours (8a–7p)", C["purple_med"]),
                          ("After hours", C["gold"])]))
    parts.append(f'<div class="chart">{_hour_chart(m["hourly"])}'
                 f'<div class="chart-cap">Inbound calls by hour of day (US/Eastern). '
                 f'{s["after_hours_pct"]}% arrive outside business hours.</div></div>')
    parts.append('<h3>By day of week</h3>')
    parts.append("".join(_dow_bars(m["dow"])))

    # heatmap
    parts.append(_h2("A week in the life"))
    parts.append('<p>Inbound call volume by day and hour. The quiet Saturday band reflects Shabbos; '
                 'the helplines come back to life Saturday night.</p>')
    parts.append(f'<div class="chart">{_heatmap(m["heatmap"])}'
                 f'<div class="chart-cap">Darker cells = more calls. Each cell is one hour.</div></div>')

    # geography
    parts.append(_h2("Where callers reach out from"))
    parts.append('<p>Inbound calls by region, based on caller area code.</p>')
    parts.append("".join(_regional_section(m["geo"]["regional"])))
    parts.append('<h3>Top area codes</h3>')
    parts.append(_area_table(m["geo"]["area_codes"]))

    # 07 regional focus (omitted in the generic report)
    if region:
        parts.append(_region_section(region))

    # talking points
    parts.append(_h2("What this means for the families we serve"))
    parts.append(_talking_points(m, region_key))

    # methodology
    parts.append(_h2("How these numbers were produced"))
    parts.append(_methodology(m))

    # footer
    parts.append(
        f'<footer><strong>{esc(meta["org_name"])} &mdash; Call Activity Report ({esc(period_label)})'
        f'{(" &mdash; " + esc(rname)) if rname else ""}.</strong><br>'
        f'Prepared by PressONE Cloud Communications from NetSapiens platform CDR data. '
        f'Answer/talk/division figures cover all reporting divisions'
        f'{"; regional figures count all inbound calls from the region (excluding the main auto-attendant greeting leg)" if rname else "; internal calls are excluded"}. '
        f'Generated {esc(meta["generated_at"] or year)}.</footer>'
    )
    parts.append('</div></body></html>')
    return "".join(parts)


if __name__ == "__main__":
    import sys
    src = sys.argv[1] if len(sys.argv) > 1 else "output/metrics_2025.json"
    out = sys.argv[2] if len(sys.argv) > 2 else "output/ATIME_Report.html"
    region_key = sys.argv[3] if len(sys.argv) > 3 else "cleveland"
    with open(src) as f:
        m = json.load(f)
    html_str = render(m, region_key)
    with open(out, "w") as f:
        f.write(html_str)
    print(f"wrote {out} ({len(html_str):,} bytes)")
