package dispatch

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"time"

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

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

// seedModsFromInstance clones the Mods/ folder of srcInstanceID (the cluster
// master) onto dstInstanceID's install volume, REPLACING the destination's mod
// set with the master's. Runs a throwaway debian sidecar that mounts both
// instances' install volumes and tar-copies /src-game/Mods → /dst-game/Mods.
//
// This is the inverse of tryWarmSeedFromSibling (which copies the install but
// EXCLUDES Mods): here we copy ONLY Mods, so a freshly-joined cluster member
// ends up running exactly the master's mod set without an operator re-uploading
// every mod by hand.
//
// Per-server STATE is preserved/excluded so a clone never leaks one server's
// player data onto another:
//   - The master's RefugeBot homes.json (teleport homes, keyed by SteamID) is
//     EXCLUDED from the copy — it's per-server state, not mod config. This is
//     the exact file that leaked when an operator rsync'd Mods by hand.
//   - The destination's OWN homes.json is snapshotted before the wipe and
//     restored after, so the joining server keeps its own teleports.
//
// Both instances must live on THIS agent (same Docker host) — the sidecar
// mounts local named volumes. If the master isn't found here the seed returns
// an error and the caller treats it as best-effort (the server still boots with
// whatever mods it already had).
func (d *Dispatcher) seedModsFromInstance(ctx context.Context, srcInstanceID, dstInstanceID, dstDataPath string, manifest *modulepkg.Manifest) (int, error) {
	if manifest == nil {
		return 0, fmt.Errorf("mod-seed: nil manifest")
	}
	if srcInstanceID == dstInstanceID {
		return 0, fmt.Errorf("mod-seed: refusing to seed instance %q from itself", dstInstanceID)
	}
	installPath := ""
	if len(manifest.UpdateProviders) > 0 {
		installPath = manifest.UpdateProviders[0].InstallPath
	}
	if installPath == "" {
		installPath = manifest.Runtime.Docker.BrowseableRoot
	}
	if installPath == "" {
		return 0, fmt.Errorf("mod-seed: no install path for module %q", manifest.ID)
	}

	// Resolve the master's data path from our local instance table.
	d.mu.Lock()
	srcRec, ok := d.instances[srcInstanceID]
	d.mu.Unlock()
	if !ok {
		return 0, fmt.Errorf("mod-seed: master instance %q is not on this agent", srcInstanceID)
	}
	if srcRec.ModuleID != manifest.ID {
		return 0, fmt.Errorf("mod-seed: master %q is module %q, not %q", srcInstanceID, srcRec.ModuleID, manifest.ID)
	}

	srcVolumes := agentmodule.ResolveVolumes(manifest, srcInstanceID, srcRec.DataPath)
	dstVolumes := agentmodule.ResolveVolumes(manifest, dstInstanceID, dstDataPath)
	srcGameVol := volumeNameForPath(srcVolumes, installPath)
	dstGameVol := volumeNameForPath(dstVolumes, installPath)
	if srcGameVol == "" || dstGameVol == "" {
		return 0, fmt.Errorf("mod-seed: install_path %q not a named volume on both instances", installPath)
	}

	// tar, not rsync — debian:12-slim ships no rsync. Replace /dst-game/Mods
	// from /src-game/Mods, excluding the master's per-server homes.json, and
	// preserving the destination's own homes.json across the wipe.
	const seedScript = `set -e
echo "mod-seed: cloning master Mods set (excl per-server homes.json)"
HOMES=/dst-game/Mods/zzzzzzzRefugeBot/homes.json
if [ -f "$HOMES" ]; then cp -a "$HOMES" /tmp/dst-homes.json; echo "mod-seed: preserved destination homes.json"; fi
mkdir -p /dst-game/Mods
find /dst-game/Mods -mindepth 1 -delete
( cd /src-game/Mods && tar --exclude='./zzzzzzzRefugeBot/homes.json' -cf - . ) | ( cd /dst-game/Mods && tar -xf - )
if [ -f /tmp/dst-homes.json ]; then mkdir -p /dst-game/Mods/zzzzzzzRefugeBot; cp -a /tmp/dst-homes.json "$HOMES"; echo "mod-seed: restored destination homes.json"; fi
echo "mod-seed: $(ls /dst-game/Mods 2>/dev/null | wc -l) mods now present"
echo SEED_OK
`

	sidecarID := fmt.Sprintf("%s-modseed", dstInstanceID)
	spec := runtime.InstanceSpec{
		InstanceID:    sidecarID,
		IsSidecar:     true,
		Image:         "debian:12-slim",
		Command:       []string{"bash", "-c", seedScript},
		RestartPolicy: "no",
		Volumes: []runtime.VolumeSpec{
			{Type: "volume", VolumeName: srcGameVol, ContainerPath: "/src-game", ReadOnly: true},
			{Type: "volume", VolumeName: dstGameVol, ContainerPath: "/dst-game"},
		},
	}

	d.log.Info("mod-seed: starting sidecar", "src", srcInstanceID, "dst", dstInstanceID, "src_vol", srcGameVol, "dst_vol", dstGameVol)
	contID, err := d.runtime.Create(ctx, spec)
	if err != nil {
		return 0, fmt.Errorf("mod-seed: create sidecar: %w", err)
	}
	defer func() {
		rmCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
		defer cancel()
		_ = d.runtime.Remove(rmCtx, contID)
	}()
	if err := d.runtime.Start(ctx, contID); err != nil {
		return 0, fmt.Errorf("mod-seed: start sidecar: %w", err)
	}
	logCtx, cancelLogs := context.WithCancel(ctx)
	defer cancelLogs()
	logDone := make(chan struct{})
	var modCount int
	go func() {
		defer close(logDone)
		_ = d.runtime.StreamLogs(logCtx, contID, func(_ runtime.LogStream, line string, _ time.Time) {
			d.log.Info("mod-seed log", "instance_id", dstInstanceID, "line", line)
			// Parse the sidecar's "mod-seed: N mods now present" echo so the
			// caller (and the cluster-sync UI) can report how many mods landed.
			if strings.Contains(line, "mods now present") {
				fields := strings.Fields(line)
				for i, f := range fields {
					if f == "mods" && i > 0 {
						if n, err := strconv.Atoi(fields[i-1]); err == nil {
							modCount = n
						}
					}
				}
			}
		})
	}()
	exitCode, err := d.runtime.Wait(ctx, contID)
	cancelLogs()
	<-logDone
	if err != nil {
		return 0, fmt.Errorf("mod-seed: wait sidecar: %w", err)
	}
	if exitCode != 0 {
		return 0, fmt.Errorf("mod-seed: sidecar exited %d", exitCode)
	}
	d.log.Info("mod-seed: success", "src", srcInstanceID, "dst", dstInstanceID, "mods", modCount)
	return modCount, nil
}

