package hcloud

import (
	"context"
	"fmt"
	"net"
	"net/url"
	"slices"
	"strconv"
	"time"

	"github.com/hetznercloud/hcloud-go/v2/hcloud/exp/ctxutil"
	"github.com/hetznercloud/hcloud-go/v2/hcloud/schema"
)

// Server represents a server in the Hetzner Cloud.
type Server struct {
	ID              int64
	Name            string
	Status          ServerStatus
	Created         time.Time
	PublicNet       ServerPublicNet
	PrivateNet      []ServerPrivateNet
	ServerType      *ServerType
	Location        *Location
	IncludedTraffic uint64
	OutgoingTraffic uint64
	IngoingTraffic  uint64
	BackupWindow    string
	RescueEnabled   bool
	Locked          bool
	ISO             *ISO
	Image           *Image
	Protection      ServerProtection
	Labels          map[string]string
	Volumes         []*Volume
	PrimaryDiskSize int
	PlacementGroup  *PlacementGroup
	LoadBalancers   []*LoadBalancer
}

func (o *Server) pathID() (string, error) {
	if o.ID == 0 {
		return "", missingField(o, "ID")
	}
	return strconv.FormatInt(o.ID, 10), nil
}

// ServerProtection represents the protection level of a server.
type ServerProtection struct {
	Delete, Rebuild bool
}

// ServerStatus specifies a server's status.
type ServerStatus string

const (
	// ServerStatusInitializing is the status when a server is initializing.
	ServerStatusInitializing ServerStatus = "initializing"

	// ServerStatusOff is the status when a server is off.
	ServerStatusOff ServerStatus = "off"

	// ServerStatusRunning is the status when a server is running.
	ServerStatusRunning ServerStatus = "running"

	// ServerStatusStarting is the status when a server is being started.
	ServerStatusStarting ServerStatus = "starting"

	// ServerStatusStopping is the status when a server is being stopped.
	ServerStatusStopping ServerStatus = "stopping"

	// ServerStatusMigrating is the status when a server is being migrated.
	ServerStatusMigrating ServerStatus = "migrating"

	// ServerStatusRebuilding is the status when a server is being rebuilt.
	ServerStatusRebuilding ServerStatus = "rebuilding"

	// ServerStatusDeleting is the status when a server is being deleted.
	ServerStatusDeleting ServerStatus = "deleting"

	// ServerStatusUnknown is the status when a server's state is unknown.
	ServerStatusUnknown ServerStatus = "unknown"
)

// FirewallStatus specifies a Firewall's status.
type FirewallStatus string

const (
	// FirewallStatusPending is the status when a Firewall is pending.
	FirewallStatusPending FirewallStatus = "pending"

	// FirewallStatusApplied is the status when a Firewall is applied.
	FirewallStatusApplied FirewallStatus = "applied"
)

// ServerPublicNet represents a server's public network.
type ServerPublicNet struct {
	IPv4        ServerPublicNetIPv4
	IPv6        ServerPublicNetIPv6
	FloatingIPs []*FloatingIP
	Firewalls   []*ServerFirewallStatus
}

// ServerPublicNetIPv4 represents a server's public IPv4 address.
type ServerPublicNetIPv4 struct {
	ID      int64
	IP      net.IP
	Blocked bool
	DNSPtr  string
}

func (o *ServerPublicNetIPv4) IsUnspecified() bool {
	return o.IP == nil || o.IP.Equal(net.IPv4zero)
}

// ServerPublicNetIPv6 represents a Server's public IPv6 network and address.
type ServerPublicNetIPv6 struct {
	ID      int64
	IP      net.IP
	Network *net.IPNet
	Blocked bool
	DNSPtr  map[string]string
}

func (o *ServerPublicNetIPv6) IsUnspecified() bool {
	return o.IP == nil || o.IP.Equal(net.IPv6unspecified)
}

// ServerPrivateNet defines the schema of a Server's private network information.
type ServerPrivateNet struct {
	Network    *Network
	IP         net.IP
	Aliases    []net.IP
	MACAddress string
}

// DNSPtrForIP returns the reverse dns pointer of the ip address.
func (o *ServerPublicNetIPv6) DNSPtrForIP(ip net.IP) string {
	return o.DNSPtr[ip.String()]
}

// ServerFirewallStatus represents a Firewall and its status on a Server's
// network interface.
type ServerFirewallStatus struct {
	Firewall Firewall
	Status   FirewallStatus
}

// ServerRescueType represents rescue types.
type ServerRescueType string

// List of rescue types.
const (
	// Deprecated: Use ServerRescueTypeLinux64 instead.
	ServerRescueTypeLinux32 ServerRescueType = "linux32"
	ServerRescueTypeLinux64 ServerRescueType = "linux64"
)

