package incus

import (
	"bufio"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"path/filepath"
	"slices"
	"strings"

	"github.com/gorilla/websocket"
	"github.com/pkg/sftp"

	"github.com/lxc/incus/v6/shared/api"
	"github.com/lxc/incus/v6/shared/cancel"
	"github.com/lxc/incus/v6/shared/ioprogress"
	"github.com/lxc/incus/v6/shared/tcp"
	localtls "github.com/lxc/incus/v6/shared/tls"
	"github.com/lxc/incus/v6/shared/units"
	"github.com/lxc/incus/v6/shared/ws"
)

// Instance handling functions.

// instanceTypeToPath converts the instance type to a URL path prefix and query string values.
func (r *ProtocolIncus) instanceTypeToPath(instanceType api.InstanceType) (string, url.Values, error) {
	v := url.Values{}

	// If a specific instance type has been requested, add the instance-type filter parameter
	// to the returned URL values so that it can be used in the final URL if needed to filter
	// the result set being returned.
	if instanceType != api.InstanceTypeAny {
		v.Set("instance-type", string(instanceType))
	}

	return "/instances", v, nil
}

// GetInstanceNames returns a list of instance names.
func (r *ProtocolIncus) GetInstanceNames(instanceType api.InstanceType) ([]string, error) {
	baseURL, v, err := r.instanceTypeToPath(instanceType)
	if err != nil {
		return nil, err
	}

	// Fetch the raw URL values.
	urls := []string{}
	_, err = r.queryStruct("GET", fmt.Sprintf("%s?%s", baseURL, v.Encode()), nil, "", &urls)
	if err != nil {
		return nil, err
	}

	// Parse it.
	return urlsToResourceNames(baseURL, urls...)
}

// GetInstanceNamesAllProjects returns a list of instance names from all projects.
func (r *ProtocolIncus) GetInstanceNamesAllProjects(instanceType api.InstanceType) (map[string][]string, error) {
	instances := []api.Instance{}

	path, v, err := r.instanceTypeToPath(instanceType)
	if err != nil {
		return nil, err
	}

	v.Set("recursion", "1")
	v.Set("all-projects", "true")

	// Fetch the raw URL values.
	_, err = r.queryStruct("GET", fmt.Sprintf("%s?%s", path, v.Encode()), nil, "", &instances)
	if err != nil {
		return nil, err
	}

	names := map[string][]string{}
	for _, instance := range instances {
		names[instance.Project] = append(names[instance.Project], instance.Name)
	}

	return names, nil
}

// GetInstances returns a list of instances.
func (r *ProtocolIncus) GetInstances(instanceType api.InstanceType) ([]api.Instance, error) {
	instances := []api.Instance{}

	path, v, err := r.instanceTypeToPath(instanceType)
	if err != nil {
		return nil, err
	}

	v.Set("recursion", "1")

	// Fetch the raw value
	_, err = r.queryStruct("GET", fmt.Sprintf("%s?%s", path, v.Encode()), nil, "", &instances)
	if err != nil {
		return nil, err
	}

	return instances, nil
}

// GetInstancesWithFilter returns a filtered list of instances.
func (r *ProtocolIncus) GetInstancesWithFilter(instanceType api.InstanceType, filters []string) ([]api.Instance, error) {
	if !r.HasExtension("api_filtering") {
		return nil, errors.New("The server is missing the required \"api_filtering\" API extension")
	}

	instances := []api.Instance{}

	path, v, err := r.instanceTypeToPath(instanceType)
	if err != nil {
		return nil, err
	}

	v.Set("recursion", "1")
	v.Set("filter", parseFilters(filters))

	// Fetch the raw value
	_, err = r.queryStruct("GET", fmt.Sprintf("%s?%s", path, v.Encode()), nil, "", &instances)
	if err != nil {
		return nil, err
	}

	return instances, nil
}

// GetInstancesAllProjects returns a list of instances from all projects.
func (r *ProtocolIncus) GetInstancesAllProjects(instanceType api.InstanceType) ([]api.Instance, error) {
	instances := []api.Instance{}

	path, v, err := r.instanceTypeToPath(instanceType)
	if err != nil {
		return nil, err
	}

	v.Set("recursion", "1")
	v.Set("all-projects", "true")

	if !r.HasExtension("instance_all_projects") {
		return nil, errors.New("The server is missing the required \"instance_all_projects\" API extension")
	}

	// Fetch the raw value
	_, err = r.queryStruct("GET", fmt.Sprintf("%s?%s", path, v.Encode()), nil, "", &instances)
	if err != nil {
		return nil, err
	}

	return instances, nil
}

// GetInstancesAllProjectsWithFilter returns a filtered list of instances from all projects.
func (r *ProtocolIncus) GetInstancesAllProjectsWithFilter(instanceType api.InstanceType, filters []string) ([]api.Instance, error) {
	if !r.HasExtension("api_filtering") {
		return nil, errors.New("The server is missing the required \"api_filtering\" API extension")
	}

	instances := []api.Instance{}

	path, v, err := r.instanceTypeToPath(instanceType)
	if err != nil {
		return nil, err
	}

	v.Set("recursion", "1")
	v.Set("all-projects", "true")
	v.Set("filter", parseFilters(filters))

	if !r.HasExtension("instance_all_projects") {
		return nil, errors.New("The server is missing the required \"instance_all_projects\" API extension")
	}

	// Fetch the raw value
	_, err = r.queryStruct("GET", fmt.Sprintf("%s?%s", path, v.Encode()), nil, "", &instances)
	if err != nil {
		return nil, err
	}

	return instances, nil
}

// UpdateInstances updates all instances to match the requested state.
func (r *ProtocolIncus) UpdateInstances(state api.InstancesPut, ETag string) (Operation, error) {
	path, v, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, err
	}

	// Send the request
	op, _, err := r.queryOperation("PUT", fmt.Sprintf("%s?%s", path, v.Encode()), state, ETag)
	if err != nil {
		return nil, err
	}

	return op, nil
}

// rebuildInstance initiates a rebuild of a given instance on the Incus Protocol server and returns the corresponding operation or an error.
func (r *ProtocolIncus) rebuildInstance(instanceName string, instance api.InstanceRebuildPost) (Operation, error) {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, err
	}

	// Send the request
	op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s/rebuild", path, url.PathEscape(instanceName)), instance, "")
	if err != nil {
		return nil, err
	}

	return op, nil
}

// tryRebuildInstance attempts to rebuild a specific instance on multiple target servers identified by their URLs.
// It runs the rebuild process asynchronously and returns a RemoteOperation to monitor the progress and any errors.
func (r *ProtocolIncus) tryRebuildInstance(instanceName string, req api.InstanceRebuildPost, urls []string, op Operation) (RemoteOperation, error) {
	if len(urls) == 0 {
		return nil, errors.New("The source server isn't listening on the network")
	}

	rop := remoteOperation{
		chDone: make(chan bool),
	}

	operation := req.Source.Operation

	// Forward targetOp to remote op
	go func() {
		success := false
		var errors []remoteOperationResult
		for _, serverURL := range urls {
			if operation == "" {
				req.Source.Server = serverURL
			} else {
				req.Source.Operation = fmt.Sprintf("%s/1.0/operations/%s", serverURL, url.PathEscape(operation))
			}

			op, err := r.rebuildInstance(instanceName, req)
			if err != nil {
				errors = append(errors, remoteOperationResult{URL: serverURL, Error: err})
				continue
			}

			rop.handlerLock.Lock()
			rop.targetOp = op
			rop.handlerLock.Unlock()

			for _, handler := range rop.handlers {
				_, _ = rop.targetOp.AddHandler(handler)
			}

			err = rop.targetOp.Wait()
			if err != nil {
				errors = append(errors, remoteOperationResult{URL: serverURL, Error: err})
				if localtls.IsConnectionError(err) {
					continue
				}

				break
			}

			success = true
			break
		}

		if !success {
			rop.err = remoteOperationError("Failed instance rebuild", errors)
			if op != nil {
				_ = op.Cancel()
			}
		}

		close(rop.chDone)
	}()

	return &rop, nil
}

// RebuildInstanceFromImage rebuilds an instance from an image.
func (r *ProtocolIncus) RebuildInstanceFromImage(source ImageServer, image api.Image, instanceName string, req api.InstanceRebuildPost) (RemoteOperation, error) {
	err := r.CheckExtension("instances_rebuild")
	if err != nil {
		return nil, err
	}

	info, err := r.getSourceImageConnectionInfo(source, image, &req.Source)
	if err != nil {
		return nil, err
	}

	if info == nil {
		op, err := r.rebuildInstance(instanceName, req)
		if err != nil {
			return nil, err
		}

		rop := remoteOperation{
			targetOp: op,
			chDone:   make(chan bool),
		}

		// Forward targetOp to remote op
		go func() {
			rop.err = rop.targetOp.Wait()
			close(rop.chDone)
		}()

		return &rop, nil
	}

	return r.tryRebuildInstance(instanceName, req, info.Addresses, nil)
}

// RebuildInstance rebuilds an instance as empty.
func (r *ProtocolIncus) RebuildInstance(instanceName string, instance api.InstanceRebuildPost) (op Operation, err error) {
	err = r.CheckExtension("instances_rebuild")
	if err != nil {
		return nil, err
	}

	return r.rebuildInstance(instanceName, instance)
}

// GetInstancesFull returns a list of instances including snapshots, backups and state.
func (r *ProtocolIncus) GetInstancesFull(instanceType api.InstanceType) ([]api.InstanceFull, error) {
	instances := []api.InstanceFull{}

	path, v, err := r.instanceTypeToPath(instanceType)
	if err != nil {
		return nil, err
	}

	v.Set("recursion", "2")

	if !r.HasExtension("container_full") {
		return nil, errors.New("The server is missing the required \"container_full\" API extension")
	}

	// Fetch the raw value
	_, err = r.queryStruct("GET", fmt.Sprintf("%s?%s", path, v.Encode()), nil, "", &instances)
	if err != nil {
		return nil, err
	}

	return instances, nil
}

// GetInstancesFullWithFilter returns a filtered list of instances including snapshots, backups and state.
func (r *ProtocolIncus) GetInstancesFullWithFilter(instanceType api.InstanceType, filters []string) ([]api.InstanceFull, error) {
	if !r.HasExtension("api_filtering") {
		return nil, errors.New("The server is missing the required \"api_filtering\" API extension")
	}

	instances := []api.InstanceFull{}

	path, v, err := r.instanceTypeToPath(instanceType)
	if err != nil {
		return nil, err
	}

	v.Set("recursion", "2")
	v.Set("filter", parseFilters(filters))

	if !r.HasExtension("container_full") {
		return nil, errors.New("The server is missing the required \"container_full\" API extension")
	}

	// Fetch the raw value
	_, err = r.queryStruct("GET", fmt.Sprintf("%s?%s", path, v.Encode()), nil, "", &instances)
	if err != nil {
		return nil, err
	}

	return instances, nil
}

