#!/usr/bin/python3
"""
site_issues_dev.py — DEV/scratch page for the proactive "site issues" summary.

Stage 1: dumps EVERY harvested candidate signal per site so we can eyeball the
raw data and work through what becomes an issue vs what gets suppressed.
Not fancy on purpose. Reuses page_key 'status' for auth (no new nav entry).

Backend harvester: /opt/ngon/apps/monitoring/site_issues.py
"""

import cgi
import sys
import html
import time

sys.path.insert(0, '/opt/ngon/apps')
from managers.auth_manager import AuthManager, generate_login_page_html
from monitoring.site_issues import harvest, derive_issues

form = cgi.FieldStorage()
auth = AuthManager('status')
auth_required, should_exit, headers = auth.require_auth(form)

if should_exit:
    print("Content-Type: application/json")
    if headers:
        print(headers)
    print("")
    if auth_required:
        print('{"success": false, "error": "Authentication required"}')
    else:
        print('{"success": true}')
    sys.exit(0)

if auth_required:
    print("Content-Type: text/html\n")
    print(generate_login_page_html("Site Issues (dev)"))
    sys.exit(0)


def esc(v):
    if v is None:
        return "<span class='na'>–</span>"
    if isinstance(v, bool):
        return "yes" if v else "no"
    if isinstance(v, float):
        v = round(v, 1)
    return html.escape(str(v))


def cell(v, bad=False, warn=False):
    cls = " class='bad'" if bad else (" class='warn'" if warn else "")
    return f"<td{cls}>{esc(v)}</td>"


def age_str(sec):
    if sec is None:
        return "–"
    sec = int(sec)
    if sec < 120:
        return f"{sec}s"
    if sec < 7200:
        return f"{sec // 60}m"
    if sec < 172800:
        return f"{sec // 3600}h"
    return f"{sec // 86400}d"


data = harvest()
issues = derive_issues(data)

print("Content-Type: text/html\n")

