/*
 * MalmarCoin ESP32 Dual-Core Miner
 * Wersja: 4.0.0 - Captive Portal Edition
 *
 * NOWOSC w v4: Zero-config onboarding
 *  - Brak hardkodowanego WiFi/portfela - user konfiguruje przez stronke
 *  - Pierwsze uruchomienie: ESP32 tworzy WiFi "MalmarMiner-XXXX"
 *    User laczy sie, otwiera 192.168.4.1 (lub czeka na auto-redirect)
 *    Wpisuje: SSID, haslo WiFi, kod parowania (np "7K2P9X4"), nazwe minera
 *  - Konfiguracja zapisywana w NVS (Preferences) - przezywa restart/odlaczenie pradu
 *  - Reset konfiguracji: trzymaj BOOT (GPIO0) podczas startu lub 5s podczas pracy
 *
 * Wymagane biblioteki (Menedzer bibliotek Arduino):
 *   - ArduinoJson (Benoit Blanchon)
 *   - WiFiClientSecure (wbudowana)
 *   - WebServer (wbudowana)
 *   - DNSServer (wbudowana)
 *   - Preferences (wbudowana)
 *
 * Plytka: ESP32 Dev Module (klasyczny ESP32 WROOM-32)
 */

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <WebServer.h>
#include <DNSServer.h>
#include <Preferences.h>
#include <ArduinoJson.h>
#include "mbedtls/sha256.h"

// ============================================
// STALE (nie konfiguruje user)
// ============================================

const char* HOST          = "malmar.portaleai.pl";
const char* FIRMWARE_VER  = "4.0.0";
const int   MAX_HASHRATE_PER_CORE   = 0;
const unsigned long TELEMETRY_INTERVAL = 30000;

#define LED_PIN       2
#define RESET_BTN_PIN 0       // BOOT button (GPIO0)
#define RESET_HOLD_MS 5000

// ============================================
// KONFIGURACJA USERA (ladowana z NVS)
// ============================================

String cfgWifiSsid;
String cfgWifiPass;
String cfgWallet;
String cfgDeviceName;

Preferences prefs;

// ============================================
// CAPTIVE PORTAL
// ============================================

WebServer  cpServer(80);
DNSServer  dnsServer;
const byte DNS_PORT = 53;
bool       cpDone   = false;

// ============================================
// ZMIENNE MINERA
// ============================================

String deviceId;
SemaphoreHandle_t xMutex;

volatile bool          miningActive  = false;
volatile bool          solutionFound = false;
volatile unsigned long foundNonce    = 0;

char jobId[64]          = {0};
char blockTemplate[128] = {0};
int  shareDifficulty    = 3;

volatile unsigned long hashCount[2]      = {0, 0};
volatile unsigned long statsHashCount[2] = {0, 0};

unsigned long statsWindowStart  = 0;
unsigned long lastJobTime       = 0;
unsigned long lastStatsTime     = 0;
unsigned long lastBlinkTime     = 0;
unsigned long lastTelemetryTime = 0;
bool          ledState          = false;

TaskHandle_t miningTask0;
TaskHandle_t miningTask1;

volatile unsigned long idleCounter    = 0;
volatile float         cpuLoadPercent = 0.0f;
unsigned long          idleBaseline   = 0;

enum AppMode { MODE_BOOT, MODE_PORTAL, MODE_MINING };
AppMode appMode = MODE_BOOT;

unsigned long resetBtnPressedAt = 0;

// ============================================
// HELPERY: NVS
// ============================================

bool loadConfig() {
    prefs.begin("malmar", true);
    cfgWifiSsid   = prefs.getString("ssid",   "");
    cfgWifiPass   = prefs.getString("pass",   "");
    cfgWallet     = prefs.getString("wallet", "");
    cfgDeviceName = prefs.getString("name",   "");
    prefs.end();

    bool ok = cfgWifiSsid.length() > 0 && cfgWallet.length() > 0;
    Serial.printf("[Config] SSID:'%s' Wallet:%.12s... Name:'%s' -> %s\n",
                  cfgWifiSsid.c_str(), cfgWallet.c_str(),
                  cfgDeviceName.c_str(), ok ? "OK" : "INCOMPLETE");
    return ok;
}