// GetInstancesFullAllProjects returns a list of instances including snapshots, backups and state from all projects.
func (r *ProtocolIncus) GetInstancesFullAllProjects(instanceType api.InstanceType) ([]api.InstanceFull, error) {
	instances := []api.InstanceFull{}

	path, v, err := r.instanceTypeToPath(instanceType)
	if err != nil {
		return nil, err
	}

	v.Set("recursion", "2")
	v.Set("all-projects", "true")

	if !r.HasExtension("container_full") {
		return nil, errors.New("The server is missing the required \"container_full\" API extension")
	}

	if !r.HasExtension("instance_all_projects") {
		return nil, errors.New("The server is missing the required \"instance_all_projects\" API extension")
	}

	// Fetch the raw value
	_, err = r.queryStruct("GET", fmt.Sprintf("%s?%s", path, v.Encode()), nil, "", &instances)
	if err != nil {
		return nil, err
	}

	return instances, nil
}

// GetInstancesFullAllProjectsWithFilter returns a filtered list of instances including snapshots, backups and state from all projects.
func (r *ProtocolIncus) GetInstancesFullAllProjectsWithFilter(instanceType api.InstanceType, filters []string) ([]api.InstanceFull, error) {
	if !r.HasExtension("api_filtering") {
		return nil, errors.New("The server is missing the required \"api_filtering\" API extension")
	}

	instances := []api.InstanceFull{}

	path, v, err := r.instanceTypeToPath(instanceType)
	if err != nil {
		return nil, err
	}

	v.Set("recursion", "2")
	v.Set("all-projects", "true")
	v.Set("filter", parseFilters(filters))

	if !r.HasExtension("container_full") {
		return nil, errors.New("The server is missing the required \"container_full\" API extension")
	}

	if !r.HasExtension("instance_all_projects") {
		return nil, errors.New("The server is missing the required \"instance_all_projects\" API extension")
	}

	// Fetch the raw value
	_, err = r.queryStruct("GET", fmt.Sprintf("%s?%s", path, v.Encode()), nil, "", &instances)
	if err != nil {
		return nil, err
	}

	return instances, nil
}

// GetInstance returns the instance entry for the provided name.
func (r *ProtocolIncus) GetInstance(name string) (*api.Instance, string, error) {
	instance := api.Instance{}

	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, "", err
	}

	// Fetch the raw value
	etag, err := r.queryStruct("GET", fmt.Sprintf("%s/%s", path, url.PathEscape(name)), nil, "", &instance)
	if err != nil {
		return nil, "", err
	}

	return &instance, etag, nil
}

// GetInstanceFull returns the instance entry for the provided name along with snapshot information.
func (r *ProtocolIncus) GetInstanceFull(name string) (*api.InstanceFull, string, error) {
	instance := api.InstanceFull{}

	if !r.HasExtension("instance_get_full") {
		// Backward compatibility.
		ct, _, err := r.GetInstance(name)
		if err != nil {
			return nil, "", err
		}

		cs, _, err := r.GetInstanceState(name)
		if err != nil {
			return nil, "", err
		}

		snaps, err := r.GetInstanceSnapshots(name)
		if err != nil {
			return nil, "", err
		}

		backups, err := r.GetInstanceBackups(name)
		if err != nil {
			return nil, "", err
		}

		instance.Instance = *ct
		instance.State = cs
		instance.Snapshots = snaps
		instance.Backups = backups

		return &instance, "", nil
	}

	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, "", err
	}

	// Fetch the raw value
	etag, err := r.queryStruct("GET", fmt.Sprintf("%s/%s?recursion=1", path, url.PathEscape(name)), nil, "", &instance)
	if err != nil {
		return nil, "", err
	}

	return &instance, etag, nil
}

// CreateInstanceFromBackup is a convenience function to make it easier to
// create a instance from a backup.
func (r *ProtocolIncus) CreateInstanceFromBackup(args InstanceBackupArgs) (Operation, error) {
	if !r.HasExtension("container_backup") {
		return nil, errors.New("The server is missing the required \"container_backup\" API extension")
	}

	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, err
	}

	if args.PoolName == "" && args.Name == "" && args.Config == nil && args.Devices == nil {
		// Send the request
		op, _, err := r.queryOperation("POST", path, args.BackupFile, "")
		if err != nil {
			return nil, err
		}

		return op, nil
	}

	if args.PoolName != "" && !r.HasExtension("container_backup_override_pool") {
		return nil, errors.New(`The server is missing the required "container_backup_override_pool" API extension`)
	}

	if args.Name != "" && !r.HasExtension("backup_override_name") {
		return nil, errors.New(`The server is missing the required "backup_override_name" API extension`)
	}

	if (args.Config != nil || args.Devices != nil) && !r.HasExtension("backup_override_config") {
		return nil, errors.New(`The server is missing the required "backup_override_config" API extension`)
	}

	// Prepare the HTTP request
	reqURL, err := r.setQueryAttributes(fmt.Sprintf("%s/1.0%s", r.httpBaseURL.String(), path))
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("POST", reqURL, args.BackupFile)
	if err != nil {
		return nil, err
	}

	req.Header.Set("Content-Type", "application/octet-stream")

	if args.PoolName != "" {
		req.Header.Set("X-Incus-pool", args.PoolName)
	}

	if args.Name != "" {
		req.Header.Set("X-Incus-name", args.Name)
	}

	if args.Config != nil {
		configOverride := strings.Join(args.Config, " ")
		req.Header.Set("X-Incus-config", configOverride)
	}

	if args.Devices != nil {
		devicesOverride := strings.Join(args.Devices, " ")
		req.Header.Set("X-Incus-devices", devicesOverride)
	}

	// Send the request
	resp, err := r.DoHTTP(req)
	if err != nil {
		return nil, err
	}

	defer func() { _ = resp.Body.Close() }()

	// Handle errors
	response, _, err := incusParseResponse(resp)
	if err != nil {
		return nil, err
	}

	// Get to the operation
	respOperation, err := response.MetadataAsOperation()
	if err != nil {
		return nil, err
	}

	// Setup an Operation wrapper
	op := operation{
		Operation: *respOperation,
		r:         r,
		chActive:  make(chan bool),
	}

	return &op, nil
}

// CreateInstance requests that Incus creates a new instance.
func (r *ProtocolIncus) CreateInstance(instance api.InstancesPost) (Operation, error) {
	path, _, err := r.instanceTypeToPath(instance.Type)
	if err != nil {
		return nil, err
	}

	if instance.Source.InstanceOnly {
		if !r.HasExtension("container_only_migration") {
			return nil, errors.New("The server is missing the required \"container_only_migration\" API extension")
		}
	}

	// Send the request
	op, _, err := r.queryOperation("POST", path, instance, "")
	if err != nil {
		return nil, err
	}

	return op, nil
}

// tryCreateInstance attempts to create a new instance on multiple target servers specified by their URLs.
// It runs the instance creation asynchronously and returns a RemoteOperation to monitor the progress and any errors.
func (r *ProtocolIncus) tryCreateInstance(req api.InstancesPost, urls []string, op Operation) (RemoteOperation, error) {
	if len(urls) == 0 {
		return nil, errors.New("The source server isn't listening on the network")
	}

	rop := remoteOperation{
		chDone: make(chan bool),
	}

	operation := req.Source.Operation

	// Forward targetOp to remote op
	chConnect := make(chan error, 1)
	chWait := make(chan error, 1)

	go func() {
		success := false
		var errors []remoteOperationResult
		for _, serverURL := range urls {
			if operation == "" {
				req.Source.Server = serverURL
			} else {
				req.Source.Operation = fmt.Sprintf("%s/1.0/operations/%s", serverURL, url.PathEscape(operation))
			}

			op, err := r.CreateInstance(req)
			if err != nil {
				errors = append(errors, remoteOperationResult{URL: serverURL, Error: err})
				continue
			}

			rop.handlerLock.Lock()
			rop.targetOp = op
			rop.handlerLock.Unlock()

			for _, handler := range rop.handlers {
				_, _ = rop.targetOp.AddHandler(handler)
			}

			err = rop.targetOp.Wait()
			if err != nil {
				errors = append(errors, remoteOperationResult{URL: serverURL, Error: err})

				if localtls.IsConnectionError(err) {
					continue
				}

				break
			}

			success = true
			break
		}

		if success {
			chConnect <- nil
			close(chConnect)
		} else {
			chConnect <- remoteOperationError("Failed instance creation", errors)
			close(chConnect)

			if op != nil {
				_ = op.Cancel()
			}
		}
	}()

	if op != nil {
		go func() {
			chWait <- op.Wait()
			close(chWait)
		}()
	}

	go func() {
		var err error

		select {
		case err = <-chConnect:
		case err = <-chWait:
		}

		rop.err = err
		close(rop.chDone)
	}()

	return &rop, nil
}

// CreateInstanceFromImage is a convenience function to make it easier to create a instance from an existing image.
func (r *ProtocolIncus) CreateInstanceFromImage(source ImageServer, image api.Image, req api.InstancesPost) (RemoteOperation, error) {
	info, err := r.getSourceImageConnectionInfo(source, image, &req.Source)
	if err != nil {
		return nil, err
	}

	// If the source server is the same as the target server, create the instance directly.
	if info == nil {
		op, err := r.CreateInstance(req)
		if err != nil {
			return nil, err
		}

		rop := remoteOperation{
			targetOp: op,
			chDone:   make(chan bool),
		}

		// Forward targetOp to remote op
		go func() {
			rop.err = rop.targetOp.Wait()
			close(rop.chDone)
		}()

		return &rop, nil
	}

	return r.tryCreateInstance(req, info.Addresses, nil)
}

