package docker

import (
	"archive/tar"
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"maps"
	"net/netip"
	"os"
	"path/filepath"
	"regexp"
	"slices"
	"strconv"
	"strings"
	"time"

	"github.com/containerd/errdefs"
	"github.com/distribution/reference"
	dockercommand "github.com/docker/cli/cli/command"
	dockerconfig "github.com/docker/cli/cli/config"
	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/filters"
	"github.com/docker/docker/api/types/image"
	"github.com/docker/docker/api/types/mount"
	"github.com/docker/docker/api/types/network"
	"github.com/docker/docker/api/types/volume"
	"github.com/docker/docker/client"
	"github.com/docker/docker/pkg/stdcopy"
	"github.com/docker/go-connections/nat"
	"github.com/docker/go-units"
	"github.com/google/go-containerregistry/pkg/authn"
	"github.com/google/go-containerregistry/pkg/name"
	"github.com/google/go-containerregistry/pkg/v1/remote"
	"github.com/jmoiron/sqlx"
	"github.com/opencontainers/go-digest"
	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/psviderski/uncloud/internal/machine/api/pb"
	"github.com/psviderski/uncloud/internal/machine/dns"
	"github.com/psviderski/uncloud/internal/secret"
	"github.com/psviderski/uncloud/pkg/api"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/emptypb"
	"google.golang.org/protobuf/types/known/timestamppb"
)

var fullDockerIDRegex = regexp.MustCompile(`^[a-f0-9]{64}$`)

// Server implements the gRPC Docker service that proxies requests to the Docker daemon.
type Server struct {
	pb.UnimplementedDockerServer
	client  *client.Client
	service *Service
	db      *sqlx.DB
	// internalDNSIP is a function that returns the IP address of the internal DNS server. It may return an empty
	// address if the address is unknown (e.g. when the machine is not initialised yet).
	internalDNSIP func() netip.Addr
	// machineID is a function that returns the machine ID. It may return an empty string if the machine
	// is not initialised yet.
	machineID func() string
	// networkReady is a function that returns true if the Docker network is ready for containers.
	networkReady func() bool
	// waitForNetworkReady is a function that waits for the Docker network to be ready for containers.
	waitForNetworkReady func(ctx context.Context) error
}

type ServerOptions struct {
	// TODO: verify if we still need the network readiness checks as the cluster controller ensures the network
	//  is ready before starting the network API server. It may still be needed when communicating with the local
	//  API server but in this case we should probably fail until the cluster is initialised.
	NetworkReady        func() bool
	WaitForNetworkReady func(ctx context.Context) error
}

// NewServer creates a new Docker gRPC server with the provided Docker service.
func NewServer(service *Service, db *sqlx.DB, internalDNSIP func() netip.Addr, machineID func() string, opts ServerOptions) *Server {
	s := &Server{
		client:        service.Client,
		service:       service,
		db:            db,
		internalDNSIP: internalDNSIP,
		machineID:     machineID,
	}

	s.networkReady = opts.NetworkReady
	s.waitForNetworkReady = opts.WaitForNetworkReady

	return s
}

// CreateContainer creates a new container based on the given configuration.
func (s *Server) CreateContainer(ctx context.Context, req *pb.CreateContainerRequest) (*pb.CreateContainerResponse, error) {
	var config container.Config
	var hostConfig container.HostConfig
	var networkConfig network.NetworkingConfig
	var platform ocispec.Platform

	// Unmarshal configurations from the request.
	if err := json.Unmarshal(req.Config, &config); err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "unmarshal container config: %v", err)
	}
	if err := json.Unmarshal(req.HostConfig, &hostConfig); err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "unmarshal host config: %v", err)
	}
	if err := json.Unmarshal(req.NetworkConfig, &networkConfig); err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "unmarshal network config: %v", err)
	}
	if err := json.Unmarshal(req.Platform, &platform); err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "unmarshal platform: %v", err)
	}

	resp, err := s.client.ContainerCreate(ctx, &config, &hostConfig, &networkConfig, &platform, req.Name)
	if err != nil {
		if errdefs.IsNotFound(err) {
			return nil, status.Error(codes.NotFound, err.Error())
		}
		return nil, status.Error(codes.Internal, err.Error())
	}

	respBytes, err := json.Marshal(resp)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "marshal response: %v", err)
	}

	return &pb.CreateContainerResponse{Response: respBytes}, nil
}

// InspectContainer returns the container information for the given container ID.
func (s *Server) InspectContainer(ctx context.Context, req *pb.InspectContainerRequest) (*pb.InspectContainerResponse, error) {
	resp, err := s.client.ContainerInspect(ctx, req.Id)
	if err != nil {
		if errdefs.IsNotFound(err) {
			return nil, status.Error(codes.NotFound, err.Error())
		}
		return nil, status.Error(codes.Internal, err.Error())
	}

	respBytes, err := json.Marshal(resp)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "marshal response: %v", err)
	}

	return &pb.InspectContainerResponse{Response: respBytes}, nil
}

// StartContainer starts a container with the given ID and options.
func (s *Server) StartContainer(ctx context.Context, req *pb.StartContainerRequest) (*emptypb.Empty, error) {
	// Wait for Docker network to be ready before starting the container
	if s.waitForNetworkReady != nil {
		if err := s.waitForNetworkReady(ctx); err != nil {
			return nil, status.Errorf(codes.Unavailable, "Docker network not ready: %v", err)
		}
	} else if s.networkReady != nil && !s.networkReady() {
		return nil, status.Errorf(codes.Unavailable, "Docker network not ready")
	}

	var opts container.StartOptions
	if len(req.Options) > 0 {
		if err := json.Unmarshal(req.Options, &opts); err != nil {
			return nil, status.Errorf(codes.InvalidArgument, "unmarshal options: %v", err)
		}
	}

	if err := s.client.ContainerStart(ctx, req.Id, opts); err != nil {
		if errdefs.IsNotFound(err) {
			return nil, status.Error(codes.NotFound, err.Error())
		}
		return nil, status.Error(codes.Internal, err.Error())
	}

	return &emptypb.Empty{}, nil
}

// StopContainer stops a container with the given ID and options.
func (s *Server) StopContainer(ctx context.Context, req *pb.StopContainerRequest) (*emptypb.Empty, error) {
	var opts container.StopOptions
	if len(req.Options) > 0 {
		if err := json.Unmarshal(req.Options, &opts); err != nil {
			return nil, status.Errorf(codes.InvalidArgument, "unmarshal options: %v", err)
		}
	}

	if err := s.client.ContainerStop(ctx, req.Id, opts); err != nil {
		if errdefs.IsNotFound(err) {
			return nil, status.Error(codes.NotFound, err.Error())
		}
		return nil, status.Error(codes.Internal, err.Error())
	}

	return &emptypb.Empty{}, nil
}

