package agenthttp

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

	"zlh-agent/internal/alloy"
	agentfiles "zlh-agent/internal/files"
	agenthandlers "zlh-agent/internal/handlers"
	mcstatus "zlh-agent/internal/minecraft"
	"zlh-agent/internal/provision"
	"zlh-agent/internal/provision/addons/codeserver"
	"zlh-agent/internal/provision/devcontainer"
	"zlh-agent/internal/provision/devcontainer/dotnet"
	"zlh-agent/internal/provision/devcontainer/go"
	"zlh-agent/internal/provision/devcontainer/java"
	"zlh-agent/internal/provision/devcontainer/node"
	"zlh-agent/internal/provision/devcontainer/python"
	"zlh-agent/internal/provision/minecraft"
	"zlh-agent/internal/state"
	"zlh-agent/internal/system"
	"zlh-agent/internal/update"
	"zlh-agent/internal/util"
	"zlh-agent/internal/version"
)

const ReadinessTimeout = 60 * time.Second

/*
	--------------------------------------------------------------------------
	  Helpers

----------------------------------------------------------------------------
*/
func fileExists(path string) bool {
	_, err := os.Stat(path)
	return err == nil
}

func dirExists(path string) bool {
	s, err := os.Stat(path)
	return err == nil && s.IsDir()
}

var (
	provisionAll        = provision.ProvisionAll
	devIsProvisioned    = devcontainer.IsProvisioned
	devRuntimeInstalled = devcontainer.RuntimeInstalled
	codeServerInstall   = codeserver.Install
	codeServerStart     = codeserver.Start
	codeServerVerify    = codeserver.Verify
	codeServerInstalled = codeserver.Installed
	codeServerRunning   = codeserver.Running
)

func lifecycleLog(cfg *state.Config, phase string, attempt int, started time.Time, format string, args ...any) {
	elapsed := time.Since(started).Milliseconds()
	msg := fmt.Sprintf(format, args...)
	util.LogLifecycle("[lifecycle] vmid=%d phase=%s attempt=%d elapsed_ms=%d %s", cfg.VMID, phase, attempt, elapsed, msg)
}

func requireDevContainer() (*state.Config, error) {
	cfg, err := state.LoadConfig()
	if err != nil {
		return nil, fmt.Errorf("load config: %w", err)
	}
	if cfg.ContainerType != "dev" {
		return nil, fmt.Errorf("code-server controls are only available for dev containers")
	}
	if !cfg.EnableCodeServer {
		return nil, fmt.Errorf("code-server is not enabled for this container")
	}
	return cfg, nil
}

func beginHTTPOperation(w http.ResponseWriter, opType string, maintenance bool, message string) (func(), bool) {
	end, ok, current := state.TryStartOperation(opType, maintenance, message)
	if ok {
		return end, true
	}
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusConflict)
	_ = json.NewEncoder(w).Encode(map[string]any{
		"error":          "operation already in progress",
		"operationType":  current.Type,
		"maintenance":    current.Maintenance,
		"operationSince": current.StartedAt.UTC().Format(time.RFC3339),
		"message":        current.Message,
	})
	return nil, false
}

/*
	--------------------------------------------------------------------------
	  Shared provision pipeline (installer + Minecraft verify)

----------------------------------------------------------------------------
*/
func runProvisionPipeline(cfg *state.Config) error {
	state.SetState(state.StateInstalling)
	state.SetInstallStep("provision_all")

	if err := provisionAll(*cfg); err != nil {
		state.SetError(err)
		state.SetState(state.StateError)
		return err
	}

	if strings.ToLower(cfg.Game) == "minecraft" {
		if err := minecraft.VerifyMinecraftInstallWithRepair(*cfg); err != nil {
			state.SetError(err)
			state.SetState(state.StateError)
			return fmt.Errorf("minecraft verification failed: %w", err)
		}
	}

	state.SetInstallStep("")
	state.SetState(state.StateIdle)
	return nil
}