// CopyInstance copies a instance from a remote server. Additional options can be passed using InstanceCopyArgs.
func (r *ProtocolIncus) CopyInstance(source InstanceServer, instance api.Instance, args *InstanceCopyArgs) (RemoteOperation, error) {
	// Base request
	req := api.InstancesPost{
		Name:        instance.Name,
		InstancePut: instance.Writable(),
		Type:        api.InstanceType(instance.Type),
	}

	req.Source.BaseImage = instance.Config["volatile.base_image"]

	// Process the copy arguments
	if args != nil {
		// Quick checks.
		if args.InstanceOnly {
			if !r.HasExtension("container_only_migration") {
				return nil, errors.New("The target server is missing the required \"container_only_migration\" API extension")
			}

			if !source.HasExtension("container_only_migration") {
				return nil, errors.New("The source server is missing the required \"container_only_migration\" API extension")
			}
		}

		if slices.Contains([]string{"push", "relay"}, args.Mode) {
			if !r.HasExtension("container_push") {
				return nil, errors.New("The target server is missing the required \"container_push\" API extension")
			}

			if !source.HasExtension("container_push") {
				return nil, errors.New("The source server is missing the required \"container_push\" API extension")
			}
		}

		if args.Mode == "push" && !source.HasExtension("container_push_target") {
			return nil, errors.New("The source server is missing the required \"container_push_target\" API extension")
		}

		if args.Refresh {
			if !r.HasExtension("container_incremental_copy") {
				return nil, errors.New("The target server is missing the required \"container_incremental_copy\" API extension")
			}

			if !source.HasExtension("container_incremental_copy") {
				return nil, errors.New("The source server is missing the required \"container_incremental_copy\" API extension")
			}
		}

		if args.RefreshExcludeOlder && !source.HasExtension("custom_volume_refresh_exclude_older_snapshots") {
			return nil, errors.New("The source server is missing the required \"custom_volume_refresh_exclude_older_snapshots\" API extension")
		}

		if args.AllowInconsistent {
			if !r.HasExtension("instance_allow_inconsistent_copy") {
				return nil, errors.New("The source server is missing the required \"instance_allow_inconsistent_copy\" API extension")
			}
		}

		// Allow overriding the target name
		if args.Name != "" {
			req.Name = args.Name
		}

		req.Source.Live = args.Live
		req.Source.InstanceOnly = args.InstanceOnly
		req.Source.Refresh = args.Refresh
		req.Source.RefreshExcludeOlder = args.RefreshExcludeOlder
		req.Source.AllowInconsistent = args.AllowInconsistent
	}

	if req.Source.Live {
		req.Source.Live = instance.StatusCode == api.Running
	}

	sourceInfo, err := source.GetConnectionInfo()
	if err != nil {
		return nil, fmt.Errorf("Failed to get source connection info: %w", err)
	}

	destInfo, err := r.GetConnectionInfo()
	if err != nil {
		return nil, fmt.Errorf("Failed to get destination connection info: %w", err)
	}

	// Optimization for the local copy case
	if destInfo.URL == sourceInfo.URL && destInfo.SocketPath == sourceInfo.SocketPath && (!r.IsClustered() || instance.Location == r.clusterTarget || r.HasExtension("cluster_internal_copy")) {
		// Project handling
		if destInfo.Project != sourceInfo.Project {
			if !r.HasExtension("container_copy_project") {
				return nil, errors.New("The server is missing the required \"container_copy_project\" API extension")
			}

			req.Source.Project = sourceInfo.Project
		}

		// Local copy source fields
		req.Source.Type = "copy"
		req.Source.Source = instance.Name

		// Copy the instance
		op, err := r.CreateInstance(req)
		if err != nil {
			return nil, err
		}

		rop := remoteOperation{
			targetOp: op,
			chDone:   make(chan bool),
		}

		// Forward targetOp to remote op
		go func() {
			rop.err = rop.targetOp.Wait()
			close(rop.chDone)
		}()

		return &rop, nil
	}

	// Source request
	sourceReq := api.InstancePost{
		Migration:         true,
		Live:              req.Source.Live,
		InstanceOnly:      req.Source.InstanceOnly,
		AllowInconsistent: req.Source.AllowInconsistent,
	}

	// Push mode migration
	if args != nil && args.Mode == "push" {
		// Get target server connection information
		info, err := r.GetConnectionInfo()
		if err != nil {
			return nil, err
		}

		// Create the instance
		req.Source.Type = "migration"
		req.Source.Mode = "push"
		req.Source.Refresh = args.Refresh
		req.Source.RefreshExcludeOlder = args.RefreshExcludeOlder

		op, err := r.CreateInstance(req)
		if err != nil {
			return nil, err
		}

		opAPI := op.Get()

		targetSecrets := map[string]string{}
		for k, v := range opAPI.Metadata {
			val, ok := v.(string)
			if ok {
				targetSecrets[k] = val
			}
		}

		// Prepare the source request
		target := api.InstancePostTarget{}
		target.Operation = opAPI.ID
		target.Websockets = targetSecrets
		target.Certificate = info.Certificate
		sourceReq.Target = &target

		return r.tryMigrateInstance(source, instance.Name, sourceReq, info.Addresses, op)
	}

	// Get source server connection information
	info, err := source.GetConnectionInfo()
	if err != nil {
		return nil, err
	}

	op, err := source.MigrateInstance(instance.Name, sourceReq)
	if err != nil {
		return nil, err
	}

	opAPI := op.Get()

	sourceSecrets := map[string]string{}
	for k, v := range opAPI.Metadata {
		val, ok := v.(string)
		if ok {
			sourceSecrets[k] = val
		}
	}

	// Relay mode migration
	if args != nil && args.Mode == "relay" {
		// Push copy source fields
		req.Source.Type = "migration"
		req.Source.Mode = "push"

		// Start the process
		targetOp, err := r.CreateInstance(req)
		if err != nil {
			return nil, err
		}

		targetOpAPI := targetOp.Get()

		// Extract the websockets
		targetSecrets := map[string]string{}
		for k, v := range targetOpAPI.Metadata {
			val, ok := v.(string)
			if ok {
				targetSecrets[k] = val
			}
		}

		// Launch the relay
		err = r.proxyMigration(targetOp.(*operation), targetSecrets, source, op.(*operation), sourceSecrets)
		if err != nil {
			return nil, err
		}

		// Prepare a tracking operation
		rop := remoteOperation{
			targetOp: targetOp,
			chDone:   make(chan bool),
		}

		// Forward targetOp to remote op
		go func() {
			rop.err = rop.targetOp.Wait()
			close(rop.chDone)
		}()

		return &rop, nil
	}

	// Pull mode migration
	req.Source.Type = "migration"
	req.Source.Mode = "pull"
	req.Source.Operation = opAPI.ID
	req.Source.Websockets = sourceSecrets
	req.Source.Certificate = info.Certificate

	return r.tryCreateInstance(req, info.Addresses, op)
}

// UpdateInstance updates the instance definition.
func (r *ProtocolIncus) UpdateInstance(name string, instance api.InstancePut, ETag string) (Operation, error) {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, err
	}

	// Send the request
	op, _, err := r.queryOperation("PUT", fmt.Sprintf("%s/%s", path, url.PathEscape(name)), instance, ETag)
	if err != nil {
		return nil, err
	}

	return op, nil
}

// RenameInstance requests that Incus renames the instance.
func (r *ProtocolIncus) RenameInstance(name string, instance api.InstancePost) (Operation, error) {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, err
	}

	// Quick check.
	if instance.Migration {
		return nil, errors.New("Can't ask for a migration through RenameInstance")
	}

	// Send the request
	op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s", path, url.PathEscape(name)), instance, "")
	if err != nil {
		return nil, err
	}

	return op, nil
}

// tryMigrateInstance attempts to migrate a specific instance from a source server to one of the target URLs.
// The function runs the migration operation asynchronously and returns a RemoteOperation to track the progress and handle any errors.
func (r *ProtocolIncus) tryMigrateInstance(source InstanceServer, name string, req api.InstancePost, urls []string, op Operation) (RemoteOperation, error) {
	if len(urls) == 0 {
		return nil, errors.New("The target server isn't listening on the network")
	}

	rop := remoteOperation{
		chDone: make(chan bool),
	}

	operation := req.Target.Operation

	// Forward targetOp to remote op
	chConnect := make(chan error, 1)
	chWait := make(chan error, 1)

	go func() {
		success := false
		var errors []remoteOperationResult
		for _, serverURL := range urls {
			req.Target.Operation = fmt.Sprintf("%s/1.0/operations/%s", serverURL, url.PathEscape(operation))

			op, err := source.MigrateInstance(name, req)
			if err != nil {
				errors = append(errors, remoteOperationResult{URL: serverURL, Error: err})
				continue
			}

			rop.targetOp = op

			for _, handler := range rop.handlers {
				_, _ = rop.targetOp.AddHandler(handler)
			}

			err = rop.targetOp.Wait()
			if err != nil {
				errors = append(errors, remoteOperationResult{URL: serverURL, Error: err})

				if localtls.IsConnectionError(err) {
					continue
				}

				break
			}

			success = true
			break
		}

		if success {
			chConnect <- nil
			close(chConnect)
		} else {
			chConnect <- remoteOperationError("Failed instance migration", errors)
			close(chConnect)

			if op != nil {
				_ = op.Cancel()
			}
		}
	}()

	if op != nil {
		go func() {
			chWait <- op.Wait()
			close(chWait)
		}()
	}

	go func() {
		var err error

		select {
		case err = <-chConnect:
		case err = <-chWait:
		}

		rop.err = err
		close(rop.chDone)
	}()

	return &rop, nil
}

// MigrateInstance requests that Incus prepares for a instance migration.
func (r *ProtocolIncus) MigrateInstance(name string, instance api.InstancePost) (Operation, error) {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, err
	}

	if instance.InstanceOnly {
		if !r.HasExtension("container_only_migration") {
			return nil, errors.New("The server is missing the required \"container_only_migration\" API extension")
		}
	}

	if instance.Pool != "" && !r.HasExtension("instance_pool_move") {
		return nil, errors.New("The server is missing the required \"instance_pool_move\" API extension")
	}

	if instance.Project != "" && !r.HasExtension("instance_project_move") {
		return nil, errors.New("The server is missing the required \"instance_project_move\" API extension")
	}

	if instance.AllowInconsistent && !r.HasExtension("cluster_migration_inconsistent_copy") {
		return nil, errors.New("The server is missing the required \"cluster_migration_inconsistent_copy\" API extension")
	}

	// Quick check.
	if !instance.Migration {
		return nil, errors.New("Can't ask for a rename through MigrateInstance")
	}

	// Send the request
	op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s", path, url.PathEscape(name)), instance, "")
	if err != nil {
		return nil, err
	}

	return op, nil
}

// DeleteInstance requests that Incus deletes the instance.
func (r *ProtocolIncus) DeleteInstance(name string) (Operation, error) {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, err
	}

	// Send the request
	op, _, err := r.queryOperation("DELETE", fmt.Sprintf("%s/%s", path, url.PathEscape(name)), nil, "")
	if err != nil {
		return nil, err
	}

	return op, nil
}