func (s *Server) ListContainers(ctx context.Context, req *pb.ListContainersRequest) (*pb.ListContainersResponse, error) {
	var opts container.ListOptions
	if len(req.Options) > 0 {
		if err := json.Unmarshal(req.Options, &opts); err != nil {
			return nil, status.Errorf(codes.InvalidArgument, "unmarshal options: %v", err)
		}

		// Handle filters separately because they implement custom JSON unmarshalling.
		var raw map[string]json.RawMessage
		if err := json.Unmarshal(req.Options, &raw); err != nil {
			return nil, status.Errorf(codes.InvalidArgument, "unmarshal options to raw map: %v", err)
		}

		if filtersBytes, ok := raw["Filters"]; ok {
			args, err := filters.FromJSON(string(filtersBytes))
			if err != nil {
				return nil, status.Errorf(codes.InvalidArgument, "unmarshal filters: %v", err)
			}
			opts.Filters = args
		}
	}

	containerSummaries, err := s.client.ContainerList(ctx, opts)
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}
	containers := make([]container.InspectResponse, 0, len(containerSummaries))
	for _, cs := range containerSummaries {
		c, err := s.client.ContainerInspect(ctx, cs.ID)
		if err != nil {
			if errdefs.IsNotFound(err) {
				// The listed container may have been removed while we were inspecting other containers.
				continue
			}
			return nil, status.Errorf(codes.Internal, "inspect container %s: %v", cs.ID, err)
		}
		containers = append(containers, c)
	}

	containersBytes, err := json.Marshal(containers)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "marshal containers: %v", err)
	}

	return &pb.ListContainersResponse{
		Messages: []*pb.MachineContainers{
			{
				Containers: containersBytes,
			},
		},
	}, nil
}

// RemoveContainer stops (kills after grace period) and removes a container with the given ID.
func (s *Server) RemoveContainer(ctx context.Context, req *pb.RemoveContainerRequest) (*emptypb.Empty, error) {
	var opts container.RemoveOptions
	if len(req.Options) > 0 {
		if err := json.Unmarshal(req.Options, &opts); err != nil {
			return nil, status.Errorf(codes.InvalidArgument, "unmarshal options: %v", err)
		}
	}

	if err := s.client.ContainerRemove(ctx, req.Id, opts); err != nil {
		if errdefs.IsNotFound(err) {
			return nil, status.Error(codes.NotFound, err.Error())
		}
		return nil, status.Error(codes.Internal, err.Error())
	}

	return &emptypb.Empty{}, nil
}

func (s *Server) PullImage(req *pb.PullImageRequest, stream grpc.ServerStreamingServer[pb.JSONMessage]) error {
	ctx := stream.Context()

	var opts image.PullOptions
	if len(req.Options) > 0 {
		if err := json.Unmarshal(req.Options, &opts); err != nil {
			return status.Errorf(codes.InvalidArgument, "unmarshal options: %v", err)
		}
	}

	if opts.RegistryAuth == "" {
		// Try to retrieve the authentication token for the image from the default local Docker config file.
		dockerConfig := dockerconfig.LoadDefaultConfigFile(os.Stderr)
		if encodedAuth, err := dockercommand.RetrieveAuthTokenFromImage(dockerConfig, req.Image); err == nil {
			opts.RegistryAuth = encodedAuth
		}
	}

	respBody, err := s.client.ImagePull(ctx, req.Image, opts)
	if err != nil {
		return status.Error(codes.Internal, err.Error())
	}
	defer respBody.Close()

	decoder := json.NewDecoder(respBody)
	errCh := make(chan error, 1)

	go func() {
		var raw json.RawMessage
		for {
			if err = decoder.Decode(&raw); err != nil {
				if errors.Is(err, io.EOF) {
					errCh <- nil
					return
				}
				errCh <- status.Errorf(codes.Internal, "decode image pull message: %v", err)
				return
			}

			if err = stream.Send(&pb.JSONMessage{Message: raw}); err != nil {
				errCh <- status.Errorf(codes.Internal, "send image pull message to stream: %v", err)
				return
			}
		}
	}()

	for {
		select {
		case err = <-errCh:
			return err
		case <-ctx.Done():
			return status.Error(codes.Canceled, ctx.Err().Error())
		}
	}
}

// InspectImage returns the image information for the given image ID.
func (s *Server) InspectImage(ctx context.Context, req *pb.InspectImageRequest) (*pb.InspectImageResponse, error) {
	resp, err := s.client.ImageInspect(ctx, req.Id)
	if err != nil {
		if errdefs.IsNotFound(err) {
			return nil, status.Error(codes.NotFound, err.Error())
		}
		return nil, status.Error(codes.Internal, err.Error())
	}

	respBytes, err := json.Marshal(resp)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "marshal response: %v", err)
	}

	return &pb.InspectImageResponse{
		Messages: []*pb.Image{
			{
				Image: respBytes,
			},
		},
	}, nil
}

// InspectRemoteImage returns the image metadata for an image in a remote registry using the machine's Docker auth
// credentials if necessary.
func (s *Server) InspectRemoteImage(
	_ context.Context, req *pb.InspectRemoteImageRequest,
) (*pb.InspectRemoteImageResponse, error) {
	ref, err := name.ParseReference(req.Id)
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "parse image: %v", err)
	}

	desc, err := remote.Get(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain))
	if err != nil {
		return nil, status.Errorf(codes.Internal, "fetch image manifest: %v", err)
	}

	namedRef, err := reference.ParseNormalizedNamed(ref.String())
	if err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "parse image: %v", err)
	}

	var canonicalRef reference.Canonical
	if _, ok := namedRef.(reference.Canonical); ok {
		canonicalRef = namedRef.(reference.Canonical)
	} else {
		if canonicalRef, err = reference.WithDigest(namedRef, digest.Digest(desc.Digest.String())); err != nil {
			return nil, status.Errorf(codes.Internal, "add digest to image: %v", err)
		}
	}

	return &pb.InspectRemoteImageResponse{
		Messages: []*pb.RemoteImage{
			{
				Reference: reference.FamiliarString(canonicalRef),
				Manifest:  desc.Manifest,
			},
		},
	}, nil
}

