// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package api

import "net/url"

// HostVolume represents a Dynamic Host Volume: a volume associated with a
// specific Nomad client agent but created via API.
type HostVolume struct {
	// Namespace is the Nomad namespace for the host volume, which constrains
	// which jobs can mount it.
	Namespace string `mapstructure:"namespace" hcl:"namespace"`

	// ID is a UUID-like string generated by the server.
	ID string `mapstructure:"id" hcl:"id"`

	// Name is the name that group.volume will use to identify the volume
	// source. Not expected to be unique.
	Name string `mapstructure:"name" hcl:"name"`

	// PluginID is the name of the host volume plugin on the client that will be
	// used for creating the volume. If omitted, the client will use its default
	// built-in plugin.
	PluginID string `mapstructure:"plugin_id" hcl:"plugin_id"`

	// NodePool is the node pool of the node where the volume is placed. If the
	// user doesn't provide a node ID, a node will be selected using the
	// NodePool and Constraints. If the user provides both NodePool and NodeID,
	// NodePool will be used to validate the request. If omitted, the server
	// will populate this value in before writing the volume to Raft.
	NodePool string `mapstructure:"node_pool" hcl:"node_pool"`

	// NodeID is the node where the volume is placed. If the user doesn't
	// provide a NodeID, one will be selected using the NodePool and
	// Constraints. If omitted, this field will then be populated by the server
	// before writing the volume to Raft.
	NodeID string `mapstructure:"node_id" hcl:"node_id"`

	// Constraints are optional. If the NodeID is not provided, the NodePool and
	// Constraints are used to select a node. If the NodeID is provided,
	// Constraints are used to validate that the node meets those constraints at
	// the time of volume creation.
	Constraints []*Constraint `json:",omitempty" hcl:"constraint"`

	// Because storage may allow only specific intervals of size, we accept a
	// min and max and return the actual capacity when the volume is created or
	// updated on the client
	RequestedCapacityMinBytes int64 `mapstructure:"capacity_min" hcl:"capacity_min"`
	RequestedCapacityMaxBytes int64 `mapstructure:"capacity_max" hcl:"capacity_max"`
	CapacityBytes             int64 `mapstructure:"capacity" hcl:"capacity"`

	// RequestedCapabilities defines the options available to group.volume
	// blocks. The scheduler checks against the listed capability blocks and
	// selects a node for placement if *any* capability block works.
	RequestedCapabilities []*HostVolumeCapability `hcl:"capability"`

	// Parameters are an opaque map of parameters for the host volume plugin.
	Parameters map[string]string `json:",omitempty"`

	// HostPath is the path on disk where the volume's mount point was
	// created. We record this to make debugging easier.
	HostPath string `mapstructure:"host_path" hcl:"host_path"`

	// State represents the overall state of the volume. One of pending, ready,
	// deleted.
	State HostVolumeState

	CreateIndex uint64
	CreateTime  int64

	ModifyIndex uint64
	ModifyTime  int64

	// Allocations is the list of non-client-terminal allocations with claims on
	// this host volume. They are denormalized on read and this field will be
	// never written to Raft
	Allocations []*AllocationListStub `json:",omitempty" mapstructure:"-" hcl:"-"`
}

// HostVolume state reports the current status of the host volume
type HostVolumeState string

const (
	HostVolumeStatePending     HostVolumeState = "pending"
	HostVolumeStateReady       HostVolumeState = "ready"
	HostVolumeStateUnavailable HostVolumeState = "unavailable"
)

// HostVolumeCapability is the requested attachment and access mode for a volume
type HostVolumeCapability struct {
	AttachmentMode HostVolumeAttachmentMode `mapstructure:"attachment_mode" hcl:"attachment_mode"`
	AccessMode     HostVolumeAccessMode     `mapstructure:"access_mode" hcl:"access_mode"`
}

// HostVolumeAttachmentMode chooses the type of storage API that will be used to
// interact with the device.
type HostVolumeAttachmentMode string

const (
	HostVolumeAttachmentModeUnknown     HostVolumeAttachmentMode = ""
	HostVolumeAttachmentModeBlockDevice HostVolumeAttachmentMode = "block-device"
	HostVolumeAttachmentModeFilesystem  HostVolumeAttachmentMode = "file-system"
)

// HostVolumeAccessMode indicates how Nomad should make the volume available to
// concurrent allocations.
type HostVolumeAccessMode string