// ExecInstance requests that Incus spawns a command inside the instance.
func (r *ProtocolIncus) ExecInstance(instanceName string, exec api.InstanceExecPost, args *InstanceExecArgs) (Operation, error) {
	// Ensure args are equivalent to empty InstanceExecArgs.
	if args == nil {
		args = &InstanceExecArgs{}
	}

	if exec.RecordOutput {
		if !r.HasExtension("container_exec_recording") {
			return nil, errors.New("The server is missing the required \"container_exec_recording\" API extension")
		}
	}

	if exec.User > 0 || exec.Group > 0 || exec.Cwd != "" {
		if !r.HasExtension("container_exec_user_group_cwd") {
			return nil, errors.New("The server is missing the required \"container_exec_user_group_cwd\" API extension")
		}
	}

	var uri string

	if r.IsAgent() {
		uri = "/exec"
	} else {
		path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
		if err != nil {
			return nil, err
		}

		uri = fmt.Sprintf("%s/%s/exec", path, url.PathEscape(instanceName))
	}

	// Send the request
	op, _, err := r.queryOperation("POST", uri, exec, "")
	if err != nil {
		return nil, err
	}

	opAPI := op.Get()

	// Process additional arguments

	// Parse the fds
	fds := map[string]string{}

	value, ok := opAPI.Metadata["fds"]
	if ok {
		values, ok := value.(map[string]any)
		if ok {
			for k, v := range values {
				val, ok := v.(string)
				if ok {
					fds[k] = val
				}
			}
		}
	}

	if exec.RecordOutput && (args.Stdout != nil || args.Stderr != nil) {
		err = op.Wait()
		if err != nil {
			return nil, err
		}

		opAPI = op.Get()
		outputFiles := map[string]string{}
		outputs, ok := opAPI.Metadata["output"].(map[string]any)
		if ok {
			for k, v := range outputs {
				val, ok := v.(string)
				if ok {
					outputFiles[k] = val
				}
			}
		}

		if outputFiles["1"] != "" {
			reader, _ := r.getInstanceExecOutputLogFile(instanceName, filepath.Base(outputFiles["1"]))
			if args.Stdout != nil {
				_, errCopy := io.Copy(args.Stdout, reader)
				// Regardless of errCopy value, we want to delete the file after a copy operation
				errDelete := r.deleteInstanceExecOutputLogFile(instanceName, filepath.Base(outputFiles["1"]))
				if errDelete != nil {
					return nil, errDelete
				}

				if errCopy != nil {
					return nil, fmt.Errorf("Could not copy the content of the exec output log file to stdout: %w", err)
				}
			}

			err = r.deleteInstanceExecOutputLogFile(instanceName, filepath.Base(outputFiles["1"]))
			if err != nil {
				return nil, err
			}
		}

		if outputFiles["2"] != "" {
			reader, _ := r.getInstanceExecOutputLogFile(instanceName, filepath.Base(outputFiles["2"]))
			if args.Stderr != nil {
				_, errCopy := io.Copy(args.Stderr, reader)
				errDelete := r.deleteInstanceExecOutputLogFile(instanceName, filepath.Base(outputFiles["1"]))
				if errDelete != nil {
					return nil, errDelete
				}

				if errCopy != nil {
					return nil, fmt.Errorf("Could not copy the content of the exec output log file to stderr: %w", err)
				}
			}

			err = r.deleteInstanceExecOutputLogFile(instanceName, filepath.Base(outputFiles["2"]))
			if err != nil {
				return nil, err
			}
		}
	}

	if fds[api.SecretNameControl] != "" {
		conn, err := r.GetOperationWebsocket(opAPI.ID, fds[api.SecretNameControl])
		if err != nil {
			return nil, err
		}

		go func() {
			_, _, _ = conn.ReadMessage() // Consume pings from server.
		}()

		if args.Control != nil {
			// Call the control handler with a connection to the control socket
			go args.Control(conn)
		}
	}

	if exec.Interactive {
		// Handle interactive sections
		if args.Stdin != nil && args.Stdout != nil {
			// Connect to the websocket
			conn, err := r.GetOperationWebsocket(opAPI.ID, fds["0"])
			if err != nil {
				return nil, err
			}

			// And attach stdin and stdout to it
			go func() {
				ws.MirrorRead(conn, args.Stdin)
				<-ws.MirrorWrite(conn, args.Stdout)
				_ = conn.Close()

				if args.DataDone != nil {
					close(args.DataDone)
				}
			}()
		} else {
			if args.DataDone != nil {
				close(args.DataDone)
			}
		}
	} else {
		// Handle non-interactive sessions
		dones := make(map[int]chan error)
		conns := []*websocket.Conn{}

		// Handle stdin
		if fds["0"] != "" {
			conn, err := r.GetOperationWebsocket(opAPI.ID, fds["0"])
			if err != nil {
				return nil, err
			}

			go func() {
				_, _, _ = conn.ReadMessage() // Consume pings from server.
			}()

			conns = append(conns, conn)
			dones[0] = ws.MirrorRead(conn, args.Stdin)
		}

		waitConns := 0 // Used for keeping track of when stdout and stderr have finished.

		// Handle stdout
		if fds["1"] != "" {
			conn, err := r.GetOperationWebsocket(opAPI.ID, fds["1"])
			if err != nil {
				return nil, err
			}

			// Discard Stdout from remote command if output writer not supplied.
			if args.Stdout == nil {
				args.Stdout = io.Discard
			}

			conns = append(conns, conn)
			dones[1] = ws.MirrorWrite(conn, args.Stdout)
			waitConns++
		}

		// Handle stderr
		if fds["2"] != "" {
			conn, err := r.GetOperationWebsocket(opAPI.ID, fds["2"])
			if err != nil {
				return nil, err
			}

			// Discard Stderr from remote command if output writer not supplied.
			if args.Stderr == nil {
				args.Stderr = io.Discard
			}

			conns = append(conns, conn)
			dones[2] = ws.MirrorWrite(conn, args.Stderr)
			waitConns++
		}

		// Wait for everything to be done
		go func() {
			for {
				select {
				case <-dones[0]:
					// Handle stdin finish, but don't wait for it if output channels
					// have all finished.
					dones[0] = nil
					_ = conns[0].Close()
				case <-dones[1]:
					dones[1] = nil
					_ = conns[1].Close()
					waitConns--
				case <-dones[2]:
					dones[2] = nil
					_ = conns[2].Close()
					waitConns--
				}

				if waitConns <= 0 {
					// Close stdin websocket if defined and not already closed.
					if dones[0] != nil {
						conns[0].Close()
					}

					break
				}
			}

			if args.DataDone != nil {
				close(args.DataDone)
			}
		}()
	}

	return op, nil
}

// GetInstanceFile retrieves the provided path from the instance.
func (r *ProtocolIncus) GetInstanceFile(instanceName string, filePath string) (io.ReadCloser, *InstanceFileResponse, error) {
	var err error
	var requestURL string

	urlEncode := func(path string, query map[string]string) (string, error) {
		u, err := url.Parse(path)
		if err != nil {
			return "", err
		}

		params := url.Values{}
		for key, value := range query {
			params.Add(key, value)
		}

		u.RawQuery = params.Encode()
		return u.String(), nil
	}

	if r.IsAgent() {
		requestURL, err = urlEncode(
			fmt.Sprintf("%s/1.0/files", r.httpBaseURL.String()),
			map[string]string{"path": filePath})
	} else {
		var path string

		path, _, err = r.instanceTypeToPath(api.InstanceTypeAny)
		if err != nil {
			return nil, nil, err
		}

		// Prepare the HTTP request
		requestURL, err = urlEncode(
			fmt.Sprintf("%s/1.0%s/%s/files", r.httpBaseURL.String(), path, url.PathEscape(instanceName)),
			map[string]string{"path": filePath})
	}

	if err != nil {
		return nil, nil, err
	}

	requestURL, err = r.setQueryAttributes(requestURL)
	if err != nil {
		return nil, nil, err
	}

	req, err := http.NewRequest("GET", requestURL, nil)
	if err != nil {
		return nil, nil, err
	}

	// Send the request
	resp, err := r.DoHTTP(req)
	if err != nil {
		return nil, nil, err
	}

	// Check the return value for a cleaner error
	if resp.StatusCode != http.StatusOK {
		_, _, err := incusParseResponse(resp)
		if err != nil {
			return nil, nil, err
		}
	}

	// Parse the headers
	uid, gid, mode, fileType, _ := api.ParseFileHeaders(resp.Header)
	fileResp := InstanceFileResponse{
		UID:  uid,
		GID:  gid,
		Mode: mode,
		Type: fileType,
	}

	if fileResp.Type == "directory" {
		// Decode the response
		response := api.Response{}
		decoder := json.NewDecoder(resp.Body)

		err = decoder.Decode(&response)
		if err != nil {
			return nil, nil, err
		}

		// Get the file list
		entries := []string{}
		err = response.MetadataAsStruct(&entries)
		if err != nil {
			return nil, nil, err
		}

		fileResp.Entries = entries

		return nil, &fileResp, err
	}

	return resp.Body, &fileResp, err
}

// CreateInstanceFile tells Incus to create a file in the instance.
func (r *ProtocolIncus) CreateInstanceFile(instanceName string, filePath string, args InstanceFileArgs) error {
	if args.Type == "directory" {
		if !r.HasExtension("directory_manipulation") {
			return errors.New("The server is missing the required \"directory_manipulation\" API extension")
		}
	}

	if args.Type == "symlink" {
		if !r.HasExtension("file_symlinks") {
			return errors.New("The server is missing the required \"file_symlinks\" API extension")
		}
	}

	if args.WriteMode == "append" {
		if !r.HasExtension("file_append") {
			return errors.New("The server is missing the required \"file_append\" API extension")
		}
	}

	var requestURL string

	if r.IsAgent() {
		requestURL = fmt.Sprintf("%s/1.0/files?path=%s", r.httpBaseURL.String(), url.QueryEscape(filePath))
	} else {
		path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
		if err != nil {
			return err
		}

		// Prepare the HTTP request
		requestURL = fmt.Sprintf("%s/1.0%s/%s/files?path=%s", r.httpBaseURL.String(), path, url.PathEscape(instanceName), url.QueryEscape(filePath))
	}

	requestURL, err := r.setQueryAttributes(requestURL)
	if err != nil {
		return err
	}

	req, err := http.NewRequest("POST", requestURL, args.Content)
	if err != nil {
		return err
	}

	req.GetBody = func() (io.ReadCloser, error) {
		_, err := args.Content.Seek(0, 0)
		if err != nil {
			return nil, err
		}

		return io.NopCloser(args.Content), nil
	}

	// Set the various headers
	if args.UID > -1 {
		req.Header.Set("X-Incus-uid", fmt.Sprintf("%d", args.UID))
	}

	if args.GID > -1 {
		req.Header.Set("X-Incus-gid", fmt.Sprintf("%d", args.GID))
	}

	if args.Mode > -1 {
		req.Header.Set("X-Incus-mode", fmt.Sprintf("%04o", args.Mode))
	}

	if args.Type != "" {
		req.Header.Set("X-Incus-type", args.Type)
	}

	if args.WriteMode != "" {
		req.Header.Set("X-Incus-write", args.WriteMode)
	}

	// Send the request
	resp, err := r.DoHTTP(req)
	if err != nil {
		return err
	}

	// Check the return value for a cleaner error
	_, _, err = incusParseResponse(resp)
	if err != nil {
		return err
	}

	return nil
}