// changeDNSPtr changes or resets the reverse DNS pointer for a IP address.
// Pass a nil ptr to reset the reverse DNS pointer to its default value.
func (o *Server) changeDNSPtr(ctx context.Context, client *Client, ip net.IP, ptr *string) (*Action, *Response, error) {
	const opPath = "/servers/%d/actions/change_dns_ptr"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	reqPath := fmt.Sprintf(opPath, o.ID)

	reqBody := schema.ServerActionChangeDNSPtrRequest{
		IP:     ip.String(),
		DNSPtr: ptr,
	}

	respBody, resp, err := postRequest[schema.ServerActionChangeDNSPtrResponse](ctx, client, reqPath, reqBody)
	if err != nil {
		return nil, resp, err
	}

	return ActionFromSchema(respBody.Action), resp, nil
}

// GetDNSPtrForIP searches for the dns assigned to the given IP address.
// It returns an error if there is no dns set for the given IP address.
func (o *Server) GetDNSPtrForIP(ip net.IP) (string, error) {
	if net.IP.Equal(o.PublicNet.IPv4.IP, ip) {
		return o.PublicNet.IPv4.DNSPtr, nil
	} else if dns, ok := o.PublicNet.IPv6.DNSPtr[ip.String()]; ok {
		return dns, nil
	}

	return "", DNSNotFoundError{ip}
}

// PrivateNetFor returns the server's network attachment information in the given
// Network, and nil if no attachment was found.
func (o *Server) PrivateNetFor(network *Network) *ServerPrivateNet {
	index := slices.IndexFunc(o.PrivateNet, func(n ServerPrivateNet) bool {
		return n.Network != nil && n.Network.ID == network.ID
	})
	if index < 0 {
		return nil
	}
	return &o.PrivateNet[index]
}

// ServerClient is a client for the servers API.
type ServerClient struct {
	client *Client
	Action *ResourceActionClient[*Server]
}

// GetByID retrieves a server by its ID. If the server does not exist, nil is returned.
func (c *ServerClient) GetByID(ctx context.Context, id int64) (*Server, *Response, error) {
	const opPath = "/servers/%d"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	reqPath := fmt.Sprintf(opPath, id)

	respBody, resp, err := getRequest[schema.ServerGetResponse](ctx, c.client, reqPath)
	if err != nil {
		if IsError(err, ErrorCodeNotFound) {
			return nil, resp, nil
		}
		return nil, resp, err
	}

	return ServerFromSchema(respBody.Server), resp, nil
}

// GetByName retrieves a server by its name. If the server does not exist, nil is returned.
func (c *ServerClient) GetByName(ctx context.Context, name string) (*Server, *Response, error) {
	return firstByName(name, func() ([]*Server, *Response, error) {
		return c.List(ctx, ServerListOpts{Name: name})
	})
}

// Get retrieves a server by its ID if the input can be parsed as an integer, otherwise it
// retrieves a server by its name. If the server does not exist, nil is returned.
func (c *ServerClient) Get(ctx context.Context, idOrName string) (*Server, *Response, error) {
	return getByIDOrName(ctx, c.GetByID, c.GetByName, idOrName)
}

// ServerListOpts specifies options for listing servers.
type ServerListOpts struct {
	ListOpts
	Name   string
	Status []ServerStatus
	Sort   []string
}

func (l ServerListOpts) Values() url.Values {
	vals := l.ListOpts.Values()
	if l.Name != "" {
		vals.Add("name", l.Name)
	}
	for _, status := range l.Status {
		vals.Add("status", string(status))
	}
	for _, sort := range l.Sort {
		vals.Add("sort", sort)
	}
	return vals
}

// List returns a list of servers for a specific page.
//
// Please note that filters specified in opts are not taken into account
// when their value corresponds to their zero value or when they are empty.
func (c *ServerClient) List(ctx context.Context, opts ServerListOpts) ([]*Server, *Response, error) {
	const opPath = "/servers?%s"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	reqPath := fmt.Sprintf(opPath, opts.Values().Encode())

	respBody, resp, err := getRequest[schema.ServerListResponse](ctx, c.client, reqPath)
	if err != nil {
		return nil, resp, err
	}

	return allFromSchemaFunc(respBody.Servers, ServerFromSchema), resp, nil
}

// All returns all servers.
func (c *ServerClient) All(ctx context.Context) ([]*Server, error) {
	return c.AllWithOpts(ctx, ServerListOpts{})
}