/*
	--------------------------------------------------------------------------
	  ensureProvisioned() — idempotent, unified

----------------------------------------------------------------------------
*/
func ensureProvisioned(cfg *state.Config) error {

	if cfg.ContainerType == "dev" {

		if !devIsProvisioned() || !devRuntimeInstalled(cfg.Runtime, cfg.Version) {
			if err := runProvisionPipeline(cfg); err != nil {
				return err
			}
		}

		var err error

		switch strings.ToLower(cfg.Runtime) {
		case "node":
			err = node.Verify(*cfg)
		case "python":
			err = python.Verify(*cfg)
		case "go":
			err = goenv.Verify(*cfg)
		case "java":
			err = java.Verify(*cfg)
		case "dotnet":
			err = dotnet.Verify(*cfg)
		default:
			return fmt.Errorf("unsupported devcontainer runtime: %s", cfg.Runtime)
		}

		if err != nil {
			return err
		}

		if err := ensureDevCodeServer(cfg); err != nil {
			return err
		}

		state.SetState(state.StateIdle)
		state.SetError(nil)
		return nil
	}

	dir := provision.ServerDir(*cfg)
	game := strings.ToLower(cfg.Game)
	variant := strings.ToLower(cfg.Variant)

	_ = os.MkdirAll(dir, 0o755)

	isSteam := provision.IsSteamGame(game)
	isForgeLike := variant == "forge" || variant == "neoforge"

	if isSteam {
		return runProvisionPipeline(cfg)
	}

	if isForgeLike {
		runSh := filepath.Join(dir, "run.sh")
		libraries := filepath.Join(dir, "libraries")

		if fileExists(runSh) && dirExists(libraries) {
			return nil
		}
		return runProvisionPipeline(cfg)
	}

	jar := filepath.Join(dir, provision.ExpectedMinecraftJar(*cfg))
	if fileExists(jar) {
		return nil
	}

	return runProvisionPipeline(cfg)
}

func ensureDevCodeServer(cfg *state.Config) error {
	if cfg == nil || !devCodeServerRequested(*cfg) {
		return nil
	}

	if !codeServerInstalled() {
		if err := codeServerInstall(*cfg); err != nil {
			return fmt.Errorf("code-server install failed: %w", err)
		}
	}
	if !codeServerRunning() {
		if err := codeServerStart(*cfg); err != nil {
			return fmt.Errorf("code-server start failed: %w", err)
		}
	}
	if err := codeServerVerify(); err != nil {
		return fmt.Errorf("code-server verification failed: %w", err)
	}
	if !codeServerRunning() {
		return fmt.Errorf("code-server did not stay running")
	}
	return nil
}

func devCodeServerRequested(cfg state.Config) bool {
	if cfg.EnableCodeServer {
		return true
	}
	for _, addon := range cfg.Addons {
		if strings.EqualFold(addon, "codeserver") {
			return true
		}
	}
	return false
}

