// ─────────────────────────────────────────────────────────────
// Client Hours by Month and Year — current year, last year, year before
// Filters: Agency, Start Date, End Date
// Rules:
//   - include only sessions with hours > 0.5
// Graph:
//   - for each month: three bars, one per year
//   - bars are stacked by agency, matching the existing client-activity chart style
// Tables:
//   - monthly totals by year
//   - yearly totals by agency
// ─────────────────────────────────────────────────────────────
 
const MIN_HOURS = 0.5;
const getFM = (k) => dv.current()?.[k];
 
const FILTER_AGENCY = (getFM("Agency Filter") ?? getFM("agency filter") ?? "").toString().trim();
const rawStart = getFM("Start Date") ?? getFM("start date") ?? "";
const rawEnd   = getFM("End Date")   ?? getFM("end date")   ?? "";
 
const SVGNS = "http://www.w3.org/2000/svg";
const monthOrder = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
 
const firstOf = (v) => Array.isArray(v) ? v[0] : v;
 
const toDate = (v) => {
  v = firstOf(v);
  if (!v) return null;
  if (typeof v?.toFormat === "function") return new Date(`${v.toFormat("yyyy-MM-dd")}T00:00:00`);
  if (typeof v?.toISODate === "function") return new Date(`${v.toISODate()}T00:00:00`);
  const match = String(v).match(/^(\d{4})-(\d{2})-(\d{2})/);
  if (match) return new Date(`${match[1]}-${match[2]}-${match[3]}T00:00:00`);
  const d = new Date(v);
  return Number.isNaN(d.getTime()) ? null : d;
};
 
const toNumber = (v) => {
  v = firstOf(v);
  if (v == null || v === "") return 0;
  const n = parseFloat(String(v).trim());
  return Number.isNaN(n) ? 0 : n;
};
 
const fmtHours = (n) => Number(n || 0).toFixed(2);
const fmtISODate = (d) => {
  if (!d) return "—";
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, "0");
  const day = String(d.getDate()).padStart(2, "0");
  return `${y}-${m}-${day}`;
};
const svgEl = (name, attrs = {}) => {
  const el = document.createElementNS(SVGNS, name);
  for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, String(v));
  return el;
};
 
let START_DATE = toDate(rawStart);
let END_DATE = toDate(rawEnd);
if (START_DATE && END_DATE && START_DATE > END_DATE) {
  const tmp = START_DATE;
  START_DATE = END_DATE;
  END_DATE = tmp;
}
 
const startOfMonth = (d) => new Date(d.getFullYear(), d.getMonth(), 1);
const endOfMonth = (d) => new Date(d.getFullYear(), d.getMonth() + 1, 0, 23, 59, 59, 999);
const addMonths = (d, n) => new Date(d.getFullYear(), d.getMonth() + n, 1);
const monthKey = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
const fmtMonthYear = (d) => d.toLocaleString("en-GB", { month: "short", year: "numeric" });
 
const currentPeriodEndMonth = startOfMonth(new Date());
const currentMonths = Array.from({ length: 12 }, (_, i) => addMonths(currentPeriodEndMonth, i - 11));
const periods = [
  { key: "yearBefore", label: "Year before", offsetMonths: -24 },
  { key: "lastYear", label: "Last year", offsetMonths: -12 },
  { key: "thisYear", label: "This year", offsetMonths: 0 }
].map(period => ({
  ...period,
  months: currentMonths.map(d => addMonths(d, period.offsetMonths))
}));
const periodRangeLabel = (period) => `${fmtMonthYear(period.months[0])}–${fmtMonthYear(period.months[period.months.length - 1])}`;
 
// 1) Load + normalise records
const src = dv.pages("#clienthours").where(p => p.clientref && p.date);
const records = Array.from(src).map(p => ({
  agency: (firstOf(p.agency) ?? "Unknown").toString().trim() || "Unknown",
  date: toDate(p.date),
  hours: toNumber(p.hours)
})).filter(r => r.date !== null);
 
