#!/usr/bin/env python3
import cgi, sys
sys.path.insert(0, '/opt/ngon/apps')
from managers.auth_manager import AuthManager, generate_login_page_html

_form = cgi.FieldStorage()
_auth = AuthManager('report_builder')
_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")
    print("")
    print(generate_login_page_html("Report Builder"))
    sys.exit(0)

import json
import datetime as dt

# ------------------------------------------------------------------ action: run
action = _form.getvalue('action', '')
if action == 'run':
    print("Content-Type: application/json\n")
    try:
        from reporting.engine import run_report
        metrics = [m for m in (_form.getvalue('metrics', '') or '').split(',') if m]
        sites = [s for s in (_form.getvalue('sites', '') or '').split(',') if s]
        spec = {
            "start": _form.getvalue('start'),
            "end": _form.getvalue('end'),
            "bucket": _form.getvalue('bucket', 'month'),
            "group_by": _form.getvalue('group_by', 'site'),
            "metrics": metrics,
        }
        if sites:
            spec["sites"] = sites
        print(json.dumps({"success": True, "result": run_report(spec)}))
    except Exception as e:
        import traceback
        print(json.dumps({"success": False, "error": str(e),
                          "trace": traceback.format_exc()}))
    sys.exit(0)

REPORTING_CFG_PATH = '/opt/ngon/config/reporting_config.json'

if action == 'save_config':
    print("Content-Type: application/json\n")
    try:
        usd_per_mcf = float(_form.getvalue('usd_per_mcf'))
        cuft_per_kwh = float(_form.getvalue('cuft_per_kwh'))
        # per-site $/MCF overrides: JSON {site: rate}; blanks already dropped by JS.
        by_site_in = json.loads(_form.getvalue('usd_per_mcf_by_site', '') or '{}')
        usd_per_mcf_by_site = {}
        for _s, _v in by_site_in.items():
            if _v is None or str(_v).strip() == '':
                continue
            usd_per_mcf_by_site[_s] = float(_v)
        running_kw_threshold = float(_form.getvalue('running_kw_threshold'))
        spec_th = float(_form.getvalue('spec_th'))
        if spec_th <= 0:
            raise ValueError('miner spec TH/s must be positive')
        ga_pct = float(_form.getvalue('ga_pct')) / 100.0
        ga_base = _form.getvalue('ga_base', 'on_location_service')
        if ga_base not in ('on_location_service', 'operating_cost', 'revenue',
                           'no_location_service'):
            raise ValueError('invalid G&A base')
        no_loc = [s.strip() for s in (_form.getvalue('no_location_sites', '') or '').split(',')
                  if s.strip()]
        if not no_loc:
            raise ValueError('no-location sites cannot be empty')
        # read-modify-write in place: preserves foundry keys, comments, and file
        # ownership/mode (do NOT recreate the file or it drifts off ngon:ngon 660).
        with open(REPORTING_CFG_PATH) as f:
            cfg = json.load(f)
        cfg.setdefault('gas', {})['usd_per_mcf'] = usd_per_mcf
        cfg['gas']['cuft_per_kwh'] = cuft_per_kwh
        cfg['gas']['usd_per_mcf_by_site'] = usd_per_mcf_by_site
        cfg.setdefault('gens', {})['running_kw_threshold'] = running_kw_threshold
        cfg.setdefault('miner', {})['spec_th'] = spec_th
        cfg.setdefault('ga', {})['pct'] = ga_pct
        cfg['ga']['base'] = ga_base
        cfg['ga']['no_location_sites'] = no_loc
        with open(REPORTING_CFG_PATH, 'w') as f:
            json.dump(cfg, f, indent=2)
        print(json.dumps({"success": True}))
    except Exception as e:
        print(json.dumps({"success": False, "error": str(e)}))
    sys.exit(0)

# ------------------------------------------------------------------ render page
import cgitb
cgitb.enable()
sys.path.insert(0, '/var/www/html/ngon')
from links import generate_dropdown_html, generate_dropdown_css, generate_dropdown_js
from reporting.engine import registry_for_ui, gas_site_list

# default range: previous 6 whole months up to the start of the current month
_today = dt.date.today()
_end = _today.replace(day=1)
_m = _end.month - 6
_y = _end.year
while _m <= 0:
    _m += 12
    _y -= 1
_start = dt.date(_y, _m, 1)
# Site Economics tab default range (inclusive months, YYYY-MM)
_econ_from = _start.strftime('%Y-%m')
_econ_to = (_end - dt.timedelta(days=1)).strftime('%Y-%m')

# current config values for the Settings modal
with open(REPORTING_CFG_PATH) as _f:
    _cfg = json.load(_f)
_gas, _gens, _ga = _cfg.get('gas', {}), _cfg.get('gens', {}), _cfg.get('ga', {})
_miner = _cfg.get('miner', {})
_gas_sites = gas_site_list()
_by_site = _gas.get('usd_per_mcf_by_site', {}) or {}
_cfg_js = json.dumps({
    'usd_per_mcf': _gas.get('usd_per_mcf'),
    'cuft_per_kwh': _gas.get('cuft_per_kwh'),
    'usd_per_mcf_by_site': {s: _by_site.get(s) for s in _gas_sites},
    'gas_sites': _gas_sites,
    'running_kw_threshold': _gens.get('running_kw_threshold'),
    'spec_th': _miner.get('spec_th', 180),
    'ga_pct': round(_ga.get('pct', 0.1) * 100, 4),
    'ga_base': _ga.get('base', 'on_location_service'),
    'no_location_sites': ', '.join(_ga.get('no_location_sites', [])),
})

# metric checkboxes grouped by registry "group"
_reg = registry_for_ui()
_groups = {}
for _mt in _reg:
    _groups.setdefault(_mt['group'], []).append(_mt)
_default_on = {'avg_hashrate', 'theoretical_hashrate', 'revenue_usd', 'gas_mcf',
               'gas_cost', 'avg_gens', 'avg_gens_deployed', 'pm_cost', 'sc_cost',
               'pm_visits', 'sc_visits', 'ga_cost'}

