#!/usr/bin/env python3
"""
Sound Block Miner — SHA-256 mining proof of concept.

GPU-accelerated SHA-256 mining using the Sound Block compute architecture.
Optimized for AMD GFX1103 iGPU (1536 cores, 8GB shared VRAM).

Usage:
    cd afs-engine

    # Benchmark mode — measure peak hashrate
    python3 -m afs.gpu.sb_mine_runner --benchmark

    # Mine with specific difficulty for N seconds
    python3 -m afs.gpu.sb_mine_runner --difficulty 24 --duration 60

    # Tune parameters
    python3 -m afs.gpu.sb_mine_runner --benchmark --nonces-per-thread 64 --threads 524288

    # Full mining run with all options
    python3 -m afs.gpu.sb_mine_runner --difficulty 28 --duration 300 --fused 20 --report-interval 10

All results are saved to docs/AtomataFieldTheory/SoundBlock-Mining-Results.md
"""

import argparse
import hashlib
import struct
import sys
import time
from datetime import datetime
from pathlib import Path

try:
    from afs.gpu.sb_miner import SoundBlockMiner, cpu_double_sha256, count_leading_zero_bits
except ImportError:
    # Allow running from afs-engine directory
    sys.path.insert(0, str(Path(__file__).parent.parent.parent))
    from afs.gpu.sb_miner import SoundBlockMiner, cpu_double_sha256, count_leading_zero_bits


def create_test_header() -> bytes:
    """Create a deterministic 80-byte test block header.

    Layout (mimics Bitcoin block header):
      bytes  0- 3: version (0x20000000)
      bytes  4-35: previous block hash (32 bytes, zeros)
      bytes 36-67: merkle root (32 bytes, SHA-256 of "SoundBlock")
      bytes 68-71: timestamp (current time)
      bytes 72-75: difficulty bits (0x1d00ffff — standard initial difficulty)
      bytes 76-79: nonce placeholder (zeros — miner fills this)
    """
    version = struct.pack('>I', 0x20000000)
    prev_hash = b'\x00' * 32
    merkle = hashlib.sha256(b"SoundBlock").digest()
    timestamp = struct.pack('>I', int(time.time()))
    bits = struct.pack('>I', 0x1d00ffff)
    nonce_placeholder = b'\x00' * 4

    header = version + prev_hash + merkle + timestamp + bits + nonce_placeholder
    assert len(header) == 80, f"Header length {len(header)}, expected 80"
    return header


def run_benchmark(args):
    """Run benchmark to measure peak hashrate at various configurations."""
    header = create_test_header()

    print("=" * 72)
    print("SOUND BLOCK MINER — BENCHMARK")
    print("=" * 72)

    configs = []

    if args.nonces_per_thread and args.threads:
        # Single config from command line
        configs.append((args.nonces_per_thread, args.threads, args.fused))
    else:
        # Sweep configurations to find optimal
        configs = [
            # (nonces_per_thread, threads_per_dispatch, fused_dispatches)
            (1,   65536,   1),     # baseline: matches old sb_sha256.wgsl
            (8,   65536,   1),     # 8x nonces per thread
            (16,  65536,   1),     # 16x nonces per thread
            (32,  65536,   1),     # 32x nonces per thread
            (32,  131072,  1),     # larger dispatch
            (32,  262144,  1),     # 262K threads
            (32,  262144,  10),    # fused: 10 dispatches per submit
            (32,  262144,  20),    # fused: 20 dispatches per submit
            (64,  262144,  10),    # 64 nonces per thread
            (32,  524288,  10),    # 524K threads
            (32,  1048576, 10),    # 1M threads
        ]

    results = []
    best_rate = 0
    best_config = None

    for npt, threads, fused in configs:
        try:
            miner = SoundBlockMiner(
                block_header=header,
                difficulty=64,  # impossibly high — no hits, pure throughput
                nonces_per_thread=npt,
                threads_per_dispatch=threads,
            )

            if len(configs) == 1:
                info = miner.adapter_info
                print(f"GPU: {info.get('description', 'unknown')}")
                print(f"Driver: {info.get('driver_description', 'unknown')}")
                print()

            bm = miner.benchmark(
                warmup_rounds=3,
                measure_rounds=10,
                fused_dispatches=fused,
            )

            rate = bm['hashrate']
            nonces_per_submit = bm['nonces_per_submit']

            label = (f"npt={npt:>3}, threads={threads:>8,}, "
                     f"fused={fused:>2}, nonces/submit={nonces_per_submit:>12,}")
            rate_str = f"{rate:>14,.0f} H/s"

            marker = ""
            if rate > best_rate:
                best_rate = rate
                best_config = bm
                marker = " <-- BEST"

            print(f"  {label}  =>  {rate_str}{marker}")
            results.append(bm)

        except Exception as e:
            print(f"  npt={npt}, threads={threads}, fused={fused}  =>  ERROR: {e}")

    print()
    if best_config:
        print(f"Peak hashrate: {best_rate:,.0f} H/s "
              f"({best_rate/1e6:.1f} MH/s)")
        print(f"Config: nonces_per_thread={best_config['nonces_per_thread']}, "
              f"threads={best_config['threads_per_dispatch']:,}, "
              f"fused={best_config['fused_dispatches']}")
        gpu = best_config['adapter_info'].get('description', 'unknown')
        print(f"GPU: {gpu}")

    return results, best_config


