"""
GPU-Mining für Jam (AMD RX 6900 XT) – 90 % Standard, bis ~110 % Turbo.

- **Ohne NiceHash:** lolMiner + UnMineable (ETC/Etchash → Auszahlung in BTC)
- **Mit NiceHash:** QuickMiner (optional)

Echtes Bitcoin-GPU-Mining gibt es nicht mehr; es wird eine andere Coin gemined, BTC kommt als Auszahlung.
"""

from __future__ import annotations

import json
import os
import shutil
import subprocess
import time
import webbrowser
import zipfile
from datetime import datetime
from pathlib import Path
from urllib.request import urlretrieve

ROOT = Path(__file__).resolve().parent.parent
GPU_ROOT = Path.home() / "Documents" / "Jam-GPU-Mining"
GPU_CONFIG = ROOT / "data" / "jam-gpu-mining.json"
GPU_PID_FILE = ROOT / "data" / "jam-gpu-mining.pid"
GPU_LOG = ROOT / "data" / "jam-gpu-mining.log"

NICEHASH_URL = "https://www.nicehash.com/my/mining"
NICEHASH_DOWNLOAD = "https://www.nicehash.com/download"
DEFAULT_POWER_PERCENT = 90
TURBO_POWER_PERCENT = 110
MAX_POWER_PERCENT = 115
# RX 6900 XT Etchash ~63 MH/s @ 100 % (WhatToMine-Richtwert, variiert)
REF_HASHRATE_MH = 63.0
REF_REVENUE_USD_PER_DAY = 0.42
REF_BTC_USD = 100_000.0
REF_EUR_USD = 0.92

# Ohne NiceHash: lolMiner + UnMineable (BTC-Adresse direkt im Miner)
LOLMINER_ZIP_URL = (
    "https://github.com/Lolliedieb/lolMiner-releases/releases/download/1.98/"
    "lolMiner_v1.98_Win64.zip"
)
LOLMINER_DIR = GPU_ROOT / "lolMiner"
LOLMINER_EXE = LOLMINER_DIR / "lolMiner.exe"
UNMINEABLE_POOL = "etchash-eu.unmineable.com:3333"
UNMINEABLE_ALGO = "ETCHASH"
LOLMINER_RUN_LOG = GPU_ROOT / "lolminer-run.log"
UNMINEABLE_INFO = "https://unmineable.com"


def detect_amd_gpu() -> list[dict]:
    gpus: list[dict] = []
    try:
        ps = subprocess.run(
            [
                "powershell",
                "-NoProfile",
                "-Command",
                "Get-CimInstance Win32_VideoController | "
                "Select-Object Name, AdapterRAM, DriverVersion | ConvertTo-Json -Compress",
            ],
            capture_output=True,
            text=True,
            timeout=30,
            encoding="utf-8",
            errors="replace",
        )
        raw = ps.stdout.strip()
        if not raw:
            return gpus
        items = json.loads(raw)
        if isinstance(items, dict):
            items = [items]
        for it in items:
            if not isinstance(it, dict):
                continue
            name = str(it.get("Name", ""))
            if "amd" in name.lower() or "radeon" in name.lower() or "nvidia" in name.lower():
                gpus.append(
                    {
                        "name": name,
                        "vram_mb": int(int(it.get("AdapterRAM", 0) or 0) / (1024 * 1024)),
                        "driver": str(it.get("DriverVersion", "")),
                    }
                )
    except (subprocess.TimeoutExpired, json.JSONDecodeError, OSError):
        pass
    return gpus


def _load_gpu_cfg() -> dict:
    if GPU_CONFIG.is_file():
        try:
            return json.loads(GPU_CONFIG.read_text(encoding="utf-8"))
        except (json.JSONDecodeError, OSError):
            pass
    return {"mode": "direct", "enabled": True, "power_percent": DEFAULT_POWER_PERCENT}