_metric_html = ''
for _gname, _items in _groups.items():
    _metric_html += f'<div class="mgroup"><div class="mgroup-h">{_gname}</div>'
    for _it in _items:
        _lbl = _it['label'] + (f" ({_it['unit']})" if _it['unit'] else '')
        _chk = 'checked' if _it['key'] in _default_on else ''
        _metric_html += (f'<label class="mchk"><input type="checkbox" value="{_it["key"]}" '
                         f'{_chk}> {_lbl}</label>')
    _metric_html += '</div>'

print("Content-Type: text/html\n")
print("""<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>NGON Econs by Site Report Builder</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
       background: #1a1a1a; color: #e0e0e0; line-height: 1.4; }
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-thumb { background: #666; border-radius: 4px; }
* { scrollbar-width: thin; scrollbar-color: #666 transparent; }
""" + generate_dropdown_css() + """
.header { background-color: rgba(255,255,255,0.05); border-radius: 8px; padding: 24px 30px;
    margin: 20px; border: 1px solid rgba(255,255,255,0.1);
    display: flex; justify-content: space-between; align-items: flex-start; }
.header .sub { color: #888; font-size: 13px; margin-top: 4px; }
.wrap { margin: 0 20px 40px; }
.panel { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.1);
    border-radius: 8px; padding: 18px 20px; margin-bottom: 18px; }
.controls { display: flex; flex-wrap: wrap; gap: 22px; align-items: flex-end; }
.ctl { display: flex; flex-direction: column; gap: 5px; }
.ctl label.l { font-size: 11px; text-transform: uppercase; letter-spacing: .5px; color: #888; }
input, select { background: #2a2a2a; border: 1px solid #3a3a3a; color: #e0e0e0;
    padding: 7px 10px; border-radius: 5px; font-size: 13px; }
.btn { background: #00ff00; color: #072; border: none; font-weight: 700; cursor: pointer;
    padding: 9px 20px; border-radius: 5px; font-size: 13px; }
.btn:hover { background: #4dff4d; }
.btn.sec { background: #2a2a2a; color: #e0e0e0; border: 1px solid #3a3a3a; }
.metrics { display: flex; flex-wrap: wrap; gap: 20px; margin-top: 4px; }
.mgroup { min-width: 150px; }
.mgroup-h { font-size: 11px; text-transform: uppercase; letter-spacing: .5px;
    color: #4a9eff; margin-bottom: 6px; }
.mchk { display: block; font-size: 13px; margin: 3px 0; cursor: pointer; }
.mchk input { margin-right: 6px; vertical-align: middle; }
.tablewrap { overflow-x: auto; border-radius: 8px; border: 1px solid rgba(255,255,255,0.1); }
table { border-collapse: collapse; width: 100%; font-size: 13px; white-space: nowrap; }
th, td { padding: 8px 12px; text-align: right; border-bottom: 1px solid #2a2a2a; }
th { position: sticky; top: 0; background: #222; color: #aaa; font-weight: 600;
    text-align: right; z-index: 1; }
th:first-child, td:first-child, th:nth-child(2), td:nth-child(2) { text-align: left; }
tbody tr:hover { background: rgba(255,255,255,0.04); }
td.na { color: #555; }
tr.ga td { color: #ffaa00; }
tr.newbucket td { border-top: 2px solid #3a3a3a; }
.msg { color: #888; font-size: 13px; padding: 12px 4px; }
.err { color: #ff6600; font-size: 12px; margin-top: 10px; }
.notes { color: #777; font-size: 12px; margin-top: 12px; line-height: 1.6; }
.modal-ov { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.65);
    z-index: 3000; }
.modal-ov.show { display: flex; align-items: center; justify-content: center; }
.modal { background: #1f1f1f; border: 1px solid #3a3a3a; border-radius: 10px; padding: 24px;
    width: 460px; max-width: 92vw; max-height: 90vh; overflow-y: auto; }
.modal h2 { color: #00ff00; font-size: 18px; margin-bottom: 4px; }
.modal .mh { color: #888; font-size: 12px; margin-bottom: 18px; }
.frow { display: flex; flex-direction: column; gap: 5px; margin-bottom: 14px; }
.frow > label { font-size: 12px; color: #aaa; }
.frow input, .frow select { width: 100%; }
.frow .hint { font-size: 11px; color: #666; }
.site-rates { display: grid; grid-template-columns: 1fr 110px; gap: 6px 10px;
    align-items: center; margin: 2px 0 2px; }
.site-rates label { font-size: 12px; color: #ccc; }
.site-rates input { width: 100%; }
.modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 6px; }
/* --- tab nav --- */
.tabnav { display: flex; gap: 8px; margin: 0 20px; }
.tabbtn { background: #2a2a2a; color: #aaa; border: 1px solid #3a3a3a; border-bottom: none;
    padding: 9px 20px; border-radius: 8px 8px 0 0; cursor: pointer; font-size: 13px; font-weight: 600; }
.tabbtn.active { background: rgba(255,255,255,0.05); color: #e0e0e0; border-color: rgba(255,255,255,0.1); }
/* --- Site Economics tab (all scoped under #tab-econ) --- */
#tab-econ .ebar { display: flex; flex-wrap: wrap; gap: 16px; align-items: flex-end; }
#tab-econ .ectl { display: flex; flex-direction: column; gap: 5px; }
#tab-econ .ectl label.l { font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: .5px; }
#tab-econ .ectl.chk { flex-direction: row; align-items: center; gap: 6px; }
#tab-econ .sitewrap { position: relative; }
#tab-econ .sitebtn { background: #2a2a2a; border: 1px solid #3a3a3a; color: #e0e0e0; border-radius: 5px;
    padding: 7px 10px; cursor: pointer; font-size: 13px; }
#tab-econ .sitemenu { position: absolute; z-index: 20; top: 110%; left: 0; background: #1f1f1f;
    border: 1px solid #3a3a3a; border-radius: 8px; padding: 8px; max-height: 300px; overflow: auto;
    display: none; min-width: 190px; box-shadow: 0 10px 30px rgba(0,0,0,.5); }
#tab-econ .sitemenu.open { display: block; }
#tab-econ .sitemenu .row { display: flex; align-items: center; gap: 7px; padding: 3px 4px; font-size: 13px; }
#tab-econ .sitemenu .tools { display: flex; gap: 8px; justify-content: space-between;
    border-bottom: 1px solid #3a3a3a; padding-bottom: 6px; margin-bottom: 6px; }
#tab-econ .sitemenu .tools a { color: #4a9eff; cursor: pointer; font-size: 12px; }
#tab-econ .card { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.1);
    border-radius: 8px; padding: 14px; margin-top: 16px; }
#tab-econ #echart { width: 100%; height: 360px; display: block; }
#tab-econ .legend { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 8px; }
#tab-econ .lg { display: flex; align-items: center; gap: 6px; font-size: 12px; color: #888;
    cursor: pointer; user-select: none; }
#tab-econ .lg .sw { width: 12px; height: 3px; border-radius: 2px; }
#tab-econ .lg.off { opacity: .35; text-decoration: line-through; }
#tab-econ .tblwrap { overflow: auto; max-height: 62vh; border: 1px solid rgba(255,255,255,0.1);
    border-radius: 8px; margin-top: 16px; }
#tab-econ table { border-collapse: separate; border-spacing: 0; width: 100%; font-size: 12.5px;
    white-space: nowrap; }
#tab-econ th, #tab-econ td { padding: 6px 11px; text-align: right; border-bottom: 1px solid #2a2a2a; }
#tab-econ th { position: sticky; top: 0; background: #222; z-index: 5; font-weight: 600; color: #aaa; }
#tab-econ td.site, #tab-econ th.site { position: sticky; left: 0; background: #222; text-align: left; z-index: 6; }
#tab-econ th.site { z-index: 7; }
#tab-econ tr.total td { background: #1e2a44; font-weight: 700; border-top: 2px solid #4a9eff; }
#tab-econ tr.total td.site { background: #1e2a44; }
#tab-econ tbody tr:hover td { background: rgba(255,255,255,0.04); }
#tab-econ tbody tr.total:hover td { background: #1e2a44; }
#tab-econ .neg { color: #ff6b6b; }
#tab-econ .mut { color: #555; }
#tab-econ .ehint { color: #888; font-size: 12px; margin: 14px 0 0; }
#tab-econ .estatus { color: #888; font-size: 13px; padding: 12px 4px; }
</style>
</head>
<body>
<div class="header">
  <div class="dropdown">
    <h1 class="dropdown-title">Econs by Site Report Builder</h1>
    <div class="dropdown-content">""" + generate_dropdown_html(AuthManager.get_user_access()) + """</div>
    <div class="sub">Live cross-source econ reports &mdash; no snapshots, queried on demand</div>
  </div>
</div>
<div class="tabnav">
  <button class="tabbtn active" data-tab="classic" onclick="showTab('classic')">Report Builder</button>
  <button class="tabbtn" data-tab="econ" onclick="showTab('econ')">Site Economics</button>
</div>
<div class="wrap">
  <div id="tab-classic">
  <div class="panel">
    <div class="controls">
      <div class="ctl"><label class="l">From</label><input type="date" id="start" value=\"""" + _start.isoformat() + """\"></div>
      <div class="ctl"><label class="l">To (exclusive)</label><input type="date" id="end" value=\"""" + _end.isoformat() + """\"></div>
      <div class="ctl"><label class="l">Bucket</label>
        <select id="bucket"><option value="month">Month</option><option value="week">Week</option><option value="day">Day</option></select></div>
      <div class="ctl"><label class="l">Group by</label>
        <select id="group_by"><option value="site">Site</option><option value="fleet">Fleet (all)</option></select></div>
      <button class="btn" onclick="runReport()">Run Report</button>
      <button class="btn sec" onclick="exportCSV()">Export CSV</button>
      <button class="btn sec" onclick="openConfig()">&#9881; Settings</button>
    </div>
    <div class="metrics">""" + _metric_html + """</div>
  </div>
  <div id="out"><div class="msg">Pick a range and metrics, then Run Report.</div></div>
  </div><!-- /tab-classic -->

  <div id="tab-econ" style="display:none">
    <div class="panel">
      <div class="ebar">
        <div class="ectl"><label class="l">Metric</label><select id="emetric"></select></div>
        <div class="ectl"><label class="l">From</label><input type="month" id="efrom" value=\"""" + _econ_from + """\"></div>
        <div class="ectl"><label class="l">To</label><input type="month" id="eto" value=\"""" + _econ_to + """\"></div>
        <div class="ectl"><label class="l">Sites</label>
          <div class="sitewrap"><button type="button" class="sitebtn" id="esitebtn">All sites &#9662;</button>
            <div class="sitemenu" id="esitemenu"></div></div></div>
        <div class="ectl chk"><input type="checkbox" id="ehidezero" checked><label for="ehidezero" style="text-transform:none; color:#aaa; font-size:13px">Hide all-zero months</label></div>
      </div>
    </div>
    <div id="econStatus" class="estatus" style="display:none"></div>
    <div class="card">
      <svg id="echart" viewBox="0 0 1000 360" preserveAspectRatio="none"></svg>
      <div class="legend" id="elegend"></div>
    </div>
    <div class="ehint" id="etblhint"></div>
    <div class="tblwrap"><table id="etbl"></table></div>
  </div><!-- /tab-econ -->
</div>

<div class="modal-ov" id="cfgModal">
  <div class="modal">
    <h2>Report Settings</h2>
    <div class="mh">Saved to reporting_config.json &mdash; applies to every report.</div>
    <div class="frow"><label>Default gas price ($ / MCF)</label>
      <input type="number" step="0.01" id="cfg_usd_per_mcf">
      <span class="hint">Fallback for any site without a specific rate below.</span></div>
    <div class="frow"><label>Per-site gas price ($ / MCF)</label>
      <div id="cfg_site_rates" class="site-rates"></div>
      <span class="hint">Leave blank to use the default. ND sites are typically ~$0.50/MCF.</span></div>
    <div class="frow"><label>Gas conversion (cuft per kWh)</label>
      <input type="number" step="0.1" id="cfg_cuft_per_kwh">
      <span class="hint">Keep in sync with GAS_CUFT_PER_KWH in the status gas modal.</span></div>
    <div class="frow"><label>Gen &ldquo;running&rdquo; threshold (kW)</label>
      <input type="number" step="0.5" id="cfg_running_kw_threshold"></div>
    <div class="frow"><label>Miner spec hashrate (TH/s each)</label>
      <input type="number" step="1" id="cfg_spec_th">
      <span class="hint">Nameplate per miner for the Theoretical Max column. Fleet is M60 (180).</span></div>
    <div class="frow"><label>G&amp;A rate (%)</label>
      <input type="number" step="0.5" id="cfg_ga_pct"></div>
    <div class="frow"><label>G&amp;A base (what the % applies to)</label>
      <select id="cfg_ga_base">
        <option value="on_location_service">On-location service (PM + SC at sites)</option>
        <option value="operating_cost">Operating cost (gas + PM + SC)</option>
        <option value="revenue">Revenue</option>
        <option value="no_location_service">No-location spend (fleet G&amp;A line)</option>
      </select>
      <span class="hint">Per-site column for the first three; a single fleet line for the last.</span></div>
    <div class="frow"><label>No-location sites (comma-separated)</label>
      <input type="text" id="cfg_no_location_sites"></div>
    <div id="cfgMsg" class="err" style="display:none"></div>
    <div class="modal-actions">
      <button class="btn sec" onclick="closeConfig()">Cancel</button>
      <button class="btn" onclick="saveConfig()">Save</button>
    </div>
  </div>
</div>
<script>
const CFG = """ + _cfg_js + """;
""" + generate_dropdown_js() + """
let LAST = null;
function selectedMetrics() {
  return Array.from(document.querySelectorAll('.mchk input:checked')).map(c => c.value);
}
function fmtCell(fmt, v) {
  if (v === null || v === undefined) return {t:'N/A', na:true};
  let s;
  if (fmt==='ph') s = (v/1e15).toLocaleString('en-US',{maximumFractionDigits:1});
  else if (fmt==='usd') s = '$'+Math.round(v).toLocaleString('en-US');
  else if (fmt==='btc') s = v.toLocaleString('en-US',{minimumFractionDigits:4,maximumFractionDigits:4});
  else if (fmt==='num0') s = Math.round(v).toLocaleString('en-US');
  else if (fmt==='num1') s = v.toLocaleString('en-US',{minimumFractionDigits:1,maximumFractionDigits:1});
  else if (fmt==='int') s = Math.round(v).toLocaleString('en-US');
  else s = String(v);
  return {t:s, na:false};
}
async function runReport() {
  const metrics = selectedMetrics();
  if (!metrics.length) { document.getElementById('out').innerHTML='<div class="err">Select at least one metric.</div>'; return; }
  const p = new URLSearchParams({action:'run', start:document.getElementById('start').value,
    end:document.getElementById('end').value, bucket:document.getElementById('bucket').value,
    group_by:document.getElementById('group_by').value, metrics:metrics.join(',')});
  document.getElementById('out').innerHTML='<div class="msg">Running&hellip;</div>';
  try {
    const r = await fetch('?'+p.toString());
    const j = await r.json();
    if (!j.success) { document.getElementById('out').innerHTML='<div class="err">'+(j.error||'error')+'</div>'; return; }
    LAST = j.result; render(j.result);
  } catch(e) { document.getElementById('out').innerHTML='<div class="err">'+e+'</div>'; }
}
function render(res) {
  if (!res.rows.length) { document.getElementById('out').innerHTML='<div class="msg">No data for this selection.</div>'; return; }
  const cols = res.columns, cm = res.column_meta;
  let h = '<div class="tablewrap"><table><thead><tr><th>Period</th><th>'+
    (res.meta.group_by==='fleet'?'Fleet':'Site')+'</th>';
  cols.forEach(m => { const meta=cm[m]; h += '<th>'+meta.label+(meta.unit?' <span style="color:#666">'+meta.unit+'</span>':'')+'</th>'; });
  h += '</tr></thead><tbody>';
  let prev = null;
  res.rows.forEach(row => {
    const cls = [];
    if (row.dim==='G&A') cls.push('ga');
    if (prev !== null && row.bucket !== prev) cls.push('newbucket');
    prev = row.bucket;
    h += '<tr'+(cls.length?' class="'+cls.join(' ')+'"':'')+'><td>'+row.bucket+'</td><td>'+row.dim+'</td>';
    cols.forEach(m => { const c=fmtCell(cm[m].fmt, row[m]); h += '<td'+(c.na?' class="na"':'')+'>'+c.t+'</td>'; });
    h += '</tr>';
  });
  h += '</tbody></table></div>';

  const errs = Object.entries(res.errors||{});
  let notes = '<div class="notes"><b>Notes</b><br>';
  notes += 'Gas is inferred from generator power (same method as the status-page gas modal). ';
  notes += 'Theoretical Max = current deployed miner count (inventory) &times; spec TH/miner &mdash; present capacity, shown against every period. ';
  notes += 'Revenue = Foundry net earnings &times; daily BTC price. Shared subaccounts (EW) split by hashrate share. ';
  notes += 'G&amp;A = configured rate &times; the selected base (see &#9881; Settings).';
  if (errs.length) { notes += '<br><span style="color:#ff6600">Source issues:</span> ' +
    errs.map(([k,v]) => k+': '+v).join(' &middot; '); }
  notes += '</div>';
  document.getElementById('out').innerHTML = h + notes;
}
function exportCSV() {
  if (!LAST) return;
  const cols = LAST.columns, cm = LAST.column_meta;
  const head = ['Period', LAST.meta.group_by==='fleet'?'Fleet':'Site']
    .concat(cols.map(m => cm[m].label + (cm[m].unit?' ('+cm[m].unit+')':'')));
  const lines = [head.join(',')];
  LAST.rows.forEach(row => {
    const cells = [row.bucket, '"'+row.dim.replace(/"/g,'""')+'"'];
    cols.forEach(m => {
      let v = row[m];
      if (v===null||v===undefined) { cells.push(''); return; }
      if (cm[m].fmt==='ph') v = v/1e15;
      cells.push(typeof v==='number' ? v : ('"'+String(v)+'"'));
    });
    lines.push(cells.join(','));
  });
  const blob = new Blob([lines.join('\\n')], {type:'text/csv'});
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'report_'+LAST.meta.start+'_'+LAST.meta.end+'.csv';
  a.click();
}
function cfgVal(id){ return document.getElementById(id).value; }
function openConfig() {
  document.getElementById('cfg_usd_per_mcf').value = CFG.usd_per_mcf;
  const wrap = document.getElementById('cfg_site_rates');
  wrap.innerHTML = '';
  (CFG.gas_sites || []).forEach(s => {
    const id = 'cfg_rate_' + s.replace(/[^A-Za-z0-9_]/g, '_');
    const lab = document.createElement('label'); lab.textContent = s; lab.htmlFor = id;
    const inp = document.createElement('input');
    inp.type = 'number'; inp.step = '0.01'; inp.id = id; inp.dataset.site = s;
    inp.placeholder = CFG.usd_per_mcf;
    const v = CFG.usd_per_mcf_by_site ? CFG.usd_per_mcf_by_site[s] : null;
    if (v !== null && v !== undefined) inp.value = v;
    wrap.appendChild(lab); wrap.appendChild(inp);
  });
  document.getElementById('cfg_cuft_per_kwh').value = CFG.cuft_per_kwh;
  document.getElementById('cfg_running_kw_threshold').value = CFG.running_kw_threshold;
  document.getElementById('cfg_spec_th').value = CFG.spec_th;
  document.getElementById('cfg_ga_pct').value = CFG.ga_pct;
  document.getElementById('cfg_ga_base').value = CFG.ga_base;
  document.getElementById('cfg_no_location_sites').value = CFG.no_location_sites;
  document.getElementById('cfgMsg').style.display = 'none';
  document.getElementById('cfgModal').classList.add('show');
}
function closeConfig() { document.getElementById('cfgModal').classList.remove('show'); }
async function saveConfig() {
  const bySite = {};
  document.querySelectorAll('#cfg_site_rates input').forEach(inp => {
    const val = inp.value.trim();
    if (val !== '') bySite[inp.dataset.site] = val;
  });
  const p = new URLSearchParams({action:'save_config',
    usd_per_mcf: cfgVal('cfg_usd_per_mcf'), cuft_per_kwh: cfgVal('cfg_cuft_per_kwh'),
    usd_per_mcf_by_site: JSON.stringify(bySite),
    running_kw_threshold: cfgVal('cfg_running_kw_threshold'),
    spec_th: cfgVal('cfg_spec_th'), ga_pct: cfgVal('cfg_ga_pct'),
    ga_base: cfgVal('cfg_ga_base'),
    no_location_sites: cfgVal('cfg_no_location_sites')});
  try {
    const r = await fetch('?'+p.toString());
    const j = await r.json();
    if (!j.success) {
      const m = document.getElementById('cfgMsg');
      m.textContent = j.error || 'Save failed'; m.style.display = 'block'; return;
    }
    // reflect saved values locally, then re-run if a report is on screen
    CFG.usd_per_mcf = parseFloat(cfgVal('cfg_usd_per_mcf'));
    CFG.usd_per_mcf_by_site = {};
    Object.keys(bySite).forEach(s => { CFG.usd_per_mcf_by_site[s] = parseFloat(bySite[s]); });
    CFG.cuft_per_kwh = parseFloat(cfgVal('cfg_cuft_per_kwh'));
    CFG.running_kw_threshold = parseFloat(cfgVal('cfg_running_kw_threshold'));
    CFG.spec_th = parseFloat(cfgVal('cfg_spec_th'));
    CFG.ga_pct = parseFloat(cfgVal('cfg_ga_pct'));
    CFG.ga_base = cfgVal('cfg_ga_base');
    CFG.no_location_sites = cfgVal('cfg_no_location_sites');
    closeConfig();
    if (LAST) runReport();
    if (typeof Econ !== 'undefined' && _econLoaded) Econ.refetch();
  } catch(e) {
    const m = document.getElementById('cfgMsg');
    m.textContent = String(e); m.style.display = 'block';
  }
}
document.getElementById('cfgModal').addEventListener('click', function(e){
  if (e.target === this) closeConfig();
});
</script>
<script>
/* ============================ Site Economics tab ============================ */
var _econLoaded = false;
function showTab(which){
  document.querySelectorAll('.tabbtn').forEach(function(b){ b.classList.toggle('active', b.dataset.tab===which); });
  document.getElementById('tab-classic').style.display = (which==='classic') ? '' : 'none';
  document.getElementById('tab-econ').style.display = (which==='econ') ? '' : 'none';
  if (which==='econ' && !_econLoaded){ _econLoaded = true; Econ.activate(); }
}

var Econ = (function(){
  // metric catalog (spec section 3). sum = additive; ratio = recompute from summed components.
  var METRICS = [
    {k:"revenue", g:"Dollars", label:"Revenue", unit:"$", type:"sum", val:function(d){return d.revenue;}},
    {k:"net", g:"Dollars", label:"Net Income", unit:"$", type:"sum", val:function(d){return S(S(d.revenue,d.gas_cost),A(d.pm,d.sc));}},
    {k:"gas", g:"Dollars", label:"Gas Cost", unit:"$", type:"sum", val:function(d){return d.gas_cost;}},
    {k:"gen", g:"Dollars", label:"Gen Cost (PM+SC)", unit:"$", type:"sum", val:function(d){return A(d.pm,d.sc);}},
    {k:"ga", g:"Dollars", label:"G&A", unit:"$", type:"sum", val:function(d){return d.ga;}},
    {k:"rev_gen", g:"Per Gen Deployed", label:"Revenue / Gen", unit:"$/gen", type:"ratio", num:function(d){return d.revenue;}, den:function(d){return d.gens_deployed;}},
    {k:"net_gen", g:"Per Gen Deployed", label:"Net Income / Gen", unit:"$/gen", type:"ratio", num:function(d){return S(S(d.revenue,d.gas_cost),A(d.pm,d.sc));}, den:function(d){return d.gens_deployed;}},
    {k:"gen_gen", g:"Per Gen Deployed", label:"Gen Cost / Gen", unit:"$/gen", type:"ratio", num:function(d){return A(d.pm,d.sc);}, den:function(d){return d.gens_deployed;}},
    {k:"run_dep", g:"Operational", label:"Gens Running / Deployed", unit:"%", type:"ratio", num:function(d){return d.gens_running;}, den:function(d){return d.gens_deployed;}},
    {k:"hasheff", g:"Operational", label:"Hashrate Efficiency", unit:"%", type:"ratio", num:function(d){return d.hashrate;}, den:function(d){return d.theomax;}},
    {k:"gdep", g:"Operational", label:"Avg Gens Deployed", unit:"gens", type:"sum", val:function(d){return d.gens_deployed;}},
    {k:"grun", g:"Operational", label:"Avg Gens Running", unit:"gens", type:"sum", val:function(d){return d.gens_running;}},
    {k:"pm", g:"Service $ by Type (PM/SC)", label:"PM $", unit:"$", type:"sum", val:function(d){return d.pm;}},
    {k:"sc", g:"Service $ by Type (PM/SC)", label:"SC $", unit:"$", type:"sum", val:function(d){return d.sc;}},
    {k:"pmv", g:"Service $ by Type (PM/SC)", label:"# PM Visits", unit:"count", type:"sum", val:function(d){return d.pm_visits;}},
    {k:"scv", g:"Service $ by Type (PM/SC)", label:"# SC Visits", unit:"count", type:"sum", val:function(d){return d.sc_visits;}},
    // Shop/field is DERIVED from whether the invoice billed mileage -- Mesa's export
    // has no service_location column. Labelled (derived) so nobody reads it as billed.
    {k:"shop", g:"Service $ by Where (derived)", label:"Shop $ (derived)", unit:"$", type:"sum", val:function(d){return d.shop;}},
    {k:"fieldc", g:"Service $ by Where (derived)", label:"Field $ (derived)", unit:"$", type:"sum", val:function(d){return d.field;}},
    {k:"partsc", g:"Service $ by Where (derived)", label:"Parts-only $ (derived)", unit:"$", type:"sum", val:function(d){return d.parts_only;}},
    {k:"shopv", g:"Service $ by Where (derived)", label:"# Shop Visits (derived)", unit:"count", type:"sum", val:function(d){return d.shop_visits;}},
    {k:"fieldv", g:"Service $ by Where (derived)", label:"# Field Visits (derived)", unit:"count", type:"sum", val:function(d){return d.field_visits;}},
    {k:"shop_share", g:"Service $ by Where (derived)", label:"Shop Share of Gen Cost", unit:"%", type:"ratio", num:function(d){return d.shop;}, den:function(d){return A(d.shop,d.field,d.parts_only);}},
    // visit_type x service_type -- independent axes, so shop/field never costs you PM/SC.
    {k:"shop_pm", g:"Service $ by Type x Where", label:"Shop PM $", unit:"$", type:"sum", val:function(d){return d.shop_pm;}},
    {k:"shop_sc", g:"Service $ by Type x Where", label:"Shop SC $", unit:"$", type:"sum", val:function(d){return d.shop_sc;}},
    {k:"field_pm", g:"Service $ by Type x Where", label:"Field PM $", unit:"$", type:"sum", val:function(d){return d.field_pm;}},
    {k:"field_sc", g:"Service $ by Type x Where", label:"Field SC $", unit:"$", type:"sum", val:function(d){return d.field_sc;}},
    {k:"parts_pm", g:"Service $ by Type x Where", label:"Parts-only PM $", unit:"$", type:"sum", val:function(d){return d.parts_pm;}},
    {k:"parts_sc", g:"Service $ by Type x Where", label:"Parts-only SC $", unit:"$", type:"sum", val:function(d){return d.parts_sc;}},
    {k:"shop_sc_share", g:"Service $ by Type x Where", label:"SC Share of Shop $", unit:"%", type:"ratio", num:function(d){return d.shop_sc;}, den:function(d){return A(d.shop_pm,d.shop_sc);}}
  ];
  var METMAP = {}; METRICS.forEach(function(m){ METMAP[m.k]=m; });
  var COLORS = ["#4da3ff","#38d39f","#ff6b6b","#f7b955","#b07cff","#37c0d6","#ff8ac4","#9bd45b","#ff9f5a","#6ea8ff","#5be0c0","#e36bd8","#c2d24d","#5ad1ff","#ff7d7d","#a0f0a0"];
  // base engine metrics we request; everything above derives from these client-side.
  var BASE = ["avg_hashrate","theoretical_hashrate","revenue_usd","gas_mcf","gas_cost",
              "avg_gens","avg_gens_deployed","pm_cost","sc_cost","pm_visits","sc_visits","ga_cost",
              "shop_cost","field_cost","parts_only_cost","shop_visits","field_visits",
              "shop_pm_cost","shop_sc_cost","field_pm_cost","field_sc_cost",
              "parts_only_pm_cost","parts_only_sc_cost"];

  var DATA=[], IDX={}, PERIODS=[], SITES=[], siteColor={};
  var selSites=new Set(), offSites=new Set();
  var _built=false;

  // null-aware combinators: any null input -> null result (honest N/A propagation).
  function A(){ var t=0; for(var i=0;i<arguments.length;i++){ var x=arguments[i]; if(x===null||x===undefined) return null; t+=x; } return t; }
  function S(a,b){ return (a===null||a===undefined||b===null||b===undefined) ? null : a-b; }

  function fmt(v,unit){
    if(v===null||v===undefined||Number.isNaN(v)) return "—";
    if(unit==="%") return (v*100).toFixed(1)+"%";
    if(unit==="gens") return v.toFixed(1);
    if(unit==="count") return Math.round(v).toLocaleString();
    var s = "$"+Math.round(Math.abs(v)).toLocaleString();
    return v<0 ? "-"+s : s;
  }
  function cellVal(m,site,period){
    var d=IDX[site+"|"+period]; if(!d) return null;
    if(m.type==="sum"){ var v=m.val(d); return (v===null||v===undefined||Number.isNaN(v)) ? null : v; }
    var nu=m.num(d), de=m.den(d);
    if(nu===null||nu===undefined||de===null||de===undefined||!de) return null;
    return nu/de;
  }
  function totalVal(m,period,sites){
    // additive: sum non-null cells. ratio: recompute from summed components (never avg of ratios).
    if(m.type==="sum"){
      var s=0, any=false;
      sites.forEach(function(si){ var d=IDX[si+"|"+period]; if(d){ var v=m.val(d); if(v!==null&&v!==undefined&&!Number.isNaN(v)){ s+=v; any=true; } } });
      return any ? s : null;
    }
    var n=0, de=0, any2=false;
    sites.forEach(function(si){ var d=IDX[si+"|"+period]; if(d){ var nu=m.num(d), dd=m.den(d);
      if(nu!==null&&nu!==undefined&&dd!==null&&dd!==undefined){ n+=nu; de+=dd; any2=true; } } });
    return (any2&&de) ? n/de : null;
  }
  function monthNonZero(period,sites){
    var s=0; var z=function(x){ return (x===null||x===undefined) ? 0 : Math.abs(x); };
    sites.forEach(function(si){ var d=IDX[si+"|"+period]; if(d){ s += z(d.revenue)+z(d.gas_cost)+z(d.pm)+z(d.sc)+z(d.gens_deployed); } });
    return s>0;
  }
  function activePeriods(){
    var hz=document.getElementById("ehidezero").checked; var sites=[].concat.apply([], [Array.from(selSites)]);
    return PERIODS.filter(function(p){ return !hz || monthNonZero(p,sites); });
  }

  function monthAfter(to){
    var parts=to.split("-"); var y=+parts[0], mm=+parts[1];
    return (mm===12) ? (y+1)+"-01-01" : y+"-"+String(mm+1).padStart(2,"0")+"-01";
  }
  function setStatus(msg,isErr){ var el=document.getElementById("econStatus"); el.style.display=""; el.textContent=msg; el.style.color=isErr?"#ff6600":"#888"; }
  function clearStatus(){ document.getElementById("econStatus").style.display="none"; }

  async function fetchData(){
    var from=document.getElementById("efrom").value, to=document.getElementById("eto").value;
    if(!from||!to||from>to){ setStatus("Pick a valid month range.",true); return; }
    var p=new URLSearchParams({action:"run", start:from+"-01", end:monthAfter(to),
      bucket:"month", group_by:"site", metrics:BASE.join(",")});
    setStatus("Loading…",false);
    try{
      var r=await fetch("?"+p.toString());
      var j=await r.json();
      if(!j.success){ setStatus(j.error||"Error loading data",true); return; }
      buildData(j.result); clearStatus(); render();
    }catch(e){ setStatus(String(e),true); }
  }
  function buildData(res){
    DATA=[];
    (res.rows||[]).forEach(function(row){
      if(row.dim==="G&A") return; // skip the engine's fleet-only G&A line (not a site)
      DATA.push({period:row.bucket, site:row.dim,
        hashrate:row.avg_hashrate, theomax:row.theoretical_hashrate,
        revenue:row.revenue_usd, gas_mcf:row.gas_mcf, gas_cost:row.gas_cost,
        gens_running:row.avg_gens, gens_deployed:row.avg_gens_deployed,
        pm:row.pm_cost, sc:row.sc_cost, pm_visits:row.pm_visits, sc_visits:row.sc_visits, ga:row.ga_cost,
        shop:row.shop_cost, field:row.field_cost, parts_only:row.parts_only_cost,
        shop_visits:row.shop_visits, field_visits:row.field_visits,
        shop_pm:row.shop_pm_cost, shop_sc:row.shop_sc_cost,
        field_pm:row.field_pm_cost, field_sc:row.field_sc_cost,
        parts_pm:row.parts_only_pm_cost, parts_sc:row.parts_only_sc_cost});
    });
    PERIODS=Array.from(new Set(DATA.map(function(d){return d.period;}))).sort();
    SITES=Array.from(new Set(DATA.map(function(d){return d.site;}))).sort();
    IDX={}; DATA.forEach(function(d){ IDX[d.site+"|"+d.period]=d; });
    siteColor={}; SITES.forEach(function(s,i){ siteColor[s]=COLORS[i%COLORS.length]; });
    selSites=new Set(SITES); offSites=new Set();
    buildSiteMenu();
  }
  function buildSiteMenu(){
    var menu=document.getElementById("esitemenu");
    menu.innerHTML='<div class="tools"><a id="eselall">All</a><a id="eselnone">None</a></div>'+
      SITES.map(function(s){ return '<div class="row"><input type="checkbox" data-s="'+s+'" checked> <span style="color:'+siteColor[s]+'">&#9632;</span> '+s+'</div>'; }).join("");
    menu.querySelectorAll("input[type=checkbox]").forEach(function(cb){ cb.onchange=function(){
      if(cb.checked) selSites.add(cb.dataset.s); else selSites.delete(cb.dataset.s); render(); }; });
    document.getElementById("eselall").onclick=function(){ selSites=new Set(SITES); menu.querySelectorAll("input").forEach(function(c){c.checked=true;}); render(); };
    document.getElementById("eselnone").onclick=function(){ selSites=new Set(); menu.querySelectorAll("input").forEach(function(c){c.checked=false;}); render(); };
  }

  function drawChart(m, periods, sites){
    var svg=document.getElementById("echart");
    var W=1000,H=360, padL=64,padR=16,padT=14,padB=42;
    var iw=W-padL-padR, ih=H-padT-padB;
    var n=periods.length;
    var xOf=function(i){ return n<=1 ? padL+iw/2 : padL + iw*i/(n-1); };
    var series=[];
    sites.forEach(function(s){ if(offSites.has(s)) return;
      var pts=periods.map(function(p,i){ return {i:i, v:cellVal(m,s,p)}; });
      if(pts.some(function(o){return o.v!==null;})) series.push({name:s,color:siteColor[s],pts:pts,width:1.6,op:.85});
    });
    var tot=null;
    if(!offSites.has("__total__")){
      var tpts=periods.map(function(p,i){ return {i:i, v:totalVal(m,p,sites)}; });
      if(tpts.some(function(o){return o.v!==null;})) tot={name:"Fleet Total",color:"#ffffff",pts:tpts,width:3,op:1};
    }
    var all=series.slice(); if(tot) all.push(tot);
    var vals=[]; all.forEach(function(se){ se.pts.forEach(function(o){ if(o.v!==null&&o.v!==undefined) vals.push(o.v); }); });
    if(!vals.length){ svg.innerHTML=""; return; }
    var mn=Math.min.apply(null,[0].concat(vals)), mx=Math.max.apply(null,[0].concat(vals)); if(mn===mx) mx=mn+1;
    var padv=(mx-mn)*0.08; mx+=padv; if(mn<0) mn-=padv;
    var yOf=function(v){ return padT + ih*(1-(v-mn)/(mx-mn)); };
    var g="";
    var ticks=5;
    for(var t=0;t<=ticks;t++){ var vv=mn+(mx-mn)*t/ticks; var y=yOf(vv);
      g+='<line x1="'+padL+'" y1="'+y+'" x2="'+(W-padR)+'" y2="'+y+'" stroke="#2a2a2a" stroke-width="1"/>';
      g+='<text x="'+(padL-8)+'" y="'+(y+4)+'" fill="#888" font-size="11" text-anchor="end">'+fmt(vv,m.unit)+'</text>';
    }
    if(mn<0){ var yz=yOf(0); g+='<line x1="'+padL+'" y1="'+yz+'" x2="'+(W-padR)+'" y2="'+yz+'" stroke="#4a5a7d" stroke-width="1.2"/>'; }
    var step=Math.ceil(n/12)||1;
    periods.forEach(function(p,i){ if(i%step===0||i===n-1){ var x=xOf(i);
      g+='<text x="'+x+'" y="'+(H-padB+18)+'" fill="#888" font-size="10" text-anchor="middle">'+p+'</text>'; } });
    // path with true gaps: a null point breaks the line (moveto after any gap).
    function path(se){ var d="", pen=false;
      se.pts.forEach(function(o){ if(o.v===null||o.v===undefined){ pen=false; return; }
        d += (pen?"L":"M")+xOf(o.i).toFixed(1)+" "+yOf(o.v).toFixed(1)+" "; pen=true; });
      return d.trim();
    }
    all.forEach(function(se){ g+='<path d="'+path(se)+'" fill="none" stroke="'+se.color+'" stroke-width="'+se.width+'" opacity="'+se.op+'" stroke-linejoin="round"/>'; });
    svg.innerHTML=g;
  }
  function drawLegend(sites){
    var el=document.getElementById("elegend");
    var items=[{name:"Fleet Total",key:"__total__",color:"#ffffff"}].concat(sites.map(function(s){ return {name:s,key:s,color:siteColor[s]}; }));
    el.innerHTML=items.map(function(it){ return '<span class="lg '+(offSites.has(it.key)?"off":"")+'" data-k="'+it.key+'"><span class="sw" style="background:'+it.color+'"></span>'+it.name+'</span>'; }).join("");
    el.querySelectorAll(".lg").forEach(function(l){ l.onclick=function(){ var k=l.dataset.k; if(offSites.has(k)) offSites.delete(k); else offSites.add(k); render(); }; });
  }
  function drawTable(m,periods,sites){
    var t=document.getElementById("etbl");
    var h="<thead><tr><th class='site'>Site</th>"+periods.map(function(p){return "<th>"+p+"</th>";}).join("")+"</tr></thead><tbody>";
    sites.forEach(function(s){
      h+="<tr><td class='site'>"+s+"</td>"+periods.map(function(p){ var v=cellVal(m,s,p); var cls=(typeof v==="number"&&v<0)?"neg":(v===null?"mut":""); return "<td class='"+cls+"'>"+fmt(v,m.unit)+"</td>"; }).join("")+"</tr>";
    });
    h+="<tr class='total'><td class='site'>Fleet Total</td>"+periods.map(function(p){ var v=totalVal(m,p,sites); var cls=(typeof v==="number"&&v<0)?"neg":(v===null?"mut":""); return "<td class='"+cls+"'>"+fmt(v,m.unit)+"</td>"; }).join("")+"</tr>";
    h+="</tbody>";
    t.innerHTML=h;
    document.getElementById("etblhint").textContent = (m.type==="ratio")
      ? "Ratio metric — Fleet Total is recomputed from summed components (not an average of site ratios). — = no gens/capacity that month, or revenue unknown (no pool key)."
      : "Additive metric — Fleet Total is the sum across selected sites. — = value unavailable for that site-month.";
  }
  function render(){
    var chart=document.getElementById("echart"), tbl=document.getElementById("etbl"), leg=document.getElementById("elegend");
    if(!PERIODS.length){ chart.innerHTML=""; tbl.innerHTML=""; leg.innerHTML=""; document.getElementById("etblhint").textContent=""; return; }
    var m=METMAP[document.getElementById("emetric").value];
    var periods=activePeriods();
    var sites=Array.from(selSites).sort();
    drawChart(m,periods,sites); drawLegend(sites); drawTable(m,periods,sites);
  }

  function buildControls(){
    var sel=document.getElementById("emetric");
    var curG=null;
    METRICS.forEach(function(m){
      if(m.g!==curG){ curG=m.g; var og=document.createElement("optgroup"); og.label=m.g; sel.appendChild(og); }
      var o=document.createElement("option"); o.value=m.k; o.textContent=m.label; sel.lastChild.appendChild(o);
    });
    sel.value="revenue";
    document.getElementById("esitebtn").onclick=function(){ document.getElementById("esitemenu").classList.toggle("open"); };
    document.addEventListener("click", function(e){ if(!e.target.closest("#tab-econ .sitewrap")) document.getElementById("esitemenu").classList.remove("open"); });
    sel.onchange=render;
    document.getElementById("efrom").onchange=fetchData;
    document.getElementById("eto").onchange=fetchData;
    document.getElementById("ehidezero").onchange=render;
  }

  function activate(){ if(!_built){ _built=true; buildControls(); } fetchData(); }
  return { activate: activate, refetch: fetchData };
})();
</script>
</body>
</html>""")
