package dispatch

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

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

	"github.com/dbledeez/panel/agent/internal/runtime"
	panelv1 "github.com/dbledeez/panel/proto/panel/v1"
)

// handleDelete tears down the instance record. If the container is still
// running, it's stopped with the requested grace first. Optionally
// removes every Docker volume labeled for this instance ("purge").
//
// Never deletes the host data_path directory — operators may want to
// keep configs/logs that landed on the bind mount. If they want a full
// wipe, they can delete the dir manually after.
func (d *Dispatcher) handleDelete(ctx context.Context, corrID string, req *panelv1.InstanceDelete) {
	log := d.log.With("instance_id", req.InstanceId, "correlation_id", corrID)

	d.mu.Lock()
	rec, exists := d.instances[req.InstanceId]
	d.mu.Unlock()

	// Cancel any in-flight steamcmd / updater goroutine FIRST. The
	// updater spins up its own sidecar container (panel-<id>-steamcmd-<n>)
	// that mounts the same data volumes. If we don't cancel it before
	// trying to remove volumes, the sidecar holds them open and the
	// volume-rm fails with "volume is in use" — operator sees the
	// instance stuck on "Deleting…" while a download finishes in the
	// background.
	d.cancelUpdater(req.InstanceId)
	// Whatever state the main container is in, any Files-tab helper that
	// was running against this instance is now orphaned. Clean it up
	// before touching anything else so its volume-holds don't interfere
	// with the main container's stop/remove.
	d.teardownFsHelper(req.InstanceId)

	grace := time.Duration(req.GraceSeconds) * time.Second
	if grace == 0 {
		grace = 15 * time.Second
	}

	// If we have a record, use its container_id directly. Otherwise try
	// to find the container by its canonical name (may exist from a
	// previous session that skipped rehydrate).
	containerID := ""
	if exists {
		containerID = rec.ContainerID
	} else {
		if state, err := d.runtime.InspectByName(ctx, "panel-"+req.InstanceId); err == nil {
			containerID = state.ContainerID
		}
	}

	if containerID != "" {
		// Best-effort stop; ignore errors since container may already be stopped.
		_ = d.runtime.Stop(ctx, containerID, grace)
		removeErr := d.runtime.Remove(ctx, containerID)
		// d.runtime.Remove already swallows not-found, so a non-nil
		// removeErr means the remove genuinely failed. Before we tear down
		// any bookkeeping (in-memory record + sidecar metadata) we must be
		// SURE the container is gone — otherwise we orphan a live container
		// the controller still routes to, and the agent forgets it on the
		// next rehydrate. The classic trigger: a delete racing with agent
		// shutdown, where the Docker connection dies mid-remove
		// ("terminated signal received"). The remove errors, and the
		// follow-up existence check ALSO errors — fate unknown, not gone.
		if removeErr != nil {
			exists, existErr := d.runtime.ContainerExists(ctx, "panel-"+req.InstanceId)
			switch {
			case existErr != nil:
				// Couldn't even ask (daemon down / connection dropped /
				// ctx cancelled). Do NOT delete the sidecar — bail and let
				// the operator retry once the daemon is reachable. Leaving
				// the sidecar means the next rehydrate re-adopts the
				// instance instead of orphaning it.
				log.Error("delete: container remove failed and existence is unknown — aborting to avoid orphan",
					"remove_err", removeErr, "exists_err", existErr)
				d.replyError(corrID, req.InstanceId, "container_remove",
					fmt.Sprintf("container panel-%s remove failed and existence unverifiable: %s", req.InstanceId, removeErr.Error()))
				return
			case exists:
				// Container is confirmed still present. The controller must
				// learn — otherwise the DB row gets purged while a zombie
				// container lives on, and the next create with the same id
				// fails on a "name already in use" docker error.
				log.Error("delete: container still exists after remove", "err", removeErr)
				d.replyError(corrID, req.InstanceId, "container_remove",
					fmt.Sprintf("container panel-%s still exists after remove: %s", req.InstanceId, removeErr.Error()))
				return
			default:
				// Confirmed gone (e.g. autoremove fired between Stop and
				// Remove). Safe to proceed with bookkeeping cleanup.
				log.Warn("delete: remove returned error but container is confirmed gone", "err", removeErr)
			}
		}
	}

	// Signal any live trackers / log streams to stop.
	if exists {
		if rec.TrackerCancel != nil {
			rec.TrackerCancel()
		}
		if rec.LogCancel != nil {
			rec.LogCancel()
		}
	}

	// Purge volumes if requested.
	if req.PurgeVolumes {
		dr, ok := d.runtime.(*runtime.DockerRuntime)
		if !ok {
			log.Warn("delete: purge requested but runtime is not Docker; skipping volume cleanup")
		} else {
			// Sweep every container that mounts one of this instance's
			// volumes — running OR exited. Without this, a stranded
			// steamcmd / backup / fs-helper sidecar from a previous
			// agent crash keeps the volume "in use" and volume-rm
			// fails repeatedly. Operator sees the instance stuck
			// "Deleting…" forever.
			holders, err := dr.ListContainersHoldingInstanceVolumes(ctx, req.InstanceId)
			if err != nil {
				log.Warn("delete: scan for orphan sidecars failed", "err", err)
			}
			for _, hid := range holders {
				if err := dr.ForceRemoveContainer(ctx, hid); err != nil {
					log.Warn("delete: force-remove sidecar failed", "container_id", hid, "err", err)
				} else {
					log.Info("delete: removed orphan sidecar", "container_id", short(hid))
				}
			}
			vols, err := dr.ListVolumesByInstance(ctx, req.InstanceId)
			if err != nil {
				log.Warn("delete: list volumes failed", "err", err)
			}
			for _, v := range vols {
				// Force=true: there's no scenario where we want a panel
				// volume to survive a purge=true delete after we've
				// scrubbed every container that referenced it.
				if err := dr.RemoveVolume(ctx, v, true); err != nil {
					log.Warn("delete: remove volume failed", "volume", v, "err", err)
					d.replyError(corrID, req.InstanceId, "volume_remove", fmt.Sprintf("volume %s: %s", v, err.Error()))
					return
				}
				log.Info("delete: purged volume", "volume", v)
			}
		}
	}

	// Remove in-memory record + sidecar metadata.
	d.mu.Lock()
	delete(d.instances, req.InstanceId)
	d.mu.Unlock()
	d.deleteMeta(req.InstanceId)

	// Leave host data_path alone (see doc comment); operators can rm -rf
	// manually. We do NOT attempt os.RemoveAll here — too destructive to
	// do automatically on a shared multi-instance directory layout.

	d.sendInstanceState(req.InstanceId, panelv1.InstanceStatus_INSTANCE_STATUS_STOPPED, 0, "deleted")
	d.replyOK(corrID)
	log.Info("instance deleted", "purge_volumes", req.PurgeVolumes)
	_ = os.Stat // kept imported for future host-data_path removal flag
}

// sendDelete-specific correlation reply uses the existing replyOK/err pattern.
var _ = timestamppb.Now // keep import hint while ctx-based future expansions land