/*
	--------------------------------------------------------------------------
	  /config — the REAL provisioning trigger (async)

----------------------------------------------------------------------------
*/
func handleConfig(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "POST only", http.StatusMethodNotAllowed)
		return
	}
	endOp, ok := beginHTTPOperation(w, "provision", true, "provisioning container")
	if !ok {
		return
	}

	body, _ := io.ReadAll(r.Body)

	var cfg state.Config
	if err := json.Unmarshal(body, &cfg); err != nil {
		endOp()
		http.Error(w, "bad json", http.StatusBadRequest)
		return
	}
	log.Printf("[http] vmid=%d action=config status=received type=%s runtime=%s game=%s variant=%s version=%s", cfg.VMID, cfg.ContainerType, cfg.Runtime, cfg.Game, cfg.Variant, cfg.Version)

	if err := state.SaveConfig(&cfg); err != nil {
		endOp()
		log.Printf("[http] vmid=%d action=config status=save_failed err=%v", cfg.VMID, err)
		http.Error(w, "save config failed: "+err.Error(), http.StatusInternalServerError)
		return
	}
	if _, err := alloy.EnsureConfig(cfg); err != nil {
		log.Printf("[http] vmid=%d action=config status=alloy_failed non_fatal=true err=%v", cfg.VMID, err)
	}

	go func(c state.Config) {
		defer endOp()
		log.Printf("[http] vmid=%d async provision+start begin", c.VMID)
		started := time.Now()
		lifecycleLog(&c, "config_async", 1, started, "begin")

		if err := ensureProvisioned(&c); err != nil {
			log.Printf("[http] vmid=%d provision error: %v", c.VMID, err)
			return
		}

		if c.ContainerType != "dev" {
			state.SetState(state.StateStarting)
			state.SetReadyState(false, "", "")
			lifecycleLog(&c, "start", 1, started, "start_requested")
			if err := system.StartServerReady(&c); err != nil {
				log.Printf("[http] vmid=%d start error: %v", c.VMID, err)
				state.SetError(err)
				state.SetState(state.StateError)
				lifecycleLog(&c, "start", 1, started, "start_failed err=%v", err)
				return
			}
			lifecycleLog(&c, "start", 1, started, "process_started")

			// -------------------------------------------------
			// FORGE / NEOFORGE: wait → stop → patch → restart
			// -------------------------------------------------
			game := strings.ToLower(c.Game)
			variant := strings.ToLower(c.Variant)

			if game == "minecraft" && (variant == "forge" || variant == "neoforge") {
				lifecycleLog(&c, "forge_post", 1, started, "begin")

				// Wait for server.properties to exist before enforcing
				propsPath := filepath.Join(provision.ServerDir(c), "server.properties")
				propsDeadline := time.Now().Add(2 * time.Minute)
				for {
					if _, err := os.Stat(propsPath); err == nil {
						break
					}
					if time.Now().After(propsDeadline) {
						err := fmt.Errorf("forge server.properties not found before timeout")
						log.Printf("[http] vmid=%d forge post-start error: %v", c.VMID, err)
						state.SetError(err)
						state.SetState(state.StateError)
						return
					}
					time.Sleep(2 * time.Second)
				}

				_ = system.StopServer()
				if err := system.WaitForServerExit(20 * time.Second); err != nil {
					log.Printf("[http] vmid=%d forge stop wait error: %v", c.VMID, err)
					state.SetError(err)
					state.SetState(state.StateError)
					lifecycleLog(&c, "forge_post", 1, started, "stop_wait_failed err=%v", err)
					return
				}

				if err := minecraft.EnforceForgeServerProperties(c); err != nil {
					log.Printf("[http] vmid=%d forge post-start error: %v", c.VMID, err)
					state.SetError(err)
					state.SetState(state.StateError)
					lifecycleLog(&c, "forge_post", 1, started, "enforce_failed err=%v", err)
					return
				}

				state.SetState(state.StateStarting)
				state.SetReadyState(false, "", "")
				if err := system.StartServerReady(&c); err != nil {
					log.Printf("[http] vmid=%d restart error: %v", c.VMID, err)
					state.SetError(err)
					state.SetState(state.StateError)
					lifecycleLog(&c, "forge_post", 1, started, "restart_failed err=%v", err)
					return
				}
				lifecycleLog(&c, "forge_post", 1, started, "complete")
			}
		}

		log.Printf("[http] vmid=%d async provision+start complete", c.VMID)
	}(cfg)

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusAccepted)
	_, _ = w.Write([]byte(`{"ok": true, "state": "installing"}`))
}

/*
	--------------------------------------------------------------------------
	  /start

----------------------------------------------------------------------------
*/
func handleStart(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "POST only", http.StatusMethodNotAllowed)
		return
	}
	endOp, ok := beginHTTPOperation(w, "start", false, "starting server")
	if !ok {
		return
	}
	defer endOp()

	cfg, err := state.LoadConfig()
	if err != nil {
		http.Error(w, "no config: "+err.Error(), http.StatusBadRequest)
		return
	}

	if cfg.ContainerType == "dev" {
		http.Error(w, "dev containers do not support manual start", http.StatusBadRequest)
		return
	}
	log.Printf("[http] vmid=%d action=start status=requested", cfg.VMID)

	started := time.Now()
	state.SetState(state.StateStarting)
	state.SetReadyState(false, "", "")
	lifecycleLog(cfg, "start_manual", 1, started, "start_requested")
	if err := system.StartServerReady(cfg); err != nil {
		log.Printf("[http] vmid=%d action=start status=failed err=%v", cfg.VMID, err)
		http.Error(w, "start error: "+err.Error(), http.StatusInternalServerError)
		lifecycleLog(cfg, "start_manual", 1, started, "start_failed err=%v", err)
		return
	}
	log.Printf("[http] vmid=%d action=start status=ok", cfg.VMID)

	w.Header().Set("Content-Type", "application/json")
	_, _ = w.Write([]byte(`{"ok": true, "state": "running"}`))
}

