// Copyright (c) 2017-2026 Lethean (https://lt.hn)
//
// Licensed under the European Union Public Licence (EUPL) version 1.2.
// SPDX-License-Identifier: EUPL-1.2

package mining

import (
	"bytes"
	"context"
	"encoding/binary"
	"encoding/hex"
	"fmt"
	"strconv"
	"sync/atomic"
	"time"

	coreerr "dappco.re/go/core/log"

	"dappco.re/go/core/blockchain/consensus"
	"dappco.re/go/core/blockchain/crypto"
	"dappco.re/go/core/blockchain/rpc"
	"dappco.re/go/core/blockchain/types"
	"dappco.re/go/core/blockchain/wire"
)

// TemplateProvider abstracts the RPC methods needed by the miner.
// The real rpc.Client satisfies this interface.
type TemplateProvider interface {
	GetBlockTemplate(walletAddr string) (*rpc.BlockTemplateResponse, error)
	SubmitBlock(hexBlob string) error
	GetInfo() (*rpc.DaemonInfo, error)
}

// Config holds the miner configuration.
type Config struct {
	// DaemonURL is the JSON-RPC endpoint of the C++ daemon.
	DaemonURL string

	// WalletAddr is the address that receives mining rewards.
	WalletAddr string

	// PollInterval is how often to check for new blocks. Default: 3s.
	PollInterval time.Duration

	// OnBlockFound is called after a solution is successfully submitted.
	// May be nil.
	OnBlockFound func(height uint64, hash types.Hash)

	// OnNewTemplate is called when a new block template is fetched.
	// May be nil.
	OnNewTemplate func(height uint64, difficulty uint64)

	// Provider is the RPC provider. If nil, a default rpc.Client is
	// created from DaemonURL.
	Provider TemplateProvider
}

// Stats holds read-only mining statistics.
type Stats struct {
	Hashrate    float64
	BlocksFound uint64
	Height      uint64
	Difficulty  uint64
	Uptime      time.Duration
}

// Miner is a solo PoW miner that talks to a C++ daemon via JSON-RPC.
type Miner struct {
	cfg       Config
	provider  TemplateProvider
	startTime time.Time

	// Atomic stats — accessed from Stats() on any goroutine.
	hashCount   atomic.Uint64
	blocksFound atomic.Uint64
	height      atomic.Uint64
	difficulty  atomic.Uint64
}

// NewMiner creates a new miner with the given configuration.
func NewMiner(cfg Config) *Miner {
	if cfg.PollInterval == 0 {
		cfg.PollInterval = 3 * time.Second
	}

	var provider TemplateProvider
	if cfg.Provider != nil {
		provider = cfg.Provider
	} else {
		provider = rpc.NewClient(cfg.DaemonURL)
	}

	return &Miner{
		cfg:      cfg,
		provider: provider,
	}
}

// Stats returns a snapshot of the current mining statistics.
// Safe to call from any goroutine.
func (m *Miner) Stats() Stats {
	var uptime time.Duration
	if !m.startTime.IsZero() {
		uptime = time.Since(m.startTime)
	}

	hashes := m.hashCount.Load()
	var hashrate float64
	if uptime > 0 {
		hashrate = float64(hashes) / uptime.Seconds()
	}

	return Stats{
		Hashrate:    hashrate,
		BlocksFound: m.blocksFound.Load(),
		Height:      m.height.Load(),
		Difficulty:  m.difficulty.Load(),
		Uptime:      uptime,
	}
}

// Start runs the mining loop. It blocks until ctx is cancelled.
// Returns the context error (typically context.Canceled or context.DeadlineExceeded).
func (m *Miner) Start(ctx context.Context) error {
	m.startTime = time.Now()

	for {
		// Fetch a block template.
		tmpl, err := m.provider.GetBlockTemplate(m.cfg.WalletAddr)
		if err != nil {
			// Transient RPC error — wait and retry.
			select {
			case <-ctx.Done():
				return ctx.Err()
			case <-time.After(m.cfg.PollInterval):
				continue
			}
		}

		// Parse difficulty.
		diff, err := strconv.ParseUint(tmpl.Difficulty, 10, 64)
		if err != nil {
			return coreerr.E("Miner.Start", fmt.Sprintf("mining: invalid difficulty %q", tmpl.Difficulty), err)
		}

		// Decode the block template blob.
		blobBytes, err := hex.DecodeString(tmpl.BlockTemplateBlob)
		if err != nil {
			return coreerr.E("Miner.Start", "mining: invalid template blob hex", err)
		}
		dec := wire.NewDecoder(bytes.NewReader(blobBytes))
		block := wire.DecodeBlock(dec)
		if dec.Err() != nil {
			return coreerr.E("Miner.Start", "mining: decode template", dec.Err())
		}

		// Update stats.
		m.height.Store(tmpl.Height)
		m.difficulty.Store(diff)

		if m.cfg.OnNewTemplate != nil {
			m.cfg.OnNewTemplate(tmpl.Height, diff)
		}

		// Compute the header mining hash (once per template).
		headerHash := HeaderMiningHash(&block)

		// Mine until solution found or template becomes stale.
		if err := m.mine(ctx, &block, headerHash, diff); err != nil {
			return err
		}
	}
}

// mine grinds nonces against the given header hash and difficulty.
// Returns nil when a new template should be fetched (new block detected).
// Returns ctx.Err() when shutdown is requested.
func (m *Miner) mine(ctx context.Context, block *types.Block, headerHash [32]byte, difficulty uint64) error {
	pollTicker := time.NewTicker(m.cfg.PollInterval)
	defer pollTicker.Stop()

	currentHeight := m.height.Load()

	for nonce := uint64(0); ; nonce++ {
		// Check for shutdown or poll trigger.
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-pollTicker.C:
			// Check if chain has advanced.
			info, err := m.provider.GetInfo()
			if err == nil && info.Height > currentHeight {
				return nil // fetch new template
			}
			continue
		default:
		}

		// Compute RandomX hash.
		var input [40]byte
		copy(input[:32], headerHash[:])
		binary.LittleEndian.PutUint64(input[32:], nonce)

		powHash, err := crypto.RandomXHash(RandomXKey, input[:])
		if err != nil {
			return coreerr.E("Miner.mine", "mining: RandomX hash", err)
		}

		m.hashCount.Add(1)

		if consensus.CheckDifficulty(types.Hash(powHash), difficulty) {
			// Solution found!
			block.Nonce = nonce

			var buf bytes.Buffer
			enc := wire.NewEncoder(&buf)
			wire.EncodeBlock(enc, block)
			if enc.Err() != nil {
				return coreerr.E("Miner.mine", "mining: encode solution", enc.Err())
			}

			hexBlob := hex.EncodeToString(buf.Bytes())
			if err := m.provider.SubmitBlock(hexBlob); err != nil {
				return coreerr.E("Miner.mine", "mining: submit block", err)
			}

			m.blocksFound.Add(1)

			if m.cfg.OnBlockFound != nil {
				m.cfg.OnBlockFound(currentHeight, wire.BlockHash(block))
			}

			return nil // fetch new template
		}
	}
}
