package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/dbledeez/panel/controller/internal/db"
	panelv1 "github.com/dbledeez/panel/proto/panel/v1"
	"google.golang.org/protobuf/types/known/timestamppb"
)

// POST /api/instances/{id}/config-render — the DURABLE, no-recreate config
// save. Counterpart to /env-config (which recreates the container): this path
// merges the updates into the DB row's config_values, then asks the agent to
// re-render the module's config_file templates into the instance DataPath.
// For 7dtd that rewrites the bind-mounted serverconfig.xml.rendered in place,
// so the entrypoint applies the new values on the instance's NEXT boot — no
// stop, no recreate, no player disconnect.
//
// Body shape mirrors /env-config: {"updates": {"server_name": "..."}}.
// "" clears a key (template `or` default takes over on the next render).

type renderConfigReq struct {
	Updates map[string]string `json:"updates"`
}

func (h *httpServer) renderConfigDurable(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	if id == "" {
		writeError(w, http.StatusBadRequest, "missing_id", "instance id required")
		return
	}
	var req renderConfigReq
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeError(w, http.StatusBadRequest, "bad_json", err.Error())
		return
	}
	if len(req.Updates) == 0 {
		writeError(w, http.StatusBadRequest, "no_updates", "updates map is empty")
		return
	}

	row, err := h.db.GetInstanceRow(r.Context(), id)
	if err != nil {
		writeError(w, http.StatusNotFound, "not_found", err.Error())
		return
	}
	conn := h.registry.get(row.AgentID)
	if conn == nil {
		writeError(w, http.StatusServiceUnavailable, "agent_offline", fmt.Sprintf("agent %q is not connected", row.AgentID))
		return
	}

	// Same audit shape as env-config: log keys, hide values that could be
	// passwords, never accept one-shot _PANEL_* sentinels on this path
	// (they only mean something to a recreate).
	actorEmail := "anonymous"
	if u, ok := r.Context().Value(sessionUserKey{}).(*db.UserRow); ok && u != nil {
		actorEmail = u.Email
	}
	auditKeys := make([]string, 0, len(req.Updates))
	merged := map[string]string{}
	for k, v := range row.ConfigValues {
		merged[k] = v
	}
	for k, v := range req.Updates {
		if strings.HasPrefix(k, "_PANEL_") {
			writeError(w, http.StatusBadRequest, "bad_key", k+" is a recreate-only sentinel; not valid on the durable path")
			return
		}
		if v == "" {
			delete(merged, k)
			auditKeys = append(auditKeys, k+"=<cleared>")
		} else {
			merged[k] = v
			auditKeys = append(auditKeys, k+"=<set>")
		}
	}
	h.log.Info("durable config render requested",
		"instance_id", id, "actor_email", actorEmail,
		"remote_addr", r.RemoteAddr, "updates", auditKeys)

	// Persist FIRST: the operator's intent is durable even if the render
	// below fails (agent restart mid-flight, etc.) — any later recreate or
	// successful re-render applies these values.
	if err := h.db.UpdateInstanceConfigValues(r.Context(), id, merged); err != nil {
		writeError(w, http.StatusInternalServerError, "db", err.Error())
		return
	}

	corrID := newCorrelationID("render-config")
	env := &panelv1.ControllerEnvelope{
		CorrelationId: corrID, SentAt: timestamppb.Now(),
		Payload: &panelv1.ControllerEnvelope_InstanceRenderConfig{
			InstanceRenderConfig: &panelv1.InstanceRenderConfigRequest{
				InstanceId:   id,
				ConfigValues: merged,
			},
		},
	}
	if err := conn.Send(env); err != nil {
		writeError(w, http.StatusBadGateway, "send_failed", "values saved, but the agent render failed to dispatch: "+err.Error())
		return
	}
	reply, err := h.pending.Await(r.Context(), corrID, 30*time.Second)
	if err != nil {
		writeError(w, http.StatusBadGateway, "render_timeout", "values saved, but the agent did not confirm the render: "+err.Error())
		return
	}
	res := reply.GetInstanceRenderConfigResult()
	if res == nil {
		writeError(w, http.StatusBadGateway, "bad_reply", "agent reply missing instance_render_config_result")
		return
	}
	if res.Error != "" {
		writeError(w, http.StatusBadGateway, "render_failed", "values saved, but the render failed: "+res.Error)
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"ok":             true,
		"rendered_files": res.RenderedFiles,
		"applies":        "next restart",
	})
}