parts = []
parts.append("""<!DOCTYPE html><html><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="refresh" content="300">
<title>Site Issues (dev)</title>
<style>
  body { background:#111; color:#ddd; font-family:-apple-system,Segoe UI,Roboto,sans-serif;
         font-size:13px; margin:0; padding:12px 16px 60px; }
  h1 { font-size:18px; margin:0 0 2px; }
  .sub { color:#888; font-size:12px; margin-bottom:16px; }
  .site { border:1px solid #333; border-radius:8px; margin:0 0 18px; overflow:hidden; }
  .site > .shead { background:#1c1c1c; padding:8px 12px; font-size:15px; font-weight:600;
                   display:flex; flex-wrap:wrap; gap:14px; align-items:center; }
  .site > .shead .badges { font-size:12px; font-weight:400; color:#aaa; }
  .badge { display:inline-block; padding:1px 7px; border-radius:10px; margin-left:6px;
           background:#262626; }
  .badge.bad { background:#5a1a1a; color:#ff9a9a; }
  .badge.ok  { background:#183218; color:#9ad39a; }
  .grp { padding:6px 12px 10px; border-top:1px solid #262626; }
  .grp h3 { font-size:13px; margin:8px 0 4px; color:#bbb; font-weight:600; }
  table { border-collapse:collapse; width:100%; margin:2px 0 6px; font-size:12px; }
  th, td { text-align:left; padding:3px 8px; border-bottom:1px solid #222; white-space:nowrap; }
  th { color:#888; font-weight:500; position:sticky; }
  td.bad { color:#ff7676; font-weight:600; }
  td.warn { color:#ffb454; }
  td.ok { color:#9ad39a; }
  .na { color:#555; }
  .wtag { display:inline-block; background:#3a2a12; color:#ffb454; border-radius:4px;
          padding:0 5px; margin:1px 3px 1px 0; font-size:11px; }
  .wtag.crit { background:#4a1616; color:#ff8a8a; }
  .scroll { overflow-x:auto; }
  .errbox { background:#3a1414; color:#ff9a9a; padding:8px 12px; border-radius:6px; margin-bottom:12px; }

  /* --- mockup of the proposed status-page display --- */
  .mockwrap { border:1px solid #2f4a2f; border-radius:8px; padding:12px 14px 14px;
              margin:0 0 26px; background:#141814; max-width:560px; }
  .mockwrap h2 { font-size:14px; margin:0 0 2px; color:#9ad39a; }
  .mockwrap .mnote { color:#7a8a7a; font-size:11px; margin-bottom:10px; }
  details.msite { border-bottom:1px solid #223022; }
  details.msite > summary { list-style:none; cursor:pointer; display:flex;
      justify-content:space-between; align-items:center; padding:7px 4px; font-size:14px; }
  details.msite > summary::-webkit-details-marker { display:none; }
  details.msite > summary:hover { background:#1a201a; }
  .msite .cnt { font-size:12px; padding:1px 9px; border-radius:11px; background:#3a2a12; color:#ffb454; }
  .msite .cnt.zero { background:#183218; color:#7fbf7f; }
  .msite .issue { padding:5px 4px 5px 16px; font-size:13px; border-top:1px dashed #1e281e; }
  .msite .issue .ev { color:#7a8a7a; font-size:11px; margin:2px 0 0 12px; }
  .mockwrap .hidden-note { color:#666; font-size:11px; margin-top:10px; }
  /* category / node tree */
  details.mcat { margin:3px 0 3px 12px; }
  details.mcat > summary, details.mnode > summary { list-style:none; cursor:pointer; padding:3px 4px; }
  details.mcat > summary::-webkit-details-marker, details.mnode > summary::-webkit-details-marker { display:none; }
  details.mcat > summary:before, details.mnode > summary:before { content:'▸ '; color:#557055; }
  details.mcat[open] > summary:before, details.mnode[open] > summary:before { content:'▾ '; }
  details.mcat > summary { font-size:13px; color:#cfe3cf; font-weight:600; }
  .ccnt { color:#ffb454; font-weight:400; font-size:12px; }
  details.mnode { margin:2px 0 2px 16px; }
  details.mnode > summary { font-size:13px; color:#bcd; }
  .ncnt { color:#888; font-size:11px; }
  .node { padding:2px 0 0 30px; font-size:13px; color:#cdd; }
  .nline { padding:1px 0 1px 44px; font-size:12px; color:#9fb0a0; }
</style></head><body>""")

gen_ts = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(data.get("generated_at", time.time())))
parts.append(f"<h1>Site Issues — raw signal dump (dev)</h1>")
parts.append(f"<div class='sub'>generated {gen_ts} · auto-refresh 5 min · this is the everything-view we work through, not the final list</div>")

for e in data.get("errors", []):
    parts.append(f"<div class='errbox'>harvest error: {esc(e)}</div>")

# ---- MOCKUP: what the status-page "site issues" panel would show ----
parts.append("<div class='mockwrap'>")
parts.append("<h2>▸ Site Issues &nbsp;<span style='color:#666;font-weight:400'>(proposed status-page panel)</span></h2>")
parts.append("<div class='mnote'>This is the display. Rules so far: pod-down owns its offline miners · "
             "offline cluster only on online pods · no-bridge gens ignored · maintenance-tagged gens muted · "
             "sleeping NOT evaluated yet.</div>")
def render_node(node):
    label = esc(node.get("label"))
    cnt = node.get("count")
    head = label + (f" <span class='ncnt'>({cnt})</span>" if cnt is not None else "")
    children = node.get("children")
    if children:
        h = [f"<details class='mnode' open><summary>{head}</summary>"]
        for c in children:
            h.append(render_node(c))
        h.append("</details>")
        return "".join(h)
    h = [f"<div class='node'>{head}</div>"]
    for ln in node.get("lines") or []:
        h.append(f"<div class='nline'>{esc(ln)}</div>")
    return "".join(h)