// ListImages returns a list of all images matching the filter and indicates whether Docker is using the containerd
// image store.
func (s *Server) ListImages(ctx context.Context, req *pb.ListImagesRequest) (*pb.ListImagesResponse, error) {
	var opts image.ListOptions
	if len(req.Options) > 0 {
		if err := json.Unmarshal(req.Options, &opts); err != nil {
			return nil, status.Errorf(codes.InvalidArgument, "unmarshal options: %v", err)
		}

		// Handle filters separately because they implement custom JSON unmarshalling.
		var raw map[string]json.RawMessage
		if err := json.Unmarshal(req.Options, &raw); err != nil {
			return nil, status.Errorf(codes.InvalidArgument, "unmarshal options to raw map: %v", err)
		}

		if filtersBytes, ok := raw["Filters"]; ok {
			args, err := filters.FromJSON(string(filtersBytes))
			if err != nil {
				return nil, status.Errorf(codes.InvalidArgument, "unmarshal filters: %v", err)
			}
			opts.Filters = args
		}
	}

	images, err := s.service.ListImages(ctx, opts)
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}

	machineImages := pb.MachineImages{
		ContainerdStore: images.ContainerdStore,
	}
	if len(images.Images) > 0 {
		if machineImages.Images, err = json.Marshal(images.Images); err != nil {
			return nil, status.Errorf(codes.Internal, "marshal Docker images: %v", err)
		}
	}

	return &pb.ListImagesResponse{
		Messages: []*pb.MachineImages{&machineImages},
	}, nil
}

// CreateVolume creates a new volume with the given options.
func (s *Server) CreateVolume(ctx context.Context, req *pb.CreateVolumeRequest) (*pb.CreateVolumeResponse, error) {
	var opts volume.CreateOptions
	if err := json.Unmarshal(req.Options, &opts); err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "unmarshal options: %v", err)
	}

	// Always add the managed label to the volume to indicate that it is managed by Uncloud.
	if opts.Labels == nil {
		opts.Labels = make(map[string]string)
	}
	opts.Labels[api.LabelManaged] = ""

	vol, err := s.client.VolumeCreate(ctx, opts)
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}

	volBytes, err := json.Marshal(vol)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "marshal volume: %v", err)
	}

	return &pb.CreateVolumeResponse{Volume: volBytes}, nil
}

// ListVolumes returns a list of all volumes matching the filter.
func (s *Server) ListVolumes(ctx context.Context, req *pb.ListVolumesRequest) (*pb.ListVolumesResponse, error) {
	var opts volume.ListOptions
	if len(req.Options) > 0 {
		if err := json.Unmarshal(req.Options, &opts); err != nil {
			return nil, status.Errorf(codes.InvalidArgument, "unmarshal options: %v", err)
		}

		// Handle filters separately because they implement custom JSON unmarshalling.
		var raw map[string]json.RawMessage
		if err := json.Unmarshal(req.Options, &raw); err != nil {
			return nil, status.Errorf(codes.InvalidArgument, "unmarshal options to raw map: %v", err)
		}

		if filtersBytes, ok := raw["Filters"]; ok {
			args, err := filters.FromJSON(string(filtersBytes))
			if err != nil {
				return nil, status.Errorf(codes.InvalidArgument, "unmarshal filters: %v", err)
			}
			opts.Filters = args
		}
	}

	resp, err := s.client.VolumeList(ctx, opts)
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}

	respBytes, err := json.Marshal(resp)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "marshal response: %v", err)
	}

	return &pb.ListVolumesResponse{
		Messages: []*pb.MachineVolumes{
			{
				Response: respBytes,
			},
		},
	}, nil
}

// RemoveVolume removes a volume with the given ID.
func (s *Server) RemoveVolume(ctx context.Context, req *pb.RemoveVolumeRequest) (*emptypb.Empty, error) {
	if err := s.client.VolumeRemove(ctx, req.Id, req.Force); err != nil {
		if errdefs.IsNotFound(err) {
			return nil, status.Error(codes.NotFound, err.Error())
		}
		return nil, status.Error(codes.Internal, err.Error())
	}

	return &emptypb.Empty{}, nil
}