def _save_gpu_cfg(cfg: dict) -> None:
    GPU_CONFIG.parent.mkdir(parents=True, exist_ok=True)
    cfg["updated_at"] = datetime.now().isoformat(timespec="seconds")
    GPU_CONFIG.write_text(json.dumps(cfg, ensure_ascii=False, indent=2), encoding="utf-8")


def _log(msg: str) -> None:
    line = f"[{datetime.now().strftime('%H:%M:%S')}] {msg}\n"
    try:
        GPU_LOG.parent.mkdir(parents=True, exist_ok=True)
        prev = GPU_LOG.read_text(encoding="utf-8") if GPU_LOG.is_file() else ""
        GPU_LOG.write_text(prev[-5000:] + line, encoding="utf-8")
    except OSError:
        pass


def find_nicehash_quickminer() -> Path | None:
    candidates: list[Path] = []
    for env in ("LOCALAPPDATA", "ProgramFiles", "ProgramFiles(x86)"):
        base = os.environ.get(env, "")
        if not base:
            continue
        for folder in ("NiceHash QuickMiner", "NiceHashQuickMiner"):
            candidates.append(Path(base) / folder / "NiceHashQuickMiner.exe")
    pd = os.environ.get("PROGRAMDATA", "C:\\ProgramData")
    candidates.append(Path(pd) / "NiceHash" / "NiceHashQuickMiner" / "NiceHashQuickMiner.exe")
    for p in candidates:
        if p.is_file():
            return p
    return None


def _patch_json_power(path: Path, percent: int) -> bool:
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError):
        return False
    if not isinstance(data, dict):
        return False
    keys = (
        "powerLimit",
        "PowerLimit",
        "power_limit",
        "miningPower",
        "MiningPower",
        "primaryMiningIntensity",
        "PrimaryMiningIntensity",
        "gpuPowerLimit",
    )
    changed = False
    for k in keys:
        if k in data or k.lower() in {x.lower() for x in data}:
            data[k] = percent
            changed = True
    data["powerLimit"] = percent
    data["PowerLimit"] = percent
    try:
        path.write_text(json.dumps(data, indent=2), encoding="utf-8")
        return True
    except OSError:
        return False


def apply_nicehash_power_limit(percent: int = DEFAULT_POWER_PERCENT) -> int:
    """Sucht NiceHash-Konfigurationen und setzt Leistung auf z. B. 90 %."""
    patched = 0
    roots = [
        Path(os.environ.get("PROGRAMDATA", "C:\\ProgramData")) / "NiceHash",
        Path(os.environ.get("LOCALAPPDATA", "")) / "NiceHash",
    ]
    for root in roots:
        if not root.is_dir():
            continue
        for cfg in root.rglob("*.json"):
            if cfg.stat().st_size > 500_000:
                continue
            if _patch_json_power(cfg, percent):
                patched += 1
    return patched


def _lolminer_pids() -> list[int]:
    try:
        out = subprocess.run(
            ["tasklist", "/FI", "IMAGENAME eq lolMiner.exe", "/FO", "CSV", "/NH"],
            capture_output=True,
            text=True,
            encoding="utf-8",
            errors="replace",
            timeout=10,
        )
        pids: list[int] = []
        for line in (out.stdout or "").strip().splitlines():
            parts = line.split(",")
            if len(parts) >= 2 and "lolMiner.exe" in parts[0]:
                try:
                    pids.append(int(parts[1].strip('"')))
                except ValueError:
                    continue
        return pids
    except (OSError, subprocess.TimeoutExpired):
        return []


def is_gpu_mining_running() -> bool:
    if _lolminer_pids():
        return True
    if not GPU_PID_FILE.is_file():
        return False
    try:
        pid = int(GPU_PID_FILE.read_text(encoding="utf-8").strip())
        os.kill(pid, 0)
        return True
    except (OSError, ValueError):
        return False