// AllWithOpts returns all servers for the given options.
func (c *ServerClient) AllWithOpts(ctx context.Context, opts ServerListOpts) ([]*Server, error) {
	if opts.ListOpts.PerPage == 0 {
		opts.ListOpts.PerPage = 50
	}
	return iterPages(func(page int) ([]*Server, *Response, error) {
		opts.Page = page
		return c.List(ctx, opts)
	})
}

// ServerCreateOpts specifies options for creating a new server.
type ServerCreateOpts struct {
	Name             string
	ServerType       *ServerType
	Image            *Image
	SSHKeys          []*SSHKey
	Location         *Location
	UserData         string
	StartAfterCreate *bool
	Labels           map[string]string
	Automount        *bool
	Volumes          []*Volume
	Networks         []*Network
	Firewalls        []*ServerCreateFirewall
	PlacementGroup   *PlacementGroup
	PublicNet        *ServerCreatePublicNet
}

type ServerCreatePublicNet struct {
	EnableIPv4 bool
	EnableIPv6 bool
	IPv4       *PrimaryIP
	IPv6       *PrimaryIP
}

// ServerCreateFirewall defines which Firewalls to apply when creating a Server.
type ServerCreateFirewall struct {
	Firewall Firewall
}

// Validate checks if options are valid.
func (o ServerCreateOpts) Validate() error {
	if o.Name == "" {
		return missingField(o, "Name")
	}
	if o.ServerType == nil || (o.ServerType.ID == 0 && o.ServerType.Name == "") {
		return missingField(o, "ServerType")
	}
	if o.Image == nil || (o.Image.ID == 0 && o.Image.Name == "") {
		return missingField(o, "Image")
	}
	return nil
}

// ServerCreateResult is the result of a create server call.
type ServerCreateResult struct {
	Server       *Server
	Action       *Action
	RootPassword string
	NextActions  []*Action
}

// Create creates a new server.
func (c *ServerClient) Create(ctx context.Context, opts ServerCreateOpts) (ServerCreateResult, *Response, error) {
	const opPath = "/servers"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	result := ServerCreateResult{}

	reqPath := opPath

	if err := opts.Validate(); err != nil {
		return result, nil, err
	}

	var reqBody schema.ServerCreateRequest
	reqBody.UserData = opts.UserData
	reqBody.Name = opts.Name
	reqBody.Automount = opts.Automount
	reqBody.StartAfterCreate = opts.StartAfterCreate
	if opts.ServerType.ID != 0 || opts.ServerType.Name != "" {
		reqBody.ServerType = schema.IDOrName{ID: opts.ServerType.ID, Name: opts.ServerType.Name}
	}
	if opts.Image.ID != 0 || opts.Image.Name != "" {
		reqBody.Image = schema.IDOrName{ID: opts.Image.ID, Name: opts.Image.Name}
	}
	if opts.Labels != nil {
		reqBody.Labels = &opts.Labels
	}
	for _, sshKey := range opts.SSHKeys {
		reqBody.SSHKeys = append(reqBody.SSHKeys, sshKey.ID)
	}
	for _, volume := range opts.Volumes {
		reqBody.Volumes = append(reqBody.Volumes, volume.ID)
	}
	for _, network := range opts.Networks {
		reqBody.Networks = append(reqBody.Networks, network.ID)
	}
	for _, firewall := range opts.Firewalls {
		reqBody.Firewalls = append(reqBody.Firewalls, schema.ServerCreateFirewalls{
			Firewall: firewall.Firewall.ID,
		})
	}

	if opts.PublicNet != nil {
		reqBody.PublicNet = &schema.ServerCreatePublicNet{
			EnableIPv4: opts.PublicNet.EnableIPv4,
			EnableIPv6: opts.PublicNet.EnableIPv6,
		}
		if opts.PublicNet.IPv4 != nil {
			reqBody.PublicNet.IPv4ID = opts.PublicNet.IPv4.ID
		}
		if opts.PublicNet.IPv6 != nil {
			reqBody.PublicNet.IPv6ID = opts.PublicNet.IPv6.ID
		}
	}
	if opts.Location != nil {
		if opts.Location.ID != 0 {
			reqBody.Location = strconv.FormatInt(opts.Location.ID, 10)
		} else {
			reqBody.Location = opts.Location.Name
		}
	}
	if opts.PlacementGroup != nil {
		reqBody.PlacementGroup = opts.PlacementGroup.ID
	}

	respBody, resp, err := postRequest[schema.ServerCreateResponse](ctx, c.client, reqPath, reqBody)
	if err != nil {
		return result, resp, err
	}

	result.Server = ServerFromSchema(respBody.Server)
	result.Action = ActionFromSchema(respBody.Action)
	result.NextActions = ActionsFromSchema(respBody.NextActions)
	if respBody.RootPassword != nil {
		result.RootPassword = *respBody.RootPassword
	}

	return result, resp, nil
}