def run_mining(args):
    """Run continuous mining with difficulty target."""
    header = create_test_header()

    print("=" * 72)
    print("SOUND BLOCK MINER — MINING RUN")
    print("=" * 72)

    miner = SoundBlockMiner(
        block_header=header,
        difficulty=args.difficulty,
        nonces_per_thread=args.nonces_per_thread,
        threads_per_dispatch=args.threads,
    )

    info = miner.adapter_info
    print(f"GPU: {info.get('description', 'unknown')}")
    print(f"Driver: {info.get('driver_description', 'unknown')}")
    print(f"Difficulty: {args.difficulty} leading zero bits")
    print(f"Duration: {args.duration} seconds")
    print(f"Config: npt={args.nonces_per_thread}, threads={args.threads:,}, "
          f"fused={args.fused}")
    print()

    result = miner.mine_continuous(
        duration_seconds=args.duration,
        report_interval=args.report_interval,
        fused_dispatches=args.fused,
    )

    print()
    print("=" * 72)
    print("RESULTS")
    print("=" * 72)
    print(f"Total hashes:    {result['total_hashes']:>16,}")
    print(f"Elapsed:         {result['elapsed_seconds']:>16.2f} seconds")
    print(f"Hashrate:        {result['hashrate']:>16,.0f} H/s "
          f"({result['hashrate']/1e6:.1f} MH/s)")
    print(f"Best difficulty: {result['best_difficulty']:>16} bits")
    if result['best_hash']:
        print(f"Best hash:       {result['best_hash'][:32]}...")
        print(f"Best nonce:      {result['best_nonce']:>16,}")

    # Verify best hit on CPU
    if result['best_nonce'] > 0 and result['best_hash']:
        cpu_hash, cpu_zeros = miner.verify_hash(result['best_nonce'])
        verified = (cpu_zeros >= result['best_difficulty'])
        print(f"CPU verify:      {'PASS' if verified else 'FAIL'} "
              f"(cpu={cpu_zeros} bits, gpu={result['best_difficulty']} bits)")
        if cpu_hash != result['best_hash']:
            print(f"  WARNING: GPU hash != CPU hash")
            print(f"  GPU: {result['best_hash']}")
            print(f"  CPU: {cpu_hash}")

    print(f"Hits found:      {len(result['hits']):>16}")
    for hit in result['hits'][:10]:
        print(f"  nonce={hit['nonce']:>10,}  "
              f"difficulty={hit['difficulty']:>3} bits  "
              f"time={hit['time']:.1f}s  "
              f"hash={hit['hash'][:24]}...")

    return result