def stop_gpu_mining() -> str:
    if not GPU_PID_FILE.is_file():
        subprocess.run(
            ["taskkill", "/IM", "lolMiner.exe", "/F"],
            capture_output=True,
            timeout=15,
        )
        return "GPU-Mining läuft nicht (kein PID)."
    try:
        pid = int(GPU_PID_FILE.read_text(encoding="utf-8").strip())
        subprocess.run(
            ["taskkill", "/PID", str(pid), "/F"],
            capture_output=True,
            timeout=15,
        )
    except (OSError, ValueError):
        pass
    subprocess.run(
        ["taskkill", "/IM", "lolMiner.exe", "/F"],
        capture_output=True,
        timeout=15,
    )
    GPU_PID_FILE.unlink(missing_ok=True)
    _log("stopped")
    return "GPU-Mining gestoppt (lolMiner / NiceHash)."


def _clamp_power(percent: int) -> int:
    return max(50, min(MAX_POWER_PERCENT, int(percent)))


def _effective_hashrate_mh(power_percent: int) -> float:
    """Hashrate skaliert grob mit Leistungs-Prozent (100 % = Referenz)."""
    pct = _clamp_power(power_percent)
    if pct <= 100:
        return REF_HASHRATE_MH * (pct / 100.0)
    # Über 100 %: leichter OC-Zugewinn, realistisch begrenzt (~+8 % bei 110 %)
    return REF_HASHRATE_MH * (1.0 + min(0.08, (pct - 100) * 0.008))


def _mclk_for_power(percent: int) -> int | None:
    """Memory-Takt: unter 100 % drosseln, ab 100 % Maximum/Turbo."""
    pct = _clamp_power(percent)
    if pct >= 100:
        # 2000 MHz Basis, Turbo bis ~2150 (vorsichtig)
        return 2000 + min(150, (pct - 100) * 15)
    return max(900, int(2000 * pct / 100))


def estimate_gpu_earnings(
    *,
    hours: float = 8.0,
    power_percent: int = TURBO_POWER_PERCENT,
    include_electricity: bool = False,
    eur_per_kwh: float = 0.35,
    gpu_watts: int = 270,
) -> dict:
    """Brutto-Schätzung ohne Strom (optional mit Stromkosten)."""
    pct = _clamp_power(power_percent)
    mhs = _effective_hashrate_mh(pct)
    day_factor = max(0.0, hours) / 24.0
    usd_gross = REF_REVENUE_USD_PER_DAY * (mhs / REF_HASHRATE_MH) * day_factor * 0.98
    eur_gross = usd_gross * REF_EUR_USD
    btc_gross = usd_gross / REF_BTC_USD if REF_BTC_USD else 0.0
    kwh = (gpu_watts / 1000.0) * hours
    eur_power = kwh * eur_per_kwh if include_electricity else 0.0
    return {
        "hours": hours,
        "power_percent": pct,
        "hashrate_mh_s": round(mhs, 1),
        "usd_gross": round(usd_gross, 4),
        "eur_gross": round(eur_gross, 4),
        "btc_gross": round(btc_gross, 8),
        "eur_power": round(eur_power, 4) if include_electricity else None,
        "eur_net": round(eur_gross - eur_power, 4) if include_electricity else None,
        "note": "Richtwert; Kurs, Schwierigkeit, Pool-Gebühren schwanken.",
    }


def format_earnings_report(est: dict) -> str:
    lines = [
        f"**{est['hours']} h** bei **{est['power_percent']} %** (~{est['hashrate_mh_s']} MH/s Etchash):",
        f"Brutto **~{est['eur_gross']:.2f} €** ({est['usd_gross']:.2f} $), **~{est['btc_gross']:.8f} BTC** – **ohne Strom**.",
    ]
    if est.get("eur_power") is not None:
        lines.append(
            f"Mit Strom (~{est['eur_power']:.2f} €): Netto **~{est['eur_net']:.2f} €**."
        )
    lines.append(f"_{est['note']}_")
    return " ".join(lines)[:720]