// ServerDeleteResult is the result of a delete server call.
type ServerDeleteResult struct {
	Action *Action
}

// Delete deletes a server.
//
// Deprecated: Use [ServerClient.DeleteWithResult] instead.
func (c *ServerClient) Delete(ctx context.Context, server *Server) (*Response, error) {
	_, resp, err := c.DeleteWithResult(ctx, server)
	return resp, err
}

// DeleteWithResult deletes a server and returns the parsed response containing the action.
func (c *ServerClient) DeleteWithResult(ctx context.Context, server *Server) (*ServerDeleteResult, *Response, error) {
	const opPath = "/servers/%d"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	result := &ServerDeleteResult{}

	reqPath := fmt.Sprintf(opPath, server.ID)

	respBody, resp, err := deleteRequest[schema.ServerDeleteResponse](ctx, c.client, reqPath)
	if err != nil {
		return result, resp, err
	}

	result.Action = ActionFromSchema(respBody.Action)

	return result, resp, nil
}

// ServerUpdateOpts specifies options for updating a server.
type ServerUpdateOpts struct {
	Name   string
	Labels map[string]string
}

// Update updates a server.
func (c *ServerClient) Update(ctx context.Context, server *Server, opts ServerUpdateOpts) (*Server, *Response, error) {
	const opPath = "/servers/%d"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	reqPath := fmt.Sprintf(opPath, server.ID)

	reqBody := schema.ServerUpdateRequest{
		Name: opts.Name,
	}
	if opts.Labels != nil {
		reqBody.Labels = &opts.Labels
	}

	respBody, resp, err := putRequest[schema.ServerUpdateResponse](ctx, c.client, reqPath, reqBody)
	if err != nil {
		return nil, resp, err
	}

	return ServerFromSchema(respBody.Server), resp, nil
}

// Poweron starts a server.
func (c *ServerClient) Poweron(ctx context.Context, server *Server) (*Action, *Response, error) {
	const opPath = "/servers/%d/actions/poweron"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	reqPath := fmt.Sprintf(opPath, server.ID)

	respBody, resp, err := postRequest[schema.ServerActionPoweronResponse](ctx, c.client, reqPath, nil)
	if err != nil {
		return nil, resp, err
	}

	return ActionFromSchema(respBody.Action), resp, nil
}

// Reboot reboots a server.
func (c *ServerClient) Reboot(ctx context.Context, server *Server) (*Action, *Response, error) {
	const opPath = "/servers/%d/actions/reboot"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	reqPath := fmt.Sprintf(opPath, server.ID)

	respBody, resp, err := postRequest[schema.ServerActionRebootResponse](ctx, c.client, reqPath, nil)
	if err != nil {
		return nil, resp, err
	}

	return ActionFromSchema(respBody.Action), resp, nil
}

// Reset resets a server.
func (c *ServerClient) Reset(ctx context.Context, server *Server) (*Action, *Response, error) {
	const opPath = "/servers/%d/actions/reset"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	reqPath := fmt.Sprintf(opPath, server.ID)

	respBody, resp, err := postRequest[schema.ServerActionResetResponse](ctx, c.client, reqPath, nil)
	if err != nil {
		return nil, resp, err
	}

	return ActionFromSchema(respBody.Action), resp, nil
}

// Shutdown shuts down a server.
func (c *ServerClient) Shutdown(ctx context.Context, server *Server) (*Action, *Response, error) {
	const opPath = "/servers/%d/actions/shutdown"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	reqPath := fmt.Sprintf(opPath, server.ID)

	respBody, resp, err := postRequest[schema.ServerActionShutdownResponse](ctx, c.client, reqPath, nil)
	if err != nil {
		return nil, resp, err
	}

	return ActionFromSchema(respBody.Action), resp, nil
}

// Poweroff stops a server.
func (c *ServerClient) Poweroff(ctx context.Context, server *Server) (*Action, *Response, error) {
	const opPath = "/servers/%d/actions/poweroff"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	reqPath := fmt.Sprintf(opPath, server.ID)

	respBody, resp, err := postRequest[schema.ServerActionPoweroffResponse](ctx, c.client, reqPath, nil)
	if err != nil {
		return nil, resp, err
	}

	return ActionFromSchema(respBody.Action), resp, nil
}

// ServerResetPasswordResult is the result of resetting a server's password.
type ServerResetPasswordResult struct {
	Action       *Action
	RootPassword string
}