// 2) Apply filters
const filtered = records.filter(r => {
  const agencyOk = !FILTER_AGENCY || r.agency === FILTER_AGENCY;
  const startOk = !START_DATE || r.date >= START_DATE;
  const endOk = !END_DATE || r.date <= END_DATE;
  const hoursOk = r.hours > MIN_HOURS;
  return agencyOk && startOk && endOk && hoursOk;
});
 
// 3) Gather agencies
const agencies = Array.from(new Set(filtered.map(r => r.agency))).sort((a, b) => a.localeCompare(b));
if (agencies.length === 0) agencies.push("Unknown");
 
// 4) Aggregate by rolling comparison period, month, agency
const makeAgencyTotals = () => Object.fromEntries(agencies.map(a => [a, 0]));
const periodMonthAgencyTotals = new Map();
for (const period of periods) {
  periodMonthAgencyTotals.set(period.key, new Map(period.months.map(d => [monthKey(d), makeAgencyTotals()])));
}
 
for (const r of filtered) {
  const mk = monthKey(r.date);
  for (const period of periods) {
    const totals = periodMonthAgencyTotals.get(period.key).get(mk);
    if (!totals) continue;
    totals[r.agency] = (totals[r.agency] ?? 0) + r.hours;
  }
}
 
const monthRows = currentMonths.map((monthStart, monthIndex) => {
  const byPeriod = Object.fromEntries(periods.map(period => {
    const comparisonMonth = period.months[monthIndex];
    const byAgency = periodMonthAgencyTotals.get(period.key).get(monthKey(comparisonMonth));
    const total = agencies.reduce((acc, agency) => acc + (byAgency[agency] ?? 0), 0);
    return [period.key, { byAgency, total, month: comparisonMonth }];
  }));
  return { month: monthStart.toLocaleString("en-GB", { month: "short" }), monthIndex, currentMonth: monthStart, byPeriod };
});
 
const periodAgencyTotals = Object.fromEntries(periods.map(period => [period.key, makeAgencyTotals()]));
for (const row of monthRows) {
  for (const period of periods) {
    for (const agency of agencies) {
      periodAgencyTotals[period.key][agency] += row.byPeriod[period.key].byAgency[agency] ?? 0;
    }
  }
}
const periodTotals = Object.fromEntries(periods.map(period => [
  period.key,
  agencies.reduce((acc, agency) => acc + (periodAgencyTotals[period.key][agency] ?? 0), 0)
]));
 
// 5) Summary and tables
const bits = [];
if (FILTER_AGENCY) bits.push(`Agency: **${FILTER_AGENCY}**`);
if (START_DATE || END_DATE) bits.push(`Outer Window: **${fmtISODate(START_DATE)} → ${fmtISODate(END_DATE)}**`);
bits.push(`Min Session Hours: **>${MIN_HOURS}**`);
bits.push(`Current rolling window: **${fmtMonthYear(currentMonths[0])} → ${fmtMonthYear(currentMonths[currentMonths.length - 1])}**`);
dv.paragraph(`Client hours by month, comparing the current rolling 12 months with the same months in the previous two years (${bits.join(" · ")}):`);
 
dv.header(3, "Monthly client hours comparison");
dv.table(
  ["Month", ...periods.map(period => `${period.label} (${periodRangeLabel(period)})`)],
  monthRows.map(row => [row.month, ...periods.map(period => fmtHours(row.byPeriod[period.key].total))])
);
 
dv.header(3, "Rolling-period totals by agency");
dv.table(
  ["Agency", ...periods.map(period => period.label), "Total"],
  [
    ...agencies.map(agency => [
      agency,
      ...periods.map(period => fmtHours(periodAgencyTotals[period.key][agency])),
      fmtHours(periods.reduce((acc, period) => acc + periodAgencyTotals[period.key][agency], 0))
    ]),
    ["Total", ...periods.map(period => fmtHours(periodTotals[period.key])), fmtHours(periods.reduce((acc, period) => acc + periodTotals[period.key], 0))]
  ]
);
 
