import json
from html import escape
from pathlib import Path
from typing import Dict, List


def _safe(value: str) -> str:
    return escape(str(value or ""))


def _folder_tree(rows: List[dict]) -> dict:
    root = {"name": "ROOT", "path": "", "count": 0, "children": {}}

    for row in rows:
        p = row.get("path") or ""
        parts = [x for x in p.split("/") if x]
        if not parts:
            continue

        # Folder-only tree: for file items exclude last segment.
        folder_depth = len(parts) if row.get("item_kind") == "FOLDER" else len(parts) - 1
        if folder_depth <= 0:
            continue

        node = root
        node["count"] += 1
        current = []
        for i in range(folder_depth):
            part = parts[i]
            current.append(part)
            key = "/".join(current)
            if part not in node["children"]:
                node["children"][part] = {
                    "name": part,
                    "path": key,
                    "count": 0,
                    "children": {},
                }
            node = node["children"][part]
            node["count"] += 1

    return root


def _render_tree(node: dict) -> str:
    if not node["children"]:
        return ""

    out = ["<ul class='tree-ul'>"]
    for key in sorted(node["children"].keys()):
        child = node["children"][key]
        nested = _render_tree(child)
        label = f"{_safe(child['name'])} <span class='cnt'>({_safe(child['count'])})</span>"
        if nested:
            out.append(
                f"<li><details><summary class='tree-node' data-path='{_safe(child['path'])}'>{label}</summary>{nested}</details></li>"
            )
        else:
            out.append(
                f"<li><button class='tree-leaf tree-node' data-path='{_safe(child['path'])}'>{label}</button></li>"
            )
    out.append("</ul>")
    return "".join(out)