/*
	--------------------------------------------------------------------------
	  /stop

----------------------------------------------------------------------------
*/
func handleStop(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "POST only", http.StatusMethodNotAllowed)
		return
	}
	endOp, ok := beginHTTPOperation(w, "stop", false, "stopping server")
	if !ok {
		return
	}
	defer endOp()

	if cfg, err := state.LoadConfig(); err == nil && cfg != nil {
		log.Printf("[http] vmid=%d action=stop status=requested", cfg.VMID)
	}
	if err := system.StopServer(); err != nil {
		http.Error(w, "stop error: "+err.Error(), http.StatusInternalServerError)
		return
	}
	if cfg, err := state.LoadConfig(); err == nil && cfg != nil {
		log.Printf("[http] vmid=%d action=stop status=ok", cfg.VMID)
	}
	w.WriteHeader(http.StatusNoContent)
}

/*
	--------------------------------------------------------------------------
	  /restart

----------------------------------------------------------------------------
*/
func handleRestart(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "POST only", http.StatusMethodNotAllowed)
		return
	}
	endOp, ok := beginHTTPOperation(w, "restart", false, "restarting server")
	if !ok {
		return
	}
	defer endOp()

	cfg, err := state.LoadConfig()
	if err != nil {
		http.Error(w, "no config", http.StatusBadRequest)
		return
	}

	if cfg.ContainerType == "dev" {
		http.Error(w, "dev containers do not support restart", http.StatusBadRequest)
		return
	}
	log.Printf("[http] vmid=%d action=restart status=requested", cfg.VMID)

	_ = system.StopServer()
	if err := system.WaitForServerExit(20 * time.Second); err != nil {
		log.Printf("[http] vmid=%d action=restart status=stop_wait_failed err=%v", cfg.VMID, err)
		http.Error(w, "restart error: "+err.Error(), http.StatusInternalServerError)
		return
	}

	started := time.Now()
	state.SetState(state.StateStarting)
	state.SetReadyState(false, "", "")
	if err := system.StartServerReady(cfg); err != nil {
		log.Printf("[http] vmid=%d action=restart status=start_failed err=%v", cfg.VMID, err)
		http.Error(w, "restart error: "+err.Error(), http.StatusInternalServerError)
		return
	}
	lifecycleLog(cfg, "restart_manual", 1, started, "ready")
	log.Printf("[http] vmid=%d action=restart status=ok", cfg.VMID)

	w.Header().Set("Content-Type", "application/json")
	_, _ = w.Write([]byte(`{"ok": true, "state": "running"}`))
}

/*
	--------------------------------------------------------------------------
	  /status

----------------------------------------------------------------------------
*/
func handleStatus(w http.ResponseWriter, r *http.Request) {
	cfg, _ := state.LoadConfig()
	_, processRunning := system.GetServerPID()
	readyAt := ""
	if t := state.GetLastReadyAt(); !t.IsZero() {
		readyAt = t.UTC().Format(time.RFC3339)
	}
	lastCrashTime := ""
	lastCrashExitCode := 0
	lastCrashSignal := 0
	lastCrashUptimeSeconds := int64(0)
	lastCrashClassification := ""
	var lastCrashLogTail []string
	if crash := state.GetLastCrash(); crash != nil {
		if !crash.Time.IsZero() {
			lastCrashTime = crash.Time.UTC().Format(time.RFC3339)
		}
		lastCrashExitCode = crash.ExitCode
		lastCrashSignal = crash.Signal
		lastCrashUptimeSeconds = crash.UptimeSeconds
		lastCrashClassification = crash.Classification
		lastCrashLogTail = crash.LogTail
	}

	workspaceRoot := ""
	serverRoot := ""
	runtimeInstallPath := ""
	runtimeInstalled := false
	devProvisioned := false
	devReadyAt := ""
	codeServerInstalled := false
	codeServerRunning := false
	if cfg != nil {
		if cfg.ContainerType == "dev" {
			workspaceRoot = agentfiles.RuntimeRoot(cfg)
			runtimeInstallPath = devcontainer.RuntimeInstallDir(cfg.Runtime, cfg.Version)
			runtimeInstalled = devcontainer.RuntimeInstalled(cfg.Runtime, cfg.Version)
			devProvisioned = devcontainer.IsProvisioned()
			if readyInfo, err := devcontainer.ReadReadyMarker(); err == nil && readyInfo != nil {
				devReadyAt = readyInfo.ReadyAt
			}
			if cfg.EnableCodeServer {
				codeServerInstalled = codeserver.Installed()
				codeServerRunning = codeserver.Running()
			}
		} else {
			serverRoot = provision.ServerDir(*cfg)
		}
	}
	op := state.GetOperation()

	resp := map[string]any{
		"state":                   state.GetState(),
		"processRunning":          processRunning,
		"ready":                   state.GetReady(),
		"readySource":             state.GetReadySource(),
		"readyError":              state.GetReadyError(),
		"lastReadyAt":             readyAt,
		"installStep":             state.GetInstallStep(),
		"crashCount":              state.GetCrashCount(),
		"lastCrashTime":           lastCrashTime,
		"lastCrashExitCode":       lastCrashExitCode,
		"lastCrashSignal":         lastCrashSignal,
		"lastCrashUptimeSeconds":  lastCrashUptimeSeconds,
		"lastCrashClassification": lastCrashClassification,
		"lastCrashLogTail":        lastCrashLogTail,
		"error":                   nil,
		"config":                  cfg,
		"workspaceRoot":           workspaceRoot,
		"serverRoot":              serverRoot,
		"runtimeInstallPath":      runtimeInstallPath,
		"runtimeInstalled":        runtimeInstalled,
		"devProvisioned":          devProvisioned,
		"devReadyAt":              devReadyAt,
		"codeServerInstalled":     codeServerInstalled,
		"codeServerRunning":       codeServerRunning,
		"operationInProgress":     op.InProgress,
		"operationType":           op.Type,
		"maintenance":             op.Maintenance,
		"operationStartedAt":      "",
		"operationMessage":        op.Message,
		"timestamp":               time.Now().Unix(),
	}
	if op.InProgress && !op.StartedAt.IsZero() {
		resp["operationStartedAt"] = op.StartedAt.UTC().Format(time.RFC3339)
	}

	if err := state.GetError(); err != nil {
		resp["error"] = err.Error()
	}

	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(resp)
}

