package main

// Empyrion fresh-start: a one-shot action that announces a countdown,
// saves the world, optionally wipes playfields/POIs, and optionally
// restarts the server. Exposes a job-tracking endpoint so the UI can
// poll progress.
//
// Also registered as Action.Type "empyrion-fresh-start" in the scheduler
// so operators can schedule recurring fresh starts.

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"net/http"
	"sync"
	"time"
)

// freshStartJob tracks the progress of a single fresh-start operation.
type freshStartJob struct {
	ID                  string    `json:"id"`
	InstanceID          string    `json:"instance_id"`
	AnnouncementMinutes int       `json:"announcement_minutes"`
	WipeScope           string    `json:"wipe_scope"`
	Playfield           string    `json:"playfield,omitempty"`
	Restart             bool      `json:"restart"`
	CreatedAt           time.Time `json:"created_at"`
	Phase               string    `json:"phase"` // announcing | saving | wiping | restarting | done | failed
	Error               string    `json:"error,omitempty"`
	Done                bool      `json:"done"`
}

type freshStartManager struct {
	mu   sync.Mutex
	jobs map[string]*freshStartJob
}

func newFreshStartManager() *freshStartManager {
	return &freshStartManager{jobs: map[string]*freshStartJob{}}
}

func (m *freshStartManager) add(job *freshStartJob) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.jobs[job.ID] = job
}

func (m *freshStartManager) get(id string) *freshStartJob {
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.jobs[id]
}

func (m *freshStartManager) remove(id string) {
	m.mu.Lock()
	defer m.mu.Unlock()
	delete(m.jobs, id)
}

// freshStartReq is the JSON body for POST /api/instances/{id}/empyrion/fresh-start.
type freshStartReq struct {
	AnnouncementMinutes int    `json:"announcement_minutes"`
	WipeScope           string `json:"wipe_scope"`
	Playfield           string `json:"playfield,omitempty"`
	Restart             bool   `json:"restart"`
}

// POST /api/instances/{id}/empyrion/fresh-start
func (h *httpServer) handleEmpyrionFreshStart(w http.ResponseWriter, r *http.Request) {
	instanceID := r.PathValue("id")
	var req freshStartReq
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "bad_json", err.Error())
		return
	}
	if req.AnnouncementMinutes <= 0 {
		req.AnnouncementMinutes = 10
	}
	if req.WipeScope == "" {
		req.WipeScope = "poi"
	}

	jobID := newFreshStartJobID()
	job := &freshStartJob{
		ID:                  jobID,
		InstanceID:          instanceID,
		AnnouncementMinutes: req.AnnouncementMinutes,
		WipeScope:           req.WipeScope,
		Playfield:           req.Playfield,
		Restart:             req.Restart,
		CreatedAt:           time.Now(),
		Phase:               "announcing",
	}
	h.freshStartJobs.add(job)

	// Spawn background goroutine.
	go h.runFreshStart(r.Context(), job, req)

	writeJSON(w, http.StatusAccepted, map[string]any{"job_id": jobID})
}

// GET /api/instances/{id}/empyrion/fresh-start/jobs/{jobId}
func (h *httpServer) handleEmpyrionFreshStartJob(w http.ResponseWriter, r *http.Request) {
	instanceID := r.PathValue("id")
	jobID := r.PathValue("jobId")
	job := h.freshStartJobs.get(jobID)
	if job == nil || job.InstanceID != instanceID {
		writeError(w, http.StatusNotFound, "not_found", "job not found")
		return
	}
	writeJSON(w, http.StatusOK, job)
}

