// Package runtime defines the Target-side instance execution abstraction.
// A Runtime is the thing that actually launches and tears down game server
// processes — either inside a container (DockerRuntime) or directly on the
// host (HostRuntime — future).
package runtime

import (
	"context"
	"io"
	"time"
)

// InstanceSpec is the resolved, runtime-ready launch recipe for one instance.
// It is independent of the manifest format: a resolver in agent/internal/module
// flattens (Manifest, InstanceCreate RPC) into this struct.
type InstanceSpec struct {
	InstanceID string            // panel-side identifier, used as container name
	Image      string            // e.g. vinanrra/7dtd-server:latest
	Entrypoint []string          // override, empty = image default
	Command    []string          // override, empty = image default
	Env        map[string]string // environment variables
	User       string            // uid:gid or name, empty = image default
	WorkingDir string
	Volumes    []VolumeSpec
	Ports      []PortSpec
	Resources  ResourceLimits
	// RestartPolicy: "no", "on-failure", "unless-stopped", "always"
	RestartPolicy string
	// OpenStdin keeps the container's stdin open so we can later attach and
	// write admin-console commands (stdio RCON adapter). Auto-enabled by
	// the module resolver when rcon.adapter == "stdio".
	OpenStdin bool
	// NetworkMode is the Docker network mode. Empty → bridge (default).
	// "host" shares the host's network namespace; required for modules
	// whose TURN/STUN clients can't survive Docker's UDP NAT rebinds
	// (e.g. Windrose). In host mode, Ports become informational only.
	NetworkMode string
	// BuildContext is an absolute filesystem path containing a Dockerfile
	// that produces this Image. When set, the runtime auto-builds the
	// image on first use if it's missing locally — the AMP-class "drop in
	// an agent and any module just works" behavior. Required for `panel-*`
	// images, since they are never pushed to a registry. Empty for stock
	// public images (alpine, the acekorneya ASA image, etc.) which the
	// runtime pulls normally.
	BuildContext string
	// LogSink optionally receives runtime-side progress lines (image
	// pull / image build / etc.) so the agent can forward them to the
	// controller's log stream. Without this, first-time builds happen
	// silently and the operator sees "installing" with no visible
	// progress for ~30s–3min while the Dockerfile runs.
	LogSink func(line string)
	// SecurityOpts is passed through to Docker's HostConfig.SecurityOpt
	// (one entry per --security-opt CLI flag). The SteamCMD sidecar sets
	// "seccomp=unconfined" so that the 32-bit Steam runtime's socket
	// syscalls aren't ENOSYS'd by Docker's stricter default seccomp
	// profile in newer Docker releases (observed on Docker 29.4 + Ubuntu
	// 24.04 kernel 6.8: "CreateBoundSocket: failed to create socket,
	// error [no name available] (38)"). Same image works fine on
	// Docker 29.1 with the default profile.
	SecurityOpts []string
	// IsSidecar marks this as a transient helper container (fs browse,
	// steamcmd, backup, restore, wipe, warmseed, basemods) rather than a
	// main game-server instance. The runtime stamps a "panel.role" label
	// accordingly so orphan-detection (Rehydrate) can tell main instances
	// apart from helpers without brittle name-suffix matching.
	IsSidecar bool
}

// VolumeSpec is a mount declaration, either a bind mount (host path →
// container path) or a Docker named volume. The resolver is responsible
// for substituting $DATA_PATH / $INSTANCE_ID before we see it here.
type VolumeSpec struct {
	Type          string // "bind" (default) or "volume"
	HostPath      string // populated when Type == "bind"
	VolumeName    string // populated when Type == "volume"
	ContainerPath string
	ReadOnly      bool
}

// PortSpec is a port mapping.
type PortSpec struct {
	HostPort      uint16
	ContainerPort uint16
	Proto         string // "tcp" or "udp"
	// HostIP — bind address on the Target. Empty = 0.0.0.0.
	HostIP string
}

// ResourceLimits caps CPU / memory for the instance.
type ResourceLimits struct {
	CPUShares     uint32
	MemoryBytes   uint64
	PidsLimit     int64 // 0 = unlimited
	NanoCPUs      int64 // Docker "--cpus" as nano-CPUs (1.5 cpus = 1.5e9)
	StopGrace     time.Duration
}

// RuntimeInstanceState is the runtime's view of an instance at a point in
// time, used by the Agent to rehydrate after restart or detect drift.
type RuntimeInstanceState struct {
	ContainerID string
	Status      string // "running" | "exited" | "created" | "paused" | "restarting" | "removing" | "dead"
	ExitCode    int32
}

// LogStream identifies which stream a log line came from.
type LogStream string

const (
	LogStreamStdout LogStream = "stdout"
	LogStreamStderr LogStream = "stderr"
)

// LogHandler receives one log line at a time. Lines are already stripped of
// trailing newline. Handler must not block; if it does, log pressure will
// build up in the Runtime.
type LogHandler func(stream LogStream, line string, at time.Time)