// DeleteInstanceFile deletes a file in the instance.
func (r *ProtocolIncus) DeleteInstanceFile(instanceName string, filePath string) error {
	if !r.HasExtension("file_delete") {
		return errors.New("The server is missing the required \"file_delete\" API extension")
	}

	var requestURL string

	if r.IsAgent() {
		requestURL = fmt.Sprintf("/files?path=%s", url.QueryEscape(filePath))
	} else {
		path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
		if err != nil {
			return err
		}

		// Prepare the HTTP request
		requestURL = fmt.Sprintf("%s/%s/files?path=%s", path, url.PathEscape(instanceName), url.QueryEscape(filePath))
	}

	requestURL, err := r.setQueryAttributes(requestURL)
	if err != nil {
		return err
	}

	// Send the request
	_, _, err = r.query("DELETE", requestURL, nil, "")
	if err != nil {
		return err
	}

	return nil
}

// rawSFTPConn connects to the apiURL, upgrades to an SFTP raw connection and returns it.
func (r *ProtocolIncus) rawSFTPConn(apiURL *url.URL) (net.Conn, error) {
	// Get the HTTP transport.
	httpTransport, err := r.getUnderlyingHTTPTransport()
	if err != nil {
		return nil, err
	}

	req := &http.Request{
		Method:     http.MethodGet,
		URL:        apiURL,
		Proto:      "HTTP/1.1",
		ProtoMajor: 1,
		ProtoMinor: 1,
		Header:     make(http.Header),
		Host:       apiURL.Host,
	}

	req.Header["Upgrade"] = []string{"sftp"}
	req.Header["Connection"] = []string{"Upgrade"}

	r.addClientHeaders(req)

	// Establish the connection.
	var conn net.Conn

	if httpTransport.TLSClientConfig != nil {
		conn, err = httpTransport.DialTLSContext(context.Background(), "tcp", apiURL.Host)
	} else {
		conn, err = httpTransport.DialContext(context.Background(), "tcp", apiURL.Host)
	}

	if err != nil {
		return nil, err
	}

	remoteTCP, _ := tcp.ExtractConn(conn)
	if remoteTCP != nil {
		err = tcp.SetTimeouts(remoteTCP, 0)
		if err != nil {
			return nil, err
		}
	}

	err = req.Write(conn)
	if err != nil {
		return nil, err
	}

	resp, err := http.ReadResponse(bufio.NewReader(conn), req)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode != http.StatusSwitchingProtocols {
		_, _, err := incusParseResponse(resp)
		if err != nil {
			return nil, err
		}
	}

	if resp.Header.Get("Upgrade") != "sftp" {
		return nil, errors.New("Missing or unexpected Upgrade header in response")
	}

	return conn, err
}

// GetInstanceFileSFTPConn returns a connection to the instance's SFTP endpoint.
func (r *ProtocolIncus) GetInstanceFileSFTPConn(instanceName string) (net.Conn, error) {
	apiURL := api.NewURL()
	apiURL.URL = r.httpBaseURL // Preload the URL with the client base URL.
	apiURL.Path("1.0", "instances", instanceName, "sftp")
	r.setURLQueryAttributes(&apiURL.URL)

	return r.rawSFTPConn(&apiURL.URL)
}

// GetInstanceFileSFTP returns an SFTP connection to the instance.
func (r *ProtocolIncus) GetInstanceFileSFTP(instanceName string) (*sftp.Client, error) {
	conn, err := r.GetInstanceFileSFTPConn(instanceName)
	if err != nil {
		return nil, err
	}

	// Get a SFTP client.
	client, err := sftp.NewClientPipe(conn, conn, sftp.MaxPacketUnchecked(128*1024))
	if err != nil {
		_ = conn.Close()
		return nil, err
	}

	go func() {
		// Wait for the client to be done before closing the connection.
		_ = client.Wait()
		_ = conn.Close()
	}()

	return client, nil
}

// GetInstanceSnapshotNames returns a list of snapshot names for the instance.
func (r *ProtocolIncus) GetInstanceSnapshotNames(instanceName string) ([]string, error) {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, err
	}

	// Fetch the raw URL values.
	urls := []string{}
	baseURL := fmt.Sprintf("%s/%s/snapshots", path, url.PathEscape(instanceName))
	_, err = r.queryStruct("GET", baseURL, nil, "", &urls)
	if err != nil {
		return nil, err
	}

	// Parse it.
	return urlsToResourceNames(baseURL, urls...)
}

// GetInstanceSnapshots returns a list of snapshots for the instance.
func (r *ProtocolIncus) GetInstanceSnapshots(instanceName string) ([]api.InstanceSnapshot, error) {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, err
	}

	snapshots := []api.InstanceSnapshot{}

	// Fetch the raw value
	_, err = r.queryStruct("GET", fmt.Sprintf("%s/%s/snapshots?recursion=1", path, url.PathEscape(instanceName)), nil, "", &snapshots)
	if err != nil {
		return nil, err
	}

	return snapshots, nil
}

// GetInstanceSnapshot returns a Snapshot struct for the provided instance and snapshot names.
func (r *ProtocolIncus) GetInstanceSnapshot(instanceName string, name string) (*api.InstanceSnapshot, string, error) {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, "", err
	}

	snapshot := api.InstanceSnapshot{}

	// Fetch the raw value
	etag, err := r.queryStruct("GET", fmt.Sprintf("%s/%s/snapshots/%s", path, url.PathEscape(instanceName), url.PathEscape(name)), nil, "", &snapshot)
	if err != nil {
		return nil, "", err
	}

	return &snapshot, etag, nil
}

// CreateInstanceSnapshot requests that Incus creates a new snapshot for the instance.
func (r *ProtocolIncus) CreateInstanceSnapshot(instanceName string, snapshot api.InstanceSnapshotsPost) (Operation, error) {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, err
	}

	// Validate the request
	if snapshot.ExpiresAt != nil && !r.HasExtension("snapshot_expiry_creation") {
		return nil, errors.New("The server is missing the required \"snapshot_expiry_creation\" API extension")
	}

	// Send the request
	op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s/snapshots", path, url.PathEscape(instanceName)), snapshot, "")
	if err != nil {
		return nil, err
	}

	return op, nil
}

// CopyInstanceSnapshot copies a snapshot from a remote server into a new instance. Additional options can be passed using InstanceCopyArgs.
func (r *ProtocolIncus) CopyInstanceSnapshot(source InstanceServer, instanceName string, snapshot api.InstanceSnapshot, args *InstanceSnapshotCopyArgs) (RemoteOperation, error) {
	// Backward compatibility (with broken Name field)
	fields := strings.Split(snapshot.Name, "/")
	cName := instanceName
	sName := fields[len(fields)-1]

	// Base request
	req := api.InstancesPost{
		Name: cName,
		InstancePut: api.InstancePut{
			Architecture: snapshot.Architecture,
			Config:       snapshot.Config,
			Devices:      snapshot.Devices,
			Ephemeral:    snapshot.Ephemeral,
			Profiles:     snapshot.Profiles,
		},
	}

	if snapshot.Stateful && args.Live {
		if !r.HasExtension("container_snapshot_stateful_migration") {
			return nil, errors.New("The server is missing the required \"container_snapshot_stateful_migration\" API extension")
		}

		req.Stateful = snapshot.Stateful
		req.Source.Live = false // Snapshots are never running and so we don't need live migration.
	}

	req.Source.BaseImage = snapshot.Config["volatile.base_image"]

	// Process the copy arguments
	if args != nil {
		// Quick checks.
		if slices.Contains([]string{"push", "relay"}, args.Mode) {
			if !r.HasExtension("container_push") {
				return nil, errors.New("The target server is missing the required \"container_push\" API extension")
			}

			if !source.HasExtension("container_push") {
				return nil, errors.New("The source server is missing the required \"container_push\" API extension")
			}
		}

		if args.Mode == "push" && !source.HasExtension("container_push_target") {
			return nil, errors.New("The source server is missing the required \"container_push_target\" API extension")
		}

		// Allow overriding the target name
		if args.Name != "" {
			req.Name = args.Name
		}
	}

	sourceInfo, err := source.GetConnectionInfo()
	if err != nil {
		return nil, fmt.Errorf("Failed to get source connection info: %w", err)
	}

	destInfo, err := r.GetConnectionInfo()
	if err != nil {
		return nil, fmt.Errorf("Failed to get destination connection info: %w", err)
	}

	instance, _, err := source.GetInstance(cName)
	if err != nil {
		return nil, fmt.Errorf("Failed to get instance info: %w", err)
	}

	// Optimization for the local copy case
	if destInfo.URL == sourceInfo.URL && destInfo.SocketPath == sourceInfo.SocketPath && (!r.IsClustered() || instance.Location == r.clusterTarget || r.HasExtension("cluster_internal_copy")) {
		// Project handling
		if destInfo.Project != sourceInfo.Project {
			if !r.HasExtension("container_copy_project") {
				return nil, errors.New("The server is missing the required \"container_copy_project\" API extension")
			}

			req.Source.Project = sourceInfo.Project
		}

		// Local copy source fields
		req.Source.Type = "copy"
		req.Source.Source = fmt.Sprintf("%s/%s", cName, sName)

		// Copy the instance
		op, err := r.CreateInstance(req)
		if err != nil {
			return nil, err
		}

		rop := remoteOperation{
			targetOp: op,
			chDone:   make(chan bool),
		}

		// Forward targetOp to remote op
		go func() {
			rop.err = rop.targetOp.Wait()
			close(rop.chDone)
		}()

		return &rop, nil
	}

	// If deadling with migration, we need to set the type.
	if source.HasExtension("virtual-machines") {
		inst, _, err := source.GetInstance(instanceName)
		if err != nil {
			return nil, err
		}

		req.Type = api.InstanceType(inst.Type)
	}

	// Source request
	sourceReq := api.InstanceSnapshotPost{
		Migration: true,
		Name:      args.Name,
	}

	if snapshot.Stateful && args.Live {
		sourceReq.Live = args.Live
	}

	// Push mode migration
	if args != nil && args.Mode == "push" {
		// Get target server connection information
		info, err := r.GetConnectionInfo()
		if err != nil {
			return nil, err
		}

		// Create the instance
		req.Source.Type = "migration"
		req.Source.Mode = "push"

		op, err := r.CreateInstance(req)
		if err != nil {
			return nil, err
		}

		opAPI := op.Get()

		targetSecrets := map[string]string{}
		for k, v := range opAPI.Metadata {
			val, ok := v.(string)
			if ok {
				targetSecrets[k] = val
			}
		}

		// Prepare the source request
		target := api.InstancePostTarget{}
		target.Operation = opAPI.ID
		target.Websockets = targetSecrets
		target.Certificate = info.Certificate
		sourceReq.Target = &target

		return r.tryMigrateInstanceSnapshot(source, cName, sName, sourceReq, info.Addresses)
	}

	// Get source server connection information
	info, err := source.GetConnectionInfo()
	if err != nil {
		return nil, err
	}

	op, err := source.MigrateInstanceSnapshot(cName, sName, sourceReq)
	if err != nil {
		return nil, err
	}

	opAPI := op.Get()

	sourceSecrets := map[string]string{}
	for k, v := range opAPI.Metadata {
		val, ok := v.(string)
		if ok {
			sourceSecrets[k] = val
		}
	}

	// Relay mode migration
	if args != nil && args.Mode == "relay" {
		// Push copy source fields
		req.Source.Type = "migration"
		req.Source.Mode = "push"

		// Start the process
		targetOp, err := r.CreateInstance(req)
		if err != nil {
			return nil, err
		}

		targetOpAPI := targetOp.Get()

		// Extract the websockets
		targetSecrets := map[string]string{}
		for k, v := range targetOpAPI.Metadata {
			val, ok := v.(string)
			if ok {
				targetSecrets[k] = val
			}
		}

		// Launch the relay
		err = r.proxyMigration(targetOp.(*operation), targetSecrets, source, op.(*operation), sourceSecrets)
		if err != nil {
			return nil, err
		}

		// Prepare a tracking operation
		rop := remoteOperation{
			targetOp: targetOp,
			chDone:   make(chan bool),
		}

		// Forward targetOp to remote op
		go func() {
			rop.err = rop.targetOp.Wait()
			close(rop.chDone)
		}()

		return &rop, nil
	}

	// Pull mode migration
	req.Source.Type = "migration"
	req.Source.Mode = "pull"
	req.Source.Operation = opAPI.ID
	req.Source.Websockets = sourceSecrets
	req.Source.Certificate = info.Certificate

	return r.tryCreateInstance(req, info.Addresses, op)
}