// 6) SVG chart (render only in live Obsidian preview context)
if (typeof document !== "undefined" && this?.container?.createEl) {
  const container = this.container.createEl("div", { cls: "client-hours-chart" });
  container.style.width = "100%";
  container.style.maxWidth = "1200px";
 
  const width = 1200;
  const baseHeight = 390;
  const legendRowH = 18;
  const legendTitleH = 16;
  const legendPadding = 10;
  const legendH = (3 + agencies.length) * legendRowH + legendTitleH + legendPadding;
  const height = baseHeight + legendH;
 
  const margin = { top: 30, right: 24, bottom: 58, left: 58 };
  const innerW = width - margin.left - margin.right;
  const innerH = baseHeight - margin.top - margin.bottom;
 
  const svg = svgEl("svg", { viewBox: `0 0 ${width} ${height}`, preserveAspectRatio: "xMidYMid meet" });
  svg.style.width = "100%";
  svg.style.height = "auto";
  container.appendChild(svg);
 
  const defs = svgEl("defs");
  svg.appendChild(defs);
 
  const palette = ["#4C78A8", "#F58518", "#54A24B", "#E45756", "#72B7B2", "#B279A2", "#FF9DA6", "#9D755D", "#BAB0AC"];
  function addHatchPattern(id, stroke, angleDeg, spacing, strokeW) {
    const p = svgEl("pattern", { id, patternUnits: "userSpaceOnUse", width: spacing, height: spacing, patternTransform: `rotate(${angleDeg})` });
    p.appendChild(svgEl("line", { x1: 0, y1: 0, x2: 0, y2: spacing, stroke, "stroke-width": strokeW, "stroke-linecap": "square", "stroke-opacity": "0.9" }));
    defs.appendChild(p);
  }
 
  const agencyStyle = new Map();
  agencies.forEach((agency, idx) => {
    const colour = palette[idx % palette.length];
    const patId = `agency-hatch-${idx}`;
    addHatchPattern(patId, colour, (idx % 4) * 45, 6 + (idx % 3) * 2, 1 + ((idx % 2) * 0.4));
    agencyStyle.set(agency, { fill: colour, fillOpacity: 0.35, patternUrl: `url(#${patId})` });
  });
 
  const periodOutline = new Map([
    [periods[0].key, { strokeDasharray: "2 2", strokeOpacity: 0.35 }],
    [periods[1].key, { strokeDasharray: "5 3", strokeOpacity: 0.50 }],
    [periods[2].key, { strokeDasharray: "", strokeOpacity: 0.28 }]
  ]);
 
  const maxY = Math.max(1, ...monthRows.flatMap(row => periods.map(period => row.byPeriod[period.key].total)));
  const xStep = innerW / monthRows.length;
  const groupW = xStep * 0.82;
  const barGap = 5;
  const barW = Math.max(6, (groupW - (barGap * 2)) / 3);
  const groupX = i => margin.left + i * xStep + (xStep - groupW) / 2;
  const yScale = v => margin.top + innerH - (v / maxY) * innerH;
 
  const axisG = svg.appendChild(svgEl("g"));
  axisG.appendChild(svgEl("line", { x1: margin.left, y1: margin.top, x2: margin.left, y2: margin.top + innerH, stroke: "currentColor" }));
  axisG.appendChild(svgEl("line", { x1: margin.left, y1: margin.top + innerH, x2: margin.left + innerW, y2: margin.top + innerH, stroke: "currentColor" }));
 
  const yTicks = Math.min(6, Math.max(1, Math.ceil(maxY)));
  for (let t = 0; t <= yTicks; t++) {
    const val = (t / yTicks) * maxY;
    const yy = yScale(val);
    axisG.appendChild(svgEl("line", { x1: margin.left, y1: yy, x2: margin.left + innerW, y2: yy, stroke: "currentColor", "stroke-opacity": "0.15" }));
    const lbl = svgEl("text", { x: margin.left - 8, y: yy + 4, "text-anchor": "end", fill: "currentColor" });
    lbl.style.fontSize = "12px";
    lbl.textContent = fmtHours(val);
    axisG.appendChild(lbl);
  }
 
  const barsG = svg.appendChild(svgEl("g"));
  monthRows.forEach((row, i) => {
    const gx = groupX(i);
    periods.forEach((period, periodIdx) => {
      const x = gx + periodIdx * (barW + barGap);
      const byAgency = row.byPeriod[period.key].byAgency;
      const total = row.byPeriod[period.key].total;
      let cumulative = 0;
 
      agencies.forEach(agency => {
        const value = byAgency[agency] ?? 0;
        if (!value) return;
        const y0 = yScale(cumulative);
        const y1 = yScale(cumulative + value);
        const h = Math.max(1, y0 - y1);
        const st = agencyStyle.get(agency);
        barsG.appendChild(svgEl("rect", { x, y: y1, width: barW, height: h, fill: st.fill, "fill-opacity": String(st.fillOpacity), stroke: "none" }));
        barsG.appendChild(svgEl("rect", { x, y: y1, width: barW, height: h, fill: st.patternUrl, "fill-opacity": "1", stroke: "currentColor", "stroke-opacity": "0.10" }));
        cumulative += value;
      });
 
      if (total > 0) {
        const yy = yScale(total);
        const h = Math.max(1, margin.top + innerH - yy);
        const yo = periodOutline.get(period.key);
        barsG.appendChild(svgEl("rect", { x, y: yy, width: barW, height: h, fill: "none", stroke: "currentColor", "stroke-opacity": String(yo.strokeOpacity), "stroke-dasharray": yo.strokeDasharray }));
      } else {
        barsG.appendChild(svgEl("rect", { x, y: margin.top + innerH - 1, width: barW, height: 1, fill: "currentColor", "fill-opacity": "0.15" }));
      }
    });
 
    const xl = svgEl("text", { x: margin.left + i * xStep + xStep / 2, y: margin.top + innerH + 18, "text-anchor": "middle", fill: "currentColor" });
    xl.style.fontSize = "12px";
    xl.textContent = row.month;
    barsG.appendChild(xl);
  });
 
  const title = svgEl("text", { x: margin.left, y: 18, fill: "currentColor" });
  title.style.fontSize = "14px";
  title.style.fontWeight = "600";
  title.textContent = `Client Hours by Month (${periods.map(period => periodRangeLabel(period)).join(" / ")}; stacked by agency)`;
  svg.appendChild(title);
 
  const legendG = svg.appendChild(svgEl("g"));
  const legendX = margin.left;
  const legendTop = baseHeight + 14;
  const boxW = 14;
  const boxH = 10;
 
  const legendTitle = svgEl("text", { x: legendX, y: legendTop, fill: "currentColor" });
  legendTitle.style.fontSize = "12px";
  legendTitle.style.fontWeight = "600";
  legendTitle.textContent = "Key";
  legendG.appendChild(legendTitle);
 
  periods.forEach((period, idx) => {
    const yRow = legendTop + legendTitleH + idx * legendRowH;
    const yo = periodOutline.get(period.key);
    legendG.appendChild(svgEl("rect", { x: legendX, y: yRow - boxH + 2, width: boxW, height: boxH, fill: "none", stroke: "currentColor", "stroke-opacity": String(yo.strokeOpacity), "stroke-dasharray": yo.strokeDasharray }));
    const t = svgEl("text", { x: legendX + boxW + 8, y: yRow + 2, fill: "currentColor" });
    t.style.fontSize = "12px";
    t.textContent = `${period.label}: ${periodRangeLabel(period)}`;
    legendG.appendChild(t);
  });
 
  agencies.forEach((agency, idx) => {
    const st = agencyStyle.get(agency);
    const yRow = legendTop + legendTitleH + (3 + idx) * legendRowH;
    legendG.appendChild(svgEl("rect", { x: legendX, y: yRow - boxH + 2, width: boxW, height: boxH, fill: st.fill, "fill-opacity": String(st.fillOpacity), stroke: "none" }));
    legendG.appendChild(svgEl("rect", { x: legendX, y: yRow - boxH + 2, width: boxW, height: boxH, fill: st.patternUrl, stroke: "currentColor", "stroke-opacity": "0.35" }));
    const t = svgEl("text", { x: legendX + boxW + 8, y: yRow + 2, fill: "currentColor" });
    t.style.fontSize = "12px";
    t.textContent = agency;
    legendG.appendChild(t);
  });
} else {
  dv.paragraph("_Chart omitted in markdown export context._");
}
// Exportable Markdown/SVG version for Quartz portfolio snapshots.
// This duplicates the live report calculations, but returns a Markdown string.
const MIN_HOURS = 0.5;
// Export blocks run through the Templater exporter, where dv.current() may not
// exist. Keep export settings explicit rather than depending on dv.current().
const FILTER_AGENCY = "";
const rawStart = "2022-01-01";
const rawEnd = "";
 
