package client

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"sync"
	"time"

	"crypto-miner-agent/config"
)


// GPUVendor identifies the discrete GPU brand on the host.
type GPUVendor int

const (
	GPUVendorNone  GPUVendor = iota
	GPUVendorNVIDIA          // use T-Rex miner (KawPoW)
	GPUVendorAMD             // use TeamRedMiner (KawPoW)
	GPUVendorOther           // generic / Intel — not supported for KawPoW
)

// GPUInfo holds detected GPU metadata.
type GPUInfo struct {
	Vendor GPUVendor
	Model  string
}

// GPUMinerStats is polled from the miner's local HTTP API.
type GPUMinerStats struct {
	Hashrate15s  float64
	Hashrate1m   float64
	Hashrate15m  float64
	GPUTempC     *int
	GPUUsagePct  *int
	ActiveAlgo   string
}

// rvnEndpoint is one pool entry for the GPU miner (primary or backup).
type rvnEndpoint struct {
	host string
	port int
	tls  bool
	pass string
}

// GPUMiner manages one GPU miner sub-process (T-Rex or TeamRedMiner).
type GPUMiner struct {
	cfg        config.RuntimeConfig
	info       GPUInfo
	installDir string

	mu     sync.RWMutex
	stats  GPUMinerStats
	active bool
	proc   *os.Process // currently running subprocess (nil if stopped)

	stopCh chan struct{}
	wg     sync.WaitGroup
}

// newGPUMiner creates a GPUMiner if GPU mining is configured and a supported GPU is detected.
// Returns nil if GPU mining should not run.
func newGPUMiner(cfg config.RuntimeConfig) *GPUMiner {
	if !cfg.GPUEnabled || cfg.RVNWallet == "" {
		return nil
	}
	info := detectGPU()
	if info.Vendor == GPUVendorNone || info.Vendor == GPUVendorOther {
		log.Printf("[gpu] GPU mining enabled but no supported GPU detected (vendor=%v model=%q)", info.Vendor, info.Model)
		return nil
	}
	installDir, err := cfg.InstallDirectory()
	if err != nil {
		log.Printf("[gpu] cannot determine install dir: %v", err)
		return nil
	}
	log.Printf("[gpu] detected %s — will run KawPoW miner for RVN", info.Model)
	return &GPUMiner{
		cfg:        cfg,
		info:       info,
		installDir: installDir,
		stopCh:     make(chan struct{}),
	}
}

// Start downloads (if needed) and launches the GPU miner, then polls stats.
func (g *GPUMiner) Start() {
	g.wg.Add(1)
	go func() {
		defer g.wg.Done()
		g.run()
	}()
}

// Stop shuts down the GPU miner and waits for it to exit.
func (g *GPUMiner) Stop() {
	select {
	case <-g.stopCh:
	default:
		close(g.stopCh)
	}
	g.wg.Wait()
}

// Stats returns the latest GPU mining statistics.
func (g *GPUMiner) Stats() (GPUMinerStats, bool) {
	g.mu.RLock()
	defer g.mu.RUnlock()
	return g.stats, g.active
}

// GPUModel returns the detected GPU model string.
func (g *GPUMiner) GPUModel() string {
	return g.info.Model
}

// buildPoolList returns the primary pool followed by any configured backups.
func (g *GPUMiner) buildPoolList() []rvnEndpoint {
	eps := []rvnEndpoint{{
		host: g.cfg.RVNPoolHost,
		port: g.cfg.RVNPoolPort,
		tls:  g.cfg.RVNPoolTLS,
		pass: g.cfg.RVNPoolPass,
	}}
	for _, bp := range g.cfg.RVNBackupPools {
		if bp.Host != "" && bp.Port > 0 {
			eps = append(eps, rvnEndpoint{
				host: bp.Host,
				port: bp.Port,
				tls:  bp.TLS,
				pass: bp.Pass,
			})
		}
	}
	return eps
}

func (g *GPUMiner) run() {
	binPath, err := g.ensureMinerBinary()
	if err != nil {
		log.Printf("[gpu] could not obtain miner binary: %v", err)
		return
	}

	pools := g.buildPoolList()
	poolIdx := 0
	const retryDelay = 30 * time.Second

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

		ep := pools[poolIdx%len(pools)]
		proc, err := g.startProcessOnPool(binPath, ep)
		if err != nil {
			log.Printf("[gpu] failed to start miner: %v — retry in %s (pool %d/%d)", err, retryDelay, poolIdx%len(pools)+1, len(pools))
			select {
			case <-g.stopCh:
				return
			case <-time.After(retryDelay):
			}
			poolIdx++
			continue
		}

		g.mu.Lock()
		g.active = true
		g.proc = proc
		g.mu.Unlock()

		log.Printf("[gpu] %s started (pid=%d) → %s:%d", g.spec().fileName, proc.Pid, ep.host, ep.port)

		// pollStop signals pollStats to exit; closed when this iteration ends.
		pollStop := make(chan struct{})
		pollDone := make(chan struct{})
		go func() {
			defer close(pollDone)
			g.pollStats(pollStop)
		}()

		// Wait for process exit in a goroutine so we can also listen for stop.
		waitDone := make(chan error, 1)
		go func() {
			_, werr := proc.Wait()
			waitDone <- werr
		}()

		var stopRequested bool
		select {
		case <-g.stopCh:
			// Agent shutting down — kill the miner process immediately.
			stopRequested = true
			_ = proc.Kill()
			<-waitDone
		case waitErr := <-waitDone:
			if waitErr != nil {
				log.Printf("[gpu] miner exited: %v — rotating to next pool", waitErr)
			}
			// Miner crashed or exited cleanly — rotate to next pool on retry.
			poolIdx++
		}

		close(pollStop)
		<-pollDone

		g.mu.Lock()
		g.active = false
		g.proc = nil
		g.mu.Unlock()

		if stopRequested {
			return
		}

		// Wait before retrying, but exit cleanly if Stop() is called.
		select {
		case <-g.stopCh:
			return
		case <-time.After(retryDelay):
		}
	}
}

