from __future__ import annotations

from dataclasses import asdict
from typing import Dict, Optional

from rdflib import BNode, URIRef

from .manifest import _cf_objects, _cf_value, load_report_or_pipeline_manifest


def _local_name(iri: str) -> str:
    for sep in ("#", "/"):
        if sep in iri:
            return iri.rsplit(sep, 1)[-1]
    return iri


def _collect_step_port_meta(graph, step_iris: list[str]) -> Dict[str, Dict[str, Dict[str, str]]]:
  meta: Dict[str, Dict[str, Dict[str, str]]] = {}
  for step_iri in step_iris:
    step_ref = URIRef(step_iri)
    for port in _cf_objects(graph, step_ref, "hasOutput"):
      if not isinstance(port, (URIRef, BNode)):
        continue
      port_key = _cf_value(graph, port, "portKey")
      if not isinstance(port_key, URIRef):
        continue
      role = _cf_value(graph, port, "quantityRole")
      uncertainty_for = _cf_value(graph, port, "uncertaintyFor")
      role_name = _local_name(str(role)) if isinstance(role, URIRef) else ""
      if role_name == "ValueRole":
        role_name = "value"
      elif role_name == "UncertaintyRole":
        role_name = "uncertainty"
      entry: Dict[str, str] = {}
      if role_name:
        entry["role"] = role_name
      if isinstance(uncertainty_for, URIRef):
        entry["uncertaintyFor"] = str(uncertainty_for)
      if entry:
        meta.setdefault(step_iri, {})[str(port_key)] = entry
  return meta


def _build_uncertainty_label_map(pipeline, port_meta: Dict[str, Dict[str, Dict[str, str]]]) -> Dict[str, str]:
    node_defs = {node.node_uri: node.definition_uri for node in pipeline.nodes}
    labels_by_node: Dict[str, Dict[str, str]] = {}
    for edge in pipeline.edges:
        if edge.to_label:
            labels_by_node.setdefault(edge.from_node_uri, {})[edge.from_port_key_uri] = edge.to_label

    mapping: Dict[str, str] = {}
    for node_uri, label_map in labels_by_node.items():
        step_def = node_defs.get(node_uri)
        if not step_def:
            continue
        step_ports = port_meta.get(step_def, {})
        for port_key, label in label_map.items():
            meta = step_ports.get(port_key)
            if not meta or meta.get("role") != "uncertainty":
                continue
            target_key = meta.get("uncertaintyFor")
            if target_key and target_key in label_map:
                mapping[label] = label_map[target_key]
    return mapping


def build_report_payload(manifest_path: str, *, report_uri: Optional[str] = None) -> dict:
    graph, report, pipeline = load_report_or_pipeline_manifest(manifest_path, report_uri=report_uri)
    step_iris = [node.definition_uri for node in pipeline.nodes]
    port_meta = _collect_step_port_meta(graph, step_iris)
    uncertainty_map = _build_uncertainty_label_map(pipeline, port_meta)
    return {
        "report": asdict(report),
        "pipeline": asdict(pipeline),
        "uncertainty_map": uncertainty_map,
    }