const firstOf = (v) => Array.isArray(v) ? v[0] : v;
const toDate = (v) => {
  v = firstOf(v);
  if (!v) return null;
  if (typeof v?.toFormat === "function") return new Date(`${v.toFormat("yyyy-MM-dd")}T00:00:00`);
  if (typeof v?.toISODate === "function") return new Date(`${v.toISODate()}T00:00:00`);
  const match = String(v).match(/^(\d{4})-(\d{2})-(\d{2})/);
  if (match) return new Date(`${match[1]}-${match[2]}-${match[3]}T00:00:00`);
  const d = new Date(v);
  return Number.isNaN(d.getTime()) ? null : d;
};
const toNumber = (v) => {
  v = firstOf(v);
  if (v == null || v === "") return 0;
  const n = parseFloat(String(v).trim());
  return Number.isNaN(n) ? 0 : n;
};
const fmtHours = (n) => Number(n || 0).toFixed(2);
const fmtISODate = (d) => {
  if (!d) return "—";
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, "0");
  const day = String(d.getDate()).padStart(2, "0");
  return `${y}-${m}-${day}`;
};
const esc = (s) => String(s ?? "")
  .replace(/&/g, "&amp;")
  .replace(/</g, "&lt;")
  .replace(/>/g, "&gt;")
  .replace(/"/g, "&quot;");
const cell = (s) => String(s ?? "").replace(/\|/g, "\\|").replace(/\n/g, " ").trim();
 
let START_DATE = toDate(rawStart);
let END_DATE = toDate(rawEnd);
if (START_DATE && END_DATE && START_DATE > END_DATE) {
  const tmp = START_DATE;
  START_DATE = END_DATE;
  END_DATE = tmp;
}
 
const startOfMonth = (d) => new Date(d.getFullYear(), d.getMonth(), 1);
const endOfMonth = (d) => new Date(d.getFullYear(), d.getMonth() + 1, 0, 23, 59, 59, 999);
const addMonths = (d, n) => new Date(d.getFullYear(), d.getMonth() + n, 1);
const monthKey = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
const fmtMonthYear = (d) => d.toLocaleString("en-GB", { month: "short", year: "numeric" });
 
const currentPeriodEndMonth = startOfMonth(new Date());
const currentMonths = Array.from({ length: 12 }, (_, i) => addMonths(currentPeriodEndMonth, i - 11));
const periods = [
  { key: "yearBefore", label: "Year before", offsetMonths: -24 },
  { key: "lastYear", label: "Last year", offsetMonths: -12 },
  { key: "thisYear", label: "This year", offsetMonths: 0 }
].map(period => ({
  ...period,
  months: currentMonths.map(d => addMonths(d, period.offsetMonths))
}));
const periodRangeLabel = (period) => `${fmtMonthYear(period.months[0])}–${fmtMonthYear(period.months[period.months.length - 1])}`;
 
const records = Array.from(dv.pages("#clienthours").where(p => p.clientref && p.date)).map(p => ({
  agency: (firstOf(p.agency) ?? "Unknown").toString().trim() || "Unknown",
  date: toDate(p.date),
  hours: toNumber(p.hours)
})).filter(r => r.date !== null);
 
const filtered = records.filter(r => {
  const agencyOk = !FILTER_AGENCY || r.agency === FILTER_AGENCY;
  const startOk = !START_DATE || r.date >= START_DATE;
  const endOk = !END_DATE || r.date <= END_DATE;
  const hoursOk = r.hours > MIN_HOURS;
  return agencyOk && startOk && endOk && hoursOk;
});
 
const agencies = Array.from(new Set(filtered.map(r => r.agency))).sort((a, b) => a.localeCompare(b));
if (agencies.length === 0) agencies.push("Unknown");
 
const makeAgencyTotals = () => Object.fromEntries(agencies.map(a => [a, 0]));
const periodMonthAgencyTotals = new Map();
for (const period of periods) {
  periodMonthAgencyTotals.set(period.key, new Map(period.months.map(d => [monthKey(d), makeAgencyTotals()])));
}
for (const r of filtered) {
  const mk = monthKey(r.date);
  for (const period of periods) {
    const totals = periodMonthAgencyTotals.get(period.key).get(mk);
    if (!totals) continue;
    totals[r.agency] = (totals[r.agency] ?? 0) + r.hours;
  }
}
 
const monthRows = currentMonths.map((monthStart, monthIndex) => {
  const byPeriod = Object.fromEntries(periods.map(period => {
    const comparisonMonth = period.months[monthIndex];
    const byAgency = periodMonthAgencyTotals.get(period.key).get(monthKey(comparisonMonth));
    const total = agencies.reduce((acc, agency) => acc + (byAgency[agency] ?? 0), 0);
    return [period.key, { byAgency, total, month: comparisonMonth }];
  }));
  return { month: monthStart.toLocaleString("en-GB", { month: "short" }), monthIndex, currentMonth: monthStart, byPeriod };
});
 
const periodAgencyTotals = Object.fromEntries(periods.map(period => [period.key, makeAgencyTotals()]));
for (const row of monthRows) {
  for (const period of periods) {
    for (const agency of agencies) periodAgencyTotals[period.key][agency] += row.byPeriod[period.key].byAgency[agency] ?? 0;
  }
}
const periodTotals = Object.fromEntries(periods.map(period => [period.key, agencies.reduce((acc, agency) => acc + (periodAgencyTotals[period.key][agency] ?? 0), 0)]));
 
const lines = [];
const bits = [];
if (FILTER_AGENCY) bits.push(`Agency: **${FILTER_AGENCY}**`);
if (START_DATE || END_DATE) bits.push(`Outer Window: **${fmtISODate(START_DATE)} → ${fmtISODate(END_DATE)}**`);
bits.push(`Min Session Hours: **>${MIN_HOURS}**`);
bits.push(`Current rolling window: **${fmtMonthYear(currentMonths[0])} → ${fmtMonthYear(currentMonths[currentMonths.length - 1])}**`);
lines.push(`Client hours by month, comparing the current rolling 12 months with the same months in the previous two years (${bits.join(" · ")}):`, "");
 
lines.push("### Monthly client hours comparison", "");
lines.push(`| Month | ${periods.map(period => `${period.label} (${periodRangeLabel(period)})`).join(" | ")} |`);
lines.push(`| --- | ${periods.map(() => "---:").join(" | ")} |`);
for (const row of monthRows) lines.push(`| ${row.month} | ${periods.map(period => fmtHours(row.byPeriod[period.key].total)).join(" | ")} |`);
lines.push("");
 
lines.push("### Rolling-period totals by agency", "");
lines.push(`| Agency | ${periods.map(period => period.label).join(" | ")} | Total |`);
lines.push(`| --- | ${periods.map(() => "---:").join(" | ")} | ---: |`);
for (const agency of agencies) {
  const total = periods.reduce((acc, period) => acc + periodAgencyTotals[period.key][agency], 0);
  lines.push(`| ${cell(agency)} | ${periods.map(period => fmtHours(periodAgencyTotals[period.key][agency])).join(" | ")} | ${fmtHours(total)} |`);
}
lines.push(`| **Total** | ${periods.map(period => `**${fmtHours(periodTotals[period.key])}**`).join(" | ")} | **${fmtHours(periods.reduce((acc, period) => acc + periodTotals[period.key], 0))}** |`, "");
 
// SVG chart as text
const width = 1200;
const baseHeight = 390;
const legendRowH = 18;
const legendTitleH = 16;
const legendPadding = 10;
const legendH = (3 + agencies.length) * legendRowH + legendTitleH + legendPadding;
const height = baseHeight + legendH;
const margin = { top: 30, right: 24, bottom: 58, left: 58 };
const innerW = width - margin.left - margin.right;
const innerH = baseHeight - margin.top - margin.bottom;
const palette = ["#4C78A8", "#F58518", "#54A24B", "#E45756", "#72B7B2", "#B279A2", "#FF9DA6", "#9D755D", "#BAB0AC"];
const agencyStyle = new Map(agencies.map((agency, idx) => {
  const colour = palette[idx % palette.length];
  return [agency, { fill: colour, fillOpacity: 0.35, patId: `export-agency-hatch-${idx}`, angle: (idx % 4) * 45, spacing: 6 + (idx % 3) * 2, strokeW: 1 + ((idx % 2) * 0.4) }];
}));
const periodOutline = new Map([
  [periods[0].key, { strokeDasharray: "2 2", strokeOpacity: 0.35 }],
  [periods[1].key, { strokeDasharray: "5 3", strokeOpacity: 0.50 }],
  [periods[2].key, { strokeDasharray: "", strokeOpacity: 0.28 }]
]);
const maxY = Math.max(1, ...monthRows.flatMap(row => periods.map(period => row.byPeriod[period.key].total)));
const xStep = innerW / monthRows.length;
const groupW = xStep * 0.82;
const barGap = 5;
const barW = Math.max(6, (groupW - (barGap * 2)) / 3);
const groupX = i => margin.left + i * xStep + (xStep - groupW) / 2;
const yScale = v => margin.top + innerH - (v / maxY) * innerH;
 
const svg = [];
svg.push(`<div class="portfolio-chart client-hours-chart">`);
svg.push(`<svg viewBox="0 0 ${width} ${height}" role="img" aria-label="Client hours by rolling comparison period" xmlns="http://www.w3.org/2000/svg">`);
svg.push(`<defs>`);
for (const [agency, st] of agencyStyle.entries()) {
  svg.push(`<pattern id="${st.patId}" patternUnits="userSpaceOnUse" width="${st.spacing}" height="${st.spacing}" patternTransform="rotate(${st.angle})"><line x1="0" y1="0" x2="0" y2="${st.spacing}" stroke="${st.fill}" stroke-width="${st.strokeW}" stroke-linecap="square" stroke-opacity="0.9" /></pattern>`);
}
svg.push(`</defs>`);
svg.push(`<text x="${margin.left}" y="18" fill="currentColor" font-size="14" font-weight="600">Client Hours by Month (${periods.map(period => periodRangeLabel(period)).join(" / ")}; stacked by agency)</text>`);
svg.push(`<line x1="${margin.left}" y1="${margin.top}" x2="${margin.left}" y2="${margin.top + innerH}" stroke="currentColor" />`);
svg.push(`<line x1="${margin.left}" y1="${margin.top + innerH}" x2="${margin.left + innerW}" y2="${margin.top + innerH}" stroke="currentColor" />`);
const yTicks = Math.min(6, Math.max(1, Math.ceil(maxY)));
for (let t = 0; t <= yTicks; t++) {
  const val = (t / yTicks) * maxY;
  const yy = yScale(val);
  svg.push(`<line x1="${margin.left}" y1="${yy.toFixed(1)}" x2="${margin.left + innerW}" y2="${yy.toFixed(1)}" stroke="currentColor" stroke-opacity="0.15" />`);
  svg.push(`<text x="${margin.left - 8}" y="${(yy + 4).toFixed(1)}" text-anchor="end" fill="currentColor" font-size="12">${fmtHours(val)}</text>`);
}
for (const [i, row] of monthRows.entries()) {
  const gx = groupX(i);
  for (const [periodIdx, period] of periods.entries()) {
    const x = gx + periodIdx * (barW + barGap);
    const byAgency = row.byPeriod[period.key].byAgency;
    const total = row.byPeriod[period.key].total;
    let cumulative = 0;
    for (const agency of agencies) {
      const value = byAgency[agency] ?? 0;
      if (!value) continue;
      const y0 = yScale(cumulative);
      const y1 = yScale(cumulative + value);
      const h = Math.max(1, y0 - y1);
      const st = agencyStyle.get(agency);
      svg.push(`<rect x="${x.toFixed(1)}" y="${y1.toFixed(1)}" width="${barW.toFixed(1)}" height="${h.toFixed(1)}" fill="${st.fill}" fill-opacity="${st.fillOpacity}" />`);
      svg.push(`<rect x="${x.toFixed(1)}" y="${y1.toFixed(1)}" width="${barW.toFixed(1)}" height="${h.toFixed(1)}" fill="url(#${st.patId})" stroke="currentColor" stroke-opacity="0.10" />`);
      cumulative += value;
    }
    if (total > 0) {
      const yy = yScale(total);
      const h = Math.max(1, margin.top + innerH - yy);
      const yo = periodOutline.get(period.key);
      svg.push(`<rect x="${x.toFixed(1)}" y="${yy.toFixed(1)}" width="${barW.toFixed(1)}" height="${h.toFixed(1)}" fill="none" stroke="currentColor" stroke-opacity="${yo.strokeOpacity}" stroke-dasharray="${yo.strokeDasharray}" />`);
    } else {
      svg.push(`<rect x="${x.toFixed(1)}" y="${margin.top + innerH - 1}" width="${barW.toFixed(1)}" height="1" fill="currentColor" fill-opacity="0.15" />`);
    }
  }
  svg.push(`<text x="${(margin.left + i * xStep + xStep / 2).toFixed(1)}" y="${margin.top + innerH + 18}" text-anchor="middle" fill="currentColor" font-size="12">${row.month}</text>`);
}
const legendX = margin.left;
const legendTop = baseHeight + 14;
const boxW = 14;
const boxH = 10;
svg.push(`<text x="${legendX}" y="${legendTop}" fill="currentColor" font-size="12" font-weight="600">Key</text>`);
periods.forEach((period, idx) => {
  const yRow = legendTop + legendTitleH + idx * legendRowH;
  const yo = periodOutline.get(period.key);
  svg.push(`<rect x="${legendX}" y="${yRow - boxH + 2}" width="${boxW}" height="${boxH}" fill="none" stroke="currentColor" stroke-opacity="${yo.strokeOpacity}" stroke-dasharray="${yo.strokeDasharray}" />`);
  svg.push(`<text x="${legendX + boxW + 8}" y="${yRow + 2}" fill="currentColor" font-size="12">${period.label}: ${periodRangeLabel(period)}</text>`);
});
agencies.forEach((agency, idx) => {
  const st = agencyStyle.get(agency);
  const yRow = legendTop + legendTitleH + (3 + idx) * legendRowH;
  svg.push(`<rect x="${legendX}" y="${yRow - boxH + 2}" width="${boxW}" height="${boxH}" fill="${st.fill}" fill-opacity="${st.fillOpacity}" />`);
  svg.push(`<rect x="${legendX}" y="${yRow - boxH + 2}" width="${boxW}" height="${boxH}" fill="url(#${st.patId})" stroke="currentColor" stroke-opacity="0.35" />`);
  svg.push(`<text x="${legendX + boxW + 8}" y="${yRow + 2}" fill="currentColor" font-size="12">${esc(agency)}</text>`);
});
svg.push(`</svg>`);
svg.push(`</div>`);
 
lines.push("### Client hours graph", "", svg.join("\n"), "");
return lines.join("\n");