// ResetPassword resets a server's password.
func (c *ServerClient) ResetPassword(ctx context.Context, server *Server) (ServerResetPasswordResult, *Response, error) {
	const opPath = "/servers/%d/actions/reset_password"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	result := ServerResetPasswordResult{}

	reqPath := fmt.Sprintf(opPath, server.ID)

	respBody, resp, err := postRequest[schema.ServerActionResetPasswordResponse](ctx, c.client, reqPath, nil)
	if err != nil {
		return result, resp, err
	}

	result.Action = ActionFromSchema(respBody.Action)
	result.RootPassword = respBody.RootPassword

	return result, resp, nil
}

// ServerCreateImageOpts specifies options for creating an image from a server.
type ServerCreateImageOpts struct {
	Type        ImageType
	Description *string
	Labels      map[string]string
}

// Validate checks if options are valid.
func (o ServerCreateImageOpts) Validate() error {
	switch o.Type {
	case ImageTypeSnapshot, ImageTypeBackup:
		break
	case "":
		break
	default:
		return invalidFieldValue(o, "Type", o.Type)
	}

	return nil
}

// ServerCreateImageResult is the result of creating an image from a server.
type ServerCreateImageResult struct {
	Action *Action
	Image  *Image
}

// CreateImage creates an image from a server.
func (c *ServerClient) CreateImage(ctx context.Context, server *Server, opts *ServerCreateImageOpts) (ServerCreateImageResult, *Response, error) {
	const opPath = "/servers/%d/actions/create_image"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	result := ServerCreateImageResult{}

	reqPath := fmt.Sprintf(opPath, server.ID)

	reqBody := schema.ServerActionCreateImageRequest{}
	if opts != nil {
		if err := opts.Validate(); err != nil {
			return result, nil, err
		}
		if opts.Description != nil {
			reqBody.Description = opts.Description
		}
		if opts.Type != "" {
			reqBody.Type = Ptr(string(opts.Type))
		}
		if opts.Labels != nil {
			reqBody.Labels = &opts.Labels
		}
	}

	respBody, resp, err := postRequest[schema.ServerActionCreateImageResponse](ctx, c.client, reqPath, reqBody)
	if err != nil {
		return result, resp, err
	}

	result.Image = ImageFromSchema(respBody.Image)
	result.Action = ActionFromSchema(respBody.Action)

	return result, resp, nil
}

// ServerEnableRescueOpts specifies options for enabling rescue mode for a server.
type ServerEnableRescueOpts struct {
	Type    ServerRescueType
	SSHKeys []*SSHKey
}

// ServerEnableRescueResult is the result of enabling rescue mode for a server.
type ServerEnableRescueResult struct {
	Action       *Action
	RootPassword string
}

// EnableRescue enables rescue mode for a server.
func (c *ServerClient) EnableRescue(ctx context.Context, server *Server, opts ServerEnableRescueOpts) (ServerEnableRescueResult, *Response, error) {
	const opPath = "/servers/%d/actions/enable_rescue"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	result := ServerEnableRescueResult{}

	reqPath := fmt.Sprintf(opPath, server.ID)

	reqBody := schema.ServerActionEnableRescueRequest{
		Type: Ptr(string(opts.Type)),
	}
	for _, sshKey := range opts.SSHKeys {
		reqBody.SSHKeys = append(reqBody.SSHKeys, sshKey.ID)
	}

	respBody, resp, err := postRequest[schema.ServerActionEnableRescueResponse](ctx, c.client, reqPath, reqBody)
	if err != nil {
		return result, resp, err
	}

	result.Action = ActionFromSchema(respBody.Action)
	result.RootPassword = respBody.RootPassword

	return result, resp, nil
}

// DisableRescue disables rescue mode for a server.
func (c *ServerClient) DisableRescue(ctx context.Context, server *Server) (*Action, *Response, error) {
	const opPath = "/servers/%d/actions/disable_rescue"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	reqPath := fmt.Sprintf(opPath, server.ID)

	respBody, resp, err := postRequest[schema.ServerActionDisableRescueResponse](ctx, c.client, reqPath, nil)
	if err != nil {
		return nil, resp, err
	}

	return ActionFromSchema(respBody.Action), resp, nil
}

// ServerRebuildOpts specifies options for rebuilding a server.
type ServerRebuildOpts struct {
	Image    *Image
	UserData *string
}

// ServerRebuildResult is the result of a create server call.
type ServerRebuildResult struct {
	Action       *Action
	RootPassword string
}

// Rebuild rebuilds a server.
//
// Deprecated: Use [ServerClient.RebuildWithResult] instead.
func (c *ServerClient) Rebuild(ctx context.Context, server *Server, opts ServerRebuildOpts) (*Action, *Response, error) {
	result, resp, err := c.RebuildWithResult(ctx, server, opts)

	return result.Action, resp, err
}