func handleCodeServerStart(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "POST only", http.StatusMethodNotAllowed)
		return
	}

	cfg, err := requireDevContainer()
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	log.Printf("[http] vmid=%d action=codeserver_start status=requested", cfg.VMID)
	if err := codeserver.Start(*cfg); err != nil {
		log.Printf("[http] vmid=%d action=codeserver_start status=failed err=%v", cfg.VMID, err)
		http.Error(w, "code-server start failed: "+err.Error(), http.StatusInternalServerError)
		return
	}
	log.Printf("[http] vmid=%d action=codeserver_start status=ok", cfg.VMID)
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(map[string]any{"status": "ok", "running": codeserver.Running()})
}

func handleCodeServerStop(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "POST only", http.StatusMethodNotAllowed)
		return
	}

	cfg, err := requireDevContainer()
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	log.Printf("[http] vmid=%d action=codeserver_stop status=requested", cfg.VMID)
	if err := codeserver.Stop(); err != nil {
		log.Printf("[http] vmid=%d action=codeserver_stop status=failed err=%v", cfg.VMID, err)
		http.Error(w, "code-server stop failed: "+err.Error(), http.StatusInternalServerError)
		return
	}
	log.Printf("[http] vmid=%d action=codeserver_stop status=ok", cfg.VMID)
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(map[string]any{"status": "ok", "running": codeserver.Running()})
}

func handleCodeServerRestart(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "POST only", http.StatusMethodNotAllowed)
		return
	}

	cfg, err := requireDevContainer()
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	log.Printf("[http] vmid=%d action=codeserver_restart status=requested", cfg.VMID)
	if err := codeserver.Restart(*cfg); err != nil {
		log.Printf("[http] vmid=%d action=codeserver_restart status=failed err=%v", cfg.VMID, err)
		http.Error(w, "code-server restart failed: "+err.Error(), http.StatusInternalServerError)
		return
	}
	log.Printf("[http] vmid=%d action=codeserver_restart status=ok", cfg.VMID)
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(map[string]any{"status": "ok", "running": codeserver.Running()})
}