def render_explorer(curated_rows: List[dict], output_html: Path, build_id: str = "") -> None:
    rows = sorted(curated_rows, key=lambda x: (x.get("path") or "", x.get("name") or ""))
    tree_html = _render_tree(_folder_tree(rows))

    payload_rows = []
    for r in rows:
        payload_rows.append(
            {
                "item_id": r.get("item_id", ""),
                "name": r.get("name", ""),
                "path": r.get("path", ""),
                "root_path": r.get("root_path", ""),
                "item_kind": r.get("item_kind", ""),
                "file_category": r.get("file_category", ""),
                "original_owner_email": r.get("original_owner_email", ""),
                "shared_by_email": r.get("shared_by_email", ""),
                "access_scope": r.get("access_scope", ""),
                "shared_with_count": int(r.get("shared_with_count", 0) or 0),
                "access_targets": r.get("access_targets", ""),
                "permission_entries": r.get("permission_entries", ""),
                "risk_level": r.get("risk_level", "LOW"),
                "risk_reason": r.get("risk_reason", ""),
                "risk_score": int(r.get("risk_score", 0) or 0),
                "risk_flags": r.get("risk_flags", ""),
                "external_target_count": int(r.get("external_target_count", 0) or 0),
                "writer_target_count": int(r.get("writer_target_count", 0) or 0),
            }
        )

    rows_json = json.dumps(payload_rows, ensure_ascii=True)

    html = f"""<!doctype html>
<html lang=\"en\">
<head>
  <meta charset=\"utf-8\" />
  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
  <title>GDrive Explorer + Diagram</title>
  <style>
    :root {{
      --bg: #edf2f7;
      --panel: #ffffff;
      --line: #d7e0ea;
      --ink: #111827;
      --muted: #4b5563;
      --path: #facc15;
      --low: #475569;
      --med: #d97706;
      --high: #dc2626;
      --col1: 16fr;
      --col2: 34fr;
      --col3: 50fr;
    }}
    * {{ box-sizing: border-box; }}
    body {{ margin: 0; font-family: "Segoe UI", Arial, sans-serif; background: var(--bg); color: var(--ink); }}
    .wrap {{ width: 100%; max-width: none; margin: 0; padding: 10px; }}

    .toolbar {{
      display: grid;
      grid-template-columns: minmax(220px, 1fr) auto auto auto auto;
      gap: 8px;
      align-items: center;
      background: var(--panel);
      border: 1px solid var(--line);
      border-radius: 12px;
      padding: 8px;
      margin-bottom: 8px;
    }}
    .toolbar input {{ width: 100%; padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px; }}
    .grp {{ display: inline-flex; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }}
    .grp button {{ border: 0; padding: 8px 10px; background: #fff; cursor: pointer; font-weight: 700; color: #1f2937; }}
    .grp button.active {{ background: #0f766e; color: #fff; }}
    .risk-summary {{
      background: var(--panel);
      border: 1px solid var(--line);
      border-radius: 12px;
      padding: 8px 10px;
      margin-bottom: 8px;
    }}
    .risk-top {{ display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom: 6px; flex-wrap: wrap; }}
    .risk-bar-wrap {{
      width: 100%;
      height: 14px;
      border-radius: 999px;
      overflow: hidden;
      background: #dbe7f2;
      border: 1px solid #c7d6e5;
    }}
    .risk-bar {{
      height: 100%;
      width: 0%;
      background: linear-gradient(90deg,#16a34a,#f59e0b,#dc2626);
      transition: width .22s ease;
    }}
    .risk-stats {{ display:flex; flex-wrap: wrap; gap: 6px; margin-top: 7px; }}
    .risk-pill {{
      display:inline-flex; align-items:center; gap:5px;
      border:1px solid var(--line);
      border-radius: 999px;
      background:#fff;
      padding: 3px 8px;
      font-size: .74rem;
      font-weight: 700;
      color: #334155;
    }}
    .risk-rules {{ margin-top: 6px; font-size: .75rem; color:#334155; line-height:1.3; }}
    .breadcrumbs {{
      margin: 8px 0 10px;
      display: flex;
      flex-wrap: wrap;
      gap: 6px;
      align-items: center;
      background: var(--panel);
      border: 1px solid var(--line);
      border-radius: 10px;
      padding: 7px 10px;
      font-size: .78rem;
      color: #334155;
    }}
    .crumb-btn {{
      border: 1px solid #cfdbe8;
      background: #fff;
      border-radius: 999px;
      padding: 2px 8px;
      font-size: .74rem;
      color: #1f2937;
      cursor: pointer;
      max-width: 260px;
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
    }}
    .crumb-sep {{ color:#94a3b8; font-weight: 700; }}

    .layout {{
      display: grid;
      grid-template-columns: minmax(220px, var(--col1)) 8px minmax(430px, var(--col2)) 8px minmax(560px, var(--col3));
      gap: 10px;
      align-items: start;
      min-width: 0;
    }}
    .top-layout {{
      display: grid;
      grid-template-columns: minmax(280px, 28fr) minmax(620px, 72fr);
      gap: 10px;
      align-items: stretch;
    }}
    .bottom-layout {{
      display: grid;
      grid-template-columns: minmax(620px, 72fr) minmax(260px, 28fr);
      gap: 10px;
      margin-top: 10px;
      align-items: stretch;
    }}
    .splitter {{
      border-radius: 8px;
      background: linear-gradient(180deg,#d7e0ea,#c8d5e3);
      cursor: col-resize;
      width: 8px;
      min-height: 78vh;
      border: 1px solid #c4d0de;
    }}

    .panel {{ background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 8px; min-height: 78vh; overflow: hidden; min-width: 0; }}
    .title {{ font-size: .95rem; font-weight: 800; margin: 2px 0 6px; }}

    .tree-tools {{ display: flex; gap: 6px; margin-bottom: 6px; }}
    .tree-tools button {{ border:1px solid var(--line); background:#fff; border-radius: 7px; padding: 6px 8px; cursor:pointer; font-size:.78rem; font-weight:700; }}

    .tree-wrap {{ max-height: 74vh; overflow: auto; border: 1px solid var(--line); border-radius: 10px; padding: 6px; overscroll-behavior: contain; scroll-behavior: smooth; }}
    .tree-ul {{ list-style: none; margin: 0; padding-left: 12px; }}
    details > summary {{ cursor: pointer; font-weight: 600; color: #1f2937; padding: 2px 0; }}
    .tree-leaf {{ border: 0; background: transparent; cursor: pointer; color: #374151; padding: 2px 0; text-align: left; width: 100%; }}
    .tree-node {{ transition: color .18s ease, background-color .18s ease; }}
    .tree-node.active {{ color: #0f766e !important; font-weight: 800 !important; background: rgba(15,118,110,.10); border-radius: 6px; padding-inline: 4px; }}

    .tbl-wrap {{ max-height: 74vh; overflow: auto; border: 1px solid var(--line); border-radius: 10px; position: relative; overscroll-behavior: contain; scroll-behavior: smooth; }}
    .tbl-tools {{ display:flex; gap: 6px; align-items:center; margin-bottom: 6px; }}
    .tbl-tools button {{ border:1px solid var(--line); background:#fff; border-radius:7px; padding:5px 8px; cursor:pointer; font-size:.75rem; font-weight:700; color:#1f2937; }}
    .tbl-tools .hint {{ color: var(--muted); font-size: .74rem; }}
    .copy-row-path {{ border:1px solid var(--line); background:#fff; border-radius:7px; padding:2px 6px; cursor:pointer; font-size:.72rem; font-weight:700; color:#1f2937; }}
    table {{ width: 100%; border-collapse: collapse; table-layout: fixed; }}
    th, td {{ padding: 6px 7px; border-bottom: 1px solid var(--line); font-size: .81rem; text-align: left; vertical-align: top; }}
    th {{ position: sticky; top: 0; background: #f8fafc; z-index: 1; }}
    td code {{ font-size: .78rem; display: inline-block; max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }}

    .badge {{ font-size: .74rem; border-radius: 999px; padding: 1px 8px; color: #fff; font-weight: 800; }}
    .risk-LOW {{ background: var(--low); }}
    .risk-MEDIUM {{ background: var(--med); }}
    .risk-HIGH {{ background: var(--high); }}
    tr td {{ transition: background-color .16s ease; }}
    tr.row-focus td {{ background: #fff7ed; }}

    .diagram-wrap {{ max-height: 74vh; overflow: auto; border: 1px solid var(--line); border-radius: 10px; padding: 10px; background: linear-gradient(145deg,#f8fafc,#f4f8fb); overscroll-behavior: contain; scroll-behavior: smooth; }}
    .d-tree {{ min-width: 900px; }}
    .d-legend {{
      display:flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px;
      padding: 7px; border:1px solid var(--line); border-radius: 9px; background:#fff;
    }}
    .d-legend .chip {{
      display:inline-flex; align-items:center; gap:6px;
      border:1px solid #d5dfeb; border-radius: 999px; padding: 3px 8px;
      font-size: .74rem; font-weight: 700; color:#1f2937;
    }}
    .sw {{ width: 10px; height: 10px; border-radius: 2px; display:inline-block; }}
    .d-tree ul {{ list-style: none; margin: 0; padding-left: 28px; position: relative; }}
    .d-tree ul::before {{ content: ""; position: absolute; top: 0; left: 11px; bottom: 0; border-left: 2px solid #d4dee9; }}
    .d-tree li {{ margin: 8px 0; position: relative; }}
    .d-tree li::before {{ content: ""; position: absolute; top: 18px; left: -17px; width: 17px; border-top: 2px solid #d4dee9; }}
    .dbranch > summary {{ list-style: none; cursor: pointer; }}
    .dbranch > summary::-webkit-details-marker {{ display: none; }}
    .dbranch > summary::before {{ content: "▸"; color:#334155; font-size: .8rem; margin-right: 6px; }}
    .dbranch[open] > summary::before {{ content: "▾"; }}

    .dnode {{ border-radius: 10px; min-height: 76px; padding: 8px; border: 1px solid rgba(15,23,42,.25); color: #fff; }}
    .dnode.folder {{ border-color: rgba(76,29,149,.55); }}
    .dnode.depth-1 {{ background: #7c3aed; }}
    .dnode.depth-2 {{ background: #6d28d9; }}
    .dnode.depth-3 {{ background: #5b21b6; }}
    .dnode.depth-4 {{ background: #4c1d95; }}
    .dnode.depth-5 {{ background: #3b126f; }}
    .dnode.file {{ background: #1d4ed8; border-color: #1e40af; cursor: default; }}
    .dnode .n1 {{ font-weight: 800; font-size: .83rem; }}
    .dnode .n2 {{ font-size: .78rem; margin-top: 2px; opacity: .95; }}
    .dnode .n3 {{ font-size: .74rem; margin-top: 3px; opacity: .9; }}
    .dnode.focus {{ outline: 2px solid #f59e0b; box-shadow: 0 0 0 2px rgba(245,158,11,.3) inset; }}
    .dnode.selected {{ outline: 2px solid #f59e0b; box-shadow: 0 0 0 2px rgba(245,158,11,.28) inset; transform: translateY(-1px); transition: box-shadow .16s ease, transform .16s ease; }}

    .hover-card {{
      position: fixed;
      left: 24px;
      top: 24px;
      z-index: 30;
      width: 390px;
      max-height: 68vh;
      overflow: auto;
      background: rgba(10, 18, 32, 0.38);
      backdrop-filter: blur(18px) saturate(130%);
      -webkit-backdrop-filter: blur(18px) saturate(130%);
      color: #fff;
      border: 1px solid rgba(191, 219, 254, 0.42);
      border-radius: 10px;
      padding: 10px;
      pointer-events: auto;
      user-select: text;
      opacity: 0;
      transform: translateY(4px);
      transition: opacity .16s ease, transform .18s ease;
      box-shadow: 0 10px 28px rgba(2,6,23,.35);
    }}
    .hover-card.show {{ opacity: 1; transform: translateY(0); }}
    .hover-card .hline {{ font-size: .8rem; margin: 4px 0; line-height: 1.25; word-break: break-word; }}

    .muted {{ color: var(--muted); font-size: .78rem; }}

    .master-panel {{ margin-top: 10px; background: var(--panel); border:1px solid var(--line); border-radius:12px; padding:8px; }}
    .master-title {{ font-size: .95rem; font-weight: 800; margin: 2px 0 6px; }}
    .master-wrap {{ height: 76vh; border:1px solid var(--line); border-radius:10px; overflow:hidden; background: linear-gradient(145deg,#f7fafc,#eff4f8); position: relative; overscroll-behavior: contain; }}
    .sunburst-wrap {{ height: 76vh; }}
    .network-wrap {{ height: 52vh; border:1px solid var(--line); border-radius:10px; overflow:hidden; background: linear-gradient(145deg,#f7fafc,#eff4f8); position: relative; }}
    .network-host {{ width: 100%; height: 100%; }}
    .context-panel {{ min-height: 52vh; border:1px solid var(--line); border-radius:10px; padding: 10px; background: #f8fbff; }}
    .context-live {{ font-size: .86rem; line-height: 1.45; color:#1f2937; margin-bottom: 10px; }}
    .context-box {{ border:1px solid #d4deea; border-radius: 9px; background:#fff; padding: 8px 10px; font-size: .8rem; line-height: 1.4; color:#374151; margin-top: 8px; }}
    .legacy-hidden {{ display: none !important; }}
    .master-host {{ width: 100%; height: 100%; }}
    .master-toolbar {{
      position:absolute; right:10px; top:10px; z-index: 2;
      display:flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end;
    }}
    .master-toolbar button {{
      border:1px solid var(--line); background: rgba(255,255,255,.92);
      border-radius:8px; padding: 5px 8px; font-size:.74rem; font-weight:700; cursor:pointer; color:#334155;
    }}
    .master-toolbar button.active {{ background:#0f766e; color:#fff; border-color:#0f766e; }}
    .master-mode {{ position:absolute; left:10px; top:10px; background: rgba(255,255,255,.86); border:1px solid var(--line); border-radius:8px; padding:5px 8px; font-size:.74rem; color:#334155; font-weight:700; z-index:2; }}
    .master-help {{ position:absolute; left:10px; bottom:10px; background: rgba(255,255,255,.8); border:1px solid var(--line); border-radius:8px; padding:6px 8px; font-size:.76rem; color:#334155; }}
    .live-context {{
      position: absolute;
      right: 10px;
      top: 46px;
      z-index: 3;
      width: min(330px, 34vw);
      background: rgba(255,255,255,.9);
      border: 1px solid var(--line);
      border-radius: 10px;
      padding: 8px 10px;
      font-size: .76rem;
      color: #1f2937;
      line-height: 1.35;
      box-shadow: 0 10px 22px rgba(15,23,42,.1);
    }}
    .build-badge {{
      position: absolute;
      right: 10px;
      bottom: 10px;
      z-index: 3;
      font-size: .68rem;
      color: #64748b;
      background: rgba(255,255,255,.75);
      border: 1px solid var(--line);
      border-radius: 999px;
      padding: 4px 8px;
    }}
    .selection-strip {{
      position: absolute;
      left: 10px;
      top: 40px;
      z-index: 3;
      max-width: min(58vw, 820px);
      background: rgba(255,255,255,.92);
      border: 1px solid var(--line);
      border-radius: 8px;
      padding: 5px 8px;
      font-size: .74rem;
      color: #334155;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }}
    .network-selection-strip {{
      position: absolute;
      left: 10px;
      top: 10px;
      z-index: 2;
      max-width: min(58vw, 820px);
      background: rgba(255,255,255,.92);
      border: 1px solid var(--line);
      border-radius: 8px;
      padding: 5px 8px;
      font-size: .74rem;
      color: #334155;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }}
    .master-info {{
      position:absolute; right:10px; bottom:38px; z-index:2;
      width: min(460px, 42vw);
      max-height: 30vh;
      overflow:auto;
      background: rgba(10,18,32,.52);
      backdrop-filter: blur(12px) saturate(120%);
      -webkit-backdrop-filter: blur(12px) saturate(120%);
      border:1px solid rgba(191,219,254,.45);
      border-radius:10px;
      color:#fff;
      padding:8px 10px;
      font-size:.78rem;
      line-height: 1.35;
    }}

    @media (max-width: 1500px) {{
      .layout {{ grid-template-columns: minmax(220px, var(--col1)) 8px minmax(380px, var(--col2)) 8px minmax(420px, var(--col3)); }}
    }}
    @media (max-width: 1100px) {{
      .toolbar {{ grid-template-columns: 1fr; }}
      .layout, .top-layout, .bottom-layout {{ grid-template-columns: 1fr; }}
      .splitter {{ display:none; }}
      .panel {{ min-height: auto; }}
      .tree-wrap, .tbl-wrap, .diagram-wrap {{ max-height: 60vh; }}
      .hover-card {{ width: calc(100vw - 24px); right: 12px; }}
      .network-wrap, .sunburst-wrap {{ height: 60vh; }}
    }}
  </style>
</head>
<body>
  <div class=\"wrap\">
    <div class=\"toolbar\">
      <input id=\"q\" placeholder=\"Filter: path, owner, access, person...\" />
      <div class=\"grp\" id=\"scopeGrp\">
        <button data-scope=\"both\" class=\"active\">All Files</button>
        <button data-scope=\"my\">My Drive</button>
        <button data-scope=\"shared\">Shared With Me</button>
      </div>
      <div class=\"grp\" id=\"riskGrp\">
        <button data-risk=\"all\" class=\"active\">All Risks</button>
        <button data-risk=\"high\">High Risk</button>
        <button data-risk=\"medium_plus\">Medium+</button>
        <button data-risk=\"public_domain\">Public/Domain</button>
        <button data-risk=\"external\">External</button>
      </div>
      <button id=\"clearSel\" style=\"border:1px solid var(--line);background:#fff;border-radius:8px;padding:8px 10px;cursor:pointer;font-weight:700;\">Clear Selection</button>
      <div class=\"muted\" id=\"meta\">Sort: path/name | click selects + highlights</div>
    </div>
    <section class=\"risk-summary\">
      <div class=\"risk-top\">
        <div><b>Global Security Score</b> <span id=\"riskPct\">-</span></div>
        <div class=\"muted\" id=\"riskLabel\">Rules: public/domain/external/writer/mass-sharing</div>
      </div>
      <div class=\"risk-bar-wrap\"><div id=\"riskBar\" class=\"risk-bar\"></div></div>
      <div class=\"risk-stats\">
        <span class=\"risk-pill\" id=\"riskStatTotal\">Total: -</span>
        <span class=\"risk-pill\" id=\"riskStatHigh\">High: -</span>
        <span class=\"risk-pill\" id=\"riskStatMed\">Medium: -</span>
        <span class=\"risk-pill\" id=\"riskStatLow\">Low: -</span>
        <span class=\"risk-pill\" id=\"riskStatPublic\">Public/Domain: -</span>
        <span class=\"risk-pill\" id=\"riskStatExternal\">External share: -</span>
      </div>
      <div class=\"risk-rules\">
        HIGH: score 70+ | MEDIUM: 35-69 | LOW: 0-34
      </div>
    </section>
    <div id=\"breadcrumbsBar\" class=\"breadcrumbs\">Path: All</div>

    <div class=\"top-layout\">
      <section class=\"panel\">
        <div class=\"title\">Path Tree (Folders)</div>
        <div class=\"tree-tools\">
          <button id=\"expandAll\">Expand All</button>
          <button id=\"collapseAll\">Collapse All</button>
        </div>
        <div class=\"tree-wrap\" id=\"tree\">{tree_html}</div>
      </section>
      <section class=\"panel\">
        <div class=\"title\">Sunburst (Drive Structure)</div>
        <div class=\"master-wrap sunburst-wrap\">
          <div id=\"masterHost\" class=\"master-host\"></div>
          <div id=\"sunburstSelection\" class=\"selection-strip\">Selected item: none</div>
          <div class=\"master-toolbar\">
            <button id=\"zoomOutBtn\">-</button>
            <button id=\"zoomInBtn\">+</button>
            <button id=\"zoomResetBtn\">Reset Zoom</button>
          </div>
          <div id=\"masterMode\" class=\"master-mode\">View: Folder map</div>
          <div id=\"masterHelp\" class=\"master-help\">Scroll: zoom | Click: select + highlight</div>
          <div id=\"masterInfo\" class=\"master-info\">Hover any segment for details.</div>
          <div id=\"buildBadge\" class=\"build-badge\"></div>
        </div>
      </section>
    </div>

    <div class=\"bottom-layout\">
      <section class=\"panel\">
        <div class=\"title\">Share Network</div>
        <div class=\"network-wrap\">
          <div id=\"networkSelection\" class=\"network-selection-strip\">Selected item: none</div>
          <div id=\"networkHost\" class=\"network-host\"></div>
        </div>
      </section>
      <section class=\"panel\">
        <div class=\"title\">Live Context</div>
        <div class=\"context-panel\">
          <div id=\"liveContext\" class=\"context-live\">Loading context...</div>
          <div class=\"context-box\">Selection details appear here in plain English. Use this panel to understand what you selected and where access risk is concentrated.</div>
        </div>
      </section>
    </div>

    <section class=\"legacy-hidden\">
      <div class=\"layout\">
        <section class=\"panel\">
          <div class=\"title\">Item Access Matrix (hidden helper)</div>
          <div class=\"tbl-tools\">
            <button id=\"copyVisiblePaths\">Copy Visible Paths</button>
            <button id=\"loadMoreRows\">Load More</button>
            <div class=\"hint\" id=\"matrixHint\">Lazy matrix loading</div>
          </div>
          <div class=\"tbl-wrap\">
            <table>
              <thead>
                <tr>
                  <th style=\"width:40%\">Path/Name</th>
                  <th style=\"width:13%\">Type</th>
                  <th style=\"width:18%\">Owner</th>
                  <th style=\"width:9%\">Access</th>
                  <th style=\"width:6%\">#</th>
                  <th style=\"width:8%\">Risk</th>
                  <th style=\"width:8%\">Copy</th>
                </tr>
              </thead>
              <tbody id=\"matrixBody\"></tbody>
            </table>
          </div>
        </section>
        <section class=\"panel\">
          <div id=\"diagram\" class=\"diagram-wrap\"></div>
        </section>
      </div>
    </section>
  </div>

  <div class=\"hover-card\" id=\"hoverCard\"></div>

<script src=\"https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js\"></script>
<script>
const rows = {rows_json};
const BUILD_ID = "{_safe(build_id)}";
let selectedPath = "";
let selectedItemId = "";
let selectedItemPath = "";
let selectedScope = "both";
let selectedRisk = "all";
let textFilter = "";
let hoverTimer = null;
let hideTimer = null;
let hoverCardPinned = false;
let activeHoverEl = null;
let lastMouse = {{ x: 24, y: 24 }};
let hoverPos = {{ x: 24, y: 24 }};
let followTimer = null;
let masterChart = null;
let networkChart = null;
let masterTree = null;
let masterView = 'sunburst';
let sunZoom = 1;
const masterHost = document.getElementById('masterHost');
const networkHost = document.getElementById('networkHost');
const masterMode = document.getElementById('masterMode');
const masterHelp = document.getElementById('masterHelp');
const masterInfo = document.getElementById('masterInfo');
const liveContext = document.getElementById('liveContext');
const buildBadge = document.getElementById('buildBadge');
const sunburstSelection = document.getElementById('sunburstSelection');
const networkSelection = document.getElementById('networkSelection');
const modeSunburstBtn = document.getElementById('modeSunburst');
const modeNetworkBtn = document.getElementById('modeNetwork');
const zoomInBtn = document.getElementById('zoomInBtn');
const zoomOutBtn = document.getElementById('zoomOutBtn');
const zoomResetBtn = document.getElementById('zoomResetBtn');

const q = document.getElementById('q');
const meta = document.getElementById('meta');
const treeWrap = document.getElementById('tree');
const breadcrumbsBar = document.getElementById('breadcrumbsBar');
const matrixBody = document.getElementById('matrixBody');
const diagram = document.getElementById('diagram');
const hoverCard = document.getElementById('hoverCard');
const matrixHint = document.getElementById('matrixHint');
const loadMoreRowsBtn = document.getElementById('loadMoreRows');
const copyVisiblePathsBtn = document.getElementById('copyVisiblePaths');
const riskBar = document.getElementById('riskBar');
const riskPct = document.getElementById('riskPct');
const riskLabel = document.getElementById('riskLabel');
const riskStatTotal = document.getElementById('riskStatTotal');
const riskStatHigh = document.getElementById('riskStatHigh');
const riskStatMed = document.getElementById('riskStatMed');
const riskStatLow = document.getElementById('riskStatLow');
const riskStatPublic = document.getElementById('riskStatPublic');
const riskStatExternal = document.getElementById('riskStatExternal');

let matrixAllRows = [];
let matrixRenderedRows = 0;
const MATRIX_BATCH = 260;
let centerTreeOnNextRender = false;
let preserveTreeToggleOnce = false;

function scopeOk(r) {{
  if (selectedScope === 'both') return true;
  if (selectedScope === 'my') return r.root_path === 'My Drive';
  if (selectedScope === 'shared') return r.root_path === 'SharedWithMe';
  return true;
}}

function riskOk(r) {{
  if (selectedRisk === 'all') return true;
  if (selectedRisk === 'high') return r.risk_level === 'HIGH';
  if (selectedRisk === 'medium_plus') return r.risk_level === 'HIGH' || r.risk_level === 'MEDIUM';
  if (selectedRisk === 'public_domain') return r.access_scope === 'PUBLIC' || r.access_scope === 'DOMAIN';
  if (selectedRisk === 'external') return (r.external_target_count || 0) > 0 || (r.risk_flags || '').includes('EXTERNAL_');
  return true;
}}

function pathOk(r) {{
  return true;
}}

function textOk(r) {{
  if (!textFilter) return true;
  const hay = [r.path, r.original_owner_email, r.shared_by_email, r.access_scope, r.access_targets, r.permission_entries, r.file_category, r.risk_level].join(' ').toLowerCase();
  return hay.includes(textFilter);
}}

function filteredRows() {{
  return rows
    .filter(r => scopeOk(r) && riskOk(r) && pathOk(r) && textOk(r))
    .sort((a,b) => (a.path + '|' + a.name).localeCompare(b.path + '|' + b.name));
}}

function masterFocusRows() {{
  const base = rows.filter(r => scopeOk(r) && riskOk(r) && textOk(r));
  if (selectedItemId) {{
    return base.filter(r => r.item_id === selectedItemId);
  }}
  if (selectedItemPath) {{
    const exact = base.filter(r => r.path === selectedItemPath);
    if (exact.length && exact.some(r => r.item_kind === 'FILE')) return exact;
    return base.filter(r => r.path === selectedItemPath || r.path.startsWith(selectedItemPath + '/'));
  }}
  if (selectedPath) {{
    return base.filter(r => r.path === selectedPath || r.path.startsWith(selectedPath + '/'));
  }}
  return base.sort((a,b) => (a.path + '|' + a.name).localeCompare(b.path + '|' + b.name));
}}

function selectedItemRow() {{
  if (selectedItemId) return rows.find(r => r.item_id === selectedItemId) || null;
  if (selectedItemPath) return rows.find(r => r.path === selectedItemPath) || null;
  return null;
}}

function riskBadge(level) {{
  return `<span class=\"badge risk-${{level}}\">${{level}}</span>`;
}}

function short(v, n=48) {{
  return v && v.length > n ? v.slice(0,n-1) + '...' : (v || '');
}}

function setMasterInfo(lines) {{
  masterInfo.innerHTML = lines.map(x => `<div>${{x}}</div>`).join('');
}}

function selectedLabel() {{
  const row = selectedItemRow();
  if (row) return `${{row.name || 'item'}} | ${{row.path}}`;
  if (selectedItemPath) return selectedItemPath;
  return 'None';
}}

function selectedItemText() {{
  const row = selectedItemRow();
  if (row) return `Selected item: ${{row.name || '-'}} | Path: ${{row.path || '-'}}`;
  if (selectedItemPath) return `Selected item path: ${{selectedItemPath}}`;
  return 'Selected item: none';
}}

function renderSelectionStrips() {{
  const txt = selectedItemText();
  if (sunburstSelection) sunburstSelection.textContent = txt;
  if (networkSelection) networkSelection.textContent = txt;
}}

function currentSelectedPath() {{
  const row = selectedItemRow();
  return row?.path || selectedItemPath || selectedPath || '';
}}

function renderBreadcrumbs() {{
  if (!breadcrumbsBar) return;
  const p = currentSelectedPath();
  if (!p) {{
    breadcrumbsBar.innerHTML = 'Path: <b>All</b>';
    return;
  }}
  const parts = p.split('/').filter(Boolean);
  const chunks = [];
  chunks.push(`<button class="crumb-btn" data-crumb-path="">All</button>`);
  for (let i = 0; i < parts.length; i++) {{
    const sub = parts.slice(0, i + 1).join('/');
    chunks.push(`<span class="crumb-sep">/</span>`);
    chunks.push(`<button class="crumb-btn" data-crumb-path="${{sub}}">${{parts[i]}}</button>`);
  }}
  breadcrumbsBar.innerHTML = chunks.join('');
}}

function renderLiveContext(visibleData) {{
  const viewLabel = selectedScope === 'my'
    ? 'My Drive'
    : (selectedScope === 'shared' ? 'Shared With Me' : 'All Files');
  const data = visibleData && visibleData.length ? visibleData : filteredRows();
  const high = data.filter(r => r.risk_level === 'HIGH').length;
  const external = data.filter(r => (r.external_target_count || 0) > 0).length;
  const selRow = selectedItemRow();
  const selName = selRow ? (selRow.name || '-') : '-';
  const selPath = selRow ? (selRow.path || '-') : (selectedItemPath || '-');
  const sel = selectedLabel();
  const branch = selectedPath || 'All paths';
  liveContext.innerHTML = [
    '<b>Live Context</b>',
    `You are viewing: ${{viewLabel}}`,
    `Selected path: ${{branch}}`,
    `Items: ${{data.length}}`,
    `High risk: ${{high}}`,
    `External shares: ${{external}}`,
    `Selection: ${{selName}}`,
    `Selected full path: ${{selPath}}`,
    `Master mode: ${{masterView === 'network' ? 'Share Network' : 'Sunburst'}}`,
  ].map(x => `<div>${{x}}</div>`).join('');
}}

const SUN_LABEL_MIN_ANGLE = 7.0;
const SUN_LABEL_MIN_RING_PX = 14.0;
const SUN_LABEL_AVG_CHAR_PX = 6.8;
const SUN_LABEL_MIN_CHARS = 4;

function matrixRowHtml(r) {{
  return `
    <tr data-item-id="${{r.item_id}}" data-path="${{r.path}}">
      <td title="${{r.path}}"><code>${{short(r.path, 86)}}</code></td>
      <td>${{r.item_kind}} / ${{r.file_category}}</td>
      <td title="${{r.original_owner_email}}">${{short(r.original_owner_email, 28)}}</td>
      <td>${{r.access_scope}}</td>
      <td>${{r.shared_with_count}}</td>
      <td>${{riskBadge(r.risk_level)}} <span class="muted">(${{r.risk_score || 0}})</span></td>
      <td><button class="copy-row-path" data-copy-path="${{r.path}}">Copy</button></td>
    </tr>
  `;
}}

function renderMatrix(data) {{
  matrixAllRows = data.slice();
  matrixRenderedRows = 0;
  matrixBody.innerHTML = '';
  appendMatrixRows();
}}

function appendMatrixRows() {{
  const next = matrixAllRows.slice(matrixRenderedRows, matrixRenderedRows + MATRIX_BATCH);
  if (!next.length) return;
  matrixBody.insertAdjacentHTML('beforeend', next.map(matrixRowHtml).join(''));
  matrixRenderedRows += next.length;
  matrixHint.textContent = `Loaded ${{matrixRenderedRows}} / ${{matrixAllRows.length}} rows`;
  loadMoreRowsBtn.disabled = matrixRenderedRows >= matrixAllRows.length;
}}

function renderRiskSummary(sourceRows) {{
  const set = (sourceRows && sourceRows.length ? sourceRows : rows);
  const total = set.length || 1;
  const high = set.filter(r => r.risk_level === 'HIGH').length;
  const med = set.filter(r => r.risk_level === 'MEDIUM').length;
  const low = set.filter(r => r.risk_level === 'LOW').length;
  const publicDomain = set.filter(r => r.access_scope === 'PUBLIC' || r.access_scope === 'DOMAIN').length;
  const external = set.filter(r => (r.external_target_count || 0) > 0).length;
  const avgRisk = set.reduce((s, r) => s + (r.risk_score || 0), 0) / total;
  const securityPct = Math.max(0, Math.min(100, 100 - avgRisk));

  riskBar.style.width = `${{securityPct.toFixed(1)}}%`;
  riskPct.textContent = `${{securityPct.toFixed(1)}}%`;
  riskLabel.textContent = securityPct >= 80
    ? 'Status: stable (lower average risk).'
    : (securityPct >= 60 ? 'Status: caution (visible risk hotspots).' : 'Status: critical (cleanup needed now).');
  riskStatTotal.textContent = `Total: ${{set.length}}`;
  riskStatHigh.textContent = `High: ${{high}}`;
  riskStatMed.textContent = `Medium: ${{med}}`;
  riskStatLow.textContent = `Low: ${{low}}`;
  riskStatPublic.textContent = `Public/Domain: ${{publicDomain}}`;
  riskStatExternal.textContent = `External share: ${{external}}`;
}}

function focusLabel() {{
  const srow = selectedItemRow();
  if (srow) {{
    return `Selection: ${{srow.name || '(item)'}} | ${{srow.path}}`;
  }}
  if (selectedItemPath) return `Selection: ${{selectedItemPath}}`;
  if (selectedPath) return `Selected path: ${{selectedPath}}`;
  return 'Selection: none';
}}

function attachHover(targetEl, payload) {{
  const targetPos = (x, y) => {{
    const w = 390;
    const h = Math.min(window.innerHeight * 0.68, 420);
    const left = Math.max(10, Math.min(window.innerWidth - w - 12, x + 20));
    const top = Math.max(10, Math.min(window.innerHeight - h - 12, y + 18));
    return {{ left, top }};
  }};

  const positionCard = (x, y, soft = false) => {{
    const pos = targetPos(x, y);
    if (!soft || !hoverCard.classList.contains('show')) {{
      hoverPos = {{ x: pos.left, y: pos.top }};
    }} else {{
      hoverPos.x = hoverPos.x + (pos.left - hoverPos.x) * 0.28;
      hoverPos.y = hoverPos.y + (pos.top - hoverPos.y) * 0.28;
    }}
    const left = hoverPos.x;
    const top = hoverPos.y;
    hoverCard.style.left = left + 'px';
    hoverCard.style.top = top + 'px';
  }};

  const show = () => {{
    hoverCard.innerHTML = `
      <div class='hline'><b>Path:</b> ${{payload.path || '-'}} </div>
      <div class='hline'><b>Name:</b> ${{payload.name || payload.label || '-'}} </div>
      <div class='hline'><b>Type:</b> ${{payload.item_kind || 'FOLDER'}} / ${{payload.file_category || '-'}} </div>
      <div class='hline'><b>Owner:</b> ${{payload.original_owner_email || '-'}} </div>
      <div class='hline'><b>Shared by:</b> ${{payload.shared_by_email || '-'}} </div>
      <div class='hline'><b>Access:</b> ${{payload.access_scope || '-'}} </div>
      <div class='hline'><b>Shared with:</b> ${{payload.access_targets || '-'}} </div>
      <div class='hline'><b>Risk:</b> ${{payload.risk_level || '-'}} (score: ${{payload.risk_score || 0}}) ${{payload.risk_reason ? '(' + payload.risk_reason + ')' : ''}}</div>
      <div class='hline'><b>Risk flags:</b> ${{payload.risk_flags || '-'}}</div>
      <div class='hline'><b>Items in branch:</b> ${{payload.items || '-'}} </div>
    `;
    positionCard(lastMouse.x, lastMouse.y, false);
    hoverCard.classList.add('show');
  }};

  targetEl.addEventListener('mouseenter', (e) => {{
    activeHoverEl = targetEl;
    targetEl.classList.add('focus');
    lastMouse = {{ x: e.clientX, y: e.clientY }};
    clearTimeout(hoverTimer);
    clearTimeout(hideTimer);
    hoverTimer = setTimeout(show, 550);
    highlightMatrix(payload);
  }});

  targetEl.addEventListener('mousemove', (e) => {{
    lastMouse = {{ x: e.clientX, y: e.clientY }};
    if (hoverCard.classList.contains('show') && !hoverCardPinned) {{
      if (!followTimer) {{
        followTimer = setTimeout(() => {{
          positionCard(lastMouse.x, lastMouse.y, true);
          followTimer = null;
        }}, 90);
      }}
    }}
  }});

  targetEl.addEventListener('mouseleave', () => {{
    if (activeHoverEl === targetEl) {{
      activeHoverEl = null;
    }}
    if (followTimer) {{
      clearTimeout(followTimer);
      followTimer = null;
    }}
    targetEl.classList.remove('focus');
    clearTimeout(hoverTimer);
    hideTimer = setTimeout(() => {{
      if (!hoverCardPinned && !activeHoverEl) {{
        hoverCard.classList.remove('show');
      }}
    }}, 900);
    clearMatrixHighlight();
  }});

  targetEl.addEventListener('click', () => {{
    const p = payload.path || '';
    if (!p) return;
    selectedItemId = payload.item_id || '';
    selectedItemPath = p;
    centerTreeOnNextRender = true;
    rerender();
  }});
}}

hoverCard.addEventListener('mouseenter', () => {{
  hoverCardPinned = true;
  if (followTimer) {{
    clearTimeout(followTimer);
    followTimer = null;
  }}
  clearTimeout(hideTimer);
}});
hoverCard.addEventListener('mouseleave', () => {{
  hoverCardPinned = false;
  hideTimer = setTimeout(() => {{
    if (!activeHoverEl) hoverCard.classList.remove('show');
  }}, 420);
}});

function getDepth(path) {{
  return (path || '').split('/').filter(Boolean).length;
}}

function depthClass(path, kind) {{
  if (kind !== 'FOLDER') return '';
  const d = getDepth(path);
  if (d <= 1) return 'depth-1';
  if (d === 2) return 'depth-2';
  if (d === 3) return 'depth-3';
  if (d === 4) return 'depth-4';
  return 'depth-5';
}}

function buildDiagramTree(data) {{
  const root = {{ name: 'ROOT', path: '', kind: 'ROOT', items: 0, high: 0, children: new Map(), row: null }};

  for (const r of data) {{
    const parts = (r.path || '').split('/').filter(Boolean);
    if (!parts.length) continue;

    let node = root;
    node.items += 1;
    if (r.risk_level === 'HIGH') node.high += 1;

    for (let i = 0; i < parts.length; i++) {{
      const seg = parts[i];
      const full = parts.slice(0, i + 1).join('/');
      const isLast = i === parts.length - 1;
      const kind = isLast ? r.item_kind : 'FOLDER';

      if (!node.children.has(seg)) {{
        node.children.set(seg, {{ name: seg, path: full, kind, items: 0, high: 0, children: new Map(), row: null }});
      }}

      node = node.children.get(seg);
      node.items += 1;
      if (r.risk_level === 'HIGH') node.high += 1;
      if (isLast) node.row = r;
    }}
  }}

  return root;
}}

function renderNode(node) {{
  const children = Array.from(node.children.values()).sort((a,b) => {{
    if (a.kind !== b.kind) return a.kind === 'FOLDER' ? -1 : 1;
    return a.name.localeCompare(b.name);
  }});

  if (!children.length) return '';

  return `<ul>${{children.map(ch => {{
    const isFolder = ch.kind === 'FOLDER';
    const klass = isFolder ? `folder ${{depthClass(ch.path, ch.kind)}}` : 'file';
    const depth = getDepth(ch.path);
    const parentPath = ch.path.includes('/') ? ch.path.split('/').slice(0, -1).join('/') : 'ROOT';
    const row = ch.row || {{
      label: ch.name,
      path: ch.path,
      items: ch.items,
      risk_level: ch.high > 0 ? 'HIGH' : 'LOW',
      risk_reason: ch.high > 0 ? 'Contains higher-risk items' : 'No high-risk items'
    }};

    const card = `
      <div class=\"dnode ${{klass}}\" data-node-path=\"${{ch.path}}\" data-item-id=\"${{ch.row && ch.row.item_id ? ch.row.item_id : ''}}\">
        <div class=\"n1\">${{isFolder ? 'Folder' : 'File'}} | Depth ${{depth}} | ${{isFolder ? 'Structure' : (ch.row.file_category || 'Item')}}</div>
        <div class=\"n2\">${{ch.name}}</div>
        <div class=\"n3\">Parent: ${{parentPath}} | Path: ${{ch.path}} | Items: ${{ch.items}} | High risk: ${{ch.high}}</div>
      </div>
    `;

    if (isFolder) {{
      return `<li><details class=\"dbranch\"><summary>${{card}}</summary>${{renderNode(ch)}}</details></li>`;
    }}
    return `<li>${{card}}</li>`;
  }}).join('')}}</ul>`;
}}

function renderDiagram(data) {{
  const previewOnly = data.length > 900;
  const sourceData = previewOnly ? data.filter(r => getDepth(r.path) <= 2) : data;

  let top = `<div class=\"dnode folder depth-1\" style=\"max-width:520px\">
    <div class=\"n1\">Drive Overview | Depth 0</div>
    <div class=\"n2\">Selected path: ${{selectedPath || 'All paths'}} </div>
    <div class=\"n3\">Visible items: ${{data.length}}</div>
  </div>`;
  if (previewOnly) {{
    top += `<div class="muted" style="margin:8px 0 4px;">Large tree mode: top levels only. Click a path on the left for deeper drill-down.</div>`;
  }}

  const previewTree = buildDiagramTree(sourceData);
  const previewRoots = Array.from(previewTree.children.values()).sort((a,b)=>a.name.localeCompare(b.name));
  const content = `<div class=\"d-tree\">${{top}}<ul>${{previewRoots.map(r => `<li><div class=\"dnode folder ${{depthClass(r.path, 'FOLDER')}}\" data-node-path=\"${{r.path}}\" data-item-id=\"\"><div class=\"n1\">Root branch | Depth ${{getDepth(r.path)}}</div><div class=\"n2\">${{r.name}}</div><div class=\"n3\">Path: ${{r.path}} | Items: ${{r.items}} | High risk: ${{r.high}}</div></div>${{renderNode(r)}}</li>`).join('')}}</ul></div>`;
  diagram.innerHTML = content;

  const idToRow = new Map(sourceData.map(r => [r.path, r]));
  for (const card of diagram.querySelectorAll('.dnode[data-node-path]')) {{
    const p = card.dataset.nodePath;
    const exact = idToRow.get(p);
    const payload = exact || {{ label: p.split('/').pop(), path: p, items: '-', risk_level: '-', risk_reason: '' }};
    if (card.dataset.itemId) payload.item_id = card.dataset.itemId;
    if ((selectedItemId && payload.item_id && payload.item_id === selectedItemId) || (!selectedItemId && selectedItemPath && payload.path === selectedItemPath)) {{
      card.classList.add('selected');
    }}
    attachHover(card, payload);
  }}
}}

function accessColor(scope) {{
  if (scope === 'PUBLIC') return '#dc2626';
  if (scope === 'DOMAIN') return '#ea580c';
  if (scope === 'SHARED_USERS') return '#d97706';
  return '#64748b';
}}

function hexToRgb(hex) {{
  const h = String(hex || '').replace('#', '');
  if (h.length !== 6) return [111, 131, 149];
  return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
}}

function rgbToHex(r, g, b) {{
  const to2 = (x) => Math.max(0, Math.min(255, Math.round(x))).toString(16).padStart(2, '0');
  return `#${{to2(r)}}${{to2(g)}}${{to2(b)}}`;
}}

function mixHex(base, target, t) {{
  const [r1, g1, b1] = hexToRgb(base);
  const [r2, g2, b2] = hexToRgb(target);
  return rgbToHex(
    r1 + (r2 - r1) * t,
    g1 + (g2 - g1) * t,
    b1 + (b2 - b1) * t
  );
}}

function principalLabel(raw) {{
  const v = String(raw || '');
  if (!v) return '-';
  if (v.startsWith('group:')) return `group:${{v.slice(6)}}`;
  if (v.startsWith('domain:')) return `domain:${{v.slice(7)}}`;
  if (v === 'anyone(public)') return 'public-link';
  return v;
}}

function shortPrincipal(raw, n=26) {{
  const s = principalLabel(raw);
  return s.length > n ? s.slice(0, n - 3) + '...' : s;
}}

function activateMasterModeButtons() {{
  if (modeSunburstBtn) modeSunburstBtn.classList.toggle('active', masterView === 'sunburst');
  if (modeNetworkBtn) modeNetworkBtn.classList.toggle('active', masterView === 'network');
}}

function buildMasterSunburst(allRows) {{
  const root = {{
    name: 'DRIVE',
    path: '',
    value: 0,
    itemCount: 0,
    highCount: 0,
    childrenMap: new Map(),
    ownerSet: new Set(),
    accessMix: new Map(),
  }};

  function ensureChild(parent, segPath, segName) {{
    if (!parent.childrenMap.has(segName)) {{
      parent.childrenMap.set(segName, {{
        name: segName,
        path: segPath,
        value: 0,
        itemCount: 0,
        highCount: 0,
        kind: 'FOLDER',
        childrenMap: new Map(),
        ownerSet: new Set(),
        accessMix: new Map(),
      }});
    }}
    return parent.childrenMap.get(segName);
  }}

  for (const r of allRows) {{
    const parts = (r.path || '').split('/').filter(Boolean);
    if (!parts.length) continue;

    let node = root;
    node.value += 1;
    node.itemCount += 1;
    if (r.risk_level === 'HIGH') node.highCount += 1;
    if (r.original_owner_email) node.ownerSet.add(r.original_owner_email);
    node.accessMix.set(r.access_scope, (node.accessMix.get(r.access_scope) || 0) + 1);

    for (let i = 0; i < parts.length; i++) {{
      const seg = parts[i];
      const p = parts.slice(0, i + 1).join('/');
      const child = ensureChild(node, p, seg);
      child.value += 1;
      child.itemCount += 1;
      if (r.risk_level === 'HIGH') child.highCount += 1;
      if (r.original_owner_email) child.ownerSet.add(r.original_owner_email);
      child.accessMix.set(r.access_scope, (child.accessMix.get(r.access_scope) || 0) + 1);
      if (i === parts.length - 1 && r.item_kind === 'FILE') child.kind = 'FILE';
      node = child;
    }}
  }}

  function dominantAccess(accessMix) {{
    let best = 'PRIVATE';
    let max = -1;
    for (const [k, v] of accessMix.entries()) {{
      if (v > max) {{ max = v; best = k; }}
    }}
    return best;
  }}

  const familyPalette = ['#6f8395', '#9a5b86', '#6a7e8f', '#8d5f82'];

  function toNode(node, depth = 0, familyColor = '#6f8395', familyIdx = 0) {{
    const children = Array.from(node.childrenMap.values())
      .sort((a, b) => b.value - a.value)
      .map((ch, idx) => {{
        const isRootChild = depth === 0;
        const nextFamilyIdx = isRootChild ? (familyIdx + idx) : familyIdx;
        const nextFamilyColor = isRootChild
          ? familyPalette[nextFamilyIdx % familyPalette.length]
          : familyColor;
        return toNode(ch, depth + 1, nextFamilyColor, nextFamilyIdx);
      }});

    const highRatio = node.itemCount ? (node.highCount / node.itemCount) : 0;
    const access = dominantAccess(node.accessMix);
    const shade = Math.min(0.36, Math.max(0.02, depth * 0.07));
    const color = mixHex(familyColor, '#f8fafc', shade);
    const borderColor = mixHex(color, '#ffffff', 0.55);

    return {{
      name: node.name,
      value: Math.max(node.value, 1),
      path: node.path,
      kind: node.kind || 'FOLDER',
      itemCount: node.itemCount,
      highCount: node.highCount,
      owners: node.ownerSet.size,
      access,
      itemStyle: {{ color, borderColor, borderWidth: 1.4 }},
      children,
      riskRatio: highRatio,
    }};
  }}

  return [toNode(root, 0, '#6f8395', 0)];
}}

function _pct(v) {{
  if (typeof v === 'number') return v;
  return parseFloat(String(v || '0').replace('%', '')) || 0;
}}

function _truncateLabel(text, maxChars) {{
  const src = String(text || '');
  if (src.length <= maxChars) return src;
  if (maxChars <= 3) return '';
  return src.slice(0, maxChars - 3) + '...';
}}

function applySunLabelRules(tree, innerPct, outerPct) {{
  const hostRect = masterHost.getBoundingClientRect();
  const radiusPx = Math.max(120, Math.min(hostRect.width, hostRect.height) * 0.5);
  const total = (tree?.[0]?.value || 1);

  function walk(node, parentValue, depth, r0Pct, r1Pct) {{
    const nodeValue = Math.max(1, Number(node.value || 1));
    const angleRad = (Math.PI * 2 * nodeValue) / Math.max(1, parentValue);
    const angleDeg = angleRad * (180 / Math.PI);
    const r0Px = (_pct(r0Pct) / 100) * radiusPx;
    const r1Px = (_pct(r1Pct) / 100) * radiusPx;
    const ringPx = Math.max(1, r1Px - r0Px);
    const midRadius = (r0Px + r1Px) * 0.5;
    const arcPx = Math.max(1, angleRad * midRadius);
    const maxCharsArc = Math.floor((arcPx - 8) / SUN_LABEL_AVG_CHAR_PX);
    const maxCharsRing = Math.floor((ringPx - 4) / 1.9);
    const maxChars = Math.max(0, Math.min(maxCharsArc, maxCharsRing, 42));
    const canShow = angleDeg >= SUN_LABEL_MIN_ANGLE && ringPx >= SUN_LABEL_MIN_RING_PX && maxChars >= SUN_LABEL_MIN_CHARS;

    node.labelText = canShow ? _truncateLabel(node.name, maxChars) : '';
    node.__labelAllowed = canShow;

    const children = node.children || [];
    if (!children.length) return;
    const nextR0 = Math.min(r1Pct, innerPct + depth * ((outerPct - innerPct) / 4));
    const nextR1 = Math.min(outerPct, innerPct + (depth + 1) * ((outerPct - innerPct) / 4));
    for (const ch of children) {{
      walk(ch, nodeValue, depth + 1, nextR0, nextR1);
    }}
  }}

  for (const n of tree || []) {{
    walk(n, total, 1, innerPct, innerPct + ((outerPct - innerPct) / 4));
  }}
  return tree;
}}

function setMasterModeLabel() {{
  const flabel = focusLabel();
  if (masterView === 'network') {{
    masterMode.textContent = `View: Share Network | ${{flabel}}`;
    return;
  }}
  const modeText = sunZoom > 1.18 ? 'View: Detailed (zoomed)' : 'View: Folder map';
  masterMode.textContent = `${{modeText}} | ${{flabel}}`;
}}

function initMasterChart() {{
  if (!window.echarts) {{
    masterHelp.textContent = 'Master chart not loaded (ECharts CDN unavailable).';
    return;
  }}
  masterChart = window.echarts.init(masterHost, null, {{ renderer: 'canvas' }});
  window.addEventListener('resize', () => masterChart && masterChart.resize());

  masterHost.addEventListener('wheel', (e) => {{
    if (masterView !== 'sunburst') return;
    e.preventDefault();
    const factor = e.deltaY < 0 ? 1.08 : 0.92;
    sunZoom = Math.max(0.75, Math.min(2.4, sunZoom * factor));
    renderMasterSunburst();
    highlightMaster(masterFocusRows());
  }}, {{ passive: false }});
}}

function initNetworkChart() {{
  if (!window.echarts || !networkHost) return;
  networkChart = window.echarts.init(networkHost, null, {{ renderer: 'canvas' }});
  window.addEventListener('resize', () => networkChart && networkChart.resize());
}}

function renderMasterSunburst() {{
  if (!masterChart) return;
  masterView = 'sunburst';
  activateMasterModeButtons();
  const focusRows = masterFocusRows();
  const sourceRows = focusRows.length ? focusRows : rows;
  let sun = buildMasterSunburst(sourceRows);
  masterTree = sun;

  const inner = Math.max(2, Math.min(24, 10 + (1 - sunZoom) * 10));
  const outer = Math.max(82, Math.min(98, 91 + sunZoom * 2.0));
  const f1 = 12;
  const f2 = 11;
  const f3 = 10;
  const f4 = 9;

  const r1 = Math.min(inner + 16, outer - 50);
  const r2 = Math.min(inner + 34, outer - 30);
  const r3 = Math.min(inner + 52, outer - 14);
  sun = applySunLabelRules(sun, inner, outer);

  masterChart.setOption({{
    animationDuration: 320,
    animationDurationUpdate: 280,
    tooltip: {{
      trigger: 'item',
      backgroundColor: 'rgba(10,18,32,0.84)',
      borderColor: 'rgba(191,219,254,0.42)',
      textStyle: {{ color: '#fff', fontSize: 12 }},
      formatter: (p) => {{
        const d = p.data || {{}};
        const depth = (d.path || '').split('/').filter(Boolean).length;
        return [
          `<b>${{d.name || 'DRIVE'}}</b>`,
          `Path: ${{d.path || 'ROOT'}}`,
          `Depth: ${{depth}}`,
          `Type: ${{d.kind || 'FOLDER'}}`,
          `Items: ${{d.itemCount || 0}}`,
          `High risk: ${{d.highCount || 0}}`,
          `Dominant access: ${{d.access || 'PRIVATE'}}`,
          `Owners: ${{d.owners || 0}}`,
        ].join('<br/>');
      }},
    }},
    series: [{{
      type: 'sunburst',
      nodeClick: 'rootToNode',
      center: ['50%', '50%'],
      radius: [`${{inner}}%`, `${{outer}}%`],
      sort: (a, b) => b.getValue() - a.getValue(),
      emphasis: {{ focus: 'ancestor' }},
      blur: {{ itemStyle: {{ opacity: 0.62 }} }},
      label: {{
        rotate: 0,
        minAngle: 0,
        fontSize: f2,
        overflow: 'none',
        width: 180,
        color: '#0f172a',
        formatter: (p) => (p?.data?.labelText || ''),
      }},
      labelLayout: (params) => {{
        const lr = params?.labelRect;
        const rr = params?.rect;
        if (!lr || !rr) return {{ hide: true }};
        const outside = lr.x < rr.x || lr.y < rr.y || (lr.x + lr.width) > (rr.x + rr.width) || (lr.y + lr.height) > (rr.y + rr.height);
        return {{
          hideOverlap: true,
          hide: outside || !params?.data?.__labelAllowed,
        }};
      }},
      itemStyle: {{ borderColor: 'rgba(255,255,255,0.78)', borderWidth: 1.2 }},
      levels: [
        {{}},
        {{ r0: `${{inner}}%`, r: `${{r1}}%`, itemStyle: {{ borderWidth: 1.5 }}, label: {{ rotate: 0, fontWeight: 800, fontSize: f1, color:'#111827' }} }},
        {{ r0: `${{r1}}%`, r: `${{r2}}%`, itemStyle: {{ borderWidth: 1.4 }}, label: {{ fontSize: f2, color:'#0f172a' }} }},
        {{ r0: `${{r2}}%`, r: `${{r3}}%`, itemStyle: {{ borderWidth: 1.2 }}, label: {{ fontSize: f3, color:'#0f172a' }} }},
        {{ r0: `${{r3}}%`, r: `${{outer}}%`, itemStyle: {{ borderWidth: 1.1 }}, label: {{ fontSize: f4, color:'#0f172a' }} }},
      ],
      data: sun,
    }}],
  }}, true);

  setMasterModeLabel();
  masterHelp.textContent = 'Sunburst: scroll or +/- to zoom, click segment to select.';
  setMasterInfo([
    '<b>Master info</b>',
    'Mode: Sunburst',
    focusLabel(),
    'Click segment: selects and highlights',
    `Items rendered: ${{sourceRows.length}}`,
    `Zoom: x${{sunZoom.toFixed(2)}}`,
    `Build: ${{BUILD_ID || '-'}}`,
  ]);

  masterChart.off('click');
  masterChart.on('click', (params) => {{
    const d = params?.data || {{}};
    const p = params?.data?.path || '';
    if (!p) return;
    setMasterInfo([
      `<b>${{d.name || 'Segment'}}</b>`,
      `Path: ${{p}}`,
      `Items: ${{d.itemCount || 0}}`,
      `High risk: ${{d.highCount || 0}}`,
      `Dominant access: ${{d.access || '-'}}`,
    ]);
    const exact = rows.find(r => r.path === p);
    selectedItemId = exact ? (exact.item_id || '') : '';
    selectedItemPath = p;
    centerTreeOnNextRender = true;
    rerender();
  }});

  masterChart.off('mouseover');
  masterChart.on('mouseover', (params) => {{
    const d = params?.data || {{}};
    const p = params?.data?.path || '';
    if (!p) return;
    setMasterInfo([
      `<b>${{d.name || 'Segment'}}</b>`,
      `Path: ${{p}}`,
      `Type: ${{d.kind || 'FOLDER'}}`,
      `Items: ${{d.itemCount || 0}}`,
      `Owners: ${{d.owners || 0}}`,
      `Access: ${{d.access || '-'}}`,
    ]);
    highlightMatrix({{ path: p }}, false);
  }});
  masterChart.off('mouseout');
  masterChart.on('mouseout', () => clearMatrixHighlight());

  masterChart.off('sunburstrootchanged');
  masterChart.on('sunburstrootchanged', () => setMasterModeLabel());
}}

function buildMasterNetwork(allRows) {{
  const nodeMap = new Map();
  const edgeMap = new Map();

  function ensureNode(id, role='user') {{
    if (!id) return;
    if (!nodeMap.has(id)) nodeMap.set(id, {{ id, name: id, role, hits: 0 }});
    nodeMap.get(id).hits += 1;
  }}

  for (const r of allRows) {{
    const src = r.shared_by_email || r.original_owner_email || '';
    if (!src) continue;
    ensureNode(src, 'sharer');

    const targets = (r.access_targets || '').split('|').map(x => x.trim()).filter(Boolean);
    for (const t of targets) {{
      if (t === '-' || t === src) continue;
      ensureNode(t, t.includes('@') ? 'user' : 'group');
      const key = `${{src}}=>${{t}}`;
      if (!edgeMap.has(key)) edgeMap.set(key, {{ source: src, target: t, value: 0, samplePath: r.path || '' }});
      edgeMap.get(key).value += 1;
    }}
  }}

  const nodes = Array.from(nodeMap.values()).map(n => {{
    const size = Math.max(14, Math.min(52, 10 + Math.sqrt(n.hits) * 2.2));
    const color = n.role === 'sharer' ? '#1d4ed8' : (n.role === 'group' ? '#7c3aed' : '#0f766e');
    return {{
      id: n.id,
      name: n.name,
      shortName: shortPrincipal(n.name),
      value: n.hits,
      symbolSize: size,
      itemStyle: {{ color }},
      label: {{
        show: true,
        color: '#0f172a',
        fontSize: 10,
        width: 120,
        overflow: 'truncate',
        formatter: (p) => p?.data?.shortName || '',
      }},
    }};
  }});

  const links = Array.from(edgeMap.values()).map(e => {{
    return {{
      source: e.source,
      target: e.target,
      value: e.value,
      samplePath: e.samplePath,
      lineStyle: {{ width: Math.max(1, Math.min(5, Math.log2(e.value + 1))), color: 'rgba(30,64,175,0.28)' }},
    }};
  }});

  return {{ nodes, links }};
}}

function renderMasterNetwork() {{
  if (!masterChart) return;
  masterView = 'network';
  activateMasterModeButtons();
  const focusRows = masterFocusRows();
  const sourceRows = focusRows.length ? focusRows : rows;
  const net = buildMasterNetwork(sourceRows);

  masterChart.setOption({{
    animationDuration: 300,
    tooltip: {{
      trigger: 'item',
      backgroundColor: 'rgba(10,18,32,0.84)',
      textStyle: {{ color: '#fff' }},
      formatter: (p) => {{
        if (p.dataType === 'node') return `<b>${{principalLabel(p.data.name)}}</b><br/>Interactions: ${{p.data.value || 0}}`;
        const d = p.data || {{}};
        return `<b>${{principalLabel(d.source || '')}} ➜ ${{principalLabel(d.target || '')}}</b><br/>Shares: ${{d.value || 0}}<br/>Example path: ${{d.samplePath || '-'}}`;
      }},
    }},
    series: [{{
      type: 'graph',
      layout: 'circular',
      roam: true,
      draggable: false,
      data: net.nodes,
      links: net.links,
      edgeSymbol: ['none', 'arrow'],
      edgeSymbolSize: 6,
      circular: {{ rotateLabel: false }},
      lineStyle: {{ curveness: 0.22, opacity: 0.64 }},
      emphasis: {{ focus: 'adjacency' }},
      labelLayout: {{ hideOverlap: false }},
      label: {{ position: 'right' }},
    }}],
  }}, true);

  setMasterModeLabel();
  masterHelp.textContent = 'Network: who shares with whom. Scroll to zoom, drag to move.';
  setMasterInfo([
    '<b>Master info</b>',
    'Mode: Share Network',
    focusLabel(),
    `Items rendered: ${{sourceRows.length}}`,
    `Nodes: ${{net.nodes.length}}`,
    `Links: ${{net.links.length}}`,
    'Click edge: select related file/folder',
    `Build: ${{BUILD_ID || '-'}}`,
  ]);

  masterChart.off('click');
  masterChart.on('click', (params) => {{
    if (params.dataType === 'edge') {{
      const p = params.data.samplePath || '';
      if (!p) return;
      setMasterInfo([
        `<b>Edge: ${{principalLabel(params.data.source)}} ➜ ${{principalLabel(params.data.target)}}</b>`,
        `Shares: ${{params.data.value || 0}}`,
        `Example path: ${{p}}`,
      ]);
      const exact = rows.find(r => r.path === p);
      selectedItemId = exact ? (exact.item_id || '') : '';
      selectedItemPath = p;
      centerTreeOnNextRender = true;
      rerender();
      return;
    }}
    if (params.dataType === 'node') {{
      const id = params.data.id || params.data.name || '';
      if (!id) return;
      setMasterInfo([
        `<b>Node: ${{principalLabel(id)}}</b>`,
        `Interactions: ${{params.data.value || 0}}`,
        'Matrix highlights matching owner/shared/access rows.',
      ]);
      const filtered = rows.filter(r =>
        (r.original_owner_email || '').includes(id) ||
        (r.shared_by_email || '').includes(id) ||
        (r.access_targets || '').includes(id)
      );
      highlightMaster(filtered);
    }}
  }});

  masterChart.off('mouseover');
  masterChart.on('mouseover', (params) => {{
    if (params.dataType === 'edge') {{
      const p = params.data.samplePath || '';
      if (p) highlightMatrix({{ path: p }}, false);
      return;
    }}
    if (params.dataType === 'node') {{
      const id = params.data.id || params.data.name || '';
      clearMatrixHighlight();
      if (!id) return;
      for (const tr of matrixBody.querySelectorAll('tr')) {{
        const p = tr.dataset.path || '';
        const row = rows.find(x => x.path === p);
        if (!row) continue;
        if ((row.original_owner_email || '').includes(id) || (row.shared_by_email || '').includes(id) || (row.access_targets || '').includes(id)) {{
          tr.classList.add('row-focus');
        }}
      }}
    }}
  }});
  masterChart.off('mouseout');
  masterChart.on('mouseout', () => clearMatrixHighlight());
}}

function renderNetworkPanel() {{
  if (!networkChart) return;
  const sourceRows = masterFocusRows();
  const dataRows = sourceRows.length ? sourceRows : rows;
  const net = buildMasterNetwork(dataRows);

  networkChart.setOption({{
    animationDuration: 260,
    tooltip: {{
      trigger: 'item',
      backgroundColor: 'rgba(10,18,32,0.84)',
      textStyle: {{ color: '#fff' }},
      formatter: (p) => {{
        if (p.dataType === 'node') return `<b>${{principalLabel(p.data.name)}}</b><br/>Interactions: ${{p.data.value || 0}}`;
        const d = p.data || {{}};
        return `<b>${{principalLabel(d.source || '')}} ➜ ${{principalLabel(d.target || '')}}</b><br/>Shares: ${{d.value || 0}}<br/>Example path: ${{d.samplePath || '-'}}`;
      }},
    }},
    series: [{{
      type: 'graph',
      layout: 'circular',
      roam: true,
      draggable: false,
      data: net.nodes,
      links: net.links,
      edgeSymbol: ['none', 'arrow'],
      edgeSymbolSize: 6,
      circular: {{ rotateLabel: false }},
      lineStyle: {{ curveness: 0.22, opacity: 0.62 }},
      emphasis: {{ focus: 'adjacency' }},
      labelLayout: {{ hideOverlap: false }},
      label: {{ position: 'right' }},
    }}],
  }}, true);

  networkChart.off('click');
  networkChart.on('click', (params) => {{
    if (params.dataType === 'edge') {{
      const p = params.data.samplePath || '';
      if (!p) return;
      const exact = rows.find(r => r.path === p);
      selectedItemId = exact ? (exact.item_id || '') : '';
      selectedItemPath = p;
      centerTreeOnNextRender = true;
      setMasterInfo([
        `<b>Edge: ${{principalLabel(params.data.source)}} ➜ ${{principalLabel(params.data.target)}}</b>`,
        `Shares: ${{params.data.value || 0}}`,
        `Example path: ${{p}}`,
      ]);
      rerender();
      return;
    }}
    if (params.dataType === 'node') {{
      const id = params.data.id || params.data.name || '';
      if (!id) return;
      clearMatrixHighlight();
      for (const tr of matrixBody.querySelectorAll('tr')) {{
        const p = tr.dataset.path || '';
        const row = rows.find(x => x.path === p);
        if (!row) continue;
        if ((row.original_owner_email || '').includes(id) || (row.shared_by_email || '').includes(id) || (row.access_targets || '').includes(id)) tr.classList.add('row-focus');
      }}
    }}
  }});
}}

function applyHighlightToSunNode(node, pathSet) {{
  const on = !node.path || pathSet.has(node.path);
  const next = {{
    ...node,
    itemStyle: {{
      ...(node.itemStyle || {{}}),
      opacity: on ? 1 : 0.62,
      borderWidth: on ? 1.2 : 0.6,
    }},
    label: {{ show: true, opacity: on ? 1 : 0.55 }},
  }};
  if (node.children && node.children.length) next.children = node.children.map(ch => applyHighlightToSunNode(ch, pathSet));
  return next;
}}

function highlightMaster(data) {{
  if (!masterChart) return;

  if (masterView === 'network') {{
    const entitySet = new Set();
    for (const r of data) {{
      if (r.shared_by_email) entitySet.add(r.shared_by_email);
      if (r.original_owner_email) entitySet.add(r.original_owner_email);
      for (const t of (r.access_targets || '').split('|').map(x => x.trim()).filter(Boolean)) entitySet.add(t);
    }}

    masterChart.dispatchAction({{ type: 'downplay', seriesIndex: 0 }});
    const opt = masterChart.getOption();
    const nodes = opt?.series?.[0]?.data || [];
    nodes.forEach((n, i) => {{
      if (entitySet.has(n.id || n.name)) masterChart.dispatchAction({{ type: 'highlight', seriesIndex: 0, dataIndex: i }});
    }});
    masterHelp.textContent = `Network highlight: ${{entitySet.size}} entities`;
    return;
  }}

  const pathSet = new Set();
  for (const r of data) {{
    const parts = (r.path || '').split('/').filter(Boolean);
    for (let i = 1; i <= parts.length; i++) pathSet.add(parts.slice(0, i).join('/'));
  }}

  const focusRows = masterFocusRows();
  const sourceRows = focusRows.length ? focusRows : rows;
  const sun = buildMasterSunburst(sourceRows);
  const inner = Math.max(2, Math.min(24, 10 + (1 - sunZoom) * 10));
  const outer = Math.max(82, Math.min(98, 92 + sunZoom * 2.2));
  const prepared = applySunLabelRules(sun, inner, outer);
  const seriesData = (prepared || []).map((node) => applyHighlightToSunNode(node, pathSet));

  masterChart.setOption({{
    series: [{{
      data: seriesData,
      radius: [`${{inner}}%`, `${{outer}}%`],
    }}],
  }});

  const highlighted = sourceRows.length ? (pathSet.size / sourceRows.length) < 0.95 : false;
  masterHelp.textContent = highlighted
    ? `Sunburst highlight: ${{pathSet.size}} paths`
    : 'Sunburst: full map view';
  setMasterModeLabel();
}}
function clearMatrixHighlight() {{
  for (const tr of matrixBody.querySelectorAll('tr')) tr.classList.remove('row-focus');
}}

function highlightMatrix(payload, doScroll=false) {{
  clearMatrixHighlight();
  const rowsAll = Array.from(matrixBody.querySelectorAll('tr'));
  let matched = [];

  if (payload.item_id) {{
    matched = rowsAll.filter(tr => tr.dataset.itemId === payload.item_id);
  }} else if (payload.path) {{
    matched = rowsAll.filter(tr => tr.dataset.path === payload.path || tr.dataset.path.startsWith(payload.path + '/'));
  }}

  for (const tr of matched) tr.classList.add('row-focus');
}}

function _treePathFromSelection() {{
  const sr = selectedItemRow();
  let raw = selectedItemPath || selectedPath || '';
  if (!raw && sr?.path) raw = sr.path;
  if (!raw) return '';

  const isFile = sr?.item_kind === 'FILE';
  let candidate = raw;
  if (isFile && candidate.includes('/')) {{
    candidate = candidate.split('/').slice(0, -1).join('/');
  }}

  let seek = candidate;
  while (seek) {{
    const hit = document.querySelector(`.tree-node[data-path="${{CSS.escape(seek)}}"]`);
    if (hit) return seek;
    const idx = seek.lastIndexOf('/');
    if (idx < 0) break;
    seek = seek.slice(0, idx);
  }}
  return '';
}}

function revealPathInTree(path, center=false) {{
  if (!path) return;
  const parts = path.split('/').filter(Boolean);
  for (let i = 1; i <= parts.length; i++) {{
    const sub = parts.slice(0, i).join('/');
    const summary = document.querySelector(`summary.tree-node[data-path="${{CSS.escape(sub)}}"]`);
    if (summary) {{
      const details = summary.closest('details');
      if (details) details.open = true;
    }}
  }}
  if (!center || !treeWrap) return;
  const node =
    document.querySelector(`.tree-node[data-path="${{CSS.escape(path)}}"]`) ||
    document.querySelector(`summary.tree-node[data-path="${{CSS.escape(path)}}"]`) ||
    document.querySelector(`button.tree-node[data-path="${{CSS.escape(path)}}"]`);
  if (!node) return;
  const wrapRect = treeWrap.getBoundingClientRect();
  const nodeRect = node.getBoundingClientRect();
  const delta = (nodeRect.top - wrapRect.top) - (wrapRect.height / 2 - nodeRect.height / 2);
  treeWrap.scrollBy({{ top: delta, behavior: 'smooth' }});
}}

function syncTreeSelection(center=false, preserveToggle=false) {{
  for (const n of document.querySelectorAll('.tree-node')) n.classList.remove('active');
  const p = _treePathFromSelection();
  if (!p) return;
  if (!preserveToggle) revealPathInTree(p, center);
  for (const n of document.querySelectorAll(`.tree-node[data-path="${{CSS.escape(p)}}"]`)) {{
    n.classList.add('active');
  }}
}}

function rerender() {{
  const data = filteredRows();
  renderMatrix(data);
  renderDiagram(data);
  if (masterView === 'sunburst') renderMasterSunburst();
  renderNetworkPanel();
  highlightMaster(masterFocusRows());
  syncTreeSelection(centerTreeOnNextRender, preserveTreeToggleOnce);
  centerTreeOnNextRender = false;
  preserveTreeToggleOnce = false;
  if (selectedItemId || selectedItemPath) {{
    const sr = selectedItemRow();
    if (sr) highlightMatrix(sr, false);
    else if (selectedItemPath) highlightMatrix({{ path: selectedItemPath }}, false);
  }} else if (selectedPath) {{
    highlightMatrix({{ path: selectedPath }}, false);
  }}
  const focusText = (selectedItemId || selectedItemPath)
    ? (selectedItemRow() ? `${{selectedItemRow().name}} | ${{selectedItemRow().path}}` : selectedItemPath || '-')
    : (selectedPath || 'All paths');
  const visibleAvg = data.length ? (data.reduce((s, r) => s + (r.risk_score || 0), 0) / data.length) : 0;
  riskLabel.textContent = `Rules: public/domain/external/writer/mass-sharing | Visible avg risk: ${{visibleAvg.toFixed(1)}}`;
  meta.textContent = `Sort: path/name | Selection: ${{focusText}} | Visible rows: ${{data.length}}`;
  renderRiskSummary(data);
  renderLiveContext(data);
  renderBreadcrumbs();
  renderSelectionStrips();
  buildBadge.textContent = `Build: ${{BUILD_ID || '-'}} | Rows: ${{rows.length}}`;
}}

q.addEventListener('input', () => {{
  textFilter = q.value.toLowerCase().trim();
  rerender();
}});

document.getElementById('scopeGrp').addEventListener('click', (e) => {{
  const b = e.target.closest('button[data-scope]');
  if (!b) return;
  selectedScope = b.dataset.scope;
  for (const x of e.currentTarget.querySelectorAll('button')) x.classList.remove('active');
  b.classList.add('active');
  rerender();
}});

document.getElementById('riskGrp').addEventListener('click', (e) => {{
  const b = e.target.closest('button[data-risk]');
  if (!b) return;
  selectedRisk = b.dataset.risk;
  for (const x of e.currentTarget.querySelectorAll('button')) x.classList.remove('active');
  b.classList.add('active');
  rerender();
}});

document.getElementById('tree').addEventListener('click', (e) => {{
  const summary = e.target.closest('summary.tree-node');
  if (summary) {{
    e.preventDefault();
    const details = summary.parentElement;
    if (details && details.tagName === 'DETAILS') details.open = !details.open;
    const p = summary.dataset.path || '';
    selectedItemId = '';
    selectedItemPath = p;
    selectedPath = p;
    centerTreeOnNextRender = false;
    preserveTreeToggleOnce = true;
    rerender();
    return;
  }}

  const leaf = e.target.closest('button.tree-node');
  if (!leaf) return;
  const p = leaf.dataset.path || '';
  selectedItemId = '';
  selectedItemPath = p;
  selectedPath = p;
  centerTreeOnNextRender = false;
  rerender();
}});

breadcrumbsBar.addEventListener('click', (e) => {{
  const b = e.target.closest('button[data-crumb-path]');
  if (!b) return;
  const p = b.dataset.crumbPath || '';
  if (!p) {{
    selectedPath = '';
    selectedItemId = '';
    selectedItemPath = '';
    centerTreeOnNextRender = false;
    rerender();
    return;
  }}
  const exact = rows.find(r => r.path === p);
  selectedItemId = exact ? (exact.item_id || '') : '';
  selectedItemPath = p;
  selectedPath = p;
  centerTreeOnNextRender = true;
  rerender();
}});

document.getElementById('clearSel').addEventListener('click', () => {{
  selectedPath = '';
  selectedItemId = '';
  selectedItemPath = '';
  centerTreeOnNextRender = false;
  rerender();
}});

document.getElementById('expandAll').addEventListener('click', () => {{
  for (const d of document.querySelectorAll('#tree details')) d.open = true;
}});

document.getElementById('collapseAll').addEventListener('click', () => {{
  for (const d of document.querySelectorAll('#tree details')) d.open = false;
}});

document.querySelector('.tbl-wrap').addEventListener('scroll', (e) => {{
  const el = e.currentTarget;
  if (el.scrollTop + el.clientHeight >= el.scrollHeight - 110) {{
    appendMatrixRows();
  }}
}});

loadMoreRowsBtn.addEventListener('click', () => appendMatrixRows());

async function copyText(text) {{
  try {{
    await navigator.clipboard.writeText(text);
    return true;
  }} catch (_e) {{
    const ta = document.createElement('textarea');
    ta.value = text;
    document.body.appendChild(ta);
    ta.select();
    const ok = document.execCommand('copy');
    document.body.removeChild(ta);
    return ok;
  }}
}}

matrixBody.addEventListener('click', async (e) => {{
  const btn = e.target.closest('.copy-row-path');
  if (btn) {{
    const p = btn.dataset.copyPath || '';
    if (!p) return;
    const ok = await copyText(p);
    btn.textContent = ok ? 'Copied' : 'Fail';
    setTimeout(() => (btn.textContent = 'Copy'), 900);
    return;
  }}

  const tr = e.target.closest('tr[data-item-id]');
  if (!tr) return;
  selectedItemId = tr.dataset.itemId || '';
  selectedItemPath = tr.dataset.path || '';
  centerTreeOnNextRender = true;
  rerender();
}});

copyVisiblePathsBtn.addEventListener('click', async () => {{
  const paths = matrixAllRows.map(r => r.path).join('\\n');
  if (!paths) return;
  const ok = await copyText(paths);
  copyVisiblePathsBtn.textContent = ok ? 'Copied visible' : 'Copy failed';
  setTimeout(() => (copyVisiblePathsBtn.textContent = 'Copy Visible Paths'), 1200);
}});

function applyCols(c1, c2, c3) {{
  document.documentElement.style.setProperty('--col1', `${{c1}}fr`);
  document.documentElement.style.setProperty('--col2', `${{c2}}fr`);
  document.documentElement.style.setProperty('--col3', `${{c3}}fr`);
  localStorage.setItem('gdrive_cols', JSON.stringify([c1, c2, c3]));
}}

function initResizableLayout() {{
  const saved = localStorage.getItem('gdrive_cols');
  if (saved) {{
    try {{
      const [a, b, c] = JSON.parse(saved);
      if (a && b && c) applyCols(a, b, c);
    }} catch (_e) {{}}
  }}

  const split1 = document.getElementById('splitter1');
  const split2 = document.getElementById('splitter2');
  const layout = document.querySelector('.layout');

  function wire(splitter, leftIdx, rightIdx) {{
    splitter.addEventListener('mousedown', (ev) => {{
      if (window.innerWidth <= 1100) return;
      ev.preventDefault();
      const rect = layout.getBoundingClientRect();
      const total = rect.width;
      const onMove = (e) => {{
        const x = e.clientX - rect.left;
        let p1 = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--col1')) || 16;
        let p2 = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--col2')) || 34;
        let p3 = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--col3')) || 50;
        const sum = p1 + p2 + p3;
        const leftPx = total * ([p1, p1 + p2][leftIdx] / sum);
        const deltaPx = x - leftPx;
        const deltaFr = (deltaPx / total) * sum;

        if (splitter.id === 'splitter1') {{
          p1 = Math.max(12, Math.min(45, p1 + deltaFr));
          p2 = Math.max(20, Math.min(60, p2 - deltaFr));
        }} else {{
          p2 = Math.max(20, Math.min(60, p2 + deltaFr));
          p3 = Math.max(24, Math.min(70, p3 - deltaFr));
        }}
        applyCols(Number(p1.toFixed(2)), Number(p2.toFixed(2)), Number(p3.toFixed(2)));
      }};
      const onUp = () => {{
        window.removeEventListener('mousemove', onMove);
        window.removeEventListener('mouseup', onUp);
      }};
      window.addEventListener('mousemove', onMove);
      window.addEventListener('mouseup', onUp);
    }});
  }}

  wire(split1, 0, 1);
  wire(split2, 1, 2);
}}

function initWheelContainment() {{
  const selectors = ['#tree', '.tbl-wrap', '#diagram', '.master-wrap', '.network-wrap'];
  for (const sel of selectors) {{
    const el = document.querySelector(sel);
    if (!el) continue;
    el.addEventListener('wheel', (e) => e.stopPropagation(), {{ passive: true }});
  }}
}}

// default collapsed for clarity on large trees
for (const d of document.querySelectorAll('#tree details')) d.open = false;

if (modeSunburstBtn) {{
  modeSunburstBtn.addEventListener('click', () => {{
    masterView = 'sunburst';
    renderMasterSunburst();
    highlightMaster(masterFocusRows());
  }});
}}
if (modeNetworkBtn) {{
  modeNetworkBtn.addEventListener('click', () => {{
    masterView = 'network';
    renderMasterNetwork();
    highlightMaster(masterFocusRows());
  }});
}}
zoomInBtn.addEventListener('click', () => {{
  if (masterView !== 'sunburst') return;
  sunZoom = Math.max(0.75, Math.min(2.4, sunZoom * 1.12));
  renderMasterSunburst();
  highlightMaster(masterFocusRows());
}});
zoomOutBtn.addEventListener('click', () => {{
  if (masterView !== 'sunburst') return;
  sunZoom = Math.max(0.75, Math.min(2.4, sunZoom * 0.88));
  renderMasterSunburst();
  highlightMaster(masterFocusRows());
}});
zoomResetBtn.addEventListener('click', () => {{
  if (masterView !== 'sunburst') return;
  sunZoom = 1;
  renderMasterSunburst();
  highlightMaster(masterFocusRows());
}});

initMasterChart();
initNetworkChart();
renderMasterSunburst();
initResizableLayout();
initWheelContainment();
renderRiskSummary();

rerender();
</script>
</body>
</html>
"""

    output_html.write_text(html, encoding="utf-8")