bool saveConfig(const String& ssid, const String& pass, const String& wallet, const String& name) {
    prefs.begin("malmar", false);
    bool ok = true;
    ok &= (prefs.putString("ssid",   ssid)   > 0 || ssid.length()   == 0);
    ok &= (prefs.putString("pass",   pass)   > 0 || pass.length()   == 0);
    ok &= (prefs.putString("wallet", wallet) > 0 || wallet.length() == 0);
    ok &= (prefs.putString("name",   name)   > 0 || name.length()   == 0);
    prefs.end();
    return ok;
}

void clearConfig() {
    prefs.begin("malmar", false);
    prefs.clear();
    prefs.end();
    Serial.println("[Config] WYCZYSZCZONE");
}

// ============================================
// PAROWANIE: kod -> wallet
// ============================================

bool exchangeCodeForWallet(const String& code, String& outWallet, String& outUsername, String& outError) {
    if (WiFi.status() != WL_CONNECTED) {
        outError = "Brak WiFi";
        return false;
    }

    WiFiClientSecure client;
    client.setInsecure();
    client.setTimeout(15);
    HTTPClient http;
    http.setReuse(false);
    http.begin(client, HOST, 443, "/api.php?action=pair", true);
    http.addHeader("Content-Type", "application/x-www-form-urlencoded");
    http.setTimeout(15000);

    uint64_t mac = ESP.getEfuseMac();
    char macStr[17];
    sprintf(macStr, "%016llX", mac);

    String body = "code=" + code + "&device_id=" + String(macStr);
    int httpCode = http.POST(body);
    String response = http.getString();
    http.end();
    client.stop();

    StaticJsonDocument<512> doc;
    if (deserializeJson(doc, response) != DeserializationError::Ok) {
        outError = "Bledna odpowiedz serwera (HTTP " + String(httpCode) + ")";
        return false;
    }

    if (!doc["success"]) {
        outError = String(doc["error"].as<const char*>());
        if (outError.length() == 0) outError = "Nieznany blad";
        return false;
    }

    outWallet   = String(doc["wallet"].as<const char*>());
    outUsername = String(doc["username"].as<const char*>());
    return true;
}

// ============================================
// CAPTIVE PORTAL - HTML
// ============================================