/*
	--------------------------------------------------------------------------
	  /console/command

----------------------------------------------------------------------------
*/
func handleSendCommand(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "POST only", http.StatusMethodNotAllowed)
		return
	}
	cmd := ""
	contentType := r.Header.Get("Content-Type")
	if strings.Contains(contentType, "application/json") {
		var req struct {
			Command string `json:"command"`
			Cmd     string `json:"cmd"`
		}
		if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4096)).Decode(&req); err != nil {
			http.Error(w, "bad json", http.StatusBadRequest)
			return
		}
		cmd = req.Command
		if cmd == "" {
			cmd = req.Cmd
		}
	} else {
		if err := r.ParseForm(); err != nil {
			http.Error(w, "bad form", http.StatusBadRequest)
			return
		}
		cmd = r.Form.Get("command")
		if cmd == "" {
			cmd = r.Form.Get("cmd")
		}
	}
	if cmd == "" {
		cmd = r.URL.Query().Get("cmd")
	}
	if cmd == "" {
		http.Error(w, "cmd required", http.StatusBadRequest)
		return
	}
	if err := validateConsoleCommand(cmd); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	if err := system.SendConsoleCommand(cmd); err != nil {
		http.Error(w, "command error: "+err.Error(), http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

func validateConsoleCommand(cmd string) error {
	if len(cmd) > 512 {
		return fmt.Errorf("command exceeds 512 byte limit")
	}
	for _, r := range cmd {
		if r < 32 || r == 127 {
			return fmt.Errorf("command contains control characters")
		}
	}
	return nil
}

func handleReady(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		http.Error(w, "GET only", http.StatusMethodNotAllowed)
		return
	}

	cfg, err := state.LoadConfig()
	if err != nil || cfg == nil {
		http.Error(w, "not configured", http.StatusServiceUnavailable)
		return
	}
	op := state.GetOperation()
	if op.InProgress {
		http.Error(w, "operation in progress: "+op.Type, http.StatusServiceUnavailable)
		return
	}
	switch state.GetState() {
	case state.StateInstalling, state.StateStarting, state.StateStopping, state.StateError, state.StateCrashed:
		http.Error(w, "not ready: "+string(state.GetState()), http.StatusServiceUnavailable)
		return
	}
	if strings.EqualFold(cfg.ContainerType, "game") {
		_, running := system.GetServerPID()
		if !running {
			http.Error(w, "server process not running", http.StatusServiceUnavailable)
			return
		}
		if strings.EqualFold(cfg.Game, "minecraft") && !state.GetReady() {
			http.Error(w, "minecraft not ready", http.StatusServiceUnavailable)
			return
		}
	}
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(map[string]any{"ready": true})
}

/* --------------------------------------------------------------------------
   /agent/update
----------------------------------------------------------------------------*/

func handleAgentUpdate(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "POST only", http.StatusMethodNotAllowed)
		return
	}
	endOp, ok := beginHTTPOperation(w, "agent_update", true, "updating agent")
	if !ok {
		return
	}
	defer endOp()

	res := update.CheckAndUpdate(version.AgentVersion)
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(res)
}

/* --------------------------------------------------------------------------
   /agent/update/status
----------------------------------------------------------------------------*/

func handleAgentUpdateStatus(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		http.Error(w, "GET only", http.StatusMethodNotAllowed)
		return
	}

	res := update.ReadStatus()
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(res)
}

/* --------------------------------------------------------------------------
   /version
----------------------------------------------------------------------------*/

func handleVersion(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		http.Error(w, "GET only", http.StatusMethodNotAllowed)
		return
	}
	resp := map[string]any{
		"version": version.AgentVersion,
	}
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(resp)
}

/* --------------------------------------------------------------------------
   /game/players
----------------------------------------------------------------------------*/