// handleSeedMods runs the mod-seed sidecar on demand — no container recreate,
// no restart. Clones src_instance_id's Mods/ onto instance_id (excluding the
// master's per-server homes.json, preserving the member's own). The member
// keeps running on its currently-loaded mods; the freshly-copied files take
// effect on its NEXT restart. Backs the cluster "Sync mods to members" action,
// where the master pushes its current mod set to every other member at once.
func (d *Dispatcher) handleSeedMods(ctx context.Context, corrID string, req *panelv1.SeedModsRequest) {
	log := d.log.With("instance_id", req.InstanceId, "src", req.SrcInstanceId, "correlation_id", corrID)
	reply := func(ok bool, modCount int, errMsg string) {
		d.sendEnv(&panelv1.AgentEnvelope{
			CorrelationId: corrID,
			SentAt:        timestamppb.Now(),
			Payload: &panelv1.AgentEnvelope_SeedModsResult{
				SeedModsResult: &panelv1.SeedModsResult{
					InstanceId: req.InstanceId,
					Ok:         ok,
					ModCount:   int32(modCount),
					Error:      errMsg,
				},
			},
		})
		if !ok {
			log.Warn("mod-seed RPC failed", "err", errMsg)
		}
	}
	rec, err := d.lookupRecord(req.InstanceId)
	if err != nil {
		reply(false, 0, err.Error())
		return
	}
	manifest, ok := d.modules.Get(rec.ModuleID)
	if !ok {
		reply(false, 0, "module "+rec.ModuleID+" not in registry")
		return
	}
	modCount, err := d.seedModsFromInstance(ctx, req.SrcInstanceId, req.InstanceId, rec.DataPath, manifest)
	if err != nil {
		reply(false, 0, err.Error())
		return
	}
	log.Info("mod-seed RPC complete", "mods", modCount)
	reply(true, modCount, "")
}
