package dispatch

import (
	"context"
	"fmt"
	"os"
	"path"
	"time"

	"github.com/dbledeez/panel/agent/internal/runtime"
)

func medicEnvOr(k, def string) string {
	if v := os.Getenv(k); v != "" {
		return v
	}
	return def
}

// Medic host paths on the agent box (figaro, for 7DTD). Overridable via env so
// the same agent binary works if 7DTD ever moves hosts.
func regionRollingRoot() string {
	return medicEnvOr("PANEL_REGION_ROLLING_DIR", "/home/refuge/region-rolling")
}
func regionMedicBin() string {
	return medicEnvOr("PANEL_REGION_MEDIC_BIN", "/home/refuge/panel/bin/region-medic")
}
func discordTokenFile() string {
	return medicEnvOr("PANEL_DISCORD_TOKEN_FILE", "/home/refuge/panel/discord-bot-token")
}

func medicFileExists(p string) bool {
	st, err := os.Stat(p)
	return err == nil && !st.IsDir()
}

// runRegionMedicOnStart runs the autoheal CLI in a throwaway debian sidecar while
// the 7DTD container is STILL STOPPED, just before the agent starts it:
//   - validate every .7rg in the active world's Region dir,
//   - heal corrupt ones from the newest clean rolling snapshot, then the panel
//     tar backups as a fallback,
//   - snapshot the now-clean Region dir to a fresh rolling slot,
//   - post a Discord summary (naming the server) if anything healed.
//
// The per-server medic config (enable / rolling-keep / Discord channel) lives in
// the SAVES volume at region-medic.json — written by the panel's medic-config
// endpoint via FsWrite (NO container recreate) and read by the CLI here in the
// sidecar. The agent therefore always launches the sidecar for 7DTD and lets the
// CLI decide; an absent/empty config just means defaults + no Discord.
//
// Best-effort by contract: any failure is logged + swallowed, never blocking the
// start. 10-min hard cap so a wedged sidecar/daemon can't stall a boot.
func (d *Dispatcher) runRegionMedicOnStart(ctx context.Context, rec *instanceRecord) {
	ctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
	defer cancel()
	log := d.log.With("instance_id", rec.InstanceID, "medic", true)
	medicBin := regionMedicBin()
	if !medicFileExists(medicBin) {
		log.Warn("region-medic: binary missing, skipping pre-start heal", "bin", medicBin)
		return
	}
	savesVol := fmt.Sprintf("panel-%s-saves", rec.InstanceID)
	rollingDir := path.Join(regionRollingRoot(), rec.InstanceID)
	_ = os.MkdirAll(rollingDir, 0o755)
	backupsDir := path.Join(d.backupDir, rec.InstanceID)

	cmd := []string{"/rm", "autoheal",
		"-saves-root", "/saves",
		"-rolling-dir", "/rolling",
		"-keep", "2", // default; overridden by region-medic.json in the saves volume
		"-label", rec.InstanceID,
		"-apply",
	}
	vols := []runtime.VolumeSpec{
		{Type: "volume", VolumeName: savesVol, ContainerPath: "/saves"},
		{Type: "bind", HostPath: rollingDir, ContainerPath: "/rolling"},
		{Type: "bind", HostPath: medicBin, ContainerPath: "/rm", ReadOnly: true},
	}
	// debian:12-slim ships no CA bundle, so the CLI's HTTPS POST to Discord can't
	// verify the TLS cert ("x509: certificate signed by unknown authority"). Mount
	// the host's CA certs read-only — Go's x509 finds ca-certificates.crt there.
	if _, err := os.Stat("/etc/ssl/certs"); err == nil {
		vols = append(vols, runtime.VolumeSpec{Type: "bind", HostPath: "/etc/ssl/certs", ContainerPath: "/etc/ssl/certs", ReadOnly: true})
	}
	// Panel tar backups are the heal fallback when no rolling slot has a clean
	// copy (e.g. very first boot, before any snapshot). Only mount if present.
	if st, err := os.Stat(backupsDir); err == nil && st.IsDir() {
		cmd = append(cmd, "-backups-dir", "/backups")
		vols = append(vols, runtime.VolumeSpec{Type: "bind", HostPath: backupsDir, ContainerPath: "/backups", ReadOnly: true})
	}
	// The token is mounted whenever it exists; the CLI only posts if the saves
	// config names a channel. No -discord-channel flag — the CLI reads it from
	// /saves/region-medic.json.
	if tf := discordTokenFile(); medicFileExists(tf) {
		cmd = append(cmd, "-discord-token-file", "/discord-token")
		vols = append(vols, runtime.VolumeSpec{Type: "bind", HostPath: tf, ContainerPath: "/discord-token", ReadOnly: true})
	}

	spec := runtime.InstanceSpec{
		InstanceID:    rec.InstanceID + "-medic",
		IsSidecar:     true,
		Image:         "debian:12-slim",
		Command:       cmd,
		RestartPolicy: "no",
		Volumes:       vols,
	}

	log.Info("region-medic: pre-start scan+heal")
	contID, err := d.runtime.Create(ctx, spec)
	if err != nil {
		log.Warn("region-medic: create sidecar failed", "err", err)
		return
	}
	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 {
		log.Warn("region-medic: start sidecar failed", "err", err)
		return
	}
	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) {
			log.Info("region-medic", "line", line)
		})
	}()
	exitCode, err := d.runtime.Wait(ctx, contID)
	cancelLogs()
	<-logDone
	if err != nil {
		log.Warn("region-medic: wait sidecar failed", "err", err)
		return
	}
	if exitCode != 0 {
		log.Warn("region-medic: sidecar exited non-zero", "code", exitCode)
		return
	}
	log.Info("region-medic: pre-start heal complete")
}