// RenameInstanceSnapshot requests that Incus renames the snapshot.
func (r *ProtocolIncus) RenameInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPost) (Operation, error) {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, err
	}

	// Quick check.
	if instance.Migration {
		return nil, errors.New("Can't ask for a migration through RenameInstanceSnapshot")
	}

	// Send the request
	op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s/snapshots/%s", path, url.PathEscape(instanceName), url.PathEscape(name)), instance, "")
	if err != nil {
		return nil, err
	}

	return op, nil
}

func (r *ProtocolIncus) tryMigrateInstanceSnapshot(source InstanceServer, instanceName string, name string, req api.InstanceSnapshotPost, urls []string) (RemoteOperation, error) {
	if len(urls) == 0 {
		return nil, errors.New("The target server isn't listening on the network")
	}

	rop := remoteOperation{
		chDone: make(chan bool),
	}

	operation := req.Target.Operation

	// Forward targetOp to remote op
	go func() {
		success := false
		var errors []remoteOperationResult
		for _, serverURL := range urls {
			req.Target.Operation = fmt.Sprintf("%s/1.0/operations/%s", serverURL, url.PathEscape(operation))

			op, err := source.MigrateInstanceSnapshot(instanceName, name, req)
			if err != nil {
				errors = append(errors, remoteOperationResult{URL: serverURL, Error: err})
				continue
			}

			rop.targetOp = op

			for _, handler := range rop.handlers {
				_, _ = rop.targetOp.AddHandler(handler)
			}

			err = rop.targetOp.Wait()
			if err != nil {
				errors = append(errors, remoteOperationResult{URL: serverURL, Error: err})

				if localtls.IsConnectionError(err) {
					continue
				}

				break
			}

			success = true
			break
		}

		if !success {
			rop.err = remoteOperationError("Failed instance migration", errors)
		}

		close(rop.chDone)
	}()

	return &rop, nil
}

// MigrateInstanceSnapshot requests that Incus prepares for a snapshot migration.
func (r *ProtocolIncus) MigrateInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPost) (Operation, error) {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, err
	}

	// Quick check.
	if !instance.Migration {
		return nil, errors.New("Can't ask for a rename through MigrateInstanceSnapshot")
	}

	// Send the request
	op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s/snapshots/%s", path, url.PathEscape(instanceName), url.PathEscape(name)), instance, "")
	if err != nil {
		return nil, err
	}

	return op, nil
}

// DeleteInstanceSnapshot requests that Incus deletes the instance snapshot.
func (r *ProtocolIncus) DeleteInstanceSnapshot(instanceName string, name string) (Operation, error) {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, err
	}

	// Send the request
	op, _, err := r.queryOperation("DELETE", fmt.Sprintf("%s/%s/snapshots/%s", path, url.PathEscape(instanceName), url.PathEscape(name)), nil, "")
	if err != nil {
		return nil, err
	}

	return op, nil
}

// UpdateInstanceSnapshot requests that Incus updates the instance snapshot.
func (r *ProtocolIncus) UpdateInstanceSnapshot(instanceName string, name string, instance api.InstanceSnapshotPut, ETag string) (Operation, error) {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, err
	}

	if !r.HasExtension("snapshot_expiry") {
		return nil, errors.New("The server is missing the required \"snapshot_expiry\" API extension")
	}

	// Send the request
	op, _, err := r.queryOperation("PUT", fmt.Sprintf("%s/%s/snapshots/%s", path, url.PathEscape(instanceName), url.PathEscape(name)), instance, ETag)
	if err != nil {
		return nil, err
	}

	return op, nil
}

// GetInstanceState returns a InstanceState entry for the provided instance name.
func (r *ProtocolIncus) GetInstanceState(name string) (*api.InstanceState, string, error) {
	var uri string

	if r.IsAgent() {
		uri = "/state"
	} else {
		path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
		if err != nil {
			return nil, "", err
		}

		uri = fmt.Sprintf("%s/%s/state", path, url.PathEscape(name))
	}

	state := api.InstanceState{}

	// Fetch the raw value
	etag, err := r.queryStruct("GET", uri, nil, "", &state)
	if err != nil {
		return nil, "", err
	}

	return &state, etag, nil
}

// UpdateInstanceState updates the instance to match the requested state.
func (r *ProtocolIncus) UpdateInstanceState(name string, state api.InstanceStatePut, ETag string) (Operation, error) {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, err
	}

	// Send the request
	op, _, err := r.queryOperation("PUT", fmt.Sprintf("%s/%s/state", path, url.PathEscape(name)), state, ETag)
	if err != nil {
		return nil, err
	}

	return op, nil
}

// GetInstanceAccess returns an Access entry for the provided instance name.
func (r *ProtocolIncus) GetInstanceAccess(name string) (api.Access, error) {
	access := api.Access{}

	if !r.HasExtension("instance_access") {
		return nil, errors.New("The server is missing the required \"instance_access\" API extension")
	}

	// Fetch the raw value
	_, err := r.queryStruct("GET", fmt.Sprintf("/instances/%s/access", url.PathEscape(name)), nil, "", &access)
	if err != nil {
		return nil, err
	}

	return access, nil
}

// GetInstanceLogfiles returns a list of logfiles for the instance.
func (r *ProtocolIncus) GetInstanceLogfiles(name string) ([]string, error) {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, err
	}

	// Fetch the raw URL values.
	urls := []string{}
	baseURL := fmt.Sprintf("%s/%s/logs", path, url.PathEscape(name))
	_, err = r.queryStruct("GET", baseURL, nil, "", &urls)
	if err != nil {
		return nil, err
	}

	// Parse it.
	return urlsToResourceNames(baseURL, urls...)
}

// GetInstanceLogfile returns the content of the requested logfile.
//
// Note that it's the caller's responsibility to close the returned ReadCloser.
func (r *ProtocolIncus) GetInstanceLogfile(name string, filename string) (io.ReadCloser, error) {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, err
	}

	// Prepare the HTTP request
	uri := fmt.Sprintf("%s/1.0%s/%s/logs/%s", r.httpBaseURL.String(), path, url.PathEscape(name), url.PathEscape(filename))

	uri, err = r.setQueryAttributes(uri)
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("GET", uri, nil)
	if err != nil {
		return nil, err
	}

	// Send the request
	resp, err := r.DoHTTP(req)
	if err != nil {
		return nil, err
	}

	// Check the return value for a cleaner error
	if resp.StatusCode != http.StatusOK {
		_, _, err := incusParseResponse(resp)
		if err != nil {
			return nil, err
		}
	}

	return resp.Body, err
}

// DeleteInstanceLogfile deletes the requested logfile.
func (r *ProtocolIncus) DeleteInstanceLogfile(name string, filename string) error {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return err
	}

	// Send the request
	_, _, err = r.query("DELETE", fmt.Sprintf("%s/%s/logs/%s", path, url.PathEscape(name), url.PathEscape(filename)), nil, "")
	if err != nil {
		return err
	}

	return nil
}

// getInstanceExecOutputLogFile returns the content of the requested exec logfile.
//
// Note that it's the caller's responsibility to close the returned ReadCloser.
func (r *ProtocolIncus) getInstanceExecOutputLogFile(name string, filename string) (io.ReadCloser, error) {
	err := r.CheckExtension("container_exec_recording")
	if err != nil {
		return nil, err
	}

	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, err
	}

	// Prepare the HTTP request
	uri := fmt.Sprintf("%s/1.0%s/%s/logs/exec-output/%s", r.httpBaseURL.String(), path, url.PathEscape(name), url.PathEscape(filename))

	uri, err = r.setQueryAttributes(uri)
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("GET", uri, nil)
	if err != nil {
		return nil, err
	}

	// Send the request
	resp, err := r.DoHTTP(req)
	if err != nil {
		return nil, err
	}

	// Check the return value for a cleaner error
	if resp.StatusCode != http.StatusOK {
		_, _, err := incusParseResponse(resp)
		if err != nil {
			return nil, err
		}
	}

	return resp.Body, nil
}

