#!/usr/bin/env python3
"""Money -- what the fleet is earning right now.

Not in the nav dropdown (link-only page). Everything here is a THEORETICAL
earnings model off our live hashrate vs. network difficulty -- it is not pool
accounting. Foundry's actual payouts will differ (luck, pool fee, FPPS terms,
and our own hashrate undercount on big pods).

Model, per unit of hashrate H (TH/s):
    BTC/day = H * 1e12 * 86400 * block_reward / (difficulty * 2**32)
where block_reward = subsidy (3.125 now) + the trailing-144-block average fee.
"""

import cgi
import sys

sys.path.insert(0, '/opt/ngon/apps')
sys.path.insert(0, '/var/www/html/ngon')
from managers.auth_manager import AuthManager, generate_login_page_html

_form = cgi.FieldStorage()
_auth = AuthManager('money')
_auth_required, _should_exit, _headers = _auth.require_auth(_form)

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

if _auth_required:
    print("Content-Type: text/html")
    print("")
    print(generate_login_page_html("Money"))
    sys.exit(0)

# ---------------------------------------------------------------------------
import json
import os
import time
import urllib.request

from links import generate_dropdown_html, generate_dropdown_css, generate_dropdown_js

STATUS_API = "http://localhost:5050/api/status"
REPORTING_CONFIG = "/opt/ngon/config/reporting_config.json"
CACHE_FILE = "/var/www/html/ngon/status/caches/btc_network.json"
CACHE_TTL = 300          # network stats move slowly; one fetch per 5 min is plenty
SPEC_TH = 180            # M60 spec hashrate, TH/s (misc.miner_types is authoritative)
SPEC_W = 3500            # M60 spec draw, watts (same source)
MINERS_PER_GEN = 90      # the shop rule of thumb; the page also shows the real ratio
UA = {"User-Agent": "ngon-money"}

MEMPOOL = "https://mempool.space/api"
COINGECKO = "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd"
COINBASE_SPOT = "https://api.coinbase.com/v2/prices/BTC-USD/spot"


def _get_json(url, timeout=8):
    req = urllib.request.Request(url, headers=UA)
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        return json.loads(resp.read().decode())


