package dispatch

import (
	"bytes"
	"context"
	"os"
	"path/filepath"

	"google.golang.org/protobuf/types/known/timestamppb"

	agentmodule "github.com/dbledeez/panel/agent/internal/module"
	modulepkg "github.com/dbledeez/panel/pkg/module"
	panelv1 "github.com/dbledeez/panel/proto/panel/v1"
)

// handleRenderConfig re-renders the module's declared config_files into the
// instance's host DataPath — the same modulepkg.Render call handleCreate
// makes, minus the container recreate. For modules that bind-mount the
// rendered output into the container (7dtd's serverconfig.xml.rendered),
// os.WriteFile rewrites the host file in place (same inode), so the running
// container sees the new content immediately and the entrypoint applies it
// on the NEXT boot. The container itself is never restarted.
//
// Containers created BEFORE a config_file's bind-mount was added to the
// module manifest don't have the bind at all (docker pins mounts at create)
// — their /game-saves/<file> is a plain file inside the saves volume. For
// those we self-verify: read the file back through the container, and if it
// doesn't match the fresh render, copy the rendered bytes in directly
// (works running or stopped; the game only reads config at boot).
func (d *Dispatcher) handleRenderConfig(ctx context.Context, corrID string, req *panelv1.InstanceRenderConfigRequest) {
	log := d.log.With("instance_id", req.InstanceId, "correlation_id", corrID)
	fail := func(msg string) {
		d.sendEnv(&panelv1.AgentEnvelope{
			CorrelationId: corrID,
			SentAt:        timestamppb.Now(),
			Payload: &panelv1.AgentEnvelope_InstanceRenderConfigResult{
				InstanceRenderConfigResult: &panelv1.InstanceRenderConfigResult{Error: msg},
			},
		})
		log.Warn("render config failed", "err", msg)
	}

	rec, err := d.lookupRecord(req.InstanceId)
	if err != nil {
		fail(err.Error())
		return
	}
	if rec.DataPath == "" {
		fail("instance has no data path (pre-feature sidecar?) — recreate once to adopt")
		return
	}
	manifest, ok := d.modules.Get(rec.ModuleID)
	if !ok {
		fail("module " + rec.ModuleID + " not in registry")
		return
	}

	// Same merge handleCreate uses: generated secrets first, user values on
	// top (empties dropped → template `or` defaults kick in).
	values, err := mergeValuesAndSecrets(manifest, req.ConfigValues)
	if err != nil {
		fail("secrets: " + err.Error())
		return
	}
	// Branch-aware: a 3.0 instance renders serverconfig.v3.xml.tmpl, ≤2.6
	// keeps the default. Resolve from the instance's persisted branch (set at
	// create), falling back to the merged config_values' provider_id so a
	// durable config-render still picks the right template if the record
	// predates branch persistence.
	branch := rec.Branch
	if branch == "" {
		branch = resolveCreateBranch(manifest, values)
	}
	if err := modulepkg.RenderForBranch(manifest, rec.DataPath, values, branch); err != nil {
		fail("render: " + err.Error())
		return
	}

	// Self-verify through the container; fall back to a direct copy into
	// the volume for pre-bind containers. The mapping host-path → container
	// path comes from the manifest's volume specs.
	volumes := agentmodule.ResolveVolumes(manifest, rec.InstanceID, rec.DataPath)
	container := "panel-" + rec.InstanceID
	var rendered []string
	for _, cf := range manifest.ConfigFiles {
		if cf.TemplateForBranch(branch) == "" {
			continue
		}
		rendered = append(rendered, cf.Path)
		hostPath := filepath.Join(rec.DataPath, cf.Path)
		ctrPath := ""
		for _, v := range volumes {
			if v.Type == "bind" && v.HostPath == hostPath {
				ctrPath = v.ContainerPath
				break
			}
		}
		if ctrPath == "" {
			continue // file isn't mounted into the container; host render is all there is
		}
		want, rerr := os.ReadFile(hostPath)
		if rerr != nil {
			fail("read rendered " + cf.Path + ": " + rerr.Error())
			return
		}
		got, gerr := d.runtime.CopyFileFromContainer(ctx, container, ctrPath)
		if gerr == nil && bytes.Equal(got, want) {
			continue // live bind-mount delivered the new render
		}
		// Pre-bind container (or unreadable): write the volume copy directly.
		if cerr := d.runtime.CopyFileToContainer(ctx, container, ctrPath, want); cerr != nil {
			fail("container has no live bind for " + cf.Path + " and direct copy failed: " + cerr.Error())
			return
		}
		log.Info("rendered config copied into pre-bind container volume", "file", cf.Path, "container_path", ctrPath)
	}
	log.Info("re-rendered config files (durable, applies next boot)", "files", rendered)
	d.sendEnv(&panelv1.AgentEnvelope{
		CorrelationId: corrID,
		SentAt:        timestamppb.Now(),
		Payload: &panelv1.AgentEnvelope_InstanceRenderConfigResult{
			InstanceRenderConfigResult: &panelv1.InstanceRenderConfigResult{RenderedFiles: rendered},
		},
	})
}