// RebuildWithResult rebuilds a server.
func (c *ServerClient) RebuildWithResult(ctx context.Context, server *Server, opts ServerRebuildOpts) (ServerRebuildResult, *Response, error) {
	const opPath = "/servers/%d/actions/rebuild"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	result := ServerRebuildResult{}

	reqPath := fmt.Sprintf(opPath, server.ID)

	reqBody := schema.ServerActionRebuildRequest{UserData: opts.UserData}
	if opts.Image.ID != 0 || opts.Image.Name != "" {
		reqBody.Image = schema.IDOrName{ID: opts.Image.ID, Name: opts.Image.Name}
	}

	respBody, resp, err := postRequest[schema.ServerActionRebuildResponse](ctx, c.client, reqPath, reqBody)
	if err != nil {
		return result, resp, err
	}

	result.Action = ActionFromSchema(respBody.Action)
	if respBody.RootPassword != nil {
		result.RootPassword = *respBody.RootPassword
	}

	return result, resp, nil
}

// AttachISO attaches an ISO to a server.
func (c *ServerClient) AttachISO(ctx context.Context, server *Server, iso *ISO) (*Action, *Response, error) {
	const opPath = "/servers/%d/actions/attach_iso"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	reqPath := fmt.Sprintf(opPath, server.ID)

	reqBody := schema.ServerActionAttachISORequest{}
	if iso.ID != 0 || iso.Name != "" {
		reqBody.ISO = schema.IDOrName{ID: iso.ID, Name: iso.Name}
	}

	respBody, resp, err := postRequest[schema.ServerActionAttachISOResponse](ctx, c.client, reqPath, reqBody)
	if err != nil {
		return nil, resp, err
	}

	return ActionFromSchema(respBody.Action), resp, nil
}

// DetachISO detaches the currently attached ISO from a server.
func (c *ServerClient) DetachISO(ctx context.Context, server *Server) (*Action, *Response, error) {
	const opPath = "/servers/%d/actions/detach_iso"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	reqPath := fmt.Sprintf(opPath, server.ID)

	respBody, resp, err := postRequest[schema.ServerActionDetachISOResponse](ctx, c.client, reqPath, nil)
	if err != nil {
		return nil, resp, err
	}

	return ActionFromSchema(respBody.Action), resp, nil
}

// EnableBackup enables backup for a server.
// The window parameter is deprecated and will be ignored.
func (c *ServerClient) EnableBackup(ctx context.Context, server *Server, window string) (*Action, *Response, error) {
	_ = window

	const opPath = "/servers/%d/actions/enable_backup"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	reqPath := fmt.Sprintf(opPath, server.ID)

	respBody, resp, err := postRequest[schema.ServerActionEnableBackupResponse](ctx, c.client, reqPath, nil)
	if err != nil {
		return nil, resp, err
	}

	return ActionFromSchema(respBody.Action), resp, nil
}

// DisableBackup disables backup for a server.
func (c *ServerClient) DisableBackup(ctx context.Context, server *Server) (*Action, *Response, error) {
	const opPath = "/servers/%d/actions/disable_backup"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	reqPath := fmt.Sprintf(opPath, server.ID)

	respBody, resp, err := postRequest[schema.ServerActionDisableBackupResponse](ctx, c.client, reqPath, nil)
	if err != nil {
		return nil, resp, err
	}

	return ActionFromSchema(respBody.Action), resp, nil
}

// ServerChangeTypeOpts specifies options for changing a server's type.
type ServerChangeTypeOpts struct {
	ServerType  *ServerType // new server type
	UpgradeDisk bool        // whether disk should be upgraded
}

// ChangeType changes a server's type.
func (c *ServerClient) ChangeType(ctx context.Context, server *Server, opts ServerChangeTypeOpts) (*Action, *Response, error) {
	const opPath = "/servers/%d/actions/change_type"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	reqPath := fmt.Sprintf(opPath, server.ID)

	reqBody := schema.ServerActionChangeTypeRequest{
		UpgradeDisk: opts.UpgradeDisk,
	}
	if opts.ServerType.ID != 0 || opts.ServerType.Name != "" {
		reqBody.ServerType = schema.IDOrName{ID: opts.ServerType.ID, Name: opts.ServerType.Name}
	}

	respBody, resp, err := postRequest[schema.ServerActionChangeTypeResponse](ctx, c.client, reqPath, reqBody)
	if err != nil {
		return nil, resp, err
	}

	return ActionFromSchema(respBody.Action), resp, nil
}