// CreateServiceContainer creates a new container for the service with the given specifications.
// TODO: move the main logic to the Docker service and remove db dependency from the server.
func (s *Server) CreateServiceContainer(
	ctx context.Context, req *pb.CreateServiceContainerRequest,
) (*pb.CreateContainerResponse, error) {
	if !api.ValidateServiceID(req.ServiceId) {
		return nil, status.Errorf(codes.InvalidArgument, "invalid service ID: '%s'", req.ServiceId)
	}

	var spec api.ServiceSpec
	if err := json.Unmarshal(req.ServiceSpec, &spec); err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "unmarshal service spec: %v", err)
	}
	spec = spec.SetDefaults()
	if err := spec.Validate(); err != nil {
		return nil, status.Errorf(codes.InvalidArgument, "invalid service spec: %v", err)
	}

	containerName := req.ContainerName
	if containerName == "" {
		suffix, err := secret.RandomAlphaNumeric(4)
		if err != nil {
			return nil, fmt.Errorf("generate random suffix: %w", err)
		}
		containerName = fmt.Sprintf("%s-%s", spec.Name, suffix)
	}

	envVars := maps.Clone(spec.Container.Env)
	if envVars == nil {
		envVars = make(api.EnvVars)
	}

	// Inject the machine ID if available
	if s.machineID != nil {
		if machineID := s.machineID(); machineID != "" {
			envVars["UNCLOUD_MACHINE_ID"] = machineID
		}
	}

	config := &container.Config{
		Cmd:        spec.Container.Command,
		Env:        envVars.ToSlice(),
		Entrypoint: spec.Container.Entrypoint,
		Hostname:   containerName,
		Image:      spec.Container.Image,
		Labels: map[string]string{
			api.LabelServiceID:   req.ServiceId,
			api.LabelServiceName: spec.Name,
			api.LabelServiceMode: spec.Mode,
			api.LabelManaged:     "",
		},
		User: spec.Container.User,
	}
	if spec.Mode == "" {
		config.Labels[api.LabelServiceMode] = api.ServiceModeReplicated
	}
	if hc := spec.Container.Healthcheck; hc != nil {
		if hc.Disable {
			config.Healthcheck = &container.HealthConfig{
				Test: []string{"NONE"},
			}
		} else {
			config.Healthcheck = &container.HealthConfig{
				Test:          hc.Test,
				Interval:      hc.Interval,
				Timeout:       hc.Timeout,
				StartPeriod:   hc.StartPeriod,
				StartInterval: hc.StartInterval,
				Retries:       int(hc.Retries),
			}
		}
	}

	// TODO: do not set the ports as container labels once migrated to retrieve them from the spec in DB.
	var err error
	if len(spec.Ports) > 0 {
		encodedPorts := make([]string, len(spec.Ports))
		for i, p := range spec.Ports {
			encodedPorts[i], err = p.String()
			if err != nil {
				return nil, fmt.Errorf("encode service port spec: %w", err)
			}
		}

		config.Labels[api.LabelServicePorts] = strings.Join(encodedPorts, ",")
	}

	mounts, err := ToDockerMounts(spec.Volumes, spec.Container.VolumeMounts)
	if err != nil {
		return nil, err
	}
	if err = s.verifyDockerVolumesExist(ctx, mounts); err != nil {
		return nil, err
	}

	portBindings := make(nat.PortMap)
	for _, p := range spec.Ports {
		if p.Mode != api.PortModeHost {
			continue
		}
		port := nat.Port(fmt.Sprintf("%d/%s", p.ContainerPort, p.Protocol))
		portBindings[port] = []nat.PortBinding{
			{
				HostPort: strconv.Itoa(int(p.PublishedPort)),
			},
		}
		if p.HostIP.IsValid() {
			portBindings[port][0].HostIP = p.HostIP.String()
		}

		if p.HostPrefix.IsValid() {
			addrs, err := addrOfPrefix(p.HostPrefix)
			if err != nil {
				return nil, err
			}
			// p.HostIP was not valid, so the first IP can be set in the above added PortBindings, the rest is
			// just appended.
			portBindings[port][0].HostIP = addrs[0]
			for _, addr := range addrs[1:] {
				portBindings[port] = append(portBindings[port], nat.PortBinding{
					HostPort: strconv.Itoa(int(p.PublishedPort)),
					HostIP:   addr,
				})
			}
		}
	}

	hostConfig := &container.HostConfig{
		CapAdd:       spec.Container.CapAdd,
		CapDrop:      spec.Container.CapDrop,
		Binds:        spec.Container.Volumes,
		Init:         spec.Container.Init,
		Mounts:       mounts,
		PidMode:      container.PidMode(spec.Container.PidMode),
		PortBindings: portBindings,
		Privileged:   spec.Container.Privileged,
		Resources: container.Resources{
			NanoCPUs:          spec.Container.Resources.CPU,
			Memory:            spec.Container.Resources.Memory,
			MemoryReservation: spec.Container.Resources.MemoryReservation,
			Devices:           toDockerDevices(spec.Container.Resources.Devices),
			DeviceRequests:    spec.Container.Resources.DeviceReservations,
			Ulimits:           toDockerUlimits(spec.Container.Resources.Ulimits),
		},
		// Restart service containers if they exit or a machine restarts unless they are explicitly stopped.
		// For one-off containers and batch jobs we plan to use a different service type/mode.
		RestartPolicy: container.RestartPolicy{
			Name: container.RestartPolicyUnlessStopped,
		},
		ShmSize: spec.Container.Resources.SharedMemory,
		Sysctls: spec.Container.Sysctls,
	}

	// Configure the container to use the internal DNS server if it's available.
	dnsIP := s.internalDNSIP()
	if dnsIP.IsValid() {
		hostConfig.DNS = []string{dnsIP.String()}
		// Optimize DNS resolution for service discovery by appending the search domain to names without a dot.
		// For example, the first attempt for "my-service" will be "my-service.internal".
		hostConfig.DNSOptions = []string{"ndots:1"}
		hostConfig.DNSSearch = []string{dns.InternalDomain}
	}

	if spec.Container.LogDriver != nil {
		hostConfig.LogConfig = container.LogConfig{
			Type:   spec.Container.LogDriver.Name,
			Config: spec.Container.LogDriver.Options,
		}
	}

	// Strip the container configuration that doesn't make sense for the pre-deploy hook and apply its overrides.
	if spec.PreDeploy != nil && req.ContainerType == pb.CreateServiceContainerRequest_PRE_DEPLOY {
		config.Labels = map[string]string{
			api.LabelServiceID:   req.ServiceId,
			api.LabelServiceName: spec.Name,
			api.LabelHook:        api.LabelHookPreDeploy,
			api.LabelManaged:     "",
		}
		config.Healthcheck = &container.HealthConfig{
			Test: []string{"NONE"},
		}
		hostConfig.PortBindings = nil
		hostConfig.RestartPolicy = container.RestartPolicy{
			Name: container.RestartPolicyDisabled,
		}

		// Apply the pre-deploy hook overrides.
		config.Cmd = spec.PreDeploy.Command

		maps.Copy(envVars, spec.PreDeploy.Env)
		envVars["UNCLOUD_HOOK_PRE_DEPLOY"] = "true"
		config.Env = envVars.ToSlice()

		if spec.PreDeploy.Privileged != nil {
			hostConfig.Privileged = *spec.PreDeploy.Privileged
		}
		if spec.PreDeploy.User != "" {
			config.User = spec.PreDeploy.User
		}
	}

	networkConfig := &network.NetworkingConfig{
		EndpointsConfig: map[string]*network.EndpointSettings{
			NetworkName: {},
		},
	}

	resp, err := s.client.ContainerCreate(ctx, config, hostConfig, networkConfig, nil, containerName)
	if err != nil {
		if errdefs.IsNotFound(err) {
			return nil, status.Error(codes.NotFound, err.Error())
		}
		return nil, status.Error(codes.Internal, err.Error())
	}

	// Inject configs into the created container
	if err = s.injectConfigs(ctx, resp.ID, spec.Configs, spec.Container.ConfigMounts); err != nil {
		// Remove the container if config injection fails
		_ = s.client.ContainerRemove(ctx, resp.ID, container.RemoveOptions{RemoveVolumes: true})
		return nil, status.Errorf(codes.Internal, "inject configs: %v", err)
	}

	respBytes, err := json.Marshal(resp)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "marshal response: %v", err)
	}

	// Store the container spec in the database or remove the container with its anonymous volumes if storing fails.
	removeContainer := func() {
		_ = s.client.ContainerRemove(ctx, resp.ID, container.RemoveOptions{RemoveVolumes: true})
	}

	specBytes, err := json.Marshal(spec)
	if err != nil {
		removeContainer()
		return nil, status.Errorf(codes.Internal, "marshal service spec: %v", err)
	}

	if _, err = s.db.ExecContext(ctx, `INSERT INTO containers (id, service_spec) VALUES ($1, $2)`,
		resp.ID, string(specBytes)); err != nil {
		removeContainer()
		return nil, status.Errorf(codes.Internal, "store container in database: %v", err)
	}

	return &pb.CreateContainerResponse{Response: respBytes}, nil
}