def _lolminer_cmd(*, btc_address: str, power_percent: int) -> list[str]:
    pct = _clamp_power(power_percent)
    mclk = _mclk_for_power(pct)
    worker = f"BTC:{btc_address}.JamRig"
    cmd = [
        str(LOLMINER_EXE),
        "--algo",
        UNMINEABLE_ALGO,
        "--pool",
        UNMINEABLE_POOL,
        "--user",
        worker,
        "--pass",
        "x",
        "--ethstratum",
        "ETHPROXY",
        "--devices",
        "0",
    ]
    if mclk is not None:
        cmd.extend(["--mclk", str(mclk)])
    return cmd


def _write_jam_mine_bat(*, btc_address: str, power_percent: int) -> Path:
    """Batch-Datei für manuellen Start (%% statt %, damit CMD nicht kaputtgeht)."""
    cmd = _lolminer_cmd(btc_address=btc_address, power_percent=power_percent)
    cmd_line = subprocess.list2cmdline(["lolMiner.exe"] + cmd[1:])
    bat = LOLMINER_DIR / "jam-mine-btc.bat"
    bat.write_text(
        "\r\n".join(
            [
                "@echo off",
                f"title Jam GPU Mining {power_percent} Prozent - UnMineable BTC",
                f'cd /d "{LOLMINER_DIR}"',
                cmd_line,
                "pause",
            ]
        )
        + "\r\n",
        encoding="utf-8",
    )
    return bat


def ensure_lolminer() -> tuple[bool, str]:
    """lolMiner herunterladen falls noch nicht vorhanden."""
    if LOLMINER_EXE.is_file():
        return True, str(LOLMINER_EXE)
    GPU_ROOT.mkdir(parents=True, exist_ok=True)
    zip_path = GPU_ROOT / "lolMiner_download.zip"
    try:
        _log("download lolMiner...")
        urlretrieve(LOLMINER_ZIP_URL, zip_path)
        with zipfile.ZipFile(zip_path, "r") as zf:
            zf.extractall(GPU_ROOT)
        zip_path.unlink(missing_ok=True)
        found = list(GPU_ROOT.rglob("lolMiner.exe"))
        if not found:
            return False, "lolMiner.exe nach Download nicht gefunden."
        target_dir = found[0].parent
        if target_dir != LOLMINER_DIR:
            if LOLMINER_DIR.exists():
                shutil.rmtree(LOLMINER_DIR, ignore_errors=True)
            shutil.move(str(target_dir), str(LOLMINER_DIR))
        return True, str(LOLMINER_EXE)
    except Exception as e:
        return False, f"lolMiner-Download fehlgeschlagen: {e}"


def restart_direct_gpu_mining(
    *,
    btc_address: str,
    power_percent: int = TURBO_POWER_PERCENT,
) -> str:
    """Stoppt laufenden Miner und startet mit neuer Leistung."""
    if is_gpu_mining_running():
        stop_gpu_mining()
        time.sleep(2)
    return start_direct_gpu_mining(btc_address=btc_address, power_percent=power_percent)