// Runtime is the interface that both Docker and Host runtimes implement.
type Runtime interface {
	// Create pulls the image (if needed) and creates the container.
	// Returns a runtime-specific identifier used by Start/Stop/Remove.
	Create(ctx context.Context, spec InstanceSpec) (id string, err error)

	// Start starts a previously-created instance.
	Start(ctx context.Context, id string) error

	// Stop stops a running instance, waiting up to grace before SIGKILL.
	Stop(ctx context.Context, id string, grace time.Duration) error

	// Restart restarts a running instance, waiting up to grace before
	// SIGKILL. Container ends up running again — equivalent to
	// `docker container restart`. Used by guardrails (e.g. arkHangGuard)
	// that want a bounce without surfacing as an operator stop.
	Restart(ctx context.Context, id string, grace time.Duration) error

	// Remove deletes the instance (after Stop). Safe to call on stopped.
	Remove(ctx context.Context, id string) error

	// StreamLogs follows stdout/stderr and calls handler for each line.
	// Returns when ctx is cancelled or the instance exits.
	StreamLogs(ctx context.Context, id string, handler LogHandler) error

	// Wait blocks until the instance exits and returns its exit code.
	Wait(ctx context.Context, id string) (exitCode int, err error)

	// StatsStream returns a reader of Docker's stats stream (JSON frames,
	// one per second). Closer cancels the stream. Used by the per-instance
	// stats poller to compute CPU%/mem/net for UI + alerting.
	StatsStream(ctx context.Context, id string) (io.ReadCloser, error)

	// InspectByName looks up an instance by its runtime-level name
	// (for Docker: "panel-<instance_id>") and returns its current state.
	// Returns an error whose predicate is not-found-able if the instance
	// has been removed from the runtime — used during agent rehydrate
	// to detect stale sidecar metadata.
	InspectByName(ctx context.Context, name string) (RuntimeInstanceState, error)

	// ContainerExists reports whether a container with the given
	// runtime-level name currently exists. Unlike InspectByName, it
	// distinguishes "definitely gone" from "couldn't tell":
	//   exists=false, err=nil  → the runtime confirmed no such container
	//   exists=false, err!=nil → the query itself failed (daemon down,
	//                            connection dropped, ctx cancelled) — the
	//                            container's fate is UNKNOWN, not gone.
	// Callers that gate destructive bookkeeping (e.g. deleting sidecar
	// metadata after a container remove) must treat a non-nil err as
	// "do not assume removal succeeded."
	ContainerExists(ctx context.Context, name string) (exists bool, err error)

	// ---- Container-path file operations ----
	// These operate inside the running container (or container filesystem
	// for stopped containers where the runtime allows it). Used by the
	// file manager for volume-backed instances where the host-side bind
	// mount is absent or empty.

	// ExecCapture runs a command inside the container and returns its
	// captured stdout + stderr + exit code. Requires container to be running.
	ExecCapture(ctx context.Context, id string, cmd []string) (stdout, stderr []byte, exitCode int, err error)

	// CopyFileFromContainer returns the raw contents of a single file at
	// containerPath. Works even on stopped containers.
	CopyFileFromContainer(ctx context.Context, id, containerPath string) ([]byte, error)

	// CopyFileToContainer writes content to containerPath, creating
	// parent dirs inside the container if the container is running.
	CopyFileToContainer(ctx context.Context, id, containerPath string, content []byte) error

	// CopyTarToContainer writes a raw tar stream onto the container at
	// dstDir. The tar entries are extracted in place by the Docker API.
	// Used by the file manager's extract path so we can build one big
	// tar from the archive's entries and ship it in a single API call.
	CopyTarToContainer(ctx context.Context, id, dstDir string, tarStream io.Reader) error

	// CopyDirFromContainer returns a tar stream of the contents at
	// containerPath. For files: a single-entry tar. For directories:
	// one entry per file/dir under it. Used by the compress path so the
	// agent can iterate file contents without per-file round-trips.
	CopyDirFromContainer(ctx context.Context, id, containerPath string) (io.ReadCloser, error)

	// AttachStdio attaches to the container's stdin/stdout/stderr stream
	// and returns a read/write/close handle. Used by the stdio RCON adapter
	// for games whose admin surface is stdin-only. Requires the container
	// to have been created with OpenStdin=true.
	AttachStdio(ctx context.Context, id string) (io.ReadWriteCloser, error)

	// ContainerMounts returns the mount points of a container by name.
	// Works on stopped containers (the daemon keeps the spec). Used by the
	// cluster player-save snapshotter to find the host path backing a 7DTD
	// instance's /cluster bind without depending on a path convention.
	ContainerMounts(ctx context.Context, name string) ([]MountInfo, error)

	// Name returns the runtime kind ("docker", "host") for logging.
	Name() string
}

// MountInfo is one container mount point, host-side. Type is "bind" or
// "volume"; Source is the host path backing it (for a named volume that's
// the docker volume's _data dir); Destination is the in-container path.
type MountInfo struct {
	Type        string
	Source      string
	Destination string
	Name        string
}