for si in issues["sites"]:
    cnt = si["issue_count"]
    if cnt == 0:
        parts.append("<details class='msite'><summary>"
                     f"<span>{esc(si['name'])}</span>"
                     f"<span class='cnt zero'>✓ clear</span></summary></details>")
        continue
    parts.append(f"<details class='msite' open><summary>"
                 f"<span>{esc(si['name'])}</span>"
                 f"<span class='cnt'>{cnt}</span></summary>")
    for ln in si.get("network", []):
        parts.append(f"<div class='issue'>• {esc(ln)}</div>")
    for cat in si.get("categories", []):
        unit = cat.get("unit")
        ctitle = (f"{esc(cat['name'])} <span class='ccnt'>"
                  f"({cat['count']}{' ' + esc(unit) if unit else ''})</span>")
        parts.append(f"<details class='mcat'><summary>{ctitle}</summary>")
        for node in cat.get("nodes", []):
            parts.append(render_node(node))
        parts.append("</details>")
    parts.append("</details>")
for m in issues.get("muted_sites", []):
    parts.append("<details class='msite'><summary>"
                 f"<span>{esc(m['name'])}</span>"
                 f"<span class='cnt zero'>muted</span></summary>"
                 f"<div class='issue' style='color:#7a8a7a'>known exception — {esc(m['reason'])}</div></details>")
if issues.get("hidden_sites"):
    parts.append(f"<div class='hidden-note'>hidden as non-operational: "
                 f"{esc(', '.join(issues['hidden_sites']))} — confirm this list</div>")
parts.append("<div class='hidden-note'>muted sites &amp; chronic-warning list are placeholders for the "
             "Site Manager context field (operator-authored exceptions the LLM will read).</div>")
parts.append("</div>")

parts.append("<h1 style='font-size:15px;margin:0 0 8px'>Raw signal dump (everything harvested)</h1>")