func ToDockerMounts(volumes []api.VolumeSpec, mounts []api.VolumeMount) ([]mount.Mount, error) {
	normalisedVolumes := make([]api.VolumeSpec, len(volumes))
	for i, v := range volumes {
		normalisedVolumes[i] = v.SetDefaults()
	}

	dockerMounts := make([]mount.Mount, 0, len(mounts))
	for _, m := range mounts {
		idx := slices.IndexFunc(normalisedVolumes, func(v api.VolumeSpec) bool {
			return v.Name == m.VolumeName
		})
		if idx == -1 {
			return nil, fmt.Errorf("volume mount references a volume that doesn't exist in the volumes spec: '%s'",
				m.VolumeName)
		}

		vol := normalisedVolumes[idx]
		if err := vol.Validate(); err != nil {
			return nil, fmt.Errorf("invalid volume: %w", err)
		}

		dm := mount.Mount{
			Type:     mount.Type(vol.Type),
			Target:   m.ContainerPath,
			ReadOnly: m.ReadOnly,
		}

		switch vol.Type {
		case api.VolumeTypeBind:
			dm.Source = vol.BindOptions.HostPath
			dm.BindOptions = toDockerBindOptions(vol.BindOptions)
		case api.VolumeTypeVolume:
			dm.Source = vol.DockerVolumeName()
			dm.VolumeOptions = &mount.VolumeOptions{
				NoCopy:       vol.VolumeOptions.NoCopy,
				Labels:       vol.VolumeOptions.Labels,
				Subpath:      vol.VolumeOptions.SubPath,
				DriverConfig: vol.VolumeOptions.Driver,
			}
		case api.VolumeTypeTmpfs:
			dm.TmpfsOptions = vol.TmpfsOptions
		default:
			return nil, fmt.Errorf("unsupported volume type: '%s'", vol.Type)
		}

		dockerMounts = append(dockerMounts, dm)
	}

	return dockerMounts, nil
}

// injectConfigs writes config content directly into the container.
// It processes ConfigSpecs and ConfigMounts to mount configuration content into the container filesystem.
func (s *Server) injectConfigs(ctx context.Context, containerID string, configs []api.ConfigSpec, mounts []api.ConfigMount) error {
	if len(configs) == 0 || len(mounts) == 0 {
		return nil
	}

	if err := api.ValidateConfigsAndMounts(configs, mounts); err != nil {
		return fmt.Errorf("validate configs and mounts: %w", err)
	}

	// Create a map of config name to config spec for quick lookup
	configMap := make(map[string]api.ConfigSpec)
	for _, config := range configs {
		configMap[config.Name] = config
	}

	// Process each config mount
	for _, m := range mounts {
		config, exists := configMap[m.ConfigName]
		if !exists {
			return fmt.Errorf("config mount references a config that doesn't exist: '%s'", m.ConfigName)
		}

		// Determine target path in container
		targetPath := m.ContainerPath
		if targetPath == "" {
			// This is the default from the Compose spec
			targetPath = filepath.Join("/", m.ConfigName)
		}

		// Determine file mode
		fileMode := os.FileMode(0o444) // Default permissions
		if m.Mode != nil {
			fileMode = *m.Mode
		}

		uid, err := m.GetNumericUid()
		if err != nil {
			return fmt.Errorf("invalid Uid: %w", err)
		}

		gid, err := m.GetNumericGid()
		if err != nil {
			return fmt.Errorf("invalid Gid: %w", err)
		}

		// Copy the config content directly into the container
		if err := s.copyContentToContainer(
			ctx, containerID, config.Content, targetPath, uid, gid, fileMode,
		); err != nil {
			return fmt.Errorf("copy config file '%s' to container: %w", config.Name, err)
		}

		slog.Debug("Injected config into container",
			"config", config.Name,
			"container", containerID[:12],
			"target", targetPath)
	}

	return nil
}

// copyContentToContainer copies content directly to a file in the container using Docker's CopyToContainer API.
// It will create any intermediate directories in the target path that don't exist.
func (s *Server) copyContentToContainer(ctx context.Context, containerID string, content []byte, targetPath string, uid *uint64, gid *uint64, fileMode os.FileMode) error {
	var buf bytes.Buffer
	tw := tar.NewWriter(&buf)

	// Trim leading slash(es) to avoid double slashes in the tar path
	tarPath := strings.TrimPrefix(targetPath, "/")

	// Create tar header with full path
	header := &tar.Header{
		Name:     tarPath,
		Size:     int64(len(content)),
		Mode:     int64(fileMode),
		ModTime:  time.Now(),
		Typeflag: tar.TypeReg,
	}

	// Set ownership if specified
	if uid != nil {
		header.Uid = int(*uid)
	}
	if gid != nil {
		header.Gid = int(*gid)
	}

	// Write header and content to tar archive
	if err := tw.WriteHeader(header); err != nil {
		return fmt.Errorf("write tar header: %w", err)
	}
	if _, err := tw.Write(content); err != nil {
		return fmt.Errorf("write content to tar: %w", err)
	}
	if err := tw.Close(); err != nil {
		return fmt.Errorf("close tar writer: %w", err)
	}

	// Always extract to root. The tar archive contains the full path, so tar will
	// automatically create any intermediate directories that don't exist in the container.
	if err := s.client.CopyToContainer(
		ctx,
		containerID,
		"/",
		&buf,
		container.CopyToContainerOptions{CopyUIDGID: true},
	); err != nil {
		return fmt.Errorf("copy to container: %w", err)
	}

	return nil
}

func toDockerBindOptions(opts *api.BindOptions) *mount.BindOptions {
	if opts == nil {
		return nil
	}

	dockerOpts := &mount.BindOptions{
		Propagation:      opts.Propagation,
		CreateMountpoint: opts.CreateHostPath,
	}

	switch opts.Recursive {
	case "disabled":
		dockerOpts.NonRecursive = true
	case "writable":
		dockerOpts.ReadOnlyNonRecursive = true
	case "readonly":
		dockerOpts.ReadOnlyForceRecursive = true
	}

	return dockerOpts
}