def start_direct_gpu_mining(
    *,
    btc_address: str,
    power_percent: int = DEFAULT_POWER_PERCENT,
) -> str:
    """Mining ohne NiceHash – lolMiner → UnMineable → BTC-Auszahlung."""
    power_percent = _clamp_power(power_percent)
    if is_gpu_mining_running():
        cfg_pct = int(_load_gpu_cfg().get("power_percent", 0))
        if cfg_pct == power_percent:
            pids = _lolminer_pids()
            pid_txt = str(pids[0]) if pids else GPU_PID_FILE.read_text(encoding="utf-8").strip()
            return f"GPU-Mining läuft schon bei {power_percent} % (PID {pid_txt})."
        return restart_direct_gpu_mining(
            btc_address=btc_address, power_percent=power_percent
        )

    ok, detail = ensure_lolminer()
    if not ok:
        return (
            f"**Ohne NiceHash** geht es mit **lolMiner** – Download klappte nicht: {detail}. "
            f"Manuell: {LOLMINER_ZIP_URL} entpacken nach {LOLMINER_DIR}"
        )[:720]

    gpus = detect_amd_gpu()
    gpu_name = gpus[0]["name"] if gpus else "Grafikkarte"
    mclk = _mclk_for_power(power_percent)
    worker = f"BTC:{btc_address}.JamRig"
    _write_jam_mine_bat(btc_address=btc_address, power_percent=power_percent)
    cmd = _lolminer_cmd(btc_address=btc_address, power_percent=power_percent)

    try:
        proc = subprocess.Popen(
            cmd,
            cwd=str(LOLMINER_DIR),
            creationflags=getattr(subprocess, "CREATE_NEW_CONSOLE", 0),
        )
        time.sleep(4)
        if proc.poll() is not None:
            _log(f"direct exit code={proc.returncode}")
            return (
                f"lolMiner ist sofort beendet (Code {proc.returncode}). "
                f"Prüfe {LOLMINER_DIR / 'jam-mine-btc.bat'} manuell oder Antivirus/Firewall. "
                f"Algo: {UNMINEABLE_ALGO}, Pool: {UNMINEABLE_POOL}"
            )[:720]
        GPU_PID_FILE.parent.mkdir(parents=True, exist_ok=True)
        live = _lolminer_pids()
        pid = live[0] if live else proc.pid
        GPU_PID_FILE.write_text(str(pid), encoding="utf-8")
        _log(f"direct start pid={pid} mclk={mclk} algo={UNMINEABLE_ALGO} worker={worker[:24]}")
    except OSError as e:
        return f"lolMiner-Start fehlgeschlagen: {e}"[:400]

    cfg = _load_gpu_cfg()
    cfg["mode"] = "direct"
    cfg["power_percent"] = power_percent
    cfg["btc_payout"] = btc_address
    cfg["algo"] = UNMINEABLE_ALGO
    cfg["pool"] = UNMINEABLE_POOL
    _save_gpu_cfg(cfg)

    est = estimate_gpu_earnings(hours=8, power_percent=power_percent)
    est_txt = format_earnings_report(est)
    (GPU_ROOT / "ERTRAG_8H_OHNE_STROM.txt").write_text(
        f"{est_txt}\n\nStand: {datetime.now().isoformat()}\n",
        encoding="utf-8",
    )
    turbo = " (Turbo)" if power_percent > 100 else ""
    return (
        f"**GPU-Mining läuft{turbo}** – {gpu_name}, **{power_percent} %** (mclk {mclk}). "
        f"{est_txt} "
        f"Stoppen: stop-gpu-mining.bat"
    )[:720]