// pollStats polls the miner's HTTP API until stop is closed.
func (g *GPUMiner) pollStats(stop <-chan struct{}) {
	apiPort := g.apiPort()
	ticker := time.NewTicker(10 * time.Second)
	defer ticker.Stop()

	samples := make([]float64, 0, 90) // 15 min at 10s intervals

	for {
		select {
		case <-stop:
			return
		case <-ticker.C:
			hr, tempC, usage, err := fetchMinerStats(g.info.Vendor, apiPort)
			if err != nil {
				continue
			}
			samples = append(samples, hr)
			if len(samples) > 90 {
				samples = samples[len(samples)-90:]
			}

			g.mu.Lock()
			g.stats = GPUMinerStats{
				Hashrate15s: hr,
				Hashrate1m:  avg(samples, 6),
				Hashrate15m: avg(samples, len(samples)),
				GPUTempC:    tempC,
				GPUUsagePct: usage,
				ActiveAlgo:  "kawpow",
			}
			g.mu.Unlock()
		}
	}
}

func avg(samples []float64, last int) float64 {
	if len(samples) == 0 || last <= 0 {
		return 0
	}
	if last > len(samples) {
		last = len(samples)
	}
	slice := samples[len(samples)-last:]
	var sum float64
	for _, v := range slice {
		sum += v
	}
	return sum / float64(len(slice))
}

func (g *GPUMiner) apiPort() int {
	switch g.info.Vendor {
	case GPUVendorNVIDIA:
		return 4067
	case GPUVendorAMD:
		return 4068
	default:
		return 4067
	}
}

// buildPoolURL constructs the stratum URL for a given pool endpoint.
func buildPoolURL(ep rvnEndpoint) string {
	scheme := "stratum+tcp"
	if ep.tls {
		scheme = "stratum+ssl"
	}
	return fmt.Sprintf("%s://%s:%d", scheme, ep.host, ep.port)
}

// ---- Miner binary management ----

type minerSpec struct {
	fileName   string
	downloadURL string
}

func (g *GPUMiner) spec() minerSpec {
	switch g.info.Vendor {
	case GPUVendorNVIDIA:
		return minerSpec{
			fileName:    "t-rex.exe",
			downloadURL: "https://github.com/trexminer/T-Rex/releases/download/0.26.8/t-rex-0.26.8-win.zip",
		}
	default: // AMD
		return minerSpec{
			fileName:    "teamredminer.exe",
			downloadURL: "https://github.com/todxx/teamredminer/releases/download/v0.10.21/teamredminer-v0.10.21-win.zip",
		}
	}
}

func (g *GPUMiner) ensureMinerBinary() (string, error) {
	spec := g.spec()
	binPath := filepath.Join(g.installDir, spec.fileName)
	if _, err := os.Stat(binPath); err == nil {
		return binPath, nil
	}
	log.Printf("[gpu] downloading %s from %s", spec.fileName, spec.downloadURL)
	if err := downloadAndExtract(spec.downloadURL, g.installDir, spec.fileName); err != nil {
		return "", fmt.Errorf("download failed: %w", err)
	}
	if _, err := os.Stat(binPath); err != nil {
		return "", fmt.Errorf("binary not found after download: %s", binPath)
	}
	return binPath, nil
}

func downloadAndExtract(url, destDir, targetFile string) error {
	resp, err := http.Get(url) //nolint:noctx
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("HTTP %d from %s", resp.StatusCode, url)
	}
	data, err := io.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	return extractZipFile(data, destDir, targetFile)
}

// ---- Miner HTTP API polling ----

// T-Rex summary response (subset we care about).
type trexSummary struct {
	Hashrate int `json:"hashrate"`
	GPUs     []struct {
		Temperature int `json:"temperature"`
		GpuLoad     int `json:"gpu_load"`
	} `json:"gpus"`
}

// TeamRedMiner status response (subset).
type trmStatus struct {
	Algorithms []struct {
		Name      string  `json:"algorithm"`
		TotalMHs  float64 `json:"mhsh_total"`
	} `json:"algorithms"`
	GPUs []struct {
		TempC   int `json:"temp_c"`
		Fan     int `json:"fan_pct"`
	} `json:"gpus"`
}

func fetchMinerStats(vendor GPUVendor, port int) (hashrate float64, tempC, usagePct *int, err error) {
	url := fmt.Sprintf("http://127.0.0.1:%d/summary", port)
	resp, e := http.Get(url) //nolint:noctx
	if e != nil {
		return 0, nil, nil, e
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)

	switch vendor {
	case GPUVendorNVIDIA:
		var s trexSummary
		if e := json.Unmarshal(body, &s); e != nil {
			return 0, nil, nil, e
		}
		hashrate = float64(s.Hashrate)
		if len(s.GPUs) > 0 {
			t := s.GPUs[0].Temperature
			u := s.GPUs[0].GpuLoad
			tempC = &t
			usagePct = &u
		}
	case GPUVendorAMD:
		var s trmStatus
		if e := json.Unmarshal(body, &s); e != nil {
			return 0, nil, nil, e
		}
		for _, a := range s.Algorithms {
			if a.Name == "kawpow" || a.Name == "KawPoW" {
				hashrate = a.TotalMHs * 1e6 // convert MH/s → H/s
			}
		}
		if len(s.GPUs) > 0 {
			t := s.GPUs[0].TempC
			u := s.GPUs[0].Fan
			tempC = &t
			usagePct = &u
		}
	}
	return
}