// deleteInstanceExecOutputLogFiles deletes the requested exec logfile.
func (r *ProtocolIncus) deleteInstanceExecOutputLogFile(instanceName string, filename string) error {
	err := r.CheckExtension("container_exec_recording")
	if err != nil {
		return err
	}

	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return err
	}

	// Send the request
	_, _, err = r.query("DELETE", fmt.Sprintf("%s/%s/logs/exec-output/%s", path, url.PathEscape(instanceName), url.PathEscape(filename)), nil, "")
	if err != nil {
		return err
	}

	return nil
}

// GetInstanceMetadata returns instance metadata.
func (r *ProtocolIncus) GetInstanceMetadata(name string) (*api.ImageMetadata, string, error) {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, "", err
	}

	if !r.HasExtension("container_edit_metadata") {
		return nil, "", errors.New("The server is missing the required \"container_edit_metadata\" API extension")
	}

	metadata := api.ImageMetadata{}

	uri := fmt.Sprintf("%s/%s/metadata", path, url.PathEscape(name))
	etag, err := r.queryStruct("GET", uri, nil, "", &metadata)
	if err != nil {
		return nil, "", err
	}

	return &metadata, etag, err
}

// UpdateInstanceMetadata sets the content of the instance metadata file.
func (r *ProtocolIncus) UpdateInstanceMetadata(name string, metadata api.ImageMetadata, ETag string) error {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return err
	}

	if !r.HasExtension("container_edit_metadata") {
		return errors.New("The server is missing the required \"container_edit_metadata\" API extension")
	}

	uri := fmt.Sprintf("%s/%s/metadata", path, url.PathEscape(name))
	_, _, err = r.query("PUT", uri, metadata, ETag)
	if err != nil {
		return err
	}

	return nil
}

// GetInstanceTemplateFiles returns the list of names of template files for a instance.
func (r *ProtocolIncus) GetInstanceTemplateFiles(instanceName string) ([]string, error) {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, err
	}

	if !r.HasExtension("container_edit_metadata") {
		return nil, errors.New("The server is missing the required \"container_edit_metadata\" API extension")
	}

	templates := []string{}

	uri := fmt.Sprintf("%s/%s/metadata/templates", path, url.PathEscape(instanceName))
	_, err = r.queryStruct("GET", uri, nil, "", &templates)
	if err != nil {
		return nil, err
	}

	return templates, nil
}

// GetInstanceTemplateFile returns the content of a template file for a instance.
func (r *ProtocolIncus) GetInstanceTemplateFile(instanceName string, templateName string) (io.ReadCloser, error) {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, err
	}

	if !r.HasExtension("container_edit_metadata") {
		return nil, errors.New("The server is missing the required \"container_edit_metadata\" API extension")
	}

	uri := fmt.Sprintf("%s/1.0%s/%s/metadata/templates?path=%s", r.httpBaseURL.String(), path, url.PathEscape(instanceName), url.QueryEscape(templateName))

	uri, err = r.setQueryAttributes(uri)
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("GET", uri, nil)
	if err != nil {
		return nil, err
	}

	// Send the request
	resp, err := r.DoHTTP(req)
	if err != nil {
		return nil, err
	}

	// Check the return value for a cleaner error
	if resp.StatusCode != http.StatusOK {
		_, _, err := incusParseResponse(resp)
		if err != nil {
			return nil, err
		}
	}

	return resp.Body, err
}

// CreateInstanceTemplateFile creates an a template for a instance.
func (r *ProtocolIncus) CreateInstanceTemplateFile(instanceName string, templateName string, content io.ReadSeeker) error {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return err
	}

	if !r.HasExtension("container_edit_metadata") {
		return errors.New("The server is missing the required \"container_edit_metadata\" API extension")
	}

	uri := fmt.Sprintf("%s/1.0%s/%s/metadata/templates?path=%s", r.httpBaseURL.String(), path, url.PathEscape(instanceName), url.QueryEscape(templateName))

	uri, err = r.setQueryAttributes(uri)
	if err != nil {
		return err
	}

	req, err := http.NewRequest("POST", uri, content)
	if err != nil {
		return err
	}

	req.GetBody = func() (io.ReadCloser, error) {
		_, err := content.Seek(0, 0)
		if err != nil {
			return nil, err
		}

		return io.NopCloser(content), nil
	}

	req.Header.Set("Content-Type", "application/octet-stream")

	// Send the request
	resp, err := r.DoHTTP(req)
	// Check the return value for a cleaner error
	if resp.StatusCode != http.StatusOK {
		_, _, err := incusParseResponse(resp)
		if err != nil {
			return err
		}
	}
	return err
}

// DeleteInstanceTemplateFile deletes a template file for a instance.
func (r *ProtocolIncus) DeleteInstanceTemplateFile(name string, templateName string) error {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return err
	}

	if !r.HasExtension("container_edit_metadata") {
		return errors.New("The server is missing the required \"container_edit_metadata\" API extension")
	}

	_, _, err = r.query("DELETE", fmt.Sprintf("%s/%s/metadata/templates?path=%s", path, url.PathEscape(name), url.QueryEscape(templateName)), nil, "")
	return err
}

// ConsoleInstance requests that Incus attaches to the console device of a instance.
func (r *ProtocolIncus) ConsoleInstance(instanceName string, console api.InstanceConsolePost, args *InstanceConsoleArgs) (Operation, error) {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, err
	}

	if !r.HasExtension("console") {
		return nil, errors.New("The server is missing the required \"console\" API extension")
	}

	if console.Type == "" {
		console.Type = "console"
	}

	if console.Type == "vga" && !r.HasExtension("console_vga_type") {
		return nil, errors.New("The server is missing the required \"console_vga_type\" API extension")
	}

	if console.Force && !r.HasExtension("console_force") {
		return nil, errors.New(`The server is missing the required "console_force" API extension`)
	}

	// Send the request
	op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s/console", path, url.PathEscape(instanceName)), console, "")
	if err != nil {
		return nil, err
	}

	opAPI := op.Get()

	if args == nil || args.Terminal == nil {
		return nil, errors.New("A terminal must be set")
	}

	if args.Control == nil {
		return nil, errors.New("A control channel must be set")
	}

	// Parse the fds
	fds := map[string]string{}

	value, ok := opAPI.Metadata["fds"]
	if ok {
		values, ok := value.(map[string]any)
		if ok {
			for k, v := range values {
				val, ok := v.(string)
				if ok {
					fds[k] = val
				}
			}
		}
	}

	var controlConn *websocket.Conn
	// Call the control handler with a connection to the control socket
	if fds[api.SecretNameControl] == "" {
		return nil, errors.New("Did not receive a file descriptor for the control channel")
	}

	controlConn, err = r.GetOperationWebsocket(opAPI.ID, fds[api.SecretNameControl])
	if err != nil {
		return nil, err
	}

	go args.Control(controlConn)

	// Connect to the websocket
	conn, err := r.GetOperationWebsocket(opAPI.ID, fds["0"])
	if err != nil {
		return nil, err
	}

	// Detach from console.
	go func(consoleDisconnect <-chan bool) {
		<-consoleDisconnect
		msg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "Detaching from console")
		// We don't care if this fails. This is just for convenience.
		_ = controlConn.WriteMessage(websocket.CloseMessage, msg)
		_ = controlConn.Close()
	}(args.ConsoleDisconnect)

	// And attach stdin and stdout to it
	go func() {
		_, writeDone := ws.Mirror(conn, args.Terminal)
		<-writeDone
		_ = conn.Close()
	}()

	return op, nil
}

// ConsoleInstanceDynamic requests that Incus attaches to the console device of a
// instance with the possibility of opening multiple connections to it.
//
// Every time the returned 'console' function is called, a new connection will
// be established and proxied to the given io.ReadWriteCloser.
func (r *ProtocolIncus) ConsoleInstanceDynamic(instanceName string, console api.InstanceConsolePost, args *InstanceConsoleArgs) (Operation, func(io.ReadWriteCloser) error, error) {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, nil, err
	}

	if !r.HasExtension("console") {
		return nil, nil, errors.New("The server is missing the required \"console\" API extension")
	}

	if console.Type == "" {
		console.Type = "console"
	}

	if console.Type == "vga" && !r.HasExtension("console_vga_type") {
		return nil, nil, errors.New("The server is missing the required \"console_vga_type\" API extension")
	}

	if console.Force && !r.HasExtension("console_force") {
		return nil, nil, errors.New(`The server is missing the required "console_force" API extension`)
	}

	// Send the request.
	op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s/console", path, url.PathEscape(instanceName)), console, "")
	if err != nil {
		return nil, nil, err
	}

	opAPI := op.Get()

	if args == nil {
		return nil, nil, errors.New("No arguments provided")
	}

	if args.Control == nil {
		return nil, nil, errors.New("A control channel must be set")
	}

	// Parse the fds.
	fds := map[string]string{}

	value, ok := opAPI.Metadata["fds"]
	if ok {
		values, ok := value.(map[string]any)
		if ok {
			for k, v := range values {
				val, ok := v.(string)
				if ok {
					fds[k] = val
				}
			}
		}
	}

	// Call the control handler with a connection to the control socket.
	if fds[api.SecretNameControl] == "" {
		return nil, nil, errors.New("Did not receive a file descriptor for the control channel")
	}

	controlConn, err := r.GetOperationWebsocket(opAPI.ID, fds[api.SecretNameControl])
	if err != nil {
		return nil, nil, err
	}

	go args.Control(controlConn)

	// Handle main disconnect.
	go func(consoleDisconnect <-chan bool) {
		<-consoleDisconnect
		msg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "Detaching from console")
		// We don't care if this fails. This is just for convenience.
		_ = controlConn.WriteMessage(websocket.CloseMessage, msg)
		_ = controlConn.Close()
	}(args.ConsoleDisconnect)

	f := func(rwc io.ReadWriteCloser) error {
		// Connect to the websocket.
		conn, err := r.GetOperationWebsocket(opAPI.ID, fds["0"])
		if err != nil {
			return err
		}

		// Attach reader/writer.
		_, writeDone := ws.Mirror(conn, rwc)
		<-writeDone
		_ = conn.Close()

		return nil
	}

	return op, f, nil
}