// ChangeDNSPtr changes or resets the reverse DNS pointer for a server IP address.
// Pass a nil ptr to reset the reverse DNS pointer to its default value.
func (c *ServerClient) ChangeDNSPtr(ctx context.Context, server *Server, ip string, ptr *string) (*Action, *Response, error) {
	netIP := net.ParseIP(ip)
	if netIP == nil {
		return nil, nil, InvalidIPError{ip}
	}
	return server.changeDNSPtr(ctx, c.client, net.ParseIP(ip), ptr)
}

// ServerChangeProtectionOpts specifies options for changing the resource protection level of a server.
type ServerChangeProtectionOpts struct {
	Rebuild *bool
	Delete  *bool
}

// ChangeProtection changes the resource protection level of a server.
func (c *ServerClient) ChangeProtection(ctx context.Context, server *Server, opts ServerChangeProtectionOpts) (*Action, *Response, error) {
	const opPath = "/servers/%d/actions/change_protection"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	reqPath := fmt.Sprintf(opPath, server.ID)

	reqBody := schema.ServerActionChangeProtectionRequest{
		Rebuild: opts.Rebuild,
		Delete:  opts.Delete,
	}

	respBody, resp, err := postRequest[schema.ServerActionChangeProtectionResponse](ctx, c.client, reqPath, reqBody)
	if err != nil {
		return nil, resp, err
	}

	return ActionFromSchema(respBody.Action), resp, nil
}

// ServerRequestConsoleResult is the result of requesting a WebSocket VNC console.
type ServerRequestConsoleResult struct {
	Action   *Action
	WSSURL   string
	Password string
}

// RequestConsole requests a WebSocket VNC console.
func (c *ServerClient) RequestConsole(ctx context.Context, server *Server) (ServerRequestConsoleResult, *Response, error) {
	const opPath = "/servers/%d/actions/request_console"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	result := ServerRequestConsoleResult{}

	reqPath := fmt.Sprintf(opPath, server.ID)

	respBody, resp, err := postRequest[schema.ServerActionRequestConsoleResponse](ctx, c.client, reqPath, nil)
	if err != nil {
		return result, resp, err
	}

	result.Action = ActionFromSchema(respBody.Action)
	result.WSSURL = respBody.WSSURL
	result.Password = respBody.Password

	return result, resp, nil
}

// ServerAttachToNetworkOpts specifies options for attaching a server to a network.
type ServerAttachToNetworkOpts struct {
	Network  *Network
	IP       net.IP
	AliasIPs []net.IP
	IPRange  *net.IPNet
}

// AttachToNetwork attaches a server to a network.
func (c *ServerClient) AttachToNetwork(ctx context.Context, server *Server, opts ServerAttachToNetworkOpts) (*Action, *Response, error) {
	const opPath = "/servers/%d/actions/attach_to_network"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	reqPath := fmt.Sprintf(opPath, server.ID)

	reqBody := schema.ServerActionAttachToNetworkRequest{
		Network: opts.Network.ID,
	}
	if opts.IP != nil {
		reqBody.IP = Ptr(opts.IP.String())
	}
	for _, aliasIP := range opts.AliasIPs {
		reqBody.AliasIPs = append(reqBody.AliasIPs, Ptr(aliasIP.String()))
	}

	if opts.IPRange != nil {
		reqBody.IPRange = Ptr(opts.IPRange.String())
	}

	respBody, resp, err := postRequest[schema.ServerActionAttachToNetworkResponse](ctx, c.client, reqPath, reqBody)
	if err != nil {
		return nil, resp, err
	}

	return ActionFromSchema(respBody.Action), resp, err
}

// ServerDetachFromNetworkOpts specifies options for detaching a server from a network.
type ServerDetachFromNetworkOpts struct {
	Network *Network
}

// DetachFromNetwork detaches a server from a network.
func (c *ServerClient) DetachFromNetwork(ctx context.Context, server *Server, opts ServerDetachFromNetworkOpts) (*Action, *Response, error) {
	const opPath = "/servers/%d/actions/detach_from_network"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	reqPath := fmt.Sprintf(opPath, server.ID)

	reqBody := schema.ServerActionDetachFromNetworkRequest{
		Network: opts.Network.ID,
	}

	respBody, resp, err := postRequest[schema.ServerActionDetachFromNetworkResponse](ctx, c.client, reqPath, reqBody)
	if err != nil {
		return nil, resp, err
	}

	return ActionFromSchema(respBody.Action), resp, err
}

// ServerChangeAliasIPsOpts specifies options for changing the alias ips of an already attached network.
type ServerChangeAliasIPsOpts struct {
	Network  *Network
	AliasIPs []net.IP
}