func handleGamePlayers(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		http.Error(w, "GET only", http.StatusMethodNotAllowed)
		return
	}

	cfg, err := state.LoadConfig()
	if err != nil {
		http.Error(w, "no config loaded", http.StatusBadRequest)
		return
	}
	if strings.ToLower(cfg.ContainerType) != "game" {
		http.Error(w, "not a game container", http.StatusBadRequest)
		return
	}
	if strings.ToLower(cfg.Game) != "minecraft" {
		http.Error(w, "unsupported game", http.StatusNotImplemented)
		return
	}

	ports := make([]int, 0, 3)
	propsPath := filepath.Join(provision.ServerDir(*cfg), "server.properties")
	if b, err := os.ReadFile(propsPath); err == nil {
		lines := strings.Split(string(b), "\n")
		for _, l := range lines {
			if strings.HasPrefix(l, "server-port=") {
				if p, err := strconv.Atoi(strings.TrimPrefix(l, "server-port=")); err == nil && p > 0 {
					ports = append(ports, p)
				}
				break
			}
		}
	}
	if len(cfg.Ports) > 0 && cfg.Ports[0] > 0 {
		ports = append(ports, cfg.Ports[0])
	}
	ports = append(ports, 25565)

	seenPorts := make(map[int]struct{}, len(ports))
	uniqPorts := make([]int, 0, len(ports))
	for _, p := range ports {
		if _, ok := seenPorts[p]; ok {
			continue
		}
		seenPorts[p] = struct{}{}
		uniqPorts = append(uniqPorts, p)
	}

	protocols := []int{mcstatus.ProtocolForVersion(cfg.Version), 767, 765, 763, 762, 754}
	seenProtocols := make(map[int]struct{}, len(protocols))
	uniqProtocols := make([]int, 0, len(protocols))
	for _, pr := range protocols {
		if _, ok := seenProtocols[pr]; ok {
			continue
		}
		seenProtocols[pr] = struct{}{}
		uniqProtocols = append(uniqProtocols, pr)
	}

	var status mcstatus.StatusResponse
	var lastErr error
	for _, port := range uniqPorts {
		for _, protocol := range uniqProtocols {
			s, err := mcstatus.QueryStatus("127.0.0.1", port, protocol)
			if err != nil {
				lastErr = err
				continue
			}
			status = s
			lastErr = nil
			break
		}
		if lastErr == nil {
			break
		}
	}
	if lastErr != nil {
		http.Error(w, "status query failed: "+lastErr.Error(), http.StatusBadGateway)
		return
	}

	players := make([]string, 0, len(status.Players.Sample))
	for _, p := range status.Players.Sample {
		if p.Name != "" {
			players = append(players, p.Name)
		}
	}

	resp := map[string]any{
		"online":  status.Players.Online,
		"max":     status.Players.Max,
		"players": players,
	}
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(resp)
}

/*
	--------------------------------------------------------------------------
	  Router

----------------------------------------------------------------------------
*/
func NewMux() *http.ServeMux {
	m := http.NewServeMux()

	m.HandleFunc("/config", handleConfig)
	m.HandleFunc("/start", handleStart)
	m.HandleFunc("/stop", handleStop)
	m.HandleFunc("/restart", handleRestart)
	m.HandleFunc("/status", handleStatus)
	m.HandleFunc("/ready", handleReady)
	m.HandleFunc("/dev/codeserver/start", handleCodeServerStart)
	m.HandleFunc("/dev/codeserver/stop", handleCodeServerStop)
	m.HandleFunc("/dev/codeserver/restart", handleCodeServerRestart)
	m.HandleFunc("/console/command", handleSendCommand)
	m.HandleFunc("/agent/update", handleAgentUpdate)
	m.HandleFunc("/agent/update/status", handleAgentUpdateStatus)
	m.HandleFunc("/version", handleVersion)
	m.HandleFunc("/game/players", handleGamePlayers)
	m.HandleFunc("/game/backups", agenthandlers.HandleGameBackups)
	m.HandleFunc("/game/backups/restore", agenthandlers.HandleGameBackupRestore)
	m.HandleFunc("/game/backups/", agenthandlers.HandleGameBackupByID)
	m.HandleFunc("/game/mods", agenthandlers.HandleGameMods)
	m.HandleFunc("/game/mods/install", agenthandlers.HandleGameModsInstall)
	m.HandleFunc("/game/mods/", agenthandlers.HandleGameModByID)
	m.HandleFunc("/game/files/list", agenthandlers.HandleGameFilesList)
	m.HandleFunc("/game/files", agenthandlers.HandleGameFilesRoot)
	m.HandleFunc("/game/files/revert", agenthandlers.HandleGameFilesRevert)
	m.HandleFunc("/game/files/upload", agenthandlers.HandleGameFilesUpload)
	m.HandleFunc("/game/files/stat", agenthandlers.HandleGameFilesStat)
	m.HandleFunc("/game/files/read", agenthandlers.HandleGameFilesRead)
	m.HandleFunc("/game/files/download", agenthandlers.HandleGameFilesDownload)
	m.HandleFunc("/metrics/process", agenthandlers.HandleProcessMetrics)

	registerWebSocket(m)

	m.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("ok"))
	})

	log.Println("[agent] routes registered")
	return m
}