func toDockerUlimits(ulimits map[string]api.Ulimit) []*units.Ulimit {
	if len(ulimits) == 0 {
		return nil
	}

	dockerUlimits := make([]*units.Ulimit, 0, len(ulimits))
	for name, u := range ulimits {
		dockerUlimits = append(dockerUlimits, &units.Ulimit{
			Name: name,
			Soft: u.Soft,
			Hard: u.Hard,
		})
	}

	return dockerUlimits
}

func toDockerDevices(devices []api.DeviceMapping) []container.DeviceMapping {
	if len(devices) == 0 {
		return nil
	}

	dockerDevices := make([]container.DeviceMapping, 0, len(devices))
	for _, d := range devices {
		dockerDevices = append(dockerDevices, container.DeviceMapping{
			PathOnHost:        d.HostPath,
			PathInContainer:   d.ContainerPath,
			CgroupPermissions: d.CgroupPermissions,
		})
	}

	return dockerDevices
}

// verifyDockerVolumesExist checks if the Docker named volumes referenced in the mounts exist on the machine.
func (s *Server) verifyDockerVolumesExist(ctx context.Context, mounts []mount.Mount) error {
	for _, m := range mounts {
		if m.Type != mount.TypeVolume {
			continue
		}

		// TODO: non-local volume drivers should likely be handled differently (needs proper investigation).
		if _, err := s.client.VolumeInspect(ctx, m.Source); err != nil {
			if errdefs.IsNotFound(err) {
				return status.Errorf(codes.NotFound, "volume '%s' not found", m.Source)
			}
			return status.Errorf(codes.Internal, "inspect volume '%s': %v", m.Source, err.Error())
		}
		// TODO: check if the volume driver and options are the same as in the mount and fail if not.
		//  Should we even ignore driver-specific options in the volume spec for externally managed volumes?
		//  Instead, just inspect the existing volume and construct the mount from it.
	}

	return nil
}

// InspectServiceContainer returns the container information and service specification that was used to create the
// container with the given ID.
func (s *Server) InspectServiceContainer(
	ctx context.Context, req *pb.InspectContainerRequest,
) (*pb.ServiceContainer, error) {
	serviceCtr, err := s.service.InspectServiceContainer(ctx, req.Id)
	if err != nil {
		if errdefs.IsNotFound(err) {
			return nil, status.Error(codes.NotFound, err.Error())
		}
		return nil, status.Error(codes.Internal, err.Error())
	}

	ctrBytes, err := json.Marshal(serviceCtr.Container)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "marshal container: %v", err)
	}

	specBytes, err := json.Marshal(serviceCtr.ServiceSpec)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "marshal service spec: %v", err)
	}

	return &pb.ServiceContainer{
		Container:   ctrBytes,
		ServiceSpec: specBytes,
	}, nil
}

// ListServiceContainers returns all containers that belong to the service with the given name or ID.
// If req.ServiceId is empty, all service containers are returned.
func (s *Server) ListServiceContainers(
	ctx context.Context, req *pb.ListServiceContainersRequest,
) (*pb.ListServiceContainersResponse, error) {
	var opts container.ListOptions
	if len(req.Options) > 0 {
		if err := json.Unmarshal(req.Options, &opts); err != nil {
			return nil, status.Errorf(codes.InvalidArgument, "unmarshal options: %v", err)
		}

		// Handle filters separately because they implement custom JSON unmarshalling.
		var raw map[string]json.RawMessage
		if err := json.Unmarshal(req.Options, &raw); err != nil {
			return nil, status.Errorf(codes.InvalidArgument, "unmarshal options to raw map: %v", err)
		}

		if filtersBytes, ok := raw["Filters"]; ok {
			args, err := filters.FromJSON(string(filtersBytes))
			if err != nil {
				return nil, status.Errorf(codes.InvalidArgument, "unmarshal filters: %v", err)
			}
			opts.Filters = args
		}
	}

	result, err := s.service.ListServiceContainers(ctx, req.ServiceId, opts)
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}

	pbContainers, err := serviceContainersToProto(result.Containers)
	if err != nil {
		return nil, err
	}
	pbHookContainers, err := serviceContainersToProto(result.HookContainers)
	if err != nil {
		return nil, err
	}

	return &pb.ListServiceContainersResponse{
		Messages: []*pb.MachineServiceContainers{
			{
				Containers:     pbContainers,
				HookContainers: pbHookContainers,
			},
		},
	}, nil
}

// serviceContainersToProto converts a slice of service containers to protobuf format.
func serviceContainersToProto(containers []api.ServiceContainer) ([]*pb.ServiceContainer, error) {
	pbContainers := make([]*pb.ServiceContainer, 0, len(containers))
	for _, ctr := range containers {
		ctrBytes, err := json.Marshal(ctr.Container)
		if err != nil {
			return nil, status.Errorf(codes.Internal, "marshal container: %v", err)
		}
		specBytes, err := json.Marshal(ctr.ServiceSpec)
		if err != nil {
			return nil, status.Errorf(codes.Internal, "marshal service spec: %v", err)
		}
		pbContainers = append(pbContainers, &pb.ServiceContainer{
			Container:   ctrBytes,
			ServiceSpec: specBytes,
		})
	}

	return pbContainers, nil
}

// RemoveServiceContainer stops (kills after grace period) and removes a service container with the given ID.
// The difference between this method and RemoveContainer is that it also removes the container from the machine
// database.
func (s *Server) RemoveServiceContainer(ctx context.Context, req *pb.RemoveContainerRequest) (*emptypb.Empty, error) {
	ctrID := req.Id
	// If the ID is not a full Docker ID, inspect the container to get its full ID.
	if !fullDockerIDRegex.MatchString(req.Id) {
		ctr, err := s.client.ContainerInspect(ctx, req.Id)
		if err != nil {
			if errdefs.IsNotFound(err) {
				return nil, status.Error(codes.NotFound, err.Error())
			}
			return nil, status.Error(codes.Internal, err.Error())
		}
		ctrID = ctr.ID
	}

	resp, err := s.RemoveContainer(ctx, req)
	if err != nil {
		return nil, err
	}

	if _, err = s.db.ExecContext(ctx, `DELETE FROM containers WHERE id = $1`, ctrID); err != nil {
		slog.Error("Failed to remove container from machine database.", "err", err, "id", ctrID)
		// Do not return an error because the container has already been removed from the Docker daemon.
		// The orphaned db record will be ignored and eventually cleaned up by the garbage collector.
	}

	return resp, nil
}