def save_results(benchmark_results, mining_result, args):
    """Save results to markdown file."""
    docs_dir = Path(__file__).parent.parent.parent.parent / "docs" / "AtomataFieldTheory"
    docs_dir.mkdir(parents=True, exist_ok=True)
    results_path = docs_dir / "SoundBlock-Mining-Results.md"

    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    lines = []

    # Read existing content or start fresh
    if results_path.exists():
        existing = results_path.read_text()
        # Find insertion point (after header, before first run)
        if "---\n\n## Run:" in existing:
            header_end = existing.index("---\n\n## Run:")
            lines.append(existing[:header_end + 4])
        else:
            lines.append(existing.rstrip() + "\n\n")
    else:
        lines.append("# Sound Block Mining Results\n\n")
        lines.append("GPU SHA-256 mining using Sound Block compute architecture.\n")
        lines.append("Each run logged with full configuration and verification.\n\n")
        lines.append("---\n\n")

    lines.append(f"## Run: {timestamp}\n\n")

    if benchmark_results:
        best = benchmark_results[1]
        if best:
            gpu = best['adapter_info'].get('description', 'unknown')
            lines.append(f"### Benchmark\n\n")
            lines.append(f"- **GPU**: {gpu}\n")
            lines.append(f"- **Peak hashrate**: {best['hashrate']:,.0f} H/s "
                        f"({best['hashrate']/1e6:.1f} MH/s)\n")
            lines.append(f"- **Config**: nonces_per_thread={best['nonces_per_thread']}, "
                        f"threads={best['threads_per_dispatch']:,}, "
                        f"fused={best['fused_dispatches']}\n")
            lines.append(f"- **Nonces per submit**: {best['nonces_per_submit']:,}\n\n")

            lines.append("| nonces/thread | threads | fused | nonces/submit | H/s |\n")
            lines.append("|---:|---:|---:|---:|---:|\n")
            for bm in benchmark_results[0]:
                lines.append(
                    f"| {bm['nonces_per_thread']} "
                    f"| {bm['threads_per_dispatch']:,} "
                    f"| {bm['fused_dispatches']} "
                    f"| {bm['nonces_per_submit']:,} "
                    f"| {bm['hashrate']:,.0f} |\n")
            lines.append("\n")

    if mining_result:
        lines.append(f"### Mining\n\n")
        lines.append(f"- **Difficulty**: {args.difficulty} bits\n")
        lines.append(f"- **Duration**: {mining_result['elapsed_seconds']:.1f}s\n")
        lines.append(f"- **Total hashes**: {mining_result['total_hashes']:,}\n")
        lines.append(f"- **Hashrate**: {mining_result['hashrate']:,.0f} H/s "
                    f"({mining_result['hashrate']/1e6:.1f} MH/s)\n")
        lines.append(f"- **Best difficulty**: {mining_result['best_difficulty']} bits\n")
        if mining_result['best_hash']:
            lines.append(f"- **Best hash**: `{mining_result['best_hash']}`\n")
            lines.append(f"- **Best nonce**: {mining_result['best_nonce']:,}\n")
        lines.append(f"- **Hits found**: {len(mining_result['hits'])}\n\n")

        if mining_result['hits']:
            lines.append("| nonce | difficulty | time (s) | hash (first 32 chars) |\n")
            lines.append("|---:|---:|---:|---|\n")
            for hit in mining_result['hits'][:20]:
                lines.append(
                    f"| {hit['nonce']:,} "
                    f"| {hit['difficulty']} "
                    f"| {hit['time']:.1f} "
                    f"| `{hit['hash'][:32]}...` |\n")
            lines.append("\n")

    lines.append("---\n\n")

    # Append any remaining old content
    if results_path.exists():
        existing = results_path.read_text()
        if "---\n\n## Run:" in existing:
            old_runs = existing[existing.index("---\n\n## Run:") + 4:]
            lines.append(old_runs)

    results_path.write_text(''.join(lines))
    print(f"\nResults saved to: {results_path}")


def main():
    parser = argparse.ArgumentParser(
        description="Sound Block Miner — GPU SHA-256 mining",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=__doc__,
    )
    parser.add_argument("--benchmark", action="store_true",
                        help="Run benchmark to find optimal configuration")
    parser.add_argument("--difficulty", type=int, default=24,
                        help="Required leading zero bits (default: 24)")
    parser.add_argument("--duration", type=int, default=60,
                        help="Mining duration in seconds (default: 60)")
    parser.add_argument("--nonces-per-thread", type=int, default=32,
                        help="Nonces processed per thread (default: 32)")
    parser.add_argument("--threads", type=int, default=262144,
                        help="Threads per dispatch (default: 262144)")
    parser.add_argument("--fused", type=int, default=10,
                        help="Dispatches fused per submit (default: 10)")
    parser.add_argument("--report-interval", type=float, default=5.0,
                        help="Seconds between progress reports (default: 5.0)")
    parser.add_argument("--no-save", action="store_true",
                        help="Do not save results to markdown file")

    args = parser.parse_args()

    benchmark_results = None
    mining_result = None

    if args.benchmark:
        benchmark_results = run_benchmark(args)
        print()

    if not args.benchmark or args.duration > 0:
        if args.benchmark:
            # After benchmark, do a short mining run to verify
            print("Running short mining verification...")
            args.duration = min(args.duration, 30)

        mining_result = run_mining(args)

    if not args.no_save:
        save_results(benchmark_results, mining_result, args)


if __name__ == "__main__":
    main()