const char CP_HTML[] PROGMEM = R"HTML(
<!DOCTYPE html><html lang="pl"><head><meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MalmarMiner - Konfiguracja</title><style>
*{box-sizing:border-box;margin:0;padding:0;font-family:-apple-system,Segoe UI,Roboto,sans-serif}
body{background:linear-gradient(135deg,#0a0a0f 0%,#1a0a2e 50%,#0a0a0f 100%);color:#e0e0e0;min-height:100vh;padding:20px;display:flex;align-items:center;justify-content:center}
.card{max-width:440px;width:100%;background:rgba(21,21,32,.95);border:1px solid rgba(108,92,231,.3);border-radius:24px;padding:30px;box-shadow:0 24px 64px rgba(0,0,0,.4)}
.h{text-align:center;margin-bottom:24px}
.h .ic{width:60px;height:60px;background:linear-gradient(135deg,#6c5ce7,#a29bfe);border-radius:16px;display:flex;align-items:center;justify-content:center;font-size:1.8rem;margin:0 auto 14px;box-shadow:0 8px 24px rgba(108,92,231,.4)}
.h h1{font-size:1.4rem;background:linear-gradient(135deg,#fff,#a29bfe);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;margin-bottom:6px}
.h p{font-size:.85rem;color:#a0a0b0;line-height:1.5}
.msg{padding:13px 16px;border-radius:11px;margin-bottom:18px;font-size:.85rem;display:none}
.msg.show{display:block}
.msg.err{background:rgba(214,48,49,.15);border:1px solid rgba(214,48,49,.35);color:#ff6b6b}
.msg.ok{background:rgba(0,184,148,.15);border:1px solid rgba(0,184,148,.35);color:#00cfa3}
label{display:block;font-size:.78rem;font-weight:600;color:#c0c0d8;margin-bottom:7px;text-transform:uppercase;letter-spacing:.6px}
.fld{margin-bottom:16px}
.fld .hint{font-size:.72rem;color:#7a7a8e;margin-top:5px}
input,select{width:100%;padding:11px 14px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);border-radius:11px;color:#e0e0e0;font-size:.92rem;outline:none;font-family:inherit}
input:focus,select:focus{border-color:#6c5ce7;background:rgba(108,92,231,.06)}
input::placeholder{color:#5a5a6e}
.row{display:flex;gap:8px}
.row select{flex:1}
.row button{padding:11px 16px;background:rgba(108,92,231,.15);border:1px solid rgba(108,92,231,.3);color:#a29bfe;border-radius:11px;cursor:pointer;font-size:.85rem;font-weight:600}
.row button:hover{background:#6c5ce7;color:#fff}
.pair{font-family:monospace;letter-spacing:.15em;text-align:center;text-transform:uppercase;font-size:1.05rem;font-weight:700}
.btn{width:100%;padding:14px;background:linear-gradient(135deg,#6c5ce7,#5b4cdb);color:#fff;border:none;border-radius:12px;font-size:.95rem;font-weight:700;cursor:pointer;margin-top:8px;box-shadow:0 6px 24px rgba(108,92,231,.4)}
.btn:hover{transform:translateY(-1px)}
.btn:disabled{opacity:.5;cursor:not-allowed;transform:none}
.foot{text-align:center;font-size:.74rem;color:#5a5a6e;margin-top:18px;line-height:1.5}
.foot a{color:#a29bfe;text-decoration:none}
</style></head><body>
<div class="card">
<div class="h"><div class="ic">&#9881;</div>
<h1>MalmarMiner</h1>
<p>Wpisz dane WiFi, kod parowania z portalu i nazwe minera.</p></div>
<div id="msg" class="msg"></div>
<form id="f" onsubmit="return s(event)">
<div class="fld"><label>Siec WiFi (SSID)</label>
<div class="row">
<select id="sel" onchange="document.getElementById('ssid').value=this.value">
<option value="">-- skanuj sieci --</option></select>
<button type="button" onclick="sc()" title="Skanuj">&#8635;</button></div>
<input type="text" id="ssid" name="ssid" placeholder="lub wpisz recznie" required maxlength="32" style="margin-top:8px"></div>
<div class="fld"><label>Haslo WiFi</label>
<input type="password" name="pass" placeholder="haslo do WiFi" maxlength="63">
<div class="hint">Zostaw puste dla sieci otwartej</div></div>
<div class="fld"><label>Kod parowania</label>
<input type="text" name="code" class="pair" placeholder="MLM-XXX-XXXX" required maxlength="16" autocapitalize="characters" autocomplete="off">
<div class="hint">Wygeneruj kod: <a href="https://malmar.portaleai.pl/wallet.php" target="_blank">malmar.portaleai.pl/wallet.php</a></div></div>
<div class="fld"><label>Nazwa minera</label>
<input type="text" name="name" placeholder="np. ESP32-Salon" required maxlength="40">
<div class="hint">Pod tym tagiem zobaczysz urzadzenie w portfelu</div></div>
<button type="submit" id="btn" class="btn">Zapisz i polacz</button>
</form>
<div class="foot">Po zapisaniu ESP32 zrestartuje sie.<br>Reset: trzymaj BOOT przez 5 sekund.</div>
</div><script>
function m(t,e){var x=document.getElementById('msg');x.textContent=t;x.className='msg show '+(e?'err':'ok');}
function sc(){m('Skanowanie sieci...',false);
fetch('/scan').then(function(r){return r.json()}).then(function(d){
var s=document.getElementById('sel');s.innerHTML='<option value="">-- wybierz --</option>';
(d.networks||[]).forEach(function(n){var o=document.createElement('option');o.value=n.ssid;
o.textContent=n.ssid+' ('+n.rssi+'dBm'+(n.locked?' &#128274;':'')+')';s.appendChild(o);});
document.getElementById('msg').className='msg';}).catch(function(){m('Skanowanie nieudane',true);});}
function s(e){e.preventDefault();var b=document.getElementById('btn');b.disabled=true;
b.textContent='Laczenie i parowanie...';m('Sprawdzam WiFi i kod - do 20s...',false);
var fd=new FormData(document.getElementById('f'));
fetch('/save',{method:'POST',body:new URLSearchParams(fd)}).then(function(r){return r.json();})
.then(function(d){if(d.ok){m('Sukces! Portfel @'+d.username+'. Restart...',false);
setTimeout(function(){document.body.innerHTML='<div style="text-align:center;padding:60px;color:#a0a0b0">ESP32 startuje. Mozesz zamknac strone.</div>';},3000);}
else{m(d.error||'Nieudane',true);b.disabled=false;b.textContent='Zapisz i polacz';}})
.catch(function(){m('Blad polaczenia',true);b.disabled=false;b.textContent='Zapisz i polacz';});
return false;}
sc();
</script></body></html>
)HTML";

void cpHandleRoot() {
    cpServer.send_P(200, "text/html", CP_HTML);
}

void cpHandleScan() {
    Serial.println("[Portal] Skanowanie WiFi...");
    int n = WiFi.scanNetworks();
    StaticJsonDocument<2048> doc;
    JsonArray arr = doc.createNestedArray("networks");
    for (int i = 0; i < n && i < 20; i++) {
        JsonObject o = arr.createNestedObject();
        o["ssid"]   = WiFi.SSID(i);
        o["rssi"]   = WiFi.RSSI(i);
        o["locked"] = WiFi.encryptionType(i) != WIFI_AUTH_OPEN;
    }
    WiFi.scanDelete();
    String out;
    serializeJson(doc, out);
    cpServer.send(200, "application/json", out);
}

void cpHandleSave() {
    String ssid = cpServer.arg("ssid");
    String pass = cpServer.arg("pass");
    String code = cpServer.arg("code");
    String name = cpServer.arg("name");

    ssid.trim(); pass.trim(); code.trim(); name.trim();

    code.toUpperCase();
    code.replace("MLM-", "");
    code.replace("-", "");
    code.replace(" ", "");

    auto fail = [&](const String& msg) {
        StaticJsonDocument<256> d;
        d["ok"] = false;
        d["error"] = msg;
        String out; serializeJson(d, out);
        cpServer.send(200, "application/json", out);
    };

    if (ssid.length() == 0) return fail("Wybierz lub wpisz SSID");
    if (ssid.length() > 32) return fail("SSID za dlugi");
    if (code.length() != 7) return fail("Kod parowania musi miec 7 znakow");
    if (name.length() == 0) return fail("Wpisz nazwe minera");
    if (name.length() > 40) return fail("Nazwa za dluga");

    // Test WiFi
    Serial.printf("[Portal] Test WiFi: %s\n", ssid.c_str());
    WiFi.mode(WIFI_AP_STA);
    WiFi.begin(ssid.c_str(), pass.c_str());
    unsigned long t0 = millis();
    while (WiFi.status() != WL_CONNECTED && millis() - t0 < 12000) {
        delay(250);
    }
    if (WiFi.status() != WL_CONNECTED) {
        WiFi.disconnect();
        return fail("Nie polaczono z WiFi (zle haslo lub poza zasiegiem)");
    }
    Serial.printf("[Portal] WiFi OK: %s\n", WiFi.localIP().toString().c_str());

    // Parowanie
    String wallet, username, error;
    if (!exchangeCodeForWallet(code, wallet, username, error)) {
        WiFi.disconnect();
        return fail("Parowanie: " + error);
    }
    Serial.printf("[Portal] Sparowano: @%s -> %s\n", username.c_str(), wallet.c_str());

    if (!saveConfig(ssid, pass, wallet, name)) {
        return fail("Blad zapisu konfiguracji");
    }

    StaticJsonDocument<256> d;
    d["ok"] = true;
    d["username"] = username;
    String out; serializeJson(d, out);
    cpServer.send(200, "application/json", out);

    cpDone = true;
}

void cpHandleNotFound() {
    cpServer.sendHeader("Location", "http://192.168.4.1/", true);
    cpServer.send(302, "text/plain", "");
}

void startCaptivePortal() {
    Serial.println("\n=== TRYB KONFIGURACJI ===");
    appMode = MODE_PORTAL;

    uint8_t mac[6];
    WiFi.macAddress(mac);
    char apName[32];
    snprintf(apName, sizeof(apName), "MalmarMiner-%02X%02X", mac[4], mac[5]);

    WiFi.mode(WIFI_AP);
    WiFi.softAP(apName);
    delay(500);
    IPAddress apIP = WiFi.softAPIP();

    Serial.printf("AP SSID:    %s\n", apName);
    Serial.printf("AP IP:      %s\n", apIP.toString().c_str());
    Serial.println("\nINSTRUKCJA:");
    Serial.printf("  1. Polacz telefonem z: %s\n", apName);
    Serial.printf("  2. Otworz: http://%s\n", apIP.toString().c_str());
    Serial.println("  3. Wpisz WiFi + kod parowania + nazwe");
    Serial.println("=========================\n");

    dnsServer.setErrorReplyCode(DNSReplyCode::NoError);
    dnsServer.start(DNS_PORT, "*", apIP);

    cpServer.on("/",      cpHandleRoot);
    cpServer.on("/scan",  HTTP_GET,  cpHandleScan);
    cpServer.on("/save",  HTTP_POST, cpHandleSave);
    cpServer.onNotFound(cpHandleNotFound);
    cpServer.begin();
}

// ============================================
// MINING (z v3.3.1 - bez zmian logiki)
// ============================================

float readCpuTempC() {
    float raw = temperatureRead();
    if (raw > 120.0f) return (raw - 32.0f) * 5.0f / 9.0f;
    return raw;
}

void cpuMeasureTask(void* param) {
    unsigned long start = millis(); unsigned long cnt = 0;
    while (millis() - start < 1000) { cnt++; taskYIELD(); }
    idleBaseline = cnt > 0 ? cnt : 1;
    while (true) {
        unsigned long t0 = millis(); unsigned long c0 = 0;
        while (millis() - t0 < 1000) { c0++; taskYIELD(); }
        float ratio = (float)c0 / (float)idleBaseline;
        cpuLoadPercent = max(0.0f, min(100.0f, (1.0f - ratio) * 100.0f));
        idleCounter++;
    }
}

void computeHash(mbedtls_sha256_context* ctx, const char* tpl, unsigned long nonce, char* outHex) {
    char input[200];
    int len = snprintf(input, sizeof(input), "%s%lu", tpl, nonce);
    unsigned char hashBytes[32];
    mbedtls_sha256_starts(ctx, 0);
    mbedtls_sha256_update(ctx, (const unsigned char*)input, len);
    mbedtls_sha256_finish(ctx, hashBytes);
    for (int i = 0; i < 32; i++) sprintf(outHex + (i*2), "%02x", hashBytes[i]);
    outHex[64] = '\0';
}

bool meetsDifficulty(const char* hexHash, int diff) {
    for (int i = 0; i < diff; i++) if (hexHash[i] != '0') return false;
    return true;
}

void miningTaskFunction(void* parameter) {
    int coreId = (int)parameter;
    char hashBuf[65];
    mbedtls_sha256_context localCtx;
    mbedtls_sha256_init(&localCtx);
    unsigned long rateWindowStart = millis();
    unsigned long rateCount = 0, hashIter = 0;

    while (true) {
        if (!miningActive || solutionFound || blockTemplate[0] == '\0') {
            vTaskDelay(pdMS_TO_TICKS(50));
            rateWindowStart = millis(); rateCount = 0; hashIter = 0;
            continue;
        }
        char tpl[128];
        strncpy(tpl, blockTemplate, sizeof(tpl) - 1);
        tpl[sizeof(tpl) - 1] = '\0';
        int diff = shareDifficulty;
        unsigned long n = (unsigned long)coreId;

        while (miningActive && !solutionFound) {
            computeHash(&localCtx, tpl, n, hashBuf);
            hashCount[coreId]++; statsHashCount[coreId]++;
            rateCount++; hashIter++;

            if (meetsDifficulty(hashBuf, diff)) {
                if (xSemaphoreTake(xMutex, portMAX_DELAY)) {
                    if (!solutionFound) {
                        solutionFound = true;
                        foundNonce = n;
                        Serial.printf("[Core %d] FOUND! Nonce:%lu\n", coreId, n);
                    }
                    xSemaphoreGive(xMutex);
                }
                break;
            }
            n += 2;
            if (hashIter % 10000 == 0) { vTaskDelay(1); hashIter = 0; }
            if (MAX_HASHRATE_PER_CORE > 0 && rateCount >= (unsigned long)MAX_HASHRATE_PER_CORE) {
                unsigned long elapsed = millis() - rateWindowStart;
                if (elapsed < 1000) vTaskDelay(pdMS_TO_TICKS(1000 - elapsed));
                rateWindowStart = millis(); rateCount = 0;
            }
        }
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

bool httpsGet(const String& path, String& response) {
    WiFiClientSecure client; client.setInsecure(); client.setTimeout(15);
    HTTPClient http; http.setReuse(false);
    http.begin(client, HOST, 443, path, true);
    http.setTimeout(15000);
    int code = http.GET();
    bool ok = false;
    if (code == 200) { response = http.getString(); ok = true; }
    else Serial.printf("GET -> %d\n", code);
    http.end(); client.stop();
    return ok;
}

bool httpsPost(const String& path, const String& body, String& response) {
    WiFiClientSecure client; client.setInsecure(); client.setTimeout(15);
    HTTPClient http; http.setReuse(false);
    http.begin(client, HOST, 443, path, true);
    http.addHeader("Content-Type", "application/json");
    http.setTimeout(15000);
    int code = http.POST(body);
    bool ok = false;
    if (code == 200) { response = http.getString(); ok = true; }
    else Serial.printf("POST %s -> %d\n", path.c_str(), code);
    http.end(); client.stop();
    return ok;
}

bool getJob() {
    if (WiFi.status() != WL_CONNECTED) return false;
    float tempC = readCpuTempC();
    String path = String("/api.php?action=get_job")
                + "&wallet="      + cfgWallet
                + "&device_id="   + deviceId
                + "&device_name=" + cfgDeviceName
                + "&model="       + String(ESP.getChipModel())
                + "&rev="         + String(ESP.getChipRevision())
                + "&cores="       + String(ESP.getChipCores())
                + "&fw="          + String(FIRMWARE_VER)
                + "&flash="       + String(ESP.getFlashChipSize() / 1024)
                + "&psram="       + String(ESP.getPsramSize() / 1024)
                + "&temp="        + String(tempC, 1)
                + "&load="        + String((int)cpuLoadPercent)
                + "&heap="        + String(ESP.getFreeHeap())
                + "&rssi="        + String(WiFi.RSSI())
                + "&uptime="      + String(millis() / 1000)
                + "&mac="         + WiFi.macAddress();

    String response;
    if (!httpsGet(path, response)) return false;
    StaticJsonDocument<1536> doc;
    if (deserializeJson(doc, response) != DeserializationError::Ok) return false;
    if (!doc["success"]) {
        Serial.printf("Blad: %s\n", doc["error"].as<const char*>());
        return false;
    }
    strncpy(jobId,         doc["job_id"].as<const char*>(),         sizeof(jobId) - 1);
    strncpy(blockTemplate, doc["block_template"].as<const char*>(), sizeof(blockTemplate) - 1);
    jobId[sizeof(jobId) - 1] = '\0';
    blockTemplate[sizeof(blockTemplate) - 1] = '\0';
    shareDifficulty = doc["difficulty"].as<int>();
    Serial.printf("Job: %.8s... diff=%d\n", jobId, shareDifficulty);
    return true;
}

void sendTelemetry() {
    if (WiFi.status() != WL_CONNECTED) return;
    float tempC = readCpuTempC();
    StaticJsonDocument<384> doc;
    doc["device_id"] = deviceId;
    doc["wallet"]    = cfgWallet;
    doc["model"]     = ESP.getChipModel();
    doc["rev"]       = ESP.getChipRevision();
    doc["cores"]     = ESP.getChipCores();
    doc["mac"]       = WiFi.macAddress();
    doc["fw"]        = FIRMWARE_VER;
    doc["flash"]     = ESP.getFlashChipSize() / 1024;
    doc["psram"]     = ESP.getPsramSize() / 1024;
    doc["temp"]      = round(tempC * 10.0f) / 10.0f;
    doc["load"]      = round(cpuLoadPercent * 10.0f) / 10.0f;
    doc["heap"]      = (int)ESP.getFreeHeap();
    doc["rssi"]      = (int)WiFi.RSSI();
    doc["uptime"]    = millis() / 1000;
    String body, response;
    serializeJson(doc, body);
    if (httpsPost("/api.php?action=telemetry", body, response)) {
        Serial.printf("[Tele] temp=%.1fC heap=%d rssi=%d\n",
                      tempC, (int)doc["heap"], (int)doc["rssi"]);
    }
}

bool submitShare(unsigned long nonce, float hashrate) {
    if (WiFi.status() != WL_CONNECTED) return false;
    char hashBuf[65];
    mbedtls_sha256_context verifyCtx;
    mbedtls_sha256_init(&verifyCtx);
    computeHash(&verifyCtx, blockTemplate, nonce, hashBuf);
    mbedtls_sha256_free(&verifyCtx);

    StaticJsonDocument<512> doc;
    doc["job_id"]    = jobId;
    doc["nonce"]     = (uint32_t)nonce;
    doc["hash"]      = hashBuf;
    doc["device_id"] = deviceId;
    doc["wallet"]    = cfgWallet;
    doc["hashrate"]  = (int)hashrate;

    String body, response;
    serializeJson(doc, body);
    if (!httpsPost("/api.php?action=submit_share", body, response)) return false;

    StaticJsonDocument<256> res;
    if (deserializeJson(res, response) != DeserializationError::Ok) return false;

    if (res["accepted"]) {
        if (res["waiting"]) {
            int waitSec = res["wait_seconds"] | 10;
            vTaskDelay(pdMS_TO_TICKS(min((long)waitSec * 1000L, 3000L)));
            return true;
        }
        Serial.println(">>> Share OK!");
        for (int i = 0; i < 3; i++) {
            digitalWrite(LED_PIN, HIGH); delay(60);
            digitalWrite(LED_PIN, LOW);  delay(60);
        }
        if (res["block_found"]) {
            Serial.printf("*** BLOK! +%.2f MLM ***\n", res["reward"].as<float>());
            digitalWrite(LED_PIN, HIGH); delay(1500); digitalWrite(LED_PIN, LOW);
        }
        return true;
    }
    Serial.printf("Share odrzucony: %s\n", res["error"] | "");
    return false;
}

// ============================================
// SETUP / LOOP
// ============================================

void setup() {
    Serial.begin(115200);
    delay(1500);
    pinMode(LED_PIN, OUTPUT);
    pinMode(RESET_BTN_PIN, INPUT_PULLUP);
    digitalWrite(LED_PIN, HIGH);

    Serial.println("\n================================");
    Serial.println("  MalmarCoin ESP32 Miner v4.0");
    Serial.println("  Captive Portal Edition");
    Serial.println("================================");

    uint64_t mac = ESP.getEfuseMac();
    char macStr[17];
    sprintf(macStr, "%016llX", mac);
    deviceId = String(macStr);
    Serial.printf("Chip: %s rev%d %d rdz\n", ESP.getChipModel(), ESP.getChipRevision(), ESP.getChipCores());
    Serial.printf("MAC:  %s\n", macStr);

    // Reset config jesli BOOT trzymany na starcie
    if (digitalRead(RESET_BTN_PIN) == LOW) {
        Serial.println("[Setup] BOOT przy starcie -> reset konfiguracji");
        clearConfig();
        delay(500);
    }

    // Wczytaj konfiguracje albo wejdz w portal
    if (!loadConfig()) {
        startCaptivePortal();
        return;
    }

    // Tryb mining - polacz z WiFi
    Serial.printf("Device: %s\n", cfgDeviceName.c_str());
    Serial.printf("Wallet: %.16s...\n", cfgWallet.c_str());
    Serial.printf("WiFi:   %s", cfgWifiSsid.c_str());

    WiFi.mode(WIFI_STA);
    WiFi.begin(cfgWifiSsid.c_str(), cfgWifiPass.c_str());
    int att = 0;
    while (WiFi.status() != WL_CONNECTED && att++ < 40) {
        delay(500); Serial.print(".");
    }
    if (WiFi.status() != WL_CONNECTED) {
        Serial.println("\n[Setup] Brak WiFi - restart i retry");
        delay(3000);
        ESP.restart();
    }
    Serial.printf("\nWiFi OK! IP: %s RSSI: %d\n\n",
                  WiFi.localIP().toString().c_str(), WiFi.RSSI());

    xMutex = xSemaphoreCreateMutex();
    xTaskCreatePinnedToCore(cpuMeasureTask, "CpuMsr", 2048, NULL, 0, NULL, 0);
    xTaskCreatePinnedToCore(miningTaskFunction, "Mine0", 16000, (void*)0, 1, &miningTask0, 0);
    xTaskCreatePinnedToCore(miningTaskFunction, "Mine1", 16000, (void*)1, 1, &miningTask1, 1);

    digitalWrite(LED_PIN, LOW);
    statsWindowStart  = millis();
    lastStatsTime     = millis();
    lastTelemetryTime = millis();

    appMode = MODE_MINING;
    Serial.println("Koparka uruchomiona! (v4.0)");
}

void checkResetButton() {
    if (digitalRead(RESET_BTN_PIN) == LOW) {
        if (resetBtnPressedAt == 0) {
            resetBtnPressedAt = millis();
        } else if (millis() - resetBtnPressedAt > RESET_HOLD_MS) {
            Serial.println("\n[Reset] BOOT 5s - czyszczenie...");
            for (int i = 0; i < 5; i++) {
                digitalWrite(LED_PIN, HIGH); delay(100);
                digitalWrite(LED_PIN, LOW);  delay(100);
            }
            clearConfig();
            delay(500);
            ESP.restart();
        }
    } else {
        resetBtnPressedAt = 0;
    }
}

void loop() {
    // TRYB CAPTIVE PORTAL
    if (appMode == MODE_PORTAL) {
        dnsServer.processNextRequest();
        cpServer.handleClient();

        if (millis() - lastBlinkTime >= 300) {
            ledState = !ledState;
            digitalWrite(LED_PIN, ledState);
            lastBlinkTime = millis();
        }

        if (cpDone) {
            Serial.println("\n[Portal] Konfiguracja zapisana - restart...");
            delay(3000);
            ESP.restart();
        }
        return;
    }

    // TRYB MINING
    checkResetButton();

    if (WiFi.status() != WL_CONNECTED) {
        Serial.println("WiFi zerwane...");
        miningActive = false;
        WiFi.reconnect();
        delay(5000);
        return;
    }

    if (solutionFound || !miningActive || millis() - lastJobTime > 60000) {
        if (solutionFound) {
            float elapsed = (millis() - lastJobTime) / 1000.0f;
            float hashrate = elapsed > 0 ? (hashCount[0] + hashCount[1]) / elapsed : 0;
            miningActive = false;
            bool ok = false;
            for (int i = 1; i <= 5 && !ok; i++) {
                ok = submitShare(foundNonce, hashrate);
                if (!ok) { Serial.printf("Retry %d/5\n", i); delay(2000); }
            }
            solutionFound = false;
            foundNonce = 0;
            hashCount[0] = 0;
            hashCount[1] = 0;
        }
        if (getJob()) { miningActive = true; lastJobTime = millis(); }
        else delay(5000);
    }

    if (millis() - lastTelemetryTime > TELEMETRY_INTERVAL) {
        sendTelemetry();
        lastTelemetryTime = millis();
    }

    if (millis() - lastStatsTime > 10000) {
        unsigned long elapsed = millis() - statsWindowStart;
        if (elapsed > 0) {
            float hr = (statsHashCount[0] + statsHashCount[1]) * 1000.0f / elapsed;
            int heap = (int)ESP.getFreeHeap();
            Serial.printf("[Stats] %.0f H/s Temp:%.1fC CPU:%.0f%% Heap:%d RSSI:%d\n",
                          hr, readCpuTempC(), cpuLoadPercent, heap, (int)WiFi.RSSI());
            if (heap < 30000) {
                Serial.printf("[WARN] Malo RAM: %d B - restart\n", heap);
                delay(500); ESP.restart();
            }
            statsHashCount[0] = 0;
            statsHashCount[1] = 0;
            statsWindowStart  = millis();
        }
        lastStatsTime = millis();
    }

    unsigned long blinkMs = miningActive ? 150 : 1000;
    if (millis() - lastBlinkTime >= blinkMs) {
        ledState = !ledState;
        digitalWrite(LED_PIN, ledState);
        lastBlinkTime = millis();
    }

    delay(100);
}