def start_gpu_mining_now(
    *,
    btc_address: str,
    power_percent: int = DEFAULT_POWER_PERCENT,
    open_browser: bool = False,
    prefer_direct: bool | None = None,
) -> str:
    """Startet GPU-Mining sofort mit Ziel-Leistung ~90 %."""
    setup_gpu_mining_pack(btc_address)
    cfg = _load_gpu_cfg()
    cfg["power_percent"] = power_percent
    cfg["btc_payout"] = btc_address
    _save_gpu_cfg(cfg)

    if prefer_direct is None:
        prefer_direct = cfg.get("mode", "direct") != "nicehash"

    power_percent = _clamp_power(power_percent)
    if prefer_direct:
        direct_msg = start_direct_gpu_mining(
            btc_address=btc_address, power_percent=power_percent
        )
        if "fehlgeschlagen" not in direct_msg.lower() and "klappte nicht" not in direct_msg.lower():
            return direct_msg

    gpus = detect_amd_gpu()
    gpu_name = gpus[0]["name"] if gpus else "Grafikkarte"

    exe = find_nicehash_quickminer()
    if exe:
        patched = apply_nicehash_power_limit(power_percent)
        args = [str(exe), "-startMining"]
        try:
            proc = subprocess.Popen(
                args,
                cwd=str(exe.parent),
                creationflags=getattr(subprocess, "CREATE_NEW_CONSOLE", 0),
            )
            GPU_PID_FILE.parent.mkdir(parents=True, exist_ok=True)
            GPU_PID_FILE.write_text(str(proc.pid), encoding="utf-8")
            _log(f"started pid={proc.pid} power={power_percent}% patched={patched}")
            if open_browser:
                try:
                    webbrowser.open(NICEHASH_URL, new=2)
                except OSError:
                    pass
            return (
                f"**GPU-Mining gestartet** – {gpu_name}, Ziel **{power_percent} %** Leistung. "
                f"NiceHash QuickMiner läuft (PID {proc.pid}). "
                f"BTC-Auszahlung: {btc_address[:20]}… "
                f"Im NH-Fenster: Mining-Tab – Leistung auf {power_percent} % stellen falls nötig. "
                f"Stoppen: „Jam, GPU Mining stoppen“."
            )[:720]
        except OSError as e:
            _log(f"start fail: {e}")
            return f"NiceHash gefunden, Start fehlgeschlagen: {e}"[:400]

    # Fallback: direkt ohne NiceHash
    direct_msg = start_direct_gpu_mining(
        btc_address=btc_address, power_percent=power_percent
    )
    if "fehlgeschlagen" not in direct_msg.lower():
        return direct_msg

    (GPU_ROOT / "SOFORT_STARTEN.txt").write_text(
        f"""# GPU mit {power_percent} % – ohne NiceHash

1. **Ohne NiceHash:** start-gpu-ohne-nicehash.bat
   - lolMiner + UnMineable → BTC an {btc_address}
2. **Optional NiceHash:** {NICEHASH_DOWNLOAD}

GPU: {gpu_name}
""",
        encoding="utf-8",
    )
    if open_browser:
        try:
            webbrowser.open(UNMINEABLE_INFO, new=2)
            os.startfile(str(GPU_ROOT))
        except OSError:
            pass
    return direct_msg[:720]


def setup_gpu_mining_pack(btc_address: str) -> Path:
    GPU_ROOT.mkdir(parents=True, exist_ok=True)
    gpus = detect_amd_gpu()
    cfg = _load_gpu_cfg()
    cfg["btc_payout"] = btc_address
    cfg["gpus"] = gpus
    _save_gpu_cfg(cfg)

    gpu_lines = "\n".join(
        f"- {g['name']} ({g.get('vram_mb', '?')} MB VRAM)" for g in gpus
    ) or "- *(keine GPU erkannt)*"
    (GPU_ROOT / "ANLEITUNG_GPU.md").write_text(
        f"""# Jam GPU-Mining → Bitcoin

Stand: {datetime.now().strftime("%d.%m.%Y %H:%M")}

## Wichtig
- **Bitcoin direkt** mit der GPU minen geht nicht (nur ASIC/USB-Miner).
- **Ohne NiceHash:** lolMiner + UnMineable → Auszahlung in **BTC**
- **Mit NiceHash:** optional QuickMiner
- Ziel-Leistung: **{DEFAULT_POWER_PERCENT} %**
- BTC: `{btc_address}`

## Erkannte GPU
{gpu_lines}

## Sofort starten (ohne NiceHash)
- `start-gpu-ohne-nicehash.bat`
- Jam: **„GPU Mining ohne NiceHash 90 Prozent“**

## Stoppen
- `stop-gpu-mining.bat`
""",
        encoding="utf-8",
    )
    (GPU_ROOT / "JAM_BTC_ADRESSE.txt").write_text(btc_address + "\n", encoding="utf-8")
    return GPU_ROOT