func (h *httpServer) runFreshStart(ctx context.Context, job *freshStartJob, req freshStartReq) {
	log := h.log.With("job_id", job.ID, "instance_id", job.InstanceID)

	// Resolve bridge target once.
	tgt, err := h.resolveEmpyrionTarget(context.Background(), job.InstanceID)
	if err != nil {
		job.Phase = "failed"
		job.Error = fmt.Sprintf("resolve bridge: %v", err)
		job.Done = true
		return
	}

	// Phase 1: Announce countdown.
	if req.AnnouncementMinutes > 0 {
		job.Phase = "announcing"
		for remaining := req.AnnouncementMinutes; remaining > 0; remaining-- {
			msg := fmt.Sprintf("Server fresh start in %d minute(s)", remaining)
			postBridgeJSON(context.Background(), tgt, "/api/v1/chat/broadcast", []byte(msg))
			select {
			case <-time.After(1 * time.Minute):
			case <-ctx.Done():
				job.Phase = "failed"
				job.Error = "cancelled during announcement"
				job.Done = true
				return
			}
		}
		// Final announcement.
		postBridgeJSON(context.Background(), tgt, "/api/v1/chat/broadcast", []byte("Server fresh start NOW"))
	}

	// Phase 2: Save.
	job.Phase = "saving"
	ok, msg := postBridgeJSON(context.Background(), tgt, "/api/v1/save", nil)
	if !ok {
		log.Warn("fresh-start save failed", "msg", msg)
		// Non-fatal: continue with wipe/restart anyway.
	}

	// Phase 3: Wipe.
	job.Phase = "wiping"
	if req.Playfield != "" {
		path := fmt.Sprintf("/api/v1/playfields/%s/wipe?what=%s", req.Playfield, req.WipeScope)
		postBridgeJSON(context.Background(), tgt, path, nil)
	} else {
		cmd := fmt.Sprintf("wipe all %s", req.WipeScope)
		postBridgeBody(context.Background(), tgt, "/api/v1/console", cmd)
	}

	// Phase 4: Restart.
	if req.Restart {
		job.Phase = "restarting"
		// Use the existing controller restart endpoint via HTTP call to self.
		restartURL := fmt.Sprintf("http://localhost%s/api/instances/%s/restart", h.httpAddr, job.InstanceID)
		restartReq, _ := http.NewRequestWithContext(context.Background(), http.MethodPost, restartURL, nil)
		restartResp, rerr := (&http.Client{Timeout: 30 * time.Second}).Do(restartReq)
		if rerr != nil {
			job.Phase = "failed"
			job.Error = fmt.Sprintf("restart call failed: %v", rerr)
			job.Done = true
			return
		}
		restartResp.Body.Close()
	}

	job.Phase = "done"
	job.Done = true
	log.Info("fresh-start completed")
}

// newFreshStartJobID generates a random job id.
func newFreshStartJobID() string {
	b := make([]byte, 8)
	_, _ = rand.Read(b)
	return "fs_" + hex.EncodeToString(b)
}

// ---- Scheduler integration ----

// fireEmpyrionFreshStart is called by the scheduler executor for
// Action.Type == "empyrion-fresh-start". It spawns the same goroutine
// the HTTP endpoint uses and returns immediately.
func fireEmpyrionFreshStart(ctx context.Context, h *httpServer, instanceID string, req freshStartReq) (string, error) {
	jobID := newFreshStartJobID()
	job := &freshStartJob{
		ID:                  jobID,
		InstanceID:          instanceID,
		AnnouncementMinutes: req.AnnouncementMinutes,
		WipeScope:           req.WipeScope,
		Playfield:           req.Playfield,
		Restart:             req.Restart,
		CreatedAt:           time.Now(),
		Phase:               "announcing",
	}
	h.freshStartJobs.add(job)
	go h.runFreshStart(ctx, job, req)
	return jobID, nil
}

// parseFreshStartAction decodes a scheduler Action into a freshStartReq.
func parseFreshStartAction(act Action) freshStartReq {
	req := freshStartReq{
		AnnouncementMinutes: 10,
		WipeScope:           "poi",
		Restart:             true,
	}
	// The action body is in act.Body (raw JSON string) for empyrion actions.
	if act.Body != "" {
		_ = json.Unmarshal([]byte(act.Body), &req)
	}
	return req
}
