// Package module (agent-side) resolves a manifest + InstanceCreate RPC
// into a runtime.InstanceSpec ready for the Docker / host runtime to launch.
package module

import (
	"errors"
	"fmt"
	"path/filepath"
	"strings"

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

// ResolveDocker produces an InstanceSpec for Docker-mode launch of the given
// module manifest and RPC create request. It substitutes $DATA_PATH in volume
// targets and maps declared ports against the request's port overrides.
func ResolveDocker(manifest *modulepkg.Manifest, req *panelv1.InstanceCreate) (runtime.InstanceSpec, error) {
	if manifest == nil {
		return runtime.InstanceSpec{}, errors.New("manifest is nil")
	}
	if manifest.Runtime.Docker == nil {
		return runtime.InstanceSpec{}, fmt.Errorf("module %q has no docker runtime", manifest.ID)
	}
	if req.DataPath == "" {
		return runtime.InstanceSpec{}, errors.New("req.data_path is required")
	}

	docker := manifest.Runtime.Docker

	env := map[string]string{}
	for k, v := range docker.Env {
		env[k] = expand(v, req.DataPath)
	}
	// Per-instance config_values layer: if the operator (or controller
	// feature code) set a value for an env key the module already
	// declares, use that value instead of the manifest default. Only
	// keys that ALREADY exist in docker.Env get overridden — we don't
	// let config_values smuggle in unrelated env vars. Used today by
	// the ARK cluster feature to override CLUSTER_ID per-instance.
	for k, v := range req.ConfigValues {
		if _, ok := env[k]; ok {
			env[k] = v
		}
	}

	volumes := make([]runtime.VolumeSpec, 0, len(docker.Volumes))
	for _, v := range docker.Volumes {
		vs := runtime.VolumeSpec{
			ContainerPath: v.Container,
			ReadOnly:      v.ReadOnly,
		}
		kind := v.Type
		if kind == "" {
			// Infer from which field was populated.
			if v.Name != "" {
				kind = "volume"
			} else {
				kind = "bind"
			}
		}
		switch kind {
		case "volume":
			vs.Type = "volume"
			vs.VolumeName = expandInstance(v.Name, req.InstanceId)
		default:
			vs.Type = "bind"
			vs.HostPath = expand(v.Target, req.DataPath)
		}
		// Apply per-instance mount override if the controller asked for
		// one by container path. An absolute path (Unix `/...` or
		// Windows `C:\...`) is a bind mount host path; anything else is
		// a Docker named volume. Used by the ARK cluster feature to
		// swap the default per-instance cluster volume for a shared
		// host-visible `arkcluster/<id>/` bind mount. `$AGENT_DATA_ROOT`
		// has already been expanded by handleCreate.
		if ov, ok := req.MountOverrides[v.Container]; ok && ov != "" {
			if filepath.IsAbs(ov) {
				vs.Type = "bind"
				vs.HostPath = ov
				vs.VolumeName = ""
			} else {
				vs.Type = "volume"
				vs.VolumeName = ov
				vs.HostPath = ""
			}
		}
		volumes = append(volumes, vs)
	}

	portOverrides := indexPortOverrides(req.Ports)
	ports := make([]runtime.PortSpec, 0, len(manifest.Ports))
	for _, p := range manifest.Ports {
		containerPort := uint16(p.Default)
		hostPort := containerPort
		if ov, ok := portOverrides[p.Name]; ok {
			if ov.ContainerPort != 0 {
				containerPort = uint16(ov.ContainerPort)
			}
			if ov.HostPort != 0 {
				hostPort = uint16(ov.HostPort)
			}
		}
		// Internal ports (e.g. 7DTD's telnet RCON on 8081) must still be
		// reachable from the Target agent — it runs on the same host — so we
		// bind them to 127.0.0.1 instead of 0.0.0.0. Public ports bind to
		// all interfaces (HostIP left empty).
		hostIP := ""
		if p.Internal {
			hostIP = "127.0.0.1"
		}
		ports = append(ports, runtime.PortSpec{
			ContainerPort: containerPort,
			HostPort:      hostPort,
			Proto:         strings.ToLower(p.Proto),
			HostIP:        hostIP,
		})
	}

	// Preserve nil when the manifest doesn't override — passing []string{} to
	// Docker ContainerCreate replaces the image's default entrypoint/cmd with
	// empty, which errors out as "no command specified".
	var entrypoint, command []string
	if len(docker.Entrypoint) > 0 {
		entrypoint = append([]string{}, docker.Entrypoint...)
	}
	if len(docker.Command) > 0 {
		command = append([]string{}, docker.Command...)
	}

	spec := runtime.InstanceSpec{
		InstanceID:   req.InstanceId,
		Image:        docker.Image,
		Entrypoint:   entrypoint,
		Command:      command,
		Env:          env,
		User:         docker.User,
		Volumes:      volumes,
		Ports:        ports,
		NetworkMode:  docker.NetworkMode,
		BuildContext: manifest.Dir, // module's directory holds the Dockerfile
	}
	// Auto-enable OpenStdin whenever the module's RCON adapter is stdio —
	// it's the one feature the adapter needs from the runtime, and making
	// it implicit saves a second knob every stdio-based module would have
	// to remember to set.
	if manifest.RCON != nil && manifest.RCON.Adapter == "stdio" {
		spec.OpenStdin = true
	}
	if lim := req.Limits; lim != nil {
		spec.Resources = runtime.ResourceLimits{
			CPUShares:   lim.CpuShares,
			MemoryBytes: lim.MemBytes,
		}
	}
	return spec, nil
}

// expand substitutes $DATA_PATH in the given string.
func expand(s, dataPath string) string {
	return strings.ReplaceAll(s, "$DATA_PATH", dataPath)
}

// expandInstance substitutes $INSTANCE_ID in the given string. Used for
// named-volume templating so each instance gets its own volume.
func expandInstance(s, instanceID string) string {
	return strings.ReplaceAll(s, "$INSTANCE_ID", instanceID)
}

// ResolveVolumes translates manifest volumes into runtime.VolumeSpec form
// for a specific instance. Exposed so sidecar launchers (SteamCMD updater,
// future backup-runner, etc.) can mount the same volumes the main
// container uses, putting files where the main container expects them.
func ResolveVolumes(manifest *modulepkg.Manifest, instanceID, dataPath string) []runtime.VolumeSpec {
	if manifest == nil || manifest.Runtime.Docker == nil {
		return nil
	}
	out := make([]runtime.VolumeSpec, 0, len(manifest.Runtime.Docker.Volumes))
	for _, v := range manifest.Runtime.Docker.Volumes {
		vs := runtime.VolumeSpec{
			ContainerPath: v.Container,
			ReadOnly:      v.ReadOnly,
		}
		kind := v.Type
		if kind == "" {
			if v.Name != "" {
				kind = "volume"
			} else {
				kind = "bind"
			}
		}
		switch kind {
		case "volume":
			vs.Type = "volume"
			vs.VolumeName = expandInstance(v.Name, instanceID)
		default:
			vs.Type = "bind"
			vs.HostPath = expand(v.Target, dataPath)
		}
		out = append(out, vs)
	}
	return out
}

// indexPortOverrides builds a by-name lookup of PortMap entries from the RPC.
func indexPortOverrides(ports []*panelv1.PortMap) map[string]*panelv1.PortMap {
	out := map[string]*panelv1.PortMap{}
	for _, p := range ports {
		if p == nil {
			continue
		}
		out[p.Name] = p
	}
	return out
}