def render_report_html(
    *,
    data_url: str = "report.data.json",
    duckdb_query_url: Optional[str] = None,
    pipeline_id: Optional[str] = None,
) -> str:
    duckdb_url_literal = duckdb_query_url or ""
    pipeline_label = pipeline_id or "Pipeline"
    return f"""<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>{pipeline_label} Report</title>
  <style>
    :root {{
      --bg: #0e1016;
      --panel: #171a22;
      --text: #e7e9ef;
      --muted: #9aa1b3;
      --accent: #4bc5b8;
      --table-row: #101623;
    }}
    body {{
      margin: 0;
      font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
      background: radial-gradient(1200px 800px at 80% -20%, #233044, transparent), var(--bg);
      color: var(--text);
    }}
    header {{
      padding: 24px 28px;
      border-bottom: 1px solid #242837;
    }}
    h1 {{
      margin: 0 0 6px;
      font-size: 24px;
      letter-spacing: 0.3px;
    }}
    .meta {{
      color: var(--muted);
      font-size: 14px;
    }}
      main {{
        padding: 20px 28px 32px;
        display: grid;
        grid-template-columns: repeat(12, minmax(80px, 1fr));
        gap: 16px;
      }}
    section {{
      background: var(--panel);
      border: 1px solid #242837;
      border-radius: 12px;
      padding: 16px;
      min-width: 0;
    }}
    .card-head {{
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 8px;
      cursor: pointer;
      user-select: none;
    }}
    .card-head h2 {{
      margin: 0 0 10px;
    }}
    .card-body {{
      margin-top: 6px;
    }}
    section.collapsed .card-body {{
      display: none;
    }}
      section.wide {{
        grid-column: 1 / -1;
      }}
      section.full {{
        grid-column: span 12;
      }}
      section.half {{
        grid-column: span 6;
      }}
      section.third {{
        grid-column: span 4;
        min-width: 300px;
      }}
    h2 {{
      margin: 0 0 10px;
      font-size: 16px;
      color: var(--accent);
      letter-spacing: 0.2px;
    }}
    pre {{
      white-space: pre-wrap;
      background: #0f1218;
      padding: 12px;
      border-radius: 8px;
      border: 1px solid #262b3a;
      color: #cbd5f0;
    }}
    .graph {{
      min-height: 220px;
      overflow-x: auto;
    }}
    .graph svg {{
      display: block;
    }}
    .list {{
      display: flex;
      flex-direction: column;
      gap: 8px;
      font-size: 14px;
    }}
    .list-item {{
      padding: 8px 10px;
      border-radius: 8px;
      background: #10131a;
      border: 1px solid #232a39;
    }}
    .muted {{
      color: var(--muted);
    }}
    table {{
      width: 100%;
      border-collapse: collapse;
      font-size: 13px;
    }}
    th, td {{
      text-align: left;
      padding: 6px 8px;
      border-bottom: 1px solid #252c3b;
      vertical-align: top;
    }}
    th {{
      color: var(--accent);
      font-weight: 600;
    }}
    tbody tr:nth-child(even) {{
      background: var(--table-row);
    }}
    .table-wrap {{
      overflow-x: auto;
    }}
    .plot {{
      min-height: 220px;
    }}
    .notice {{
      font-size: 13px;
      color: var(--muted);
    }}
    .controls {{
      display: flex;
      align-items: center;
      gap: 8px;
      font-size: 13px;
      color: var(--muted);
      margin-bottom: 8px;
    }}
    .controls select {{
      background: #0f1218;
      color: var(--text);
      border: 1px solid #262b3a;
      border-radius: 6px;
      padding: 4px 6px;
    }}
    .report-tiles {{
      grid-column: 1 / -1;
      display: grid;
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 16px;
    }}
    section.report-tile {{
      border-style: dashed;
      border-color: #2d364b;
    }}
    .report-tiles > section {{
      grid-column: auto;
    }}
  </style>
</head>
<body>
  <header>
    <h1>{pipeline_label} Report</h1>
    <div class="meta">Observable Runtime Report (live DuckDB)</div>
    <div id="cf-error" class="notice"></div>
  </header>
  <main id="cf-main">
      <section class="full">
        <div class="card-head"><h2>Pipeline Graph</h2></div>
        <div class="card-body">
          <div id="cf-graph" class="graph"></div>
        </div>
      </section>
      <div id="cf-report-tiles" class="report-tiles"></div>
      <section class="third">
        <div class="card-head"><h2>Nodes</h2></div>
        <div class="card-body">
          <div id="cf-nodes" class="list"></div>
        </div>
      </section>
      <section class="third">
        <div class="card-head"><h2>Connections</h2></div>
        <div class="card-body">
          <div id="cf-edges" class="list"></div>
        </div>
      </section>
      <section class="third">
        <div class="card-head"><h2>Triggers</h2></div>
        <div class="card-body">
          <div id="cf-triggers" class="list"></div>
        </div>
      </section>
      <section class="third">
      <div class="card-head"><h2>Data Payload</h2></div>
      <div class="card-body">
        <pre id="cf-data"></pre>
      </div>
    </section>
  </main>
  <script type="module">
    import {{ Runtime, Inspector }} from "https://cdn.jsdelivr.net/npm/@observablehq/runtime@5/dist/runtime.js";

    const dataUrl = "{data_url}";
    const duckdbQueryUrl = "{duckdb_url_literal}";
    const pipelineId = "{pipeline_label}";
    const errorRoot = document.querySelector("#cf-error");

    function showError(message) {{
      if (!errorRoot) return;
      errorRoot.textContent = message;
    }}

    window.addEventListener("error", (event) => {{
      if (event && event.message) {{
        showError(event.message);
      }}
    }});
    window.addEventListener("unhandledrejection", (event) => {{
      if (event && event.reason) {{
        showError(String(event.reason));
      }}
    }});

    function define(runtime, observer) {{
      const main = runtime.module();

      main.variable(observer("dataUrl")).define("dataUrl", () => dataUrl);
      main.variable(observer("data")).define("data", async () => {{
        try {{
          const res = await fetch(dataUrl, {{ cache: "no-store" }});
          if (!res.ok) {{
            throw new Error("Failed to load report data (" + res.status + ")");
          }}
          return res.json();
        }} catch (err) {{
          showError(err && err.message ? err.message : String(err));
          return {{ report: {{ observables: [] }}, pipeline: {{ nodes: [], edges: [], triggers: [] }} }};
        }}
      }});
      main.variable(observer("pipeline")).define("pipeline", ["data"], (data) => data.pipeline);
      main.variable(observer("report")).define("report", ["data"], (data) => data.report);
      main.variable(observer("nodes")).define("nodes", ["pipeline"], (pipeline) => pipeline.nodes || []);
      main.variable(observer("edges")).define("edges", ["pipeline"], (pipeline) => pipeline.edges || []);
      main.variable(observer("triggers")).define("triggers", ["pipeline"], (pipeline) => pipeline.triggers || []);
      main.variable(observer("observables")).define("observables", ["report"], (report) => report.observables || []);
      main.variable(observer("reportNodes")).define("reportNodes", ["nodes"], (nodes) =>
        (nodes || []).filter((node) => /Report.*Step$/.test(String(node.definition_uri || "")))
      );
      main.variable(observer("uncertaintyMap")).define("uncertaintyMap", ["data"], (data) => data.uncertainty_map || {{}});

      main.variable(observer("renderList")).define("renderList", () => (items, renderItem) => {{
        const root = document.createElement("div");
        root.className = "list";
        if (!items.length) {{
          const empty = document.createElement("div");
          empty.className = "list-item muted";
          empty.textContent = "None";
          root.appendChild(empty);
          return root;
        }}
        for (const item of items) {{
          const el = document.createElement("div");
          el.className = "list-item";
          el.appendChild(renderItem(item));
          root.appendChild(el);
        }}
        return root;
      }});

      main.variable(observer("graph")).define("graph", ["nodes", "edges", "triggers"], (nodes, edges, triggers) => {{
        const idSuffix = (value) => String(value || "").split(/[#/]/).pop();
        const isOpcua = (value) => /opcua/i.test(String(value || ""));

        const pipelineNodes = nodes.map((node) => ({{
          ...node,
          label: idSuffix(node.definition_uri),
          kind: "step",
        }}));

        const triggerNodes = (triggers || []).map((trigger, idx) => {{
          const eventLabel = trigger.event_type || "trigger";
          const label = isOpcua(eventLabel) ? "OPCUA Server" : "Trigger: " + eventLabel;
          return {{
            node_uri: "trigger:" + idx,
            position: -100 + idx,
            definition_uri: trigger.trigger_uri,
            label,
            kind: "trigger",
          }};
        }});

        const allNodes = [...triggerNodes, ...pipelineNodes];
        if (!allNodes.length) {{
          const empty = document.createElement("div");
          empty.className = "notice";
          empty.textContent = "No pipeline nodes.";
          return empty;
        }}

        const minPos = Math.min(...pipelineNodes.map((n) => n.position));
        const firstNodes = pipelineNodes.filter((n) => n.position === minPos);
        const triggerEdges = [];
        for (const trig of triggerNodes) {{
          for (const first of firstNodes) {{
            triggerEdges.push({{
              from_node_uri: trig.node_uri,
              to_node_uri: first.node_uri,
              from_port_key_uri: trig.label,
              to_port_key_uri: "input",
              kind: "trigger",
            }});
          }}
        }}

        const graphEdges = [...triggerEdges, ...edges];
        const gapX = 220;
        const startX = 40;
        const baseY = 50;
        const width = Math.max(520, startX * 2 + gapX * (allNodes.length - 1) + 200);
        const height = 220;

        const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        svg.setAttribute("viewBox", "0 0 " + width + " " + height);
        svg.style.width = width + "px";
        svg.style.height = "100%";

        const nodePositions = new Map();
        allNodes.forEach((node, idx) => {{
          const x = startX + idx * gapX;
          const y = baseY;
          nodePositions.set(node.node_uri, {{ x, y }});
        }});

        const portMap = new Map();
        for (const edge of edges) {{
          const from = idSuffix(edge.from_port_key_uri);
          const to = idSuffix(edge.to_port_key_uri);
          const fromPorts = portMap.get(edge.from_node_uri) || {{ in: new Set(), out: new Set() }};
          fromPorts.out.add(from);
          portMap.set(edge.from_node_uri, fromPorts);
          const toPorts = portMap.get(edge.to_node_uri) || {{ in: new Set(), out: new Set() }};
          toPorts.in.add(to);
          portMap.set(edge.to_node_uri, toPorts);
        }}

        for (const edge of graphEdges) {{
          const from = nodePositions.get(edge.from_node_uri);
          const to = nodePositions.get(edge.to_node_uri);
          if (!from || !to) continue;
          const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
          line.setAttribute("x1", from.x + 160);
          line.setAttribute("y1", from.y + 12);
          line.setAttribute("x2", to.x);
          line.setAttribute("y2", to.y + 12);
          line.setAttribute("stroke", edge.kind === "trigger" ? "#7aa6ff" : "#4bc5b8");
          line.setAttribute("stroke-width", "2");
          svg.appendChild(line);

          if (edge.from_port_key_uri || edge.to_port_key_uri) {{
            const label = idSuffix(edge.from_port_key_uri) + " -> " + idSuffix(edge.to_port_key_uri);
            const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
            const midX = (from.x + 160 + to.x) / 2;
            text.setAttribute("x", midX);
            text.setAttribute("y", from.y - 6);
            text.setAttribute("fill", "#9aa1b3");
            text.setAttribute("font-size", "10");
            text.setAttribute("text-anchor", "middle");
            text.textContent = label;
            svg.appendChild(text);
          }}
        }}

        for (const node of allNodes) {{
          const pos = nodePositions.get(node.node_uri);
          if (!pos) continue;
          const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
          const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
          rect.setAttribute("x", pos.x);
          rect.setAttribute("y", pos.y);
          rect.setAttribute("width", "160");
          rect.setAttribute("height", "64");
          rect.setAttribute("rx", "10");
          rect.setAttribute("fill", node.kind === "trigger" ? "#162035" : "#141a24");
          rect.setAttribute("stroke", node.kind === "trigger" ? "#3f5c9a" : "#2b3346");
          group.appendChild(rect);

          const title = document.createElementNS("http://www.w3.org/2000/svg", "text");
          title.setAttribute("x", pos.x + 10);
          title.setAttribute("y", pos.y + 18);
          title.setAttribute("fill", "#e7e9ef");
          title.setAttribute("font-size", "12");
          title.textContent = node.label;
          group.appendChild(title);

          const ports = portMap.get(node.node_uri);
          if (ports) {{
            const inText = Array.from(ports.in).join(", ");
            const outText = Array.from(ports.out).join(", ");
            const detail = document.createElementNS("http://www.w3.org/2000/svg", "text");
            detail.setAttribute("x", pos.x + 10);
            detail.setAttribute("y", pos.y + 34);
            detail.setAttribute("fill", "#9aa1b3");
            detail.setAttribute("font-size", "10");
            detail.textContent = "in: " + (inText || "-") + " | out: " + (outText || "-");
            group.appendChild(detail);
          }}

          const params = node.parameters || {{}};
          const paramPairs = Object.entries(params)
            .map(([key, val]) => String(key) + "=" + String(val))
            .join(", ");
          if (paramPairs) {{
            const paramText = document.createElementNS("http://www.w3.org/2000/svg", "text");
            paramText.setAttribute("x", pos.x + 10);
            paramText.setAttribute("y", pos.y + 50);
            paramText.setAttribute("fill", "#7f8697");
            paramText.setAttribute("font-size", "10");
            paramText.textContent = paramPairs;
            group.appendChild(paramText);
          }}

          svg.appendChild(group);
        }}

        return svg;
      }});

      main.variable(observer("nodesView")).define("nodesView", ["nodes", "renderList"], (nodes, renderList) =>
        renderList(nodes, (node) => {{
          const container = document.createElement("div");
          const title = document.createElement("div");
          title.textContent = node.definition_uri;
          const meta = document.createElement("div");
          meta.className = "muted";
          meta.textContent = node.node_uri;
          container.appendChild(title);
          container.appendChild(meta);
          return container;
        }})
      );

      main.variable(observer("edgesView")).define("edgesView", ["edges", "renderList"], (edges, renderList) =>
        renderList(edges, (edge) => {{
          const container = document.createElement("div");
          container.textContent = edge.from_node_uri + " -> " + edge.to_node_uri;
          const meta = document.createElement("div");
          meta.className = "muted";
          meta.textContent = edge.from_port_key_uri + " -> " + edge.to_port_key_uri;
          container.appendChild(meta);
          return container;
        }})
      );

      main.variable(observer("triggersView")).define("triggersView", ["triggers", "renderList"], (triggers, renderList) =>
        renderList(triggers, (trigger) => {{
          const container = document.createElement("div");
          container.textContent = trigger.trigger_uri;
          const meta = document.createElement("div");
          meta.className = "muted";
          meta.textContent = trigger.event_type || "unknown";
          container.appendChild(meta);
          return container;
        }})
      );

      main.variable(observer("payloadView")).define("payloadView", ["data"], (data) => {{
        const pre = document.createElement("pre");
        pre.textContent = JSON.stringify(data, null, 2);
        return pre;
      }});

      main.variable(observer("reportTilesView")).define("reportTilesView", ["reportNodes", "uncertaintyMap"], async (reportNodes, uncertaintyMap) => {{
        const root = document.createElement("div");
        if (!reportNodes.length) {{
          return root;
        }}
        const limit = 120;
        for (const node of reportNodes) {{
          const section = document.createElement("section");
          section.className = "half report-tile";
          const head = document.createElement("div");
          head.className = "card-head";
          const title = document.createElement("h2");
          const params = node.parameters || {{}};
          const label = params.title || params.name || node.definition_uri;
          title.textContent = String(label || "Report Step");
          head.appendChild(title);
          const body = document.createElement("div");
          body.className = "card-body";
          const meta = document.createElement("div");
          meta.className = "notice";
          meta.textContent = node.definition_uri;
          body.appendChild(meta);
          const isChart = /ReportChartStep$/.test(String(node.definition_uri || ""));
          if (isChart) {{
            const spec = normalizeChartSpec(params.chartSpec);
            if (spec) {{
              spec.uncertaintyMap = uncertaintyMap || {{}};
            }}
            const chartQuery = buildChartQuery(spec, limit);
            let chartResult = chartQuery ? await runDuckdbQuery(chartQuery.sql) : {{ error: "Missing chart spec." }};
            let usedFallback = false;
            if (chartResult.error) {{
              chartResult = await runDuckdbQuery("SELECT * FROM pipeline_data LIMIT " + String(limit));
              usedFallback = true;
            }}
            const plotBox = section.getBoundingClientRect();
            const plotWidth = plotBox && plotBox.width ? Math.floor(plotBox.width) - 32 : 520;
            const plotHeight = Math.max(220, Math.floor(plotWidth * 0.45));
            const plotSize = {{ width: plotWidth, height: plotHeight }};
            const plotNode = chartResult.error
              ? (() => {{
                  const el = document.createElement("div");
                  el.textContent = chartResult.error;
                  el.className = "notice";
                  return el;
                }})()
              : (() => {{
                  const reversed = chartResult && chartResult.rows
                    ? {{ columns: chartResult.columns, rows: [...chartResult.rows].reverse() }}
                    : chartResult;
                  const plotSpec = chartQuery && !usedFallback
                    ? {{
                        ...spec,
                        xField: "x",
                        yFields: chartQuery.seriesKeys,
                        seriesLabels: chartQuery.seriesLabels,
                      }}
                    : spec;
                  return renderPlot(reversed, plotSize, plotSpec);
                }})()
            ;
            if (plotNode) {{
              body.appendChild(plotNode);
            }}
          }} else {{
            const tableSql =
              'SELECT * FROM pipeline_data ORDER BY try_cast("timestamp" AS TIMESTAMP) DESC LIMIT ' + String(limit);
            let tableResult = await runDuckdbQuery(tableSql);
            if (tableResult.error) {{
              tableResult = await runDuckdbQuery("SELECT * FROM pipeline_data LIMIT " + String(limit));
            }}
            const tableNode = tableResult.error
              ? (() => {{
                  const el = document.createElement("div");
                  el.textContent = tableResult.error;
                  el.className = "notice";
                  return el;
                }})()
              : renderTable(tableResult);
            if (tableNode) {{
              body.appendChild(tableNode);
            }}
          }}
          section.appendChild(head);
          section.appendChild(body);
          root.appendChild(section);
        }}
        return root;
      }});

      return main;
    }}

    const runtime = new Runtime();
    runtime.module(define, (name) => {{
      if (name === "graph") return new Inspector(document.querySelector("#cf-graph"));
      if (name === "nodesView") return new Inspector(document.querySelector("#cf-nodes"));
      if (name === "edgesView") return new Inspector(document.querySelector("#cf-edges"));
      if (name === "triggersView") return new Inspector(document.querySelector("#cf-triggers"));
      if (name === "payloadView") return new Inspector(document.querySelector("#cf-data"));
      if (name === "reportTilesView") return new Inspector(document.querySelector("#cf-report-tiles"));
    }});

    const OMIT_TABLE_COLUMNS = new Set(["year", "month", "day"]);

      function normalizeColumnName(name) {{
        return String(name || "").trim().toLowerCase();
      }}

      function formatTimestamp(value) {{
        if (value === null || value === undefined) return "";
        if (typeof value !== "string") return formatValue(value);
        if (!value.includes("T")) return value;
        let trimmed = value.split("+")[0].replace(/Z$/, "");
        if (trimmed.includes(".")) {{
          const parts = trimmed.split(".");
          const ms = (parts[1] || "").slice(0, 3);
          trimmed = parts[0] + (ms ? "." + ms : "");
        }}
        return trimmed;
      }}

      function formatValue(value) {{
        if (value === null || value === undefined) return "";
        if (typeof value === "number" && Number.isFinite(value)) {{
          if (Number.isInteger(value)) {{
            return String(value);
        }}
        const rounded = value.toFixed(3);
        return rounded.replace(/\\.?0+$/, "");
      }}
      return String(value);
    }}

    function renderTable(result) {{
      const container = document.createElement("div");
      if (!result || !result.columns || !result.rows) {{
        container.textContent = "No data available.";
        container.className = "notice";
        return container;
      }}
      if (result.rows.length === 0) {{
        container.textContent = "No rows yet.";
        container.className = "notice";
        return container;
      }}
      const visibleColumns = result.columns.filter(
        (col) => !OMIT_TABLE_COLUMNS.has(normalizeColumnName(col))
      );
      if (visibleColumns.length === 0) {{
        container.textContent = "No visible columns.";
        container.className = "notice";
        return container;
      }}
        const table = document.createElement("table");
        const thead = document.createElement("thead");
        const headRow = document.createElement("tr");
        const timeColumns = new Set(["timestamp", "time", "ts"]);
        for (const col of visibleColumns) {{
          const th = document.createElement("th");
          th.textContent = col;
          headRow.appendChild(th);
      }}
      thead.appendChild(headRow);
      table.appendChild(thead);
      const tbody = document.createElement("tbody");
        for (const row of result.rows) {{
          const tr = document.createElement("tr");
          for (const col of visibleColumns) {{
            const td = document.createElement("td");
            const val = row[col];
            const colKey = normalizeColumnName(col);
            td.textContent = timeColumns.has(colKey) ? formatTimestamp(val) : formatValue(val);
            tr.appendChild(td);
          }}
          tbody.appendChild(tr);
        }}
      table.appendChild(tbody);
      container.appendChild(table);
      return container;
    }}

    function renderPlot(result, size, spec) {{
      const container = document.createElement("div");
      if (!result || !result.rows || result.rows.length === 0) {{
        container.textContent = "No plot data yet.";
        container.className = "notice";
        return container;
      }}
      const columns = result.columns || [];
      const specInfo = resolvePlotSpec(columns, result.rows, spec);
      const series = specInfo.series || [];
      if (!series.length) {{
        container.textContent = "Not enough points to plot.";
        container.className = "notice";
        return container;
      }}
      const points = series.flatMap((s) => s.points || []);
      if (points.length < 2) {{
        container.textContent = "Not enough points to plot.";
        container.className = "notice";
        return container;
      }}
      const width = size && size.width ? size.width : 520;
      const height = size && size.height ? size.height : 220;
        const padLeft = 48;
        const padRight = 18;
        const padTop = 16;
        const padBottom = 34;
        const xs = points.map((p) => p.x);
        const ys = [];
        for (const p of points) {{
          ys.push(p.y);
          if (p.err !== null && p.err !== undefined) {{
            ys.push(p.y - p.err, p.y + p.err);
          }}
        }}
        const minX = Math.min(...xs);
        const maxX = Math.max(...xs);
        const minY = Math.min(...ys);
        const maxY = Math.max(...ys);
      const spanX = maxX - minX || 1;
      const spanY = maxY - minY || 1;

        const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        svg.setAttribute("viewBox", "0 0 " + width + " " + height);
        svg.setAttribute("width", String(width));
        svg.setAttribute("height", String(height));
        svg.style.width = String(width) + "px";
        svg.style.height = String(height) + "px";
        svg.style.display = "block";

        const axisColor = "#2a3242";
        const gridMajor = "rgba(255,255,255,0.08)";
        const gridMinor = "rgba(255,255,255,0.04)";
        const labelColor = "#9aa1b3";
        const textStyle = "font-size:10px; fill:" + labelColor + ";";
        const axisTitleStyle = "font-size:11px; fill:" + labelColor + "; font-weight:600;";

        const xAxis = document.createElementNS("http://www.w3.org/2000/svg", "line");
        xAxis.setAttribute("x1", String(padLeft));
        xAxis.setAttribute("y1", String(height - padBottom));
        xAxis.setAttribute("x2", String(width - padRight));
        xAxis.setAttribute("y2", String(height - padBottom));
        xAxis.setAttribute("stroke", axisColor);
        xAxis.setAttribute("stroke-width", "1");
        xAxis.setAttribute("vector-effect", "non-scaling-stroke");
        svg.appendChild(xAxis);

        const yAxis = document.createElementNS("http://www.w3.org/2000/svg", "line");
        yAxis.setAttribute("x1", String(padLeft));
        yAxis.setAttribute("y1", String(padTop));
        yAxis.setAttribute("x2", String(padLeft));
        yAxis.setAttribute("y2", String(height - padBottom));
        yAxis.setAttribute("stroke", axisColor);
        yAxis.setAttribute("stroke-width", "1");
        yAxis.setAttribute("vector-effect", "non-scaling-stroke");
        svg.appendChild(yAxis);

        const majorTicks = 3;
        const minorPerMajor = 2;
        const plotW = width - padLeft - padRight;
        const plotH = height - padTop - padBottom;
        for (let i = 0; i <= majorTicks; i++) {{
          const x = padLeft + (plotW * i) / majorTicks;
          const vline = document.createElementNS("http://www.w3.org/2000/svg", "line");
          vline.setAttribute("x1", String(x));
          vline.setAttribute("y1", String(padTop));
          vline.setAttribute("x2", String(x));
          vline.setAttribute("y2", String(height - padBottom));
          vline.setAttribute("stroke", gridMajor);
          vline.setAttribute("stroke-width", "1");
          vline.setAttribute("vector-effect", "non-scaling-stroke");
          svg.appendChild(vline);
          if (i < majorTicks) {{
            for (let m = 1; m <= minorPerMajor; m++) {{
              const xm = x + (plotW / majorTicks) * (m / (minorPerMajor + 1));
              const vminor = document.createElementNS("http://www.w3.org/2000/svg", "line");
              vminor.setAttribute("x1", String(xm));
              vminor.setAttribute("y1", String(padTop));
              vminor.setAttribute("x2", String(xm));
              vminor.setAttribute("y2", String(height - padBottom));
              vminor.setAttribute("stroke", gridMinor);
              vminor.setAttribute("stroke-width", "1");
              vminor.setAttribute("vector-effect", "non-scaling-stroke");
              svg.appendChild(vminor);
            }}
          }}
        }}

        for (let i = 0; i <= majorTicks; i++) {{
          const y = height - padBottom - (plotH * i) / majorTicks;
          const hline = document.createElementNS("http://www.w3.org/2000/svg", "line");
          hline.setAttribute("x1", String(padLeft));
          hline.setAttribute("y1", String(y));
          hline.setAttribute("x2", String(width - padRight));
          hline.setAttribute("y2", String(y));
          hline.setAttribute("stroke", gridMajor);
          hline.setAttribute("stroke-width", "1");
          hline.setAttribute("vector-effect", "non-scaling-stroke");
          svg.appendChild(hline);
          if (i < majorTicks) {{
            for (let m = 1; m <= minorPerMajor; m++) {{
              const ym = y - (plotH / majorTicks) * (m / (minorPerMajor + 1));
              const hminor = document.createElementNS("http://www.w3.org/2000/svg", "line");
              hminor.setAttribute("x1", String(padLeft));
              hminor.setAttribute("y1", String(ym));
              hminor.setAttribute("x2", String(width - padRight));
              hminor.setAttribute("y2", String(ym));
              hminor.setAttribute("stroke", gridMinor);
              hminor.setAttribute("stroke-width", "1");
              hminor.setAttribute("vector-effect", "non-scaling-stroke");
              svg.appendChild(hminor);
            }}
          }}
        }}

        for (const serie of series) {{
          const seriePoints = serie.points || [];
          if (seriePoints.length < 2) continue;
          const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
          const d = seriePoints
            .map((p, i) => {{
              const x = padLeft + ((p.x - minX) / spanX) * (width - padLeft - padRight);
              const y = height - padBottom - ((p.y - minY) / spanY) * (height - padTop - padBottom);
              return (i === 0 ? "M" : "L") + x.toFixed(1) + " " + y.toFixed(1);
            }})
            .join(" ");
          path.setAttribute("d", d);
          path.setAttribute("fill", "none");
          path.setAttribute("stroke", serie.color || "#4bc5b8");
          path.setAttribute("stroke-width", "2");
          path.setAttribute("vector-effect", "non-scaling-stroke");
          svg.appendChild(path);
        }}

        for (const serie of series) {{
          const seriePoints = serie.points || [];
          for (const p of seriePoints) {{
            if (p.err === null || p.err === undefined) continue;
            const x = padLeft + ((p.x - minX) / spanX) * (width - padLeft - padRight);
            const yTop = height - padBottom - ((p.y + p.err - minY) / spanY) * (height - padTop - padBottom);
            const yBottom = height - padBottom - ((p.y - p.err - minY) / spanY) * (height - padTop - padBottom);
            const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
            line.setAttribute("x1", x.toFixed(1));
            line.setAttribute("y1", yTop.toFixed(1));
            line.setAttribute("x2", x.toFixed(1));
            line.setAttribute("y2", yBottom.toFixed(1));
            line.setAttribute("stroke", serie.color || "#4bc5b8");
            line.setAttribute("stroke-width", "1");
            line.setAttribute("vector-effect", "non-scaling-stroke");
            svg.appendChild(line);

            const cap = 4;
            const capTop = document.createElementNS("http://www.w3.org/2000/svg", "line");
            capTop.setAttribute("x1", (x - cap).toFixed(1));
            capTop.setAttribute("y1", yTop.toFixed(1));
            capTop.setAttribute("x2", (x + cap).toFixed(1));
            capTop.setAttribute("y2", yTop.toFixed(1));
            capTop.setAttribute("stroke", serie.color || "#4bc5b8");
            capTop.setAttribute("stroke-width", "1");
            capTop.setAttribute("vector-effect", "non-scaling-stroke");
            svg.appendChild(capTop);

            const capBottom = document.createElementNS("http://www.w3.org/2000/svg", "line");
            capBottom.setAttribute("x1", (x - cap).toFixed(1));
            capBottom.setAttribute("y1", yBottom.toFixed(1));
            capBottom.setAttribute("x2", (x + cap).toFixed(1));
            capBottom.setAttribute("y2", yBottom.toFixed(1));
            capBottom.setAttribute("stroke", serie.color || "#4bc5b8");
            capBottom.setAttribute("stroke-width", "1");
            capBottom.setAttribute("vector-effect", "non-scaling-stroke");
            svg.appendChild(capBottom);
          }}
        }}

        const metricLabel = specInfo.yLabel || "Value";

        const xLabel = document.createElementNS("http://www.w3.org/2000/svg", "text");
        xLabel.textContent = "Time";
        xLabel.setAttribute("x", String((padLeft + width - padRight) / 2));
        xLabel.setAttribute("y", String(height - 6));
        xLabel.setAttribute("text-anchor", "middle");
        xLabel.setAttribute("style", axisTitleStyle);
        svg.appendChild(xLabel);

        const yLabel = document.createElementNS("http://www.w3.org/2000/svg", "text");
        yLabel.textContent = metricLabel;
        yLabel.setAttribute("x", String(12));
        yLabel.setAttribute("y", String((padTop + height - padBottom) / 2));
        yLabel.setAttribute("transform", "rotate(-90 12 " + ((padTop + height - padBottom) / 2) + ")");
        yLabel.setAttribute("text-anchor", "middle");
        yLabel.setAttribute("style", axisTitleStyle);
        svg.appendChild(yLabel);

        const tickCount = 3;
        for (let i = 0; i <= tickCount; i++) {{
          const tx = padLeft + ((width - padLeft - padRight) * i) / tickCount;
          const xVal = minX + (spanX * i) / tickCount;
          const t = new Date(xVal);
          const label = isNaN(t.getTime()) ? String(xVal) : t.toISOString().replace("Z", "").slice(0, 19);
          const tick = document.createElementNS("http://www.w3.org/2000/svg", "line");
          tick.setAttribute("x1", String(tx));
          tick.setAttribute("y1", String(height - padBottom));
          tick.setAttribute("x2", String(tx));
          tick.setAttribute("y2", String(height - padBottom + 4));
          tick.setAttribute("stroke", axisColor);
          tick.setAttribute("stroke-width", "1");
          tick.setAttribute("vector-effect", "non-scaling-stroke");
          svg.appendChild(tick);
          const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
          text.textContent = label;
          text.setAttribute("x", String(tx));
          text.setAttribute("y", String(height - padBottom + 16));
          text.setAttribute("text-anchor", "middle");
          text.setAttribute("style", textStyle);
          svg.appendChild(text);
        }}

        for (let i = 0; i <= tickCount; i++) {{
          const ty = height - padBottom - ((height - padTop - padBottom) * i) / tickCount;
          const yVal = minY + (spanY * i) / tickCount;
          const tick = document.createElementNS("http://www.w3.org/2000/svg", "line");
          tick.setAttribute("x1", String(padLeft - 4));
          tick.setAttribute("y1", String(ty));
          tick.setAttribute("x2", String(padLeft));
          tick.setAttribute("y2", String(ty));
          tick.setAttribute("stroke", axisColor);
          tick.setAttribute("stroke-width", "1");
          tick.setAttribute("vector-effect", "non-scaling-stroke");
          svg.appendChild(tick);
          const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
          text.textContent = Number.isFinite(yVal) ? yVal.toFixed(2) : String(yVal);
          text.setAttribute("x", String(padLeft - 6));
          text.setAttribute("y", String(ty + 4));
          text.setAttribute("text-anchor", "end");
          text.setAttribute("style", textStyle);
          svg.appendChild(text);
        }}

      if (series.length > 1) {{
        const legend = document.createElement("div");
        legend.className = "controls";
        legend.style.flexWrap = "wrap";
        for (const serie of series) {{
          const item = document.createElement("div");
          item.style.display = "flex";
          item.style.alignItems = "center";
          item.style.gap = "6px";
          const swatch = document.createElement("span");
          swatch.style.width = "10px";
          swatch.style.height = "10px";
          swatch.style.borderRadius = "6px";
          swatch.style.background = serie.color || "#4bc5b8";
          const label = document.createElement("span");
          label.textContent = serie.label || "series";
          item.appendChild(swatch);
          item.appendChild(label);
          legend.appendChild(item);
        }}
        container.appendChild(legend);
      }}
      container.appendChild(svg);
      return container;
    }}

    async function runDuckdbQuery(sql) {{
      if (!duckdbQueryUrl) {{
        return {{ error: "DuckDB not configured for this report." }};
      }}
      const url = duckdbQueryUrl + "?sql=" + encodeURIComponent(sql);
      const res = await fetch(url, {{ cache: "no-store" }});
      if (!res.ok) {{
        let detail = "";
        try {{
          const body = await res.json();
          detail = body.detail ? String(body.detail) : "";
        }} catch (err) {{
          detail = "HTTP " + res.status;
        }}
        return {{ error: detail }};
      }}
      return res.json();
    }}

    function plotRangeValue() {{
      const select = document.querySelector("#cf-plot-range");
      return select ? String(select.value || "day") : "day";
    }}

    function quoteIdent(name) {{
      return '"' + String(name || "").replace(/"/g, '""') + '"';
    }}

    function pickColumn(columns, patterns) {{
      for (const pattern of patterns) {{
        for (const col of columns) {{
          if (pattern.test(String(col || ""))) {{
            return col;
          }}
        }}
      }}
      return null;
    }}

    function pickNumericColumn(columns, rows, excluded) {{
      if (!rows || rows.length === 0) return null;
      const first = rows[0] || {{}};
      for (const col of columns) {{
        if (excluded && excluded.has(normalizeColumnName(col))) continue;
        const val = first[col];
        if (typeof val === "number" && Number.isFinite(val)) {{
          return col;
        }}
      }}
      return null;
    }}

    function normalizeChartSpec(spec) {{
      if (!spec || typeof spec !== "object") return null;
      const encoding = spec.encoding || {{}};
      const xField = encoding.x && encoding.x.field ? String(encoding.x.field) : null;
      const yFields = [];
      if (encoding.y) {{
        if (Array.isArray(encoding.y)) {{
          for (const y of encoding.y) {{
            if (y && y.field) yFields.push(String(y.field));
          }}
        }} else if (encoding.y.field) {{
          yFields.push(String(encoding.y.field));
        }}
      }}
      const colorField = encoding.color && encoding.color.field ? String(encoding.color.field) : null;
      const mark = normalizeMark(spec.mark || spec.type);
      return {{
        mark,
        xField,
        yFields,
        colorField,
      }};
    }}

    function normalizeMark(value) {{
      if (!value) return null;
      const raw = String(value).trim().toLowerCase();
      if (raw.endsWith("mark")) return raw.replace("mark", "");
      if (raw === "linemark") return "line";
      if (raw === "barmark") return "bar";
      if (raw === "pointmark") return "point";
      if (raw === "areamark") return "area";
      return raw;
    }}

    function resolvePlotSpec(columns, rows, spec) {{
      const timeKey =
        (spec && spec.xField) ||
        columns.find((col) => /timestamp/i.test(col)) ||
        columns.find((col) => /time/i.test(col)) ||
        columns.find((col) => /ts$/i.test(col));
      let yKeys = spec && spec.yFields && spec.yFields.length ? spec.yFields : null;
      if (!yKeys || yKeys.length === 0) {{
        let valueKey =
          columns.find((col) => /mean/i.test(col)) ||
          columns.find((col) => /value/i.test(col)) ||
          columns.find((col) => /avg/i.test(col)) ||
          columns.find((col) => /y$/i.test(col));
        if (!valueKey && rows && rows.length > 0) {{
          const first = rows[0] || {{}};
          valueKey = columns.find((col) => {{
            const lower = normalizeColumnName(col);
            if (OMIT_TABLE_COLUMNS.has(lower)) return false;
            if (col === timeKey) return false;
            const val = first[col];
            return typeof val === "number" && Number.isFinite(val);
          }});
        }}
        if (valueKey) {{
          yKeys = [valueKey];
        }}
      }}
      const series = [];
      const palette = ["#4bc5b8", "#7aa6ff", "#f2b56b", "#f06c6c", "#a97cf0"];
      const rowsSafe = rows || [];
      const seriesLabels = (spec && spec.seriesLabels) ? spec.seriesLabels : null;
      const uncertaintyMap = (spec && spec.uncertaintyMap) ? spec.uncertaintyMap : {{}};
      const valueToUncertainty = {{}};
      for (const [uncLabel, valLabel] of Object.entries(uncertaintyMap)) {{
        valueToUncertainty[String(valLabel)] = String(uncLabel);
      }}
      const labelByKey = new Map();
      if (yKeys) {{
        yKeys.forEach((key, idx) => {{
          const label = seriesLabels && seriesLabels[idx] ? seriesLabels[idx] : key;
          labelByKey.set(label, key);
        }});
      }}
      if (timeKey && yKeys && yKeys.length) {{
        for (let i = 0; i < yKeys.length; i++) {{
          const key = yKeys[i];
          const label = seriesLabels && seriesLabels[i] ? seriesLabels[i] : key;
          if (Object.prototype.hasOwnProperty.call(uncertaintyMap, label)) {{
            continue;
          }}
          const uncertaintyLabel = valueToUncertainty[label];
          const uncertaintyKey = uncertaintyLabel ? labelByKey.get(uncertaintyLabel) : null;
          const points = rowsSafe
            .map((row, idx) => {{
              const ts = (timeKey ? row[timeKey] : null) ?? row.timestamp ?? row.time ?? row.ts ?? idx;
              const x = typeof ts === "string" ? Date.parse(ts) : Number(ts);
              const y = Number(row[key]);
              const err = uncertaintyKey ? Number(row[uncertaintyKey]) : null;
              if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
              return {{
                x,
                y,
                err: Number.isFinite(err) ? err : null,
              }};
            }})
            .filter(Boolean);
          series.push({{
            key,
            label,
            color: palette[i % palette.length],
            points,
          }});
        }}
      }}
      const yLabel = yKeys && yKeys.length === 1
        ? String(yKeys[0]).replace(/(mean|se|standarderror)$/i, "")
        : "Value";
      return {{ series, yLabel }};
    }}

    function buildChartQuery(spec, limit) {{
      if (!spec || !spec.xField || !spec.yFields || spec.yFields.length === 0) {{
        return null;
      }}
      const xKey = spec.xField;
      const yKeys = spec.yFields;
      const castX = /timestamp|time|ts$/i.test(xKey)
        ? "try_cast(" + quoteIdent(xKey) + " AS TIMESTAMP)"
        : quoteIdent(xKey);
      const seriesKeys = yKeys.map((_, idx) => "y" + String(idx));
      const yParts = yKeys.map((key, idx) => quoteIdent(key) + " AS " + seriesKeys[idx]);
      const base =
        "SELECT " +
        castX +
        " AS x, " +
        yParts.join(", ") +
        " FROM pipeline_data";
      const conditions = yKeys.map((key) => quoteIdent(key) + " IS NOT NULL");
      const where = conditions.length ? " WHERE " + conditions.join(" AND ") : "";
      const order = " ORDER BY x DESC";
      const lim = " LIMIT " + String(limit);
      return {{
        sql: base + where + order + lim,
        seriesKeys,
        seriesLabels: yKeys,
      }};
    }}

    function buildPlotQuery(rangeKey, limit, timeCol, valueCol) {{
      if (!timeCol || !valueCol) {{
        return null;
      }}
      const timeExpr = "try_cast(" + quoteIdent(timeCol) + " AS TIMESTAMP)";
      const valueExpr = quoteIdent(valueCol);
      const base =
        "SELECT " +
        timeExpr +
        " AS timestamp, " +
        valueExpr +
        " AS mean FROM pipeline_data WHERE " +
        valueExpr +
        " IS NOT NULL";
      const order = " ORDER BY " + timeExpr + " DESC";
      const lim = " LIMIT " + String(limit);
      if (rangeKey === "all") {{
        return base + order + lim;
      }}
      const interval =
        rangeKey === "day" ? "1 day" :
        rangeKey === "week" ? "7 days" :
        rangeKey === "month" ? "1 month" :
        rangeKey === "year" ? "1 year" :
        rangeKey === "5years" ? "5 years" :
        "1 day";
      return (
        base +
        " AND " +
        timeExpr +
        " >= CAST(now() AS TIMESTAMP) - INTERVAL " +
        "'" + interval + "'" +
        order +
        lim
      );
    }}

    async function refreshLiveData() {{
        const tableRoot = document.querySelector("#cf-live-table");
        const plotRoot = document.querySelector("#cf-live-plot");
        if (!tableRoot || !plotRoot) return;

        const scrollY = window.scrollY;

      const limit = 20;
      const tableSql =
        'SELECT * FROM pipeline_data ORDER BY try_cast("timestamp" AS TIMESTAMP) DESC LIMIT ' + String(limit);
      let tableResult = await runDuckdbQuery(tableSql);
      if (tableResult.error) {{
        tableResult = await runDuckdbQuery("SELECT * FROM pipeline_data LIMIT " + String(limit));
      }}

        const tableNode = tableResult.error
          ? (() => {{
              const el = document.createElement("div");
              el.textContent = tableResult.error;
              el.className = "notice";
              return el;
            }})()
          : renderTable(tableResult);
        tableRoot.className = tableResult.error ? "table-wrap notice" : "table-wrap";
        if (tableNode) {{
          tableRoot.replaceChildren(tableNode);
        }}

      const tableColumns = tableResult && tableResult.columns ? tableResult.columns : [];
      const timeCol = pickColumn(tableColumns, [/timestamp/i, /time/i, /ts$/i]);
      let valueCol = pickColumn(tableColumns, [/mean/i, /value/i, /avg/i, /y$/i]);
      if (!valueCol) {{
        valueCol = pickNumericColumn(tableColumns, tableResult.rows || [], new Set(["year", "month", "day"]));
      }}
      const plotSql = buildPlotQuery(plotRangeValue(), limit, timeCol, valueCol);
      let plotResult = plotSql ? await runDuckdbQuery(plotSql) : {{ error: "Plot columns not found." }};
      if (plotResult.error) {{
        plotResult = await runDuckdbQuery("SELECT * FROM pipeline_data LIMIT " + String(limit));
      }}

        const plotBox = plotRoot.getBoundingClientRect();
        const plotWidth = plotBox && plotBox.width ? Math.floor(plotBox.width) : 520;
        const plotHeight = Math.max(220, Math.floor(plotWidth * 0.45));
        plotRoot.style.height = String(plotHeight) + "px";
        const plotSize = {{ width: plotWidth, height: plotHeight }};
        const plotNode = plotResult.error
          ? (() => {{
              const el = document.createElement("div");
              el.textContent = plotResult.error;
              el.className = "notice";
              return el;
            }})()
          : (() => {{
              const reversed = plotResult && plotResult.rows
                ? {{ columns: plotResult.columns, rows: [...plotResult.rows].reverse() }}
                : plotResult;
              return renderPlot(reversed, plotSize);
            }})();
        plotRoot.className = plotResult.error ? "plot notice" : "plot";
        if (plotNode) {{
          plotRoot.replaceChildren(plotNode);
        }}

        if (window.scrollY !== scrollY) {{
          window.requestAnimationFrame(() => window.scrollTo(0, scrollY));
        }}
      }}

      if (duckdbQueryUrl) {{
        refreshLiveData();
        setInterval(refreshLiveData, 5000);
        const rangeSelect = document.querySelector("#cf-plot-range");
        if (rangeSelect) {{
          rangeSelect.addEventListener("change", () => refreshLiveData());
        }}
        const plotRoot = document.querySelector("#cf-live-plot");
        let resizeTimer = null;
        const scheduleResize = () => {{
          if (resizeTimer) {{
            clearTimeout(resizeTimer);
          }}
          resizeTimer = setTimeout(() => refreshLiveData(), 150);
        }};
        window.addEventListener("resize", scheduleResize);
        if (plotRoot && "ResizeObserver" in window) {{
          const ro = new ResizeObserver(() => scheduleResize());
          ro.observe(plotRoot);
        }}
      }} else {{
      const tableRoot = document.querySelector("#cf-live-table");
      const plotRoot = document.querySelector("#cf-live-plot");
      if (tableRoot) {{
        tableRoot.textContent = "DuckDB is not configured for this report.";
        tableRoot.className = "table-wrap notice";
      }}
      if (plotRoot) {{
        plotRoot.textContent = "DuckDB is not configured for this report.";
        plotRoot.className = "plot notice";
      }}
    }}

    document.querySelectorAll("section .card-head").forEach((head) => {{
      head.addEventListener("dblclick", () => {{
        const section = head.closest("section");
        if (!section) return;
        section.classList.toggle("collapsed");
      }});
    }});
  </script>
</body>
</html>
"""