def _fetch_network():
    """Live difficulty / network hashrate / block reward / BTC price.

    Every piece is fetched independently so one dead upstream degrades a single
    field instead of blanking the page. Returns a dict plus an 'errors' list.
    """
    out = {"errors": []}

    try:
        hr = _get_json(MEMPOOL + "/v1/mining/hashrate/3d")
        out["difficulty"] = float(hr["currentDifficulty"])
        out["network_hashrate"] = float(hr["currentHashrate"])   # H/s
    except Exception as exc:
        out["errors"].append("hashrate/difficulty: %s" % exc)

    try:
        out["height"] = int(_get_json(MEMPOOL + "/blocks/tip/height"))
    except Exception as exc:
        out["errors"].append("height: %s" % exc)

    try:
        adj = _get_json(MEMPOOL + "/v1/difficulty-adjustment")
        out["retarget"] = {
            "change_pct": adj.get("difficultyChange"),
            "remaining_blocks": adj.get("remainingBlocks"),
            "eta_ms": adj.get("estimatedRetargetDate"),
            "progress_pct": adj.get("progressPercent"),
        }
    except Exception as exc:
        out["errors"].append("retarget: %s" % exc)

    try:
        rw = _get_json(MEMPOOL + "/v1/mining/reward-stats/144")
        blocks = max(1, int(rw["endBlock"]) - int(rw["startBlock"]) + 1)
        out["fee_per_block"] = float(rw["totalFee"]) / 1e8 / blocks
    except Exception as exc:
        out["fee_per_block"] = 0.0
        out["errors"].append("fees: %s" % exc)

    price = None
    try:
        price = float(_get_json(COINGECKO)["bitcoin"]["usd"])
        out["price_source"] = "CoinGecko"
    except Exception:
        try:
            price = float(_get_json(COINBASE_SPOT)["data"]["amount"])
            out["price_source"] = "Coinbase"
        except Exception as exc:
            out["errors"].append("price: %s" % exc)
    if price:
        out["price_usd"] = price

    if "height" in out:
        # Subsidy halves every 210,000 blocks (3.125 BTC in the current epoch).
        out["subsidy"] = 50.0 / (2 ** (out["height"] // 210000))
    out["fetched"] = time.time()
    return out


def _network_stats():
    """Cached network stats. Stale cache beats no data -- age is shown in the UI."""
    cached = None
    try:
        with open(CACHE_FILE) as fh:
            cached = json.load(fh)
    except Exception:
        pass

    if cached and (time.time() - cached.get("fetched", 0)) < CACHE_TTL:
        cached["cached"] = True
        return cached

    fresh = _fetch_network()
    # Only adopt the fetch if the two fields the whole model hangs on came back.
    if "difficulty" in fresh and "price_usd" in fresh:
        try:
            with open(CACHE_FILE, "w") as fh:
                json.dump(fresh, fh)
        except Exception:
            pass
        fresh["cached"] = False
        return fresh

    if cached:
        cached["cached"] = True
        cached["errors"] = (cached.get("errors") or []) + fresh.get("errors", [])
        return cached
    fresh["cached"] = False
    return fresh


def _gas_rates():
    """Gas-cost inputs for the repair-payoff math.

    Read from reporting_config.json so this page and the report builder price gas
    off the same number. The shipped $/MCF is still a PLACEHOLDER -- flagged here
    so the page can say so out loud instead of quietly pricing gas at $0.50/MCF.
    """
    out = {"cuft_per_kwh": 11.0, "usd_per_mcf": 0.5, "placeholder": True}
    try:
        with open(REPORTING_CONFIG) as fh:
            gas = (json.load(fh) or {}).get("gas") or {}
        out["cuft_per_kwh"] = float(gas.get("cuft_per_kwh") or out["cuft_per_kwh"])
        out["usd_per_mcf"] = float(gas.get("usd_per_mcf") or out["usd_per_mcf"])
        out["placeholder"] = "PLACEHOLDER" in (gas.get("_usd_per_mcf_note") or "")
    except Exception:
        pass
    return out


def _fleet():
    """Per-site + fleet rollup straight off the live status API."""
    data = _get_json(STATUS_API, timeout=10)
    excluded = set((((data.get("misc") or {}).get("display") or {})
                    .get("excludedSites") or []))

    spec_th, spec_w = SPEC_TH, SPEC_W
    try:
        m60 = (((data.get("misc") or {}).get("miner_types") or {}).get("M60") or {})
        spec_th = float(m60.get("spec_hashrate") or SPEC_TH)
        spec_w = float(m60.get("spec_wattage") or SPEC_W)
    except Exception:
        pass

    sites = []
    for name, site in (data.get("sites") or {}).items():
        if name in excluded:
            continue
        row = {"site": name, "hashrate": 0.0, "hashing": 0, "sleeping": 0,
               "offline": 0, "zero_hash": 0, "installed": 0, "power_w": 0.0,
               "gens_running": 0, "gens_total": 0}
        for group in (site.get("generator_groups") or {}).values():
            for gen in (group.get("generators") or {}).values():
                row["gens_total"] += 1
                if (gen.get("status") or "").lower() == "running":
                    row["gens_running"] += 1
            for pod in (group.get("pods") or {}).values():
                st = pod.get("stats") or {}
                row["hashrate"] += st.get("hashrate") or 0
                row["hashing"] += st.get("miners_hashing") or 0
                row["sleeping"] += st.get("miners_sleeping") or 0
                row["offline"] += st.get("miners_offline") or 0
                row["zero_hash"] += st.get("miners_zero_hash") or 0
                row["installed"] += (st.get("miners_installed")
                                     or st.get("total_miners") or 0)
                # stats.power is average watts PER MINER, not pod total.
                row["power_w"] += (st.get("power") or 0) * (st.get("total_miners") or 0)
        if row["installed"] or row["hashrate"]:
            sites.append(row)

    sites.sort(key=lambda r: r["hashrate"], reverse=True)
    total = {k: sum(r[k] for r in sites)
             for k in ("hashrate", "hashing", "sleeping", "offline", "zero_hash",
                       "installed", "power_w", "gens_running", "gens_total")}
    total["spec_th"] = spec_th
    total["spec_w"] = spec_w
    total["ath_hashrate"] = (data.get("summary") or {}).get("ath_hashrate")
    total["updated"] = (data.get("metadata") or {}).get("last_updated")
    return {"sites": sites, "fleet": total}


def _payload():
    net = _network_stats()
    out = {"network": net, "miners_per_gen_assumed": MINERS_PER_GEN,
           "gas": _gas_rates(), "generated": time.time()}
    try:
        out.update(_fleet())
    except Exception as exc:
        out["error"] = "status API unreachable: %s" % exc
        out["sites"] = []
        out["fleet"] = {}

    # The one number everything else is multiplied by: BTC per TH/s per day.
    diff = net.get("difficulty")
    reward = (net.get("subsidy") or 3.125) + (net.get("fee_per_block") or 0.0)
    if diff:
        out["btc_per_th_day"] = 1e12 * 86400 * reward / (diff * (2 ** 32))
    else:
        out["btc_per_th_day"] = None
    out["block_reward"] = reward
    return out


if _form.getvalue('ajax') == 'data':
    print("Content-Type: application/json")
    print("Cache-Control: no-store")
    print("")
    print(json.dumps(_payload()))
    sys.exit(0)

_payload_json = json.dumps(_payload())
_user_access = AuthManager.get_user_access()

print("Content-Type: text/html")
print("")
print("""<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Money - NGON</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <style>
        * { box-sizing: border-box; }
        body {
            background: #1a1a1a;
            color: #e0e0e0;
            font-family: Arial, Helvetica, sans-serif;
            margin: 0;
            padding: 0 0 40px 0;
        }
        .header {
            background-color: rgba(255,255,255,0.05) !important;
            border-radius: 8px !important;
            padding: 24px 30px !important;
            margin: 20px !important;
            border: 1px solid rgba(255,255,255,0.1) !important;
            border-bottom: 1px solid rgba(255,255,255,0.1) !important;
            display: flex !important;
            justify-content: space-between !important;
            align-items: center !important;
            flex-wrap: wrap;
            gap: 12px;
        }
        h1 { margin: 0; font-size: 22px; }
        .subtitle { color: #888; font-size: 12px; margin-top: 6px; }
        .wrap { padding: 0 20px; max-width: 1500px; margin: 0 auto; }
        .card {
            background: rgba(255,255,255,0.05);
            border: 1px solid rgba(255,255,255,0.1);
            border-radius: 8px;
            padding: 18px 20px;
            margin-bottom: 16px;
        }
        .card h2 {
            margin: 0 0 14px 0;
            font-size: 13px;
            color: #888;
            text-transform: uppercase;
            letter-spacing: 1px;
            font-weight: normal;
        }

        /* ---- hero ---- */
        .hero { display: flex; flex-wrap: wrap; gap: 30px; align-items: flex-end; }
        .hero-main { flex: 1 1 320px; }
        .hero-usd {
            font-size: 54px;
            font-weight: bold;
            color: #00ff00;
            line-height: 1;
            font-variant-numeric: tabular-nums;
        }
        .hero-btc { font-size: 20px; color: #4a9eff; margin-top: 8px; font-variant-numeric: tabular-nums; }
        .hero-side { display: flex; gap: 26px; flex-wrap: wrap; }
        .hero-fact .v { font-size: 22px; font-weight: bold; font-variant-numeric: tabular-nums; }
        .hero-fact .l { font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: .5px; }

        /* ---- ticker ---- */
        .ticker {
            margin-top: 18px;
            padding-top: 14px;
            border-top: 1px solid rgba(255,255,255,0.08);
            display: flex;
            align-items: baseline;
            gap: 12px;
            flex-wrap: wrap;
        }
        .ticker .sats {
            font-family: "Courier New", monospace;
            font-size: 26px;
            color: #ffaa00;
            font-weight: bold;
            font-variant-numeric: tabular-nums;
        }
        .ticker .lbl { color: #888; font-size: 12px; }

        /* ---- assumptions ---- */
        .assume { display: flex; gap: 22px; flex-wrap: wrap; align-items: center; }
        .assume label { font-size: 11px; color: #888; display: block; margin-bottom: 4px;
                        text-transform: uppercase; letter-spacing: .5px; }
        .assume input[type=number] {
            background: #2a2a2a; border: 1px solid #3a3a3a; border-radius: 4px;
            color: #e0e0e0; padding: 6px 8px; width: 110px; font-size: 14px;
        }
        .mode-btns { display: flex; gap: 0; }
        .mode-btns button {
            background: #2a2a2a; border: 1px solid #3a3a3a; color: #bbb;
            padding: 7px 14px; cursor: pointer; font-size: 12px;
        }
        .mode-btns button:first-child { border-radius: 4px 0 0 4px; }
        .mode-btns button:last-child { border-radius: 0 4px 4px 0; }
        .mode-btns button.on { background: #00ff00; color: #000; border-color: #00ff00; font-weight: bold; }
        .reset { background: none; border: none; color: #4a9eff; cursor: pointer; font-size: 11px; }

        /* ---- unit cards ---- */
        .units { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px; }
        .unit-big { font-size: 34px; font-weight: bold; color: #00ff00; font-variant-numeric: tabular-nums; }
        .unit-sub { color: #4a9eff; font-size: 14px; margin-top: 4px; font-variant-numeric: tabular-nums; }
        .unit-rows { margin-top: 14px; border-top: 1px solid rgba(255,255,255,0.08); padding-top: 10px; }
        .unit-row { display: flex; justify-content: space-between; padding: 4px 0; font-size: 13px; }
        .unit-row span:first-child { color: #888; }
        .unit-row span:last-child { font-variant-numeric: tabular-nums; }

        /* ---- repair payoff ---- */
        .payoff-head { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; }
        .payoff-head .t { font-size: 15px; font-weight: bold; color: #e0e0e0; }
        .payoff-inputs { display: flex; gap: 16px; flex-wrap: wrap; margin: 12px 0 14px 0; }
        .payoff-inputs input {
            background: #2a2a2a; border: 1px solid #3a3a3a; border-radius: 4px;
            color: #e0e0e0; padding: 6px 8px; width: 110px; font-size: 14px;
        }
        .payoff-inputs .cost input { border-color: #00ff00; width: 130px; font-size: 16px; }
        .payoff-inputs label { font-size: 11px; color: #888; display: block; margin-bottom: 4px;
                               text-transform: uppercase; letter-spacing: .5px; }
        .payoff-days { font-size: 40px; font-weight: bold; line-height: 1; font-variant-numeric: tabular-nums; }
        .payoff-days small { font-size: 16px; font-weight: normal; color: #888; }
        .verdict {
            display: inline-block; padding: 3px 10px; border-radius: 12px;
            font-size: 11px; font-weight: bold; text-transform: uppercase; letter-spacing: .5px;
        }
        .v-great { background: #00ff00; color: #000; }
        .v-good  { background: #4a9eff; color: #000; }
        .v-ok    { background: #ffaa00; color: #000; }
        .v-bad   { background: #ff4444; color: #fff; }
        .v-none  { background: #444; color: #bbb; }
        .payoff-bar {
            height: 8px; background: #2a2a2a; border-radius: 4px; margin: 12px 0 4px 0;
            overflow: hidden;
        }
        .payoff-bar div { height: 100%; border-radius: 4px; transition: width .4s ease; }
        .payoff-scale { display: flex; justify-content: space-between; color: #666; font-size: 10px; }

        /* ---- table ---- */
        table { width: 100%; border-collapse: collapse; font-size: 13px; }
        th {
            text-align: right; color: #888; font-weight: normal; font-size: 11px;
            text-transform: uppercase; letter-spacing: .5px; padding: 8px 10px;
            border-bottom: 1px solid rgba(255,255,255,0.15); white-space: nowrap;
        }
        th:first-child, td:first-child { text-align: left; }
        td { padding: 8px 10px; border-bottom: 1px solid rgba(255,255,255,0.06);
             text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
        tbody tr:hover { background: rgba(255,255,255,0.04); }
        tfoot td { font-weight: bold; border-top: 1px solid rgba(255,255,255,0.15); border-bottom: none; }
        .money { color: #00ff00; }
        .dim { color: #666; }
        .warn { color: #ff6600; }

        /* ---- network strip ---- */
        .netstrip { display: flex; gap: 28px; flex-wrap: wrap; }
        .netstrip .v { font-size: 16px; font-weight: bold; }
        .netstrip .l { font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: .5px; }
        .foot { color: #666; font-size: 11px; padding: 6px 20px 0 20px; max-width: 1500px; margin: 0 auto; line-height: 1.6; }
        @media (max-width: 700px) {
            .hero-usd { font-size: 38px; }
            .wrap { padding: 0 10px; }
            .header { padding: 18px !important; margin: 10px !important; }
        }
""")
print(generate_dropdown_css())
print("""    </style>
</head>
<body>
    <div class="header">
        <div class="dropdown">
            <h1 class="dropdown-title">NGON - Money</h1>
            <div class="dropdown-content">""")
print(generate_dropdown_html(_user_access))
print("""            </div>
            <div class="subtitle">Theoretical earnings from live hashrate, difficulty and price. Not pool accounting.</div>
        </div>
        <div style="text-align:right;">
            <div id="dataAge" class="dim" style="font-size:12px;"></div>
            <button class="reset" onclick="loadData()"><i class="fas fa-rotate"></i> Refresh</button>
        </div>
    </div>

    <div class="wrap">
        <div class="card">
            <h2 id="heroTitle">Whole operation - right now</h2>
            <div class="hero">
                <div class="hero-main">
                    <div class="hero-usd" id="heroUsd">--</div>
                    <div class="hero-btc" id="heroBtc">--</div>
                </div>
                <div class="hero-side">
                    <div class="hero-fact"><div class="v" id="heroMonth">--</div><div class="l">per month</div></div>
                    <div class="hero-fact"><div class="v" id="heroYear">--</div><div class="l">per year</div></div>
                    <div class="hero-fact"><div class="v" id="heroHash">--</div><div class="l">fleet hashrate</div></div>
                    <div class="hero-fact"><div class="v" id="heroShare">--</div><div class="l">of network</div></div>
                </div>
            </div>
            <div class="ticker">
                <span class="sats" id="ticker">0</span>
                <span class="lbl">sats earned since you opened this page
                    (<span id="tickerUsd">$0.00</span>)</span>
            </div>
        </div>

        <div class="card">
            <h2>Assumptions</h2>
            <div class="assume">
                <div>
                    <label>BTC price (USD)</label>
                    <input type="number" id="price" step="1" oninput="render()">
                </div>
                <div>
                    <label>Pool fee %</label>
                    <input type="number" id="poolfee" step="0.1" value="0" oninput="render()">
                </div>
                <div>
                    <label>Miners per gen (assumed)</label>
                    <input type="number" id="mpg" step="1" oninput="render()">
                </div>
                <div>
                    <label>Basis</label>
                    <div class="mode-btns">
                        <button id="modeNow" class="on" onclick="setMode('now')">Right now</button>
                        <button id="modeFull" onclick="setMode('full')">Full send</button>
                    </div>
                </div>
                <div>
                    <label>&nbsp;</label>
                    <button class="reset" onclick="resetAssumptions()">reset to live values</button>
                </div>
            </div>
            <div class="dim" style="font-size:11px;margin-top:10px;" id="modeNote"></div>
        </div>

        <div class="units">
            <div class="card">
                <h2>Per miner</h2>
                <div class="unit-big" id="minerUsd">--</div>
                <div class="unit-sub" id="minerBtc">--</div>
                <div class="unit-rows" id="minerRows"></div>
            </div>
            <div class="card">
                <h2>Per generator</h2>
                <div class="unit-big" id="genUsd">--</div>
                <div class="unit-sub" id="genBtc">--</div>
                <div class="unit-rows" id="genRows"></div>
            </div>
        </div>

        <div class="card">
            <h2>Repair payoff - is it worth fixing?</h2>
            <div class="assume" style="margin-bottom:18px;">
                <div>
                    <label>Gas cost $/MCF</label>
                    <input type="number" id="gasrate" step="0.01" oninput="render()">
                </div>
                <div>
                    <label>Uptime / duty cycle %</label>
                    <input type="number" id="uptime" step="1" value="100" oninput="render()">
                </div>
                <div class="dim" style="font-size:11px;max-width:420px;">
                    Payoff is on <b>net</b> revenue: mining revenue minus the gas burned to make it.
                    Uptime scales both - drop it below 100% for daytime derate, sleep time or a pod
                    that is down half the month. <span id="gasNote"></span>
                </div>
            </div>
            <div class="units">
                <div class="card" style="margin:0;background:rgba(255,255,255,0.03);">
                    <div class="payoff-head">
                        <span class="t"><i class="fas fa-microchip"></i> Miner repair</span>
                        <span class="verdict v-none" id="mrVerdict">enter a cost</span>
                    </div>
                    <div class="payoff-inputs">
                        <div class="cost">
                            <label>Repair cost $</label>
                            <input type="number" id="mrCost" step="1" placeholder="e.g. 250" oninput="render()">
                        </div>
                        <div>
                            <label>Hashrate after TH/s</label>
                            <input type="number" id="mrTh" step="1" oninput="render()">
                        </div>
                        <div>
                            <label>Draw (watts)</label>
                            <input type="number" id="mrW" step="10" oninput="render()">
                        </div>
                    </div>
                    <div class="payoff-days" id="mrDays">--</div>
                    <div class="payoff-bar"><div id="mrBar" style="width:0%;background:#444;"></div></div>
                    <div class="payoff-scale"><span>0</span><span>30d</span><span>90d</span><span>6mo</span><span>1yr+</span></div>
                    <div class="unit-rows" id="mrRows"></div>
                </div>

                <div class="card" style="margin:0;background:rgba(255,255,255,0.03);">
                    <div class="payoff-head">
                        <span class="t"><i class="fas fa-bolt"></i> Generator repair</span>
                        <span class="verdict v-none" id="grVerdict">enter a cost</span>
                    </div>
                    <div class="payoff-inputs">
                        <div class="cost">
                            <label>Repair cost $</label>
                            <input type="number" id="grCost" step="1" placeholder="e.g. 8000" oninput="render()">
                        </div>
                        <div>
                            <label>Miners it carries</label>
                            <input type="number" id="grMiners" step="1" oninput="render()">
                        </div>
                        <div>
                            <label>Per-miner TH/s</label>
                            <input type="number" id="grTh" step="1" oninput="render()">
                        </div>
                    </div>
                    <div class="payoff-days" id="grDays">--</div>
                    <div class="payoff-bar"><div id="grBar" style="width:0%;background:#444;"></div></div>
                    <div class="payoff-scale"><span>0</span><span>30d</span><span>90d</span><span>6mo</span><span>1yr+</span></div>
                    <div class="unit-rows" id="grRows"></div>
                </div>
            </div>
            <div class="dim" style="font-size:11px;margin-top:12px;">
                A gen repair only earns if there are idle miners for it to carry - it pays back at this
                rate when the gen comes back to a pod with miners waiting, not when it joins a group
                that is already covering its load.
            </div>
        </div>

        <div class="card">
            <h2>By site</h2>
            <div style="overflow-x:auto;">
            <table>
                <thead>
                    <tr>
                        <th>Site</th>
                        <th>Hashrate</th>
                        <th>Miners</th>
                        <th>Gens run</th>
                        <th>Miners / gen</th>
                        <th>BTC / day</th>
                        <th>$ / day</th>
                        <th>$ / gen / day</th>
                        <th>$ / miner / day</th>
                    </tr>
                </thead>
                <tbody id="siteRows"></tbody>
                <tfoot id="siteFoot"></tfoot>
            </table>
            </div>
        </div>

        <div class="card">
            <h2>Network</h2>
            <div class="netstrip" id="netStrip"></div>
        </div>
    </div>

    <div class="foot" id="footNote"></div>
""")
print("<script>const BOOT = " + _payload_json + ";</script>")
print("""
<script>
let DATA = BOOT;
let MODE = 'now';
let tickStart = Date.now();
let tickBtcPerSec = 0;
let userPrice = false, userMpg = false, userGas = false;
let userMrTh = false, userMrW = false, userGrMiners = false, userGrTh = false;

const fmtUsd = (v, d) => '$' + (v || 0).toLocaleString('en-US',
    {minimumFractionDigits: d === undefined ? 2 : d, maximumFractionDigits: d === undefined ? 2 : d});
const fmtBtc = (v) => (v || 0).toFixed(6) + ' BTC';
const fmtNum = (v, d) => (v || 0).toLocaleString('en-US',
    {minimumFractionDigits: d || 0, maximumFractionDigits: d || 0});

function hashStr(th) {
    if (th >= 1e6) return (th / 1e6).toFixed(2) + ' EH/s';
    if (th >= 1000) return (th / 1000).toFixed(2) + ' PH/s';
    return fmtNum(th, 0) + ' TH/s';
}

// --- the model -----------------------------------------------------------
function price()   { return parseFloat(document.getElementById('price').value) || 0; }
function feeMult() { return 1 - ((parseFloat(document.getElementById('poolfee').value) || 0) / 100); }
function mpg()     { return parseFloat(document.getElementById('mpg').value) || 90; }
function btcDay(th) { return (th || 0) * (DATA.btc_per_th_day || 0) * feeMult(); }
function gasRate() { return parseFloat(document.getElementById('gasrate').value) || 0; }
function uptime()  {
    const v = parseFloat(document.getElementById('uptime').value);
    return isNaN(v) ? 1 : Math.max(0, v) / 100;
}
// Gas burned to run `watts` for a day, priced at the $/MCF above.
// kWh/day x cuft/kWh / 1000 = MCF/day.
function gasDay(watts) {
    const cuft = (DATA.gas && DATA.gas.cuft_per_kwh) || 11;
    return (watts || 0) / 1000 * 24 * uptime() * cuft / 1000 * gasRate();
}
function usdDay(th) { return btcDay(th) * price(); }

// "Right now" = what the fleet is actually hashing. "Full send" = every
// installed miner awake at spec, which is the ceiling, not a forecast.
function siteHash(r) { return MODE === 'now' ? (r.hashrate || 0)
                                             : (r.installed || 0) * (DATA.fleet.spec_th || 180); }
function siteMiners(r) { return MODE === 'now' ? (r.hashing || 0) : (r.installed || 0); }
function siteGens(r) {
    if (MODE === 'now') return r.gens_running || 0;
    // Full send: gens needed to carry every installed miner at the assumed ratio.
    return Math.ceil((r.installed || 0) / mpg());
}

function setMode(m) {
    MODE = m;
    document.getElementById('modeNow').classList.toggle('on', m === 'now');
    document.getElementById('modeFull').classList.toggle('on', m === 'full');
    render();
}

function resetAssumptions() {
    userPrice = false; userMpg = false;
    document.getElementById('poolfee').value = 0;
    resetPayoffDefaults();
}

function applyLiveDefaults() {
    const p = document.getElementById('price');
    if (!userPrice) p.value = (DATA.network.price_usd || 0).toFixed(0);
    const m = document.getElementById('mpg');
    if (!userMpg) m.value = DATA.miners_per_gen_assumed || 90;

    const f = DATA.fleet || {};
    if (!userGas) document.getElementById('gasrate').value = (DATA.gas || {}).usd_per_mcf || 0;
    if (!userMrTh) document.getElementById('mrTh').value = f.spec_th || 180;
    if (!userMrW) document.getElementById('mrW').value = f.spec_w || 3500;
    if (!userGrMiners) document.getElementById('grMiners').value = mpg();
    if (!userGrTh) document.getElementById('grTh').value = f.spec_th || 180;

    const gn = document.getElementById('gasNote');
    if (gn) {
        gn.innerHTML = (DATA.gas || {}).placeholder
            ? '<span class="warn">The $/MCF above is still the placeholder rate in '
              + 'reporting_config.json - put the real gas cost in to trust the net numbers.</span>'
            : 'Gas rate from reporting_config.json.';
    }
}

function resetPayoffDefaults() {
    userGas = userMrTh = userMrW = userGrMiners = userGrTh = false;
    document.getElementById('uptime').value = 100;
    applyLiveDefaults();
    render();
}

// One repair, one answer: net $/day, days to pay off, and what it returns in a year.
function payoff(prefix, cost, th, watts) {
    const gross = usdDay(th * uptime());
    const gas = gasDay(watts);
    const net = gross - gas;
    const days = (cost > 0 && net > 0) ? cost / net : null;
    const yearNet = net * 365 - (cost || 0);

    const dEl = document.getElementById(prefix + 'Days');
    const vEl = document.getElementById(prefix + 'Verdict');
    const bEl = document.getElementById(prefix + 'Bar');

    let cls = 'v-none', label = 'enter a cost', color = '#444', pct = 0;
    if (!(cost > 0)) {
        dEl.innerHTML = '--';
    } else if (net <= 0) {
        dEl.innerHTML = '<span class="warn">never</span> <small>at these inputs</small>';
        cls = 'v-bad'; label = 'gas costs more than it mines'; color = '#ff4444'; pct = 100;
    } else {
        const shown = days < 1 ? days.toFixed(2)
                    : days < 10 ? days.toFixed(1)
                    : fmtNum(days, 0);
        dEl.innerHTML = shown + ' <small>days to pay off</small>';
        pct = Math.min(100, days / 365 * 100);
        if (days <= 30)       { cls = 'v-great'; label = 'no-brainer'; color = '#00ff00'; }
        else if (days <= 90)  { cls = 'v-good';  label = 'worth it';   color = '#4a9eff'; }
        else if (days <= 182) { cls = 'v-ok';    label = 'marginal';   color = '#ffaa00'; }
        else if (days <= 365) { cls = 'v-ok';    label = 'slow';       color = '#ff6600'; }
        else                  { cls = 'v-bad';   label = 'over a year'; color = '#ff4444'; }
    }
    vEl.className = 'verdict ' + cls;
    vEl.textContent = label;
    bEl.style.width = pct + '%';
    bEl.style.background = color;

    const rows = [
        ['Mining revenue', fmtUsd(gross) + ' / day'],
        ['Gas to run it', '-' + fmtUsd(gas) + ' / day'],
        ['Net', '<span class="money">' + fmtUsd(net) + ' / day</span>'],
    ];
    if (days) {
        const when = new Date(Date.now() + days * 86400000);
        rows.push(['Paid off on', when.toLocaleDateString()]);
        rows.push(['Net after 1 year', (yearNet >= 0 ? '<span class="money">' : '<span class="warn">')
            + fmtUsd(yearNet, 0) + '</span>']);
        rows.push(['Return on the repair', (net * 365 / cost).toFixed(1) + 'x per year']);
    }
    document.getElementById(prefix + 'Rows').innerHTML = rows.map(r =>
        '<div class="unit-row"><span>' + r[0] + '</span><span>' + r[1] + '</span></div>').join('');
}

function renderPayoff() {
    const num = id => parseFloat(document.getElementById(id).value) || 0;
    payoff('mr', num('mrCost'), num('mrTh'), num('mrW'));

    const gm = num('grMiners'), gth = num('grTh');
    const perMinerW = (DATA.fleet || {}).spec_w || 3500;
    payoff('gr', num('grCost'), gm * gth, gm * perMinerW);
}

function render() {
    const f = DATA.fleet || {};
    const rows = DATA.sites || [];
    const totalTh = MODE === 'now' ? (f.hashrate || 0)
                                   : (f.installed || 0) * (f.spec_th || 180);
    const miners = MODE === 'now' ? (f.hashing || 0) : (f.installed || 0);
    const gens = MODE === 'now' ? (f.gens_running || 0) : Math.ceil((f.installed || 0) / mpg());

    const btc = btcDay(totalTh), usd = usdDay(totalTh);
    document.getElementById('heroTitle').textContent = MODE === 'now'
        ? 'Whole operation - right now' : 'Whole operation - full send';
    document.getElementById('heroUsd').textContent = fmtUsd(usd, 0) + ' / day';
    document.getElementById('heroBtc').textContent = fmtBtc(btc) + ' / day';
    document.getElementById('heroMonth').textContent = fmtUsd(usd * 30.44, 0);
    document.getElementById('heroYear').textContent = fmtUsd(usd * 365, 0);
    document.getElementById('heroHash').textContent = hashStr(totalTh);
    const net = DATA.network || {};
    const netTh = (net.network_hashrate || 0) / 1e12;
    document.getElementById('heroShare').textContent =
        netTh ? (totalTh / netTh * 100).toFixed(3) + '%' : '--';

    tickBtcPerSec = btc / 86400;

    document.getElementById('modeNote').textContent = MODE === 'now'
        ? 'Right now: the ' + fmtNum(miners) + ' miners actually hashing on ' + fmtNum(gens)
          + ' running gens, at the hashrate the fleet is reporting this minute.'
        : 'Full send: all ' + fmtNum(f.installed) + ' installed miners awake at spec ('
          + fmtNum(f.spec_th) + ' TH/s each) on ' + fmtNum(gens)
          + ' gens at ' + fmtNum(mpg()) + ' miners/gen. A ceiling, not a forecast.';

    // --- per miner ---
    const perMinerTh = miners ? totalTh / miners : 0;
    const pm = usdDay(perMinerTh);
    document.getElementById('minerUsd').textContent = fmtUsd(pm) + ' / day';
    document.getElementById('minerBtc').textContent = fmtBtc(btcDay(perMinerTh)) + ' / day';
    document.getElementById('minerRows').innerHTML = [
        ['Basis', fmtNum(miners) + ' miners @ ' + fmtNum(perMinerTh, 1) + ' TH/s avg'],
        ['Per month', fmtUsd(pm * 30.44)],
        ['Per year', fmtUsd(pm * 365)],
        ['At full spec (' + fmtNum(f.spec_th) + ' TH/s)', fmtUsd(usdDay(f.spec_th || 180)) + ' / day'],
        ['Whole fleet if all ' + fmtNum(f.installed) + ' ran at spec',
            fmtUsd(usdDay((f.installed || 0) * (f.spec_th || 180)), 0) + ' / day']
    ].map(r => '<div class="unit-row"><span>' + r[0] + '</span><span>' + r[1] + '</span></div>').join('');

    // --- per gen ---
    const perGenTh = gens ? totalTh / gens : 0;
    const pg = usdDay(perGenTh);
    const realRatio = gens ? miners / gens : 0;
    document.getElementById('genUsd').textContent = fmtUsd(pg) + ' / day';
    document.getElementById('genBtc').textContent = fmtBtc(btcDay(perGenTh)) + ' / day';
    const assumedGenTh = mpg() * (f.spec_th || 180);
    document.getElementById('genRows').innerHTML = [
        ['Basis', fmtNum(gens) + ' gens carrying ' + fmtNum(miners) + ' miners'],
        ['Actual miners per gen', fmtNum(realRatio, 1) + (MODE === 'now' ? ' (vs ' + fmtNum(mpg()) + ' assumed)' : '')],
        ['Per month', fmtUsd(pg * 30.44)],
        ['Per year', fmtUsd(pg * 365)],
        ['A gen at ' + fmtNum(mpg()) + ' miners x spec', fmtUsd(usdDay(assumedGenTh)) + ' / day']
    ].map(r => '<div class="unit-row"><span>' + r[0] + '</span><span>' + r[1] + '</span></div>').join('');

    // --- sites ---
    const body = rows.map(r => {
        const th = siteHash(r), m = siteMiners(r), g = siteGens(r);
        const u = usdDay(th);
        const ratio = g ? m / g : 0;
        return '<tr>'
            + '<td>' + r.site + '</td>'
            + '<td>' + (th ? hashStr(th) : '<span class="dim">-</span>') + '</td>'
            + '<td>' + fmtNum(m) + '<span class="dim"> / ' + fmtNum(r.installed) + '</span></td>'
            + '<td>' + fmtNum(g) + '<span class="dim"> / ' + fmtNum(r.gens_total) + '</span></td>'
            + '<td' + (MODE === 'now' && g && ratio < 45 ? ' class="warn"' : '') + '>'
                + (g ? fmtNum(ratio, 1) : '<span class="dim">-</span>') + '</td>'
            + '<td>' + btcDay(th).toFixed(5) + '</td>'
            + '<td class="money">' + fmtUsd(u, 0) + '</td>'
            + '<td>' + (g ? fmtUsd(u / g, 0) : '<span class="dim">-</span>') + '</td>'
            + '<td>' + (m ? fmtUsd(u / m) : '<span class="dim">-</span>') + '</td>'
            + '</tr>';
    }).join('');
    document.getElementById('siteRows').innerHTML = body
        || '<tr><td colspan="9" class="dim">No site data</td></tr>';
    document.getElementById('siteFoot').innerHTML = '<tr>'
        + '<td>TOTAL</td><td>' + hashStr(totalTh) + '</td>'
        + '<td>' + fmtNum(miners) + '<span class="dim"> / ' + fmtNum(f.installed) + '</span></td>'
        + '<td>' + fmtNum(gens) + '<span class="dim"> / ' + fmtNum(f.gens_total) + '</span></td>'
        + '<td>' + (gens ? fmtNum(miners / gens, 1) : '-') + '</td>'
        + '<td>' + btc.toFixed(5) + '</td>'
        + '<td class="money">' + fmtUsd(usd, 0) + '</td>'
        + '<td>' + (gens ? fmtUsd(usd / gens, 0) : '-') + '</td>'
        + '<td>' + (miners ? fmtUsd(usd / miners) : '-') + '</td>'
        + '</tr>';

    renderPayoff();

    // --- network strip ---
    const rt = net.retarget || {};
    const eta = rt.eta_ms ? new Date(rt.eta_ms) : null;
    const strip = [
        ['Network hashrate', netTh ? hashStr(netTh) : '--'],
        ['Difficulty', net.difficulty ? (net.difficulty / 1e12).toFixed(2) + ' T' : '--'],
        ['Next retarget', rt.change_pct === undefined || rt.change_pct === null ? '--'
            : (rt.change_pct >= 0 ? '+' : '') + rt.change_pct.toFixed(2) + '%'],
        ['Retarget in', rt.remaining_blocks === undefined ? '--'
            : fmtNum(rt.remaining_blocks) + ' blocks' + (eta ? ' (' + eta.toLocaleDateString() + ')' : '')],
        ['Block reward', (DATA.block_reward || 0).toFixed(4) + ' BTC'],
        ['  of which fees', (net.fee_per_block || 0).toFixed(4) + ' BTC'],
        ['BTC price', fmtUsd(net.price_usd || 0, 0) + (net.price_source ? ' (' + net.price_source + ')' : '')],
        ['Block height', fmtNum(net.height)]
    ];
    document.getElementById('netStrip').innerHTML = strip.map(s =>
        '<div><div class="v">' + s[1] + '</div><div class="l">' + s[0] + '</div></div>').join('');

    const errs = (net.errors || []).length ? ' &middot; <span class="warn">upstream issues: '
        + net.errors.join('; ') + '</span>' : '';
    document.getElementById('footNote').innerHTML =
        'Model: BTC/day = hashrate x 86400 x block reward / (difficulty x 2^32), then minus the pool fee above. '
        + 'Block reward = ' + (net.subsidy || 3.125) + ' BTC subsidy + trailing-144-block average fees. '
        + 'Hashrate and miner/gen counts are live from the status API; network stats from mempool.space, price from '
        + (net.price_source || 'CoinGecko') + ' (cached 5 min). '
        + 'This is a theoretical model, not pool accounting -- actual Foundry payouts differ on luck, FPPS terms '
        + 'and our own hashrate reporting gaps.' + errs;
}

function updateAge() {
    const net = DATA.network || {};
    const age = net.fetched ? Math.round(Date.now() / 1000 - net.fetched) : null;
    const f = DATA.fleet || {};
    let txt = 'network data ' + (age === null ? '?' : age < 90 ? age + 's' : Math.round(age / 60) + 'm') + ' old';
    if (f.updated) {
        const fa = Math.round((Date.now() - new Date(f.updated).getTime()) / 1000);
        txt += ' &middot; fleet ' + (fa < 90 ? fa + 's' : Math.round(fa / 60) + 'm') + ' old';
    }
    document.getElementById('dataAge').innerHTML = txt;
}

function tick() {
    const secs = (Date.now() - tickStart) / 1000;
    const btc = tickBtcPerSec * secs;
    document.getElementById('ticker').textContent = fmtNum(btc * 1e8, 0);
    document.getElementById('tickerUsd').textContent = fmtUsd(btc * price());
    updateAge();
}

async function loadData() {
    try {
        const resp = await fetch('money.py?ajax=data', {credentials: 'same-origin'});
        const j = await resp.json();
        if (j && j.network) { DATA = j; applyLiveDefaults(); render(); }
    } catch (e) { /* keep showing the last good numbers */ }
}

const _dirty = {price: () => userPrice = true, mpg: () => userMpg = true,
                gasrate: () => userGas = true, mrTh: () => userMrTh = true,
                mrW: () => userMrW = true, grMiners: () => userGrMiners = true,
                grTh: () => userGrTh = true};
Object.keys(_dirty).forEach(id =>
    document.getElementById(id).addEventListener('input', _dirty[id]));

applyLiveDefaults();
render();
setInterval(tick, 1000);
setInterval(loadData, 60000);
</script>
""")
# generate_dropdown_js() returns bare JS with no <script> wrapper -- unwrapped it
# renders as visible text at the foot of the page (and the mobile nav never binds).
print("<script>")
print(generate_dropdown_js())
print("</script>")
print("</body></html>")