// ChangeAliasIPs changes a server's alias IPs in a network.
func (c *ServerClient) ChangeAliasIPs(ctx context.Context, server *Server, opts ServerChangeAliasIPsOpts) (*Action, *Response, error) {
	const opPath = "/servers/%d/actions/change_alias_ips"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	reqPath := fmt.Sprintf(opPath, server.ID)

	reqBody := schema.ServerActionChangeAliasIPsRequest{
		Network:  opts.Network.ID,
		AliasIPs: []string{},
	}
	for _, aliasIP := range opts.AliasIPs {
		reqBody.AliasIPs = append(reqBody.AliasIPs, aliasIP.String())
	}

	respBody, resp, err := postRequest[schema.ServerActionChangeAliasIPsResponse](ctx, c.client, reqPath, reqBody)
	if err != nil {
		return nil, resp, err
	}

	return ActionFromSchema(respBody.Action), resp, err
}

// ServerMetricType is the type of available metrics for servers.
type ServerMetricType string

// Available types of server metrics. See Hetzner Cloud API documentation for
// details.
const (
	ServerMetricCPU     ServerMetricType = "cpu"
	ServerMetricDisk    ServerMetricType = "disk"
	ServerMetricNetwork ServerMetricType = "network"
)

// ServerGetMetricsOpts configures the call to get metrics for a Server.
type ServerGetMetricsOpts struct {
	Types []ServerMetricType
	Start time.Time
	End   time.Time
	Step  int
}

func (o ServerGetMetricsOpts) Validate() error {
	if len(o.Types) == 0 {
		return missingField(o, "Types")
	}
	if o.Start.IsZero() {
		return missingField(o, "Start")
	}
	if o.End.IsZero() {
		return missingField(o, "End")
	}
	return nil
}

func (o ServerGetMetricsOpts) Values() url.Values {
	query := url.Values{}

	for _, typ := range o.Types {
		query.Add("type", string(typ))
	}

	query.Add("start", o.Start.Format(time.RFC3339))
	query.Add("end", o.End.Format(time.RFC3339))

	if o.Step > 0 {
		query.Add("step", strconv.Itoa(o.Step))
	}

	return query
}

// ServerMetrics contains the metrics requested for a Server.
type ServerMetrics struct {
	Start      time.Time
	End        time.Time
	Step       float64
	TimeSeries map[string][]ServerMetricsValue
}

// ServerMetricsValue represents a single value in a time series of metrics.
type ServerMetricsValue struct {
	Timestamp float64
	Value     string
}

// GetMetrics obtains metrics for Server.
func (c *ServerClient) GetMetrics(ctx context.Context, server *Server, opts ServerGetMetricsOpts) (*ServerMetrics, *Response, error) {
	const opPath = "/servers/%d/metrics?%s"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	if server == nil {
		return nil, nil, invalidArgument("server", server, emptyValue(server))
	}

	if err := opts.Validate(); err != nil {
		return nil, nil, err
	}

	reqPath := fmt.Sprintf(opPath, server.ID, opts.Values().Encode())

	respBody, resp, err := getRequest[schema.ServerGetMetricsResponse](ctx, c.client, reqPath)
	if err != nil {
		return nil, resp, err
	}

	metrics, err := ServerMetricsFromSchema(&respBody)
	if err != nil {
		return nil, nil, fmt.Errorf("convert response body: %w", err)
	}

	return metrics, resp, nil
}

func (c *ServerClient) AddToPlacementGroup(ctx context.Context, server *Server, placementGroup *PlacementGroup) (*Action, *Response, error) {
	const opPath = "/servers/%d/actions/add_to_placement_group"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	reqPath := fmt.Sprintf(opPath, server.ID)

	reqBody := schema.ServerActionAddToPlacementGroupRequest{
		PlacementGroup: placementGroup.ID,
	}

	respBody, resp, err := postRequest[schema.ServerActionAddToPlacementGroupResponse](ctx, c.client, reqPath, reqBody)
	if err != nil {
		return nil, resp, err
	}

	return ActionFromSchema(respBody.Action), resp, nil
}

func (c *ServerClient) RemoveFromPlacementGroup(ctx context.Context, server *Server) (*Action, *Response, error) {
	const opPath = "/servers/%d/actions/remove_from_placement_group"
	ctx = ctxutil.SetOpPath(ctx, opPath)

	reqPath := fmt.Sprintf(opPath, server.ID)

	respBody, resp, err := postRequest[schema.ServerActionRemoveFromPlacementGroupResponse](ctx, c.client, reqPath, nil)
	if err != nil {
		return nil, resp, err
	}

	return ActionFromSchema(respBody.Action), resp, nil
}