for s in data["sites"]:
    # Site header badges
    badges = []
    ps = s.get("peplink_status")
    if ps == "offline":
        badges.append("<span class='badge bad'>peplink offline</span>")
    elif ps == "online":
        badges.append("<span class='badge ok'>peplink online</span>")
    star = s.get("starlink_up")
    if star is False:
        badges.append("<span class='badge bad'>starlink DOWN</span>")
    elif star is True:
        badges.append("<span class='badge ok'>starlink up</span>")
    g = s.get("gas") or {}
    if g.get("flow_mcfd") is not None:
        badges.append(f"<span class='badge'>gas {esc(g.get('flow_mcfd'))} mcfd / {esc(g.get('sp_psia'))} psia · {age_str(g.get('age_sec'))}</span>")

    parts.append("<div class='site'>")
    parts.append(f"<div class='shead'>{esc(s['name'])} <span class='badges'>{''.join(badges)}</span></div>")

    for grp in s["groups"]:
        parts.append("<div class='grp'>")
        gbadges = []
        if grp.get("peplink_status") == "offline":
            gbadges.append("<span class='badge bad'>pep offline</span>")
        if grp.get("starlink_up") is False:
            gbadges.append("<span class='badge bad'>starlink DOWN</span>")
        parts.append(f"<h3>{esc(grp['name'])} &nbsp; "
                     f"<span style='color:#888;font-weight:400'>max {esc(grp.get('max_gen_kw'))}kW · "
                     f"running {esc(grp.get('running'))} / down {esc(grp.get('down'))}</span> "
                     f"{''.join(gbadges)}</h3>")

        # Gens table
        if grp["gens"]:
            parts.append("<div class='scroll'><table>")
            parts.append("<tr><th>gen</th><th>src</th><th>status</th><th>maint</th><th>kW</th>"
                         "<th>NG psi</th><th>run</th><th>live-age</th><th>warn</th><th>warnings</th>"
                         "<th>down reason</th><th>batt</th><th>hold</th></tr>")
            for gen in grp["gens"]:
                src = gen.get("data_source")
                live_side = src in ("live", "recent")
                kw = gen.get("live_power") if live_side else gen.get("mesa_power")
                psi = gen.get("live_ng_pressure") if live_side else gen.get("mesa_ng_pressure")
                run = gen.get("live_run_status") if live_side else None
                wl = gen.get("warning_level")
                wtags = []
                for w in gen.get("warnings", []):
                    cls = "wtag crit" if w.get("level") == "critical" else "wtag"
                    lbl = w.get("label") or "?"
                    if w.get("value") is not None:
                        _u = w.get("unit") or ""
                        lbl += f" {esc(w.get('value'))}{'' if _u in ('', '%') else ' '}{esc(_u)}"
                    wtags.append(f"<span class='{cls}'>{lbl}</span>")
                src_html = {
                    "live": "<td class='ok'>live</td>",
                    "recent": "<td class='warn'>recent?</td>",
                    "mesa": "<td class='na'>mesa</td>",
                }.get(src, f"<td>{esc(src)}</td>")
                row = ["<tr>"]
                row.append(cell(gen.get("id")))
                row.append(src_html)
                row.append(cell(gen.get("status"), bad=(gen.get("status") == "Down")))
                row.append(cell(gen.get("maintenance_status"),
                                warn=bool(gen.get("maintenance_status"))))
                row.append(cell(kw))
                row.append(cell(psi))
                row.append(cell(run))
                row.append(f"<td class='{'warn' if src == 'recent' else 'na'}'>{age_str(gen.get('live_age_sec'))}</td>")
                row.append(cell(wl, bad=(wl == "critical"), warn=(wl == "warn")))
                row.append(f"<td>{''.join(wtags) if wtags else '<span class=na>–</span>'}</td>")
                row.append(cell(gen.get("down_reason"), warn=bool(gen.get("down_reason"))))
                row.append(cell(gen.get("battery")))
                row.append(cell(gen.get("hold"), warn=bool(gen.get("hold"))))
                row.append("</tr>")
                parts.append("".join(row))
            parts.append("</table></div>")

        # Pods table
        if grp["pods"]:
            parts.append("<div class='scroll'><table>")
            parts.append("<tr><th>pod</th><th>conn</th><th>type</th><th>total</th><th>hashing</th>"
                         "<th>zero</th><th>offline</th><th>sleeping</th><th>hashrate</th><th>power</th>"
                         "<th>EMS</th><th>EMS_auto</th><th>AW</th><th>AR-reboot</th></tr>")
            for p in grp["pods"]:
                offline = p.get("miners_offline") or 0
                zero = p.get("miners_zero_hash") or 0
                sleeping = p.get("miners_sleeping") or 0
                conn_down = (p.get("conn") == "offline")
                row = ["<tr>"]
                row.append(cell(p.get("name")))
                row.append(cell(p.get("conn"), bad=conn_down))
                row.append(cell(p.get("miner_type")))
                row.append(cell(p.get("total_miners")))
                row.append(cell(p.get("miners_hashing")))
                row.append(cell(zero, warn=(zero > 10)))
                # Whole-pod-down explains its own offline (dim it). On an ONLINE
                # pod, only a CLUSTER of offline (tripped switch/breaker) is a
                # problem; the 2-3% dead-miner drip is noise. Thresholds TBD.
                if conn_down:
                    row.append(f"<td class='na'>{offline}</td>")
                else:
                    row.append(cell(offline, bad=(offline > 25), warn=(offline > 10)))
                row.append(cell(sleeping))
                row.append(cell(p.get("hashrate")))
                row.append(cell(p.get("power")))
                row.append(cell(p.get("ems"), warn=bool(p.get("ems"))))
                row.append(cell(p.get("ems_auto"), warn=bool(p.get("ems_auto"))))
                row.append(cell(p.get("auto_wake")))
                row.append(cell(p.get("auto_reboot")))
                row.append("</tr>")
                parts.append("".join(row))
            parts.append("</table></div>")

        parts.append("</div>")  # grp
    parts.append("</div>")  # site

parts.append("</body></html>")
print("".join(parts))