const (
	HostVolumeAccessModeUnknown HostVolumeAccessMode = ""

	HostVolumeAccessModeSingleNodeReader       HostVolumeAccessMode = "single-node-reader-only"
	HostVolumeAccessModeSingleNodeWriter       HostVolumeAccessMode = "single-node-writer"
	HostVolumeAccessModeSingleNodeSingleWriter HostVolumeAccessMode = "single-node-single-writer"
	HostVolumeAccessModeSingleNodeMultiWriter  HostVolumeAccessMode = "single-node-multi-writer"
)

// HostVolumeStub is used for responses for the List Volumes endpoint
type HostVolumeStub struct {
	Namespace     string
	ID            string
	Name          string
	PluginID      string
	NodePool      string
	NodeID        string
	CapacityBytes int64
	State         HostVolumeState

	CreateIndex uint64
	CreateTime  int64

	ModifyIndex uint64
	ModifyTime  int64
}

// HostVolumes is used to access the host volumes API.
type HostVolumes struct {
	client *Client
}

// HostVolumes returns a new handle on the host volumes API.
func (c *Client) HostVolumes() *HostVolumes {
	return &HostVolumes{client: c}
}

type HostVolumeCreateRequest struct {
	Volume *HostVolume

	// PolicyOverride overrides Sentinel soft-mandatory policy enforcement
	PolicyOverride bool
}

type HostVolumeRegisterRequest struct {
	Volume *HostVolume

	// PolicyOverride overrides Sentinel soft-mandatory policy enforcement
	PolicyOverride bool
}

type HostVolumeCreateResponse struct {
	Volume   *HostVolume
	Warnings string
}

type HostVolumeRegisterResponse struct {
	Volume   *HostVolume
	Warnings string
}

type HostVolumeListRequest struct {
	NodeID   string
	NodePool string
}

type HostVolumeDeleteRequest struct {
	ID    string
	Force bool
}

type HostVolumeDeleteResponse struct{}

// Create forwards to client agents so a host volume can be created on those
// hosts, and registers the volume with Nomad servers.
func (hv *HostVolumes) Create(req *HostVolumeCreateRequest, opts *WriteOptions) (*HostVolumeCreateResponse, *WriteMeta, error) {
	var out *HostVolumeCreateResponse
	wm, err := hv.client.put("/v1/volume/host/create", req, &out, opts)
	if err != nil {
		return nil, wm, err
	}
	return out, wm, nil
}

// Register registers a host volume that was created out-of-band with the Nomad
// servers.
func (hv *HostVolumes) Register(req *HostVolumeRegisterRequest, opts *WriteOptions) (*HostVolumeRegisterResponse, *WriteMeta, error) {
	var out *HostVolumeRegisterResponse
	wm, err := hv.client.put("/v1/volume/host/register", req, &out, opts)
	if err != nil {
		return nil, wm, err
	}
	return out, wm, nil
}

// Get queries for a single host volume, by ID
func (hv *HostVolumes) Get(id string, opts *QueryOptions) (*HostVolume, *QueryMeta, error) {
	var out *HostVolume
	path, err := url.JoinPath("/v1/volume/host/", url.PathEscape(id))
	if err != nil {
		return nil, nil, err
	}
	qm, err := hv.client.query(path, &out, opts)
	if err != nil {
		return nil, qm, err
	}
	return out, qm, nil
}

// List queries for a set of host volumes, by namespace, node, node pool, or
// name prefix.
func (hv *HostVolumes) List(req *HostVolumeListRequest, opts *QueryOptions) ([]*HostVolumeStub, *QueryMeta, error) {
	var out []*HostVolumeStub
	qv := url.Values{}
	qv.Set("type", "host")
	if req != nil {
		if req.NodeID != "" {
			qv.Set("node_id", req.NodeID)
		}
		if req.NodePool != "" {
			qv.Set("node_pool", req.NodePool)
		}
	}

	qm, err := hv.client.query("/v1/volumes?"+qv.Encode(), &out, opts)
	if err != nil {
		return nil, qm, err
	}
	return out, qm, nil
}

// Delete deletes a host volume
func (hv *HostVolumes) Delete(req *HostVolumeDeleteRequest, opts *WriteOptions) (*HostVolumeDeleteResponse, *WriteMeta, error) {
	var resp *HostVolumeDeleteResponse
	path, err := url.JoinPath("/v1/volume/host/", url.PathEscape(req.ID))
	if err != nil {
		return nil, nil, err
	}
	if req.Force {
		path = path + "?force=true"
	}
	wm, err := hv.client.delete(path, nil, resp, opts)
	return resp, wm, err
}