def start_gpu_mining_helper(*, btc_address: str, open_browser: bool = True) -> str:
    return start_gpu_mining_now(
        btc_address=btc_address,
        power_percent=int(_load_gpu_cfg().get("power_percent", DEFAULT_POWER_PERCENT)),
        open_browser=open_browser,
    )


def start_gpu_mining_for_brain(brain, *, power_percent: int = DEFAULT_POWER_PERCENT) -> str:
    from jam.usb_miner_setup import _load_config, ensure_jam_bitcoin

    cfg = _load_config()
    if not cfg.get("btc_address"):
        ensure_jam_bitcoin(brain)
        cfg = _load_config()
    addr = cfg.get("btc_address", "")
    msg = start_gpu_mining_now(btc_address=addr, power_percent=power_percent, open_browser=False)
    if brain:
        brain.state.jam_gpu_mining_on = True
        brain.state.jam_gpu_power_percent = power_percent
        brain.state.last_action = "gpu_mining_start"
        brain.state.remember(f"GPU Mining {power_percent}% gestartet")
        brain.save()
    return msg


def hears_gpu_mining(text: str) -> bool:
    h = text.lower().replace("jam", " ")
    if not any(
        w in h
        for w in ("gpu", "grafikkarte", "radeon", "nicehash", "6900", "lolminer", "unmineable")
    ):
        return False
    return any(
        w in h
        for w in (
            "mining",
            "minen",
            "start",
            "lauf",
            "90",
            "100",
            "110",
            "prozent",
            "%",
            "ohne",
            "turbo",
            "max",
            "voll",
            "verdien",
            "ertrag",
        )
    )


def handle_speech(brain, heard: str) -> str | None:
    h = heard.lower()
    if "stop" in h or "stopp" in h or "aus" in h:
        if any(w in h for w in ("gpu", "grafikkarte", "nicehash")):
            if brain:
                brain.state.jam_gpu_mining_on = False
                brain.save()
            return stop_gpu_mining()
    if not hears_gpu_mining(heard):
        return None
    if any(w in h for w in ("ertrag", "verdien", "rechnen", "berechnen", "8 stund", "acht stund")):
        pct = int(_load_gpu_cfg().get("power_percent", TURBO_POWER_PERCENT))
        m = __import__("re").search(r"(\d{2,3})\s*(?:%|prozent)", h)
        if m:
            pct = int(m.group(1))
        hrs = 8.0
        hm = __import__("re").search(r"(\d+)\s*(?:h|stund)", h)
        if hm:
            hrs = float(hm.group(1))
        return format_earnings_report(
            estimate_gpu_earnings(hours=hrs, power_percent=pct, include_electricity=False)
        )
    pct = DEFAULT_POWER_PERCENT
    if any(w in h for w in ("turbo", "max", "voll", "100", "110", "über 100", "ueber 100")):
        pct = TURBO_POWER_PERCENT
    m = __import__("re").search(r"(\d{2,3})\s*(?:%|prozent)", h)
    if m:
        pct = _clamp_power(int(m.group(1)))
    prefer_direct = "nicehash" not in h or "ohne" in h
    from jam.usb_miner_setup import _load_config, ensure_jam_bitcoin

    cfg = _load_config()
    if not cfg.get("btc_address"):
        ensure_jam_bitcoin(brain)
        cfg = _load_config()
    addr = cfg.get("btc_address", "")
    msg = start_gpu_mining_now(
        btc_address=addr,
        power_percent=pct,
        prefer_direct=prefer_direct,
    )
    if brain:
        brain.state.jam_gpu_mining_on = True
        brain.state.jam_gpu_power_percent = pct
        brain.state.last_action = "gpu_mining_direct" if prefer_direct else "gpu_mining_start"
        brain.state.remember(f"GPU Mining {pct}% {'ohne NH' if prefer_direct else ''}")
        brain.save()
    return msg