// GetInstanceConsoleLog requests that Incus attaches to the console device of a instance.
//
// Note that it's the caller's responsibility to close the returned ReadCloser.
func (r *ProtocolIncus) GetInstanceConsoleLog(instanceName string, _ *InstanceConsoleLogArgs) (io.ReadCloser, error) {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, err
	}

	if !r.HasExtension("console") {
		return nil, errors.New("The server is missing the required \"console\" API extension")
	}

	// Prepare the HTTP request
	uri := fmt.Sprintf("%s/1.0%s/%s/console", r.httpBaseURL.String(), path, url.PathEscape(instanceName))

	uri, err = r.setQueryAttributes(uri)
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("GET", uri, nil)
	if err != nil {
		return nil, err
	}

	// Send the request
	resp, err := r.DoHTTP(req)
	if err != nil {
		return nil, err
	}

	// Check the return value for a cleaner error
	if resp.StatusCode != http.StatusOK {
		_, _, err := incusParseResponse(resp)
		if err != nil {
			return nil, err
		}
	}

	return resp.Body, err
}

// DeleteInstanceConsoleLog deletes the requested instance's console log.
func (r *ProtocolIncus) DeleteInstanceConsoleLog(instanceName string, _ *InstanceConsoleLogArgs) error {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return err
	}

	if !r.HasExtension("console") {
		return errors.New("The server is missing the required \"console\" API extension")
	}

	// Send the request
	_, _, err = r.query("DELETE", fmt.Sprintf("%s/%s/console", path, url.PathEscape(instanceName)), nil, "")
	if err != nil {
		return err
	}

	return nil
}

// GetInstanceBackupNames returns a list of backup names for the instance.
func (r *ProtocolIncus) GetInstanceBackupNames(instanceName string) ([]string, error) {
	if !r.HasExtension("container_backup") {
		return nil, errors.New("The server is missing the required \"container_backup\" API extension")
	}

	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, err
	}

	// Fetch the raw URL values.
	urls := []string{}
	baseURL := fmt.Sprintf("%s/%s/backups", path, url.PathEscape(instanceName))
	_, err = r.queryStruct("GET", baseURL, nil, "", &urls)
	if err != nil {
		return nil, err
	}

	// Parse it.
	return urlsToResourceNames(baseURL, urls...)
}

// GetInstanceBackups returns a list of backups for the instance.
func (r *ProtocolIncus) GetInstanceBackups(instanceName string) ([]api.InstanceBackup, error) {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, err
	}

	if !r.HasExtension("container_backup") {
		return nil, errors.New("The server is missing the required \"container_backup\" API extension")
	}

	// Fetch the raw value
	backups := []api.InstanceBackup{}

	_, err = r.queryStruct("GET", fmt.Sprintf("%s/%s/backups?recursion=1", path, url.PathEscape(instanceName)), nil, "", &backups)
	if err != nil {
		return nil, err
	}

	return backups, nil
}

// GetInstanceBackup returns a Backup struct for the provided instance and backup names.
func (r *ProtocolIncus) GetInstanceBackup(instanceName string, name string) (*api.InstanceBackup, string, error) {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, "", err
	}

	if !r.HasExtension("container_backup") {
		return nil, "", errors.New("The server is missing the required \"container_backup\" API extension")
	}

	// Fetch the raw value
	backup := api.InstanceBackup{}
	etag, err := r.queryStruct("GET", fmt.Sprintf("%s/%s/backups/%s", path, url.PathEscape(instanceName), url.PathEscape(name)), nil, "", &backup)
	if err != nil {
		return nil, "", err
	}

	return &backup, etag, nil
}

// CreateInstanceBackup requests that Incus creates a new backup for the instance.
func (r *ProtocolIncus) CreateInstanceBackup(instanceName string, backup api.InstanceBackupsPost) (Operation, error) {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, err
	}

	if !r.HasExtension("container_backup") {
		return nil, errors.New("The server is missing the required \"container_backup\" API extension")
	}

	// Send the request
	op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s/backups", path, url.PathEscape(instanceName)), backup, "")
	if err != nil {
		return nil, err
	}

	return op, nil
}

// RenameInstanceBackup requests that Incus renames the backup.
func (r *ProtocolIncus) RenameInstanceBackup(instanceName string, name string, backup api.InstanceBackupPost) (Operation, error) {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, err
	}

	if !r.HasExtension("container_backup") {
		return nil, errors.New("The server is missing the required \"container_backup\" API extension")
	}

	// Send the request
	op, _, err := r.queryOperation("POST", fmt.Sprintf("%s/%s/backups/%s", path, url.PathEscape(instanceName), url.PathEscape(name)), backup, "")
	if err != nil {
		return nil, err
	}

	return op, nil
}

// DeleteInstanceBackup requests that Incus deletes the instance backup.
func (r *ProtocolIncus) DeleteInstanceBackup(instanceName string, name string) (Operation, error) {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, err
	}

	if !r.HasExtension("container_backup") {
		return nil, errors.New("The server is missing the required \"container_backup\" API extension")
	}

	// Send the request
	op, _, err := r.queryOperation("DELETE", fmt.Sprintf("%s/%s/backups/%s", path, url.PathEscape(instanceName), url.PathEscape(name)), nil, "")
	if err != nil {
		return nil, err
	}

	return op, nil
}

// GetInstanceBackupFile requests the instance backup content.
func (r *ProtocolIncus) GetInstanceBackupFile(instanceName string, name string, req *BackupFileRequest) (*BackupFileResponse, error) {
	path, _, err := r.instanceTypeToPath(api.InstanceTypeAny)
	if err != nil {
		return nil, err
	}

	if !r.HasExtension("container_backup") {
		return nil, errors.New("The server is missing the required \"container_backup\" API extension")
	}

	// Build the URL
	uri := fmt.Sprintf("%s/1.0%s/%s/backups/%s/export", r.httpBaseURL.String(), path, url.PathEscape(instanceName), url.PathEscape(name))
	if r.project != "" {
		uri += fmt.Sprintf("?project=%s", url.QueryEscape(r.project))
	}

	// Prepare the download request
	request, err := http.NewRequest("GET", uri, nil)
	if err != nil {
		return nil, err
	}

	if r.httpUserAgent != "" {
		request.Header.Set("User-Agent", r.httpUserAgent)
	}

	// Start the request
	response, doneCh, err := cancel.CancelableDownload(req.Canceler, r.DoHTTP, request)
	if err != nil {
		return nil, err
	}

	defer func() { _ = response.Body.Close() }()
	defer close(doneCh)

	if response.StatusCode != http.StatusOK {
		_, _, err := incusParseResponse(response)
		if err != nil {
			return nil, err
		}
	}

	// Handle the data
	body := response.Body
	if req.ProgressHandler != nil {
		body = &ioprogress.ProgressReader{
			ReadCloser: response.Body,
			Tracker: &ioprogress.ProgressTracker{
				Length: response.ContentLength,
				Handler: func(percent int64, speed int64) {
					req.ProgressHandler(ioprogress.ProgressData{Text: fmt.Sprintf("%d%% (%s/s)", percent, units.GetByteSizeString(speed, 2))})
				},
			},
		}
	}

	size, err := io.Copy(req.BackupFile, body)
	if err != nil {
		return nil, err
	}

	resp := BackupFileResponse{}
	resp.Size = size

	return &resp, nil
}

func (r *ProtocolIncus) proxyMigration(targetOp *operation, targetSecrets map[string]string, source InstanceServer, sourceOp *operation, sourceSecrets map[string]string) error {
	// Quick checks.
	for n := range targetSecrets {
		_, ok := sourceSecrets[n]
		if !ok {
			return fmt.Errorf("Migration target expects the \"%s\" socket but source isn't providing it", n)
		}
	}

	if targetSecrets[api.SecretNameControl] == "" {
		return errors.New("Migration target didn't setup the required \"control\" socket")
	}

	// Struct used to hold everything together
	type proxy struct {
		done       chan struct{}
		sourceConn *websocket.Conn
		targetConn *websocket.Conn
	}

	proxies := map[string]*proxy{}

	// Connect the control socket
	sourceConn, err := source.GetOperationWebsocket(sourceOp.ID, sourceSecrets[api.SecretNameControl])
	if err != nil {
		return err
	}

	targetConn, err := r.GetOperationWebsocket(targetOp.ID, targetSecrets[api.SecretNameControl])
	if err != nil {
		return err
	}

	proxies[api.SecretNameControl] = &proxy{
		done:       ws.Proxy(sourceConn, targetConn),
		sourceConn: sourceConn,
		targetConn: targetConn,
	}

	// Connect the data sockets
	for name := range sourceSecrets {
		if name == api.SecretNameControl {
			continue
		}

		// Handle resets (used for multiple objects)
		sourceConn, err := source.GetOperationWebsocket(sourceOp.ID, sourceSecrets[name])
		if err != nil {
			break
		}

		targetConn, err := r.GetOperationWebsocket(targetOp.ID, targetSecrets[name])
		if err != nil {
			break
		}

		proxies[name] = &proxy{
			sourceConn: sourceConn,
			targetConn: targetConn,
			done:       ws.Proxy(sourceConn, targetConn),
		}
	}

	// Cleanup once everything is done
	go func() {
		// Wait for control socket
		<-proxies[api.SecretNameControl].done
		_ = proxies[api.SecretNameControl].sourceConn.Close()
		_ = proxies[api.SecretNameControl].targetConn.Close()

		// Then deal with the others
		for name, proxy := range proxies {
			if name == api.SecretNameControl {
				continue
			}

			<-proxy.done
			_ = proxy.sourceConn.Close()
			_ = proxy.targetConn.Close()
		}
	}()

	return nil
}

// GetInstanceDebugMemory retrieves memory debug information for a given instance and saves it to the specified file path.
func (r *ProtocolIncus) GetInstanceDebugMemory(name string, format string) (io.ReadCloser, error) {
	path, v, err := r.instanceTypeToPath(api.InstanceTypeVM)
	if err != nil {
		return nil, err
	}

	v.Set("format", format)

	// Prepare the HTTP request
	requestURL := fmt.Sprintf("%s/1.0%s/%s/debug/memory?%s", r.httpBaseURL.String(), path, url.PathEscape(name), v.Encode())

	requestURL, err = r.setQueryAttributes(requestURL)
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("GET", requestURL, nil)
	if err != nil {
		return nil, err
	}

	// Send the request
	resp, err := r.DoHTTP(req)
	if err != nil {
		return nil, err
	}

	// Check the return value for a cleaner error
	if resp.StatusCode != http.StatusOK {
		_, _, err := incusParseResponse(resp)
		if err != nil {
			return nil, err
		}
	}

	return resp.Body, nil
}