// logsHeartbeatInterval is the interval at which heartbeat entries are sent when there are no logs to stream.
const logsHeartbeatInterval = 200 * time.Millisecond

// ContainerLogs streams logs from a container.
func (s *Server) ContainerLogs(
	req *pb.LogsRequest, stream grpc.ServerStreamingServer[pb.LogEntry],
) error {
	// Stream context is cancelled when the client has disconnected or the stream has ended.
	ctx := stream.Context()

	opts := api.ServiceLogsOptions{
		Follow: req.Follow,
		Tail:   int(req.Tail),
		Since:  req.Since,
		Until:  req.Until,
	}

	logsCh, err := s.service.ContainerLogs(ctx, req.Id, opts)
	if err != nil {
		if errdefs.IsNotFound(err) {
			return status.Error(codes.NotFound, err.Error())
		}
		return status.Errorf(codes.Internal, "get container logs: %v", err)
	}

	log := slog.With("container_id", req.Id, "stream_id", fmt.Sprintf("%p", stream)[2:])
	log.Debug("Starting container logs streaming.",
		"follow", req.Follow, "tail", req.Tail, "since", req.Since, "until", req.Until)

	// Heartbeats are needed only when following logs to let the client know when there are no new log entries
	// to allow it to advance the watermark of last received log timestamp.
	var heartbeatCh <-chan time.Time
	if req.Follow {
		heartbeatTicker := time.NewTicker(logsHeartbeatInterval)
		defer heartbeatTicker.Stop()
		heartbeatCh = heartbeatTicker.C
	}

	started := time.Now()
	lastSent := time.Time{}

	for {
		select {
		case entry, ok := <-logsCh:
			if !ok {
				// Channel closed, no more log entries.
				return nil
			}

			if entry.Err != nil {
				return status.Error(codes.Internal, entry.Err.Error())
			}

			pbEntry := &pb.LogEntry{
				Stream:    api.LogStreamTypeToProto(entry.Stream),
				Timestamp: timestamppb.New(entry.Timestamp),
				Message:   entry.Message,
			}
			if err = stream.Send(pbEntry); err != nil {
				return status.Errorf(codes.Internal, "send log entry: %v", err)
			}
			lastSent = entry.Timestamp

		case now := <-heartbeatCh:
			// Only send heartbeat if no log entries have been sent since the last heartbeat interval or
			// if no log entries have been sent at all for at least a heartbeat interval since starting.
			if now.Sub(lastSent) < logsHeartbeatInterval ||
				(lastSent.IsZero() && now.Sub(started) < logsHeartbeatInterval) {
				continue
			}

			// Use the timestamp one heartbeat in the past to be conservative. This reduces the chance of sending
			// a timestamp that is greater than a log entry currently being parsed but not yet sent, which would
			// cause the client to incorrectly believe it has received all logs up to that point.
			heartbeat := &pb.LogEntry{
				Stream:    pb.LogEntry_HEARTBEAT,
				Timestamp: timestamppb.New(now.Add(-logsHeartbeatInterval)),
			}
			if err = stream.Send(heartbeat); err != nil {
				return status.Errorf(codes.Internal, "send log stream heartbeat: %v", err)
			}
			lastSent = heartbeat.Timestamp.AsTime()
			log.Debug("Sent log stream heartbeat.", "timestamp", lastSent)

		case <-ctx.Done():
			return status.Error(codes.Canceled, ctx.Err().Error())
		}
	}
}

// receiveExecConfig receives and validates the initial exec configuration from the stream.
func (s *Server) receiveExecConfig(stream pb.Docker_ExecContainerServer) (*pb.ExecConfig, api.ExecOptions, error) {
	req, err := stream.Recv()
	if err != nil {
		return nil, api.ExecOptions{}, status.Errorf(codes.InvalidArgument, "receive config: %v", err)
	}

	execConfig := req.GetConfig()
	if execConfig == nil {
		return nil, api.ExecOptions{}, status.Error(codes.InvalidArgument, "first message must contain exec config")
	}

	// Unmarshal the Uncloud's execOpts
	var execOpts api.ExecOptions
	if err := json.Unmarshal(execConfig.Options, &execOpts); err != nil {
		return nil, api.ExecOptions{}, status.Errorf(codes.InvalidArgument, "unmarshal exec config: %v", err)
	}

	return execConfig, execOpts, nil
}

// handleServerExecInput reads from the gRPC stream and writes to Docker stdin, handling resize requests.
func (s *Server) handleServerExecInput(
	ctx context.Context,
	stream pb.Docker_ExecContainerServer,
	attachConn types.HijackedResponse,
	execID string,
	tty bool,
) error {
	slog.Debug("Input goroutine started", "exec_id", execID, "tty", tty)
	defer slog.Debug("Input goroutine exited", "exec_id", execID)

	defer attachConn.CloseWrite()
	for {
		select {
		case <-ctx.Done():
			slog.Debug("Input goroutine context canceled via context", "exec_id", execID)
			return nil
		default:
		}

		req, err := stream.Recv()
		switch {
		case errors.Is(err, io.EOF):
			slog.Debug("Input goroutine received EOF", "exec_id", execID)
			return nil
		case status.Code(err) == codes.Canceled:
			// Can be the case when the output goroutine ends and the stream context is canceled.
			slog.Debug("Input goroutine context canceled", "exec_id", execID)
			return nil
		case err == nil:
			// continue processing
		default:
			return fmt.Errorf("receive from stream: %w", err)
		}

		switch payload := req.Payload.(type) {
		case *pb.ExecContainerRequest_Stdin:
			if _, err := attachConn.Conn.Write(payload.Stdin); err != nil {
				return fmt.Errorf("write to stdin: %w", err)
			}
		case *pb.ExecContainerRequest_Resize:
			if tty {
				resizeOpts := container.ResizeOptions{
					Height: uint(payload.Resize.Height),
					Width:  uint(payload.Resize.Width),
				}
				if err := s.client.ContainerExecResize(ctx, execID, resizeOpts); err != nil {
					slog.Warn("Failed to resize TTY", "err", err, "exec_id", execID)
				}
			}
		}
	}
}

