package dispatch

import (
	"context"
	"errors"
	"fmt"
	"time"

	agentmodule "github.com/dbledeez/panel/agent/internal/module"
	"github.com/dbledeez/panel/agent/internal/runtime"
	"github.com/dbledeez/panel/agent/internal/updater"
	modulepkg "github.com/dbledeez/panel/pkg/module"
)

// installBaseMods runs a SteamCMD validate (if the module has a steamcmd
// update provider) followed by an alpine sidecar that downloads + installs
// the third-party 7DTD base-mod set:
//
//   - Allocs Server Fixes — tarball from illy.bz (canonical upstream).
//     Contains Allocs_CommandExtensions + Allocs_WebAndMapRendering +
//     Alloc_XmlPatch, all under a Mods/ prefix when extracted to /game.
//   - PrismaCore — latest GitHub release .zip from Prisma501/PrismaCore.
//
// SteamCMD validate step (new):
//   - If the module declares at least one update provider with kind="steamcmd",
//     the first such provider is used to run `app_update <app_id> validate`
//     with anonymous login BEFORE the alpine sidecar runs. This re-downloads
//     or restores stock TFP mods (TFP_*, 0_TFP_*, xMarkers) that ship with
//     the dedicated server install.
//   - If the SteamCMD validate fails, the function returns an error and does
//     NOT proceed to the alpine step.
//   - If there is no steamcmd provider (e.g., Conan Exiles), the validate is
//     skipped and the function goes straight to the alpine path.
//
// What the alpine step does NOT touch (and does not need to):
//   - TFP_* / 0_TFP_* — shipped by SteamCMD as part of the dedicated
//     server install. TFP_Harmony is the runtime patcher; wiping it
//     would brick the engine. Always present in /game/Mods/ after a
//     fresh `app_update 294420`.
//   - xMarkers — also ships with the SteamCMD 7DTD install per the
//     operator (2026-05-04). Already in /game/Mods/ after install.
//   - Custom mods (RefugeBot, AGF-VP-*, AsylumRoboticInbox, anything not
//     in the recognized base-mod name set). Those are operator territory.
//
// Behavior on existing installs: any directory with a name that matches
// the incoming mod gets renamed to `<name>.bak-<UTCstamp>` first so a
// re-run is reversible. Direct port of the safety pattern in
// refugebotserver's DependencyInstaller.cs.
//
// Caller responsibility: the instance container MUST be stopped before
// calling this — the game holds DLLs in /game/Mods open while running,
// and rename-on-extract will fail or corrupt under that.
func (d *Dispatcher) installBaseMods(ctx context.Context, instanceID string, manifest *modulepkg.Manifest, dataPath string) error {
	if manifest == nil || len(manifest.UpdateProviders) == 0 {
		return errors.New("install-base-mods: module has no update provider")
	}
	installPath := manifest.UpdateProviders[0].InstallPath
	if installPath == "" {
		installPath = manifest.Runtime.Docker.BrowseableRoot
	}
	if installPath == "" {
		return errors.New("install-base-mods: install_path missing on module")
	}
	volumes := agentmodule.ResolveVolumes(manifest, instanceID, dataPath)
	gameVol := volumeNameForPath(volumes, installPath)
	if gameVol == "" {
		return fmt.Errorf("install-base-mods: install_path %q not in module volumes", installPath)
	}

	// ── SteamCMD validate step ──────────────────────────────────────────
	// If the module has a steamcmd update provider, run app_update validate
	// with anonymous login FIRST to re-download/restore stock TFP mods.
	// This ensures TFP_*, 0_TFP_*, and xMarkers are present before the
	// alpine sidecar installs third-party base mods.
	var steamcmdSpec *modulepkg.UpdateProvider
	for i := range manifest.UpdateProviders {
		if manifest.UpdateProviders[i].Kind == "steamcmd" {
			steamcmdSpec = &manifest.UpdateProviders[i]
			break
		}
	}
	if steamcmdSpec != nil {
		d.log.Info("install-base-mods: steamcmd validate starting", "instance_id", instanceID, "app_id", steamcmdSpec.AppID)
		prov, err := updater.Get("steamcmd")
		if err != nil {
			return fmt.Errorf("install-base-mods: get steamcmd provider: %w", err)
		}
		// Build a log sink that streams through the same "install-base-mods log" logger.
		sink := func(line string) {
			d.log.Info("install-base-mods log", "instance_id", instanceID, "line", line)
		}
		uc := &updater.Context{
			InstanceID:     instanceID,
			Manifest:       manifest,
			BrowseableRoot: installPath,
			DataPath:       dataPath,
			Runtime:        d.runtime,
			Log:            sink,
			SteamUsername:  "", // anonymous login
			SteamPassword:  "", // anonymous login
		}
		if err := prov.Update(ctx, uc, steamcmdSpec); err != nil {
			return fmt.Errorf("basemods: steamcmd validate failed: %w", err)
		}
		d.log.Info("install-base-mods: steamcmd validate succeeded", "instance_id", instanceID)
	}

	// alpine:3.19 is a 7 MB image with apk available. apk add fetches
	// curl + unzip + tar + jq in ~5s on a warm cache. Cheaper than
	// shipping our own base image and good enough for occasional runs.
	script := `set -e
apk add --no-cache --quiet curl tar unzip jq ca-certificates >/dev/null
STAMP=$(date -u +%Y%m%d%H%M%S)
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT

backup_if_exists() {
  for d in "$@"; do
    if [ -d "$d" ] && [ ! -L "$d" ]; then
      mv "$d" "$d.bak-$STAMP" 2>/dev/null || true
    fi
  done
}

# ── Allocs Server Fixes ───────────────────────────────────────────────
echo "[basemods] downloading Allocs from illy.bz"
curl -fsSL -o "$TMP/allocs.tgz" "https://illy.bz/fi/7dtd/server_fixes.tar.gz"
# Magic-byte check: gzip starts with 0x1f 0x8b — protects against
# CDN-served HTML 404 pages landing as a "valid" archive.
head -c 2 "$TMP/allocs.tgz" | od -An -tx1 | tr -d ' \n' | grep -q '^1f8b' \
  || { echo "[basemods] Allocs download wasn't gzip — aborting"; exit 1; }
backup_if_exists /game/Mods/Allocs_CommandExtensions /game/Mods/Allocs_WebAndMapRendering /game/Mods/Alloc_XmlPatch
tar -xzf "$TMP/allocs.tgz" -C /game
echo "[basemods] Allocs installed"

# ── PrismaCore (latest GitHub release zip) ─────────────────────────────
echo "[basemods] resolving latest PrismaCore release"
PRISMA_JSON="$TMP/prisma.json"
curl -fsSL -A "panel-basemods/1.0" -H "Accept: application/vnd.github+json" \
  -o "$PRISMA_JSON" "https://api.github.com/repos/Prisma501/PrismaCore/releases/latest"
PRISMA_TAG=$(jq -r '.tag_name // empty' "$PRISMA_JSON")
PRISMA_URL=$(jq -r '.assets[]? | select(.name | test("\\.zip$"; "i")) | .browser_download_url' "$PRISMA_JSON" | head -1)
if [ -z "$PRISMA_URL" ]; then
  echo "[basemods] no PrismaCore .zip asset — skipping"
else
  echo "[basemods] downloading PrismaCore $PRISMA_TAG from $PRISMA_URL"
  curl -fsSL -A "panel-basemods/1.0" -o "$TMP/prisma.zip" "$PRISMA_URL"
  head -c 2 "$TMP/prisma.zip" | od -An -tx1 | tr -d ' \n' | grep -q '^504b' \
    || { echo "[basemods] PrismaCore download wasn't a zip — aborting"; exit 1; }
  for d in /game/Mods/Prisma*; do backup_if_exists "$d"; done
  unzip -o -q "$TMP/prisma.zip" -d /game/Mods/
  echo "[basemods] PrismaCore installed"
fi

# Note: TFP_* and xMarkers are shipped as part of the SteamCMD dedicated
# server install (app 294420). They're already in /game/Mods/ after a
# fresh install and we don't touch them here — wiping TFP_Harmony would
# brick the engine.
echo BASEMODS_OK
`

	sidecarID := fmt.Sprintf("%s-basemods", instanceID)
	spec := runtime.InstanceSpec{
		InstanceID: sidecarID,
		IsSidecar:  true,
		Image:      "alpine:3.19",
		Command:    []string{"sh", "-c", script},
		Volumes: []runtime.VolumeSpec{
			{Type: "volume", VolumeName: gameVol, ContainerPath: installPath},
		},
		RestartPolicy: "no",
	}
	d.log.Info("install-base-mods: starting sidecar", "instance_id", instanceID, "vol", gameVol)
	contID, err := d.runtime.Create(ctx, spec)
	if err != nil {
		return fmt.Errorf("install-base-mods: create: %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 fmt.Errorf("install-base-mods: start: %w", err)
	}
	logCtx, cancelLogs := context.WithCancel(ctx)
	defer cancelLogs()
	logDone := make(chan struct{})
	go func() {
		defer close(logDone)
		_ = d.runtime.StreamLogs(logCtx, contID, func(_ runtime.LogStream, line string, _ time.Time) {
			d.log.Info("install-base-mods log", "instance_id", instanceID, "line", line)
		})
	}()
	exitCode, err := d.runtime.Wait(ctx, contID)
	cancelLogs()
	<-logDone
	if err != nil {
		return fmt.Errorf("install-base-mods: wait: %w", err)
	}
	if exitCode != 0 {
		return fmt.Errorf("install-base-mods: sidecar exited %d", exitCode)
	}
	d.log.Info("install-base-mods: success", "instance_id", instanceID)
	return nil
}