// handleServerExecOutput reads from Docker stdout/stderr and writes to the gRPC stream.
func (s *Server) handleServerExecOutput(
	stream pb.Docker_ExecContainerServer,
	attachResp types.HijackedResponse,
	execID string,
	tty bool,
) error {
	slog.Debug("Output goroutine started", "exec_id", execID, "tty", tty)
	defer slog.Debug("Output goroutine exited", "exec_id", execID)

	if tty {
		// In TTY mode, all output is stdout - copy directly to stream
		stdoutWriter := &grpcStreamWriter{stream: stream, isStderr: false}
		_, err := io.Copy(stdoutWriter, attachResp.Reader)
		if err != nil && err != io.EOF {
			return fmt.Errorf("copy tty output: %w", err)
		}
		return nil
	} else {
		// In non-TTY mode, Docker multiplexes stdout/stderr with headers
		// Use stdcopy to demultiplex
		slog.Debug("Starting StdCopy for non-TTY", "exec_id", execID)
		stdoutWriter := &grpcStreamWriter{stream: stream, isStderr: false}
		stderrWriter := &grpcStreamWriter{stream: stream, isStderr: true}

		written, err := stdcopy.StdCopy(stdoutWriter, stderrWriter, attachResp.Reader)
		slog.Debug("StdCopy completed", "exec_id", execID, "bytes", written, "err", err)
		if err != nil && err != io.EOF {
			return fmt.Errorf("demultiplex docker output: %w", err)
		}
		return nil
	}
}

// grpcStreamWriter is a writer that sends data to a gRPC stream as stdout or stderr.
type grpcStreamWriter struct {
	stream   pb.Docker_ExecContainerServer
	isStderr bool
}

func (w *grpcStreamWriter) Write(p []byte) (n int, err error) {
	data := make([]byte, len(p))
	copy(data, p)

	var resp *pb.ExecContainerResponse
	if w.isStderr {
		resp = &pb.ExecContainerResponse{
			Payload: &pb.ExecContainerResponse_Stderr{Stderr: data},
		}
	} else {
		resp = &pb.ExecContainerResponse{
			Payload: &pb.ExecContainerResponse_Stdout{Stdout: data},
		}
	}

	if err := w.stream.Send(resp); err != nil {
		return 0, err
	}
	return len(p), nil
}

// ExecContainer executes a command in a running container with bidirectional streaming for stdin/stdout/stderr.
func (s *Server) ExecContainer(stream pb.Docker_ExecContainerServer) error {
	slog.Debug("ExecContainer server-side called")
	defer slog.Debug("ExecContainer server-side ended")

	ctx := stream.Context()

	// Receive and validate configuration
	execConfig, execOpts, err := s.receiveExecConfig(stream)
	if err != nil {
		return err
	}

	// Convert to Docker's ExecOptions
	dockerExecOpts := container.ExecOptions{
		Cmd:          execOpts.Command,
		AttachStdin:  execOpts.AttachStdin,
		AttachStdout: execOpts.AttachStdout,
		AttachStderr: execOpts.AttachStderr,
		Tty:          execOpts.Tty,
		User:         execOpts.User,
		Privileged:   execOpts.Privileged,
		WorkingDir:   execOpts.WorkingDir,
		Env:          execOpts.Env,
	}

	// Create the exec instance
	execResp, err := s.client.ContainerExecCreate(ctx, execConfig.ContainerId, dockerExecOpts)
	if err != nil {
		if errdefs.IsNotFound(err) {
			return status.Error(codes.NotFound, err.Error())
		}
		return status.Errorf(codes.Internal, "create exec: %v", err)
	}

	// Send the exec ID back to the client
	if err := stream.Send(&pb.ExecContainerResponse{
		Payload: &pb.ExecContainerResponse_ExecId{ExecId: execResp.ID},
	}); err != nil {
		return status.Errorf(codes.Internal, "send exec ID: %v", err)
	}
	slog.Debug("Sent exec ID to the client", "exec_id", execResp.ID)

	// For detached mode, start without attaching
	if execOpts.Detach {
		dockerStartOpts := container.ExecStartOptions{
			Tty:    dockerExecOpts.Tty,
			Detach: true,
		}
		if err := s.client.ContainerExecStart(ctx, execResp.ID, dockerStartOpts); err != nil {
			return status.Errorf(codes.Internal, "start exec: %v", err)
		}
		return nil
	}

	// For attached mode, attach to the exec instance
	attachOpts := container.ExecAttachOptions{
		Tty: dockerExecOpts.Tty,
	}
	attachConn, err := s.client.ContainerExecAttach(ctx, execResp.ID, attachOpts)
	if err != nil {
		return status.Errorf(codes.Internal, "attach to exec: %v", err)
	}
	defer attachConn.Close()

	// Create a cancelable context for the input handler
	handlerCtx, cancelInput := context.WithCancel(ctx)
	defer cancelInput()

	// Create a channel to wait for output completion
	outputDone := make(chan error, 1)

	// Start stdin handler if stdin is attached
	if dockerExecOpts.AttachStdin {
		go func() {
			err := s.handleServerExecInput(handlerCtx, stream, attachConn, execResp.ID, dockerExecOpts.Tty)
			if err != nil {
				slog.Warn("Error in exec input handler", "err", err, "exec_id", execResp.ID)
			}
		}()
	} else {
		// If not attaching stdin, close the write side immediately
		attachConn.CloseWrite()
	}

	// Start output handler
	// We only wait for this goroutine to complete - it signals when the exec process finishes
	go func() {
		outputDone <- s.handleServerExecOutput(stream, attachConn, execResp.ID, dockerExecOpts.Tty)
	}()

	// Wait for the output goroutine to complete (it signals when done)
	// We only wait for the output handler goroutine, not for the stdin one.
	if err := <-outputDone; err != nil {
		slog.Warn("Error in exec output handler", "err", err, "exec_id", execResp.ID)
	}
	// Do a best-effort cancellation of the stdin handler.
	// We can't guarantee immediate exit because it may be blocked on stream.Recv(), but at
	// least we want to send a cancel signal explicitly.
	cancelInput()

	inspectResp, err := s.client.ContainerExecInspect(ctx, execResp.ID)
	if err != nil {
		slog.Error("Failed to inspect exec after completion", "err", err, "exec_id", execResp.ID)
		return status.Errorf(codes.Internal, "inspect exec: %v", err)
	}

	// Send the exit code
	slog.Debug("Sending exec exit code", "exec_id", execResp.ID, "exit_code", inspectResp.ExitCode)
	if err := stream.Send(&pb.ExecContainerResponse{
		Payload: &pb.ExecContainerResponse_ExitCode{ExitCode: int32(inspectResp.ExitCode)},
	}); err != nil {
		slog.Error("Failed to send exec exit code", "err", err, "exec_id", execResp.ID)
		return status.Errorf(codes.Internal, "send exit code: %v", err)
	}

	return nil
}
