package nodes

import (
	"errors"
	"fmt"

	"github.com/rackspace/gophercloud"
	"github.com/rackspace/gophercloud/pagination"
)

// List is the operation responsible for returning a paginated collection of
// load balancer nodes. It requires the node ID, its parent load balancer ID,
// and optional limit integer (passed in either as a pointer or a nil poitner).
func List(client *gophercloud.ServiceClient, loadBalancerID int, limit *int) pagination.Pager {
	url := rootURL(client, loadBalancerID)
	if limit != nil {
		url += fmt.Sprintf("?limit=%d", limit)
	}

	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
		return NodePage{pagination.SinglePageBase(r)}
	})
}

// CreateOptsBuilder is the interface responsible for generating the JSON
// for a Create operation.
type CreateOptsBuilder interface {
	ToNodeCreateMap() (map[string]interface{}, error)
}

// CreateOpts is a slice of CreateOpt structs, that allow the user to create
// multiple nodes in a single operation (one node per CreateOpt).
type CreateOpts []CreateOpt

// CreateOpt represents the options to create a single node.
type CreateOpt struct {
	// Required - the IP address or CIDR for this back-end node. It can either be
	// a private IP (ServiceNet) or a public IP.
	Address string

	// Optional - the port on which traffic is sent and received.
	Port int

	// Optional - the condition of the node. See the consts in Results.go.
	Condition Condition

	// Optional - the type of the node. See the consts in Results.go.
	Type Type

	// Optional - a pointer to an integer between 0 and 100.
	Weight *int
}

func validateWeight(weight *int) error {
	if weight != nil && (*weight > 100 || *weight < 0) {
		return errors.New("Weight must be a valid int between 0 and 100")
	}
	return nil
}

// ToNodeCreateMap converts a slice of options into a map that can be used for
// the JSON.
func (opts CreateOpts) ToNodeCreateMap() (map[string]interface{}, error) {
	type nodeMap map[string]interface{}
	nodes := []nodeMap{}

	for k, v := range opts {
		if v.Address == "" {
			return nodeMap{}, fmt.Errorf("ID is a required attribute, none provided for %d CreateOpt element", k)
		}
		if weightErr := validateWeight(v.Weight); weightErr != nil {
			return nodeMap{}, weightErr
		}

		node := make(map[string]interface{})
		node["address"] = v.Address

		if v.Port > 0 {
			node["port"] = v.Port
		}
		if v.Condition != "" {
			node["condition"] = v.Condition
		}
		if v.Type != "" {
			node["type"] = v.Type
		}
		if v.Weight != nil {
			node["weight"] = &v.Weight
		}

		nodes = append(nodes, node)
	}

	return nodeMap{"nodes": nodes}, nil
}

// Create is the operation responsible for creating a new node on a load
// balancer. Since every load balancer exists in both ServiceNet and the public
// Internet, both private and public IP addresses can be used for nodes.
//
// If nodes need time to boot up services before they become operational, you
// can temporarily prevent traffic from being sent to that node by setting the
// Condition field to DRAINING. Health checks will still be performed; but once
// your node is ready, you can update its condition to ENABLED and have it
// handle traffic.
func Create(client *gophercloud.ServiceClient, loadBalancerID int, opts CreateOptsBuilder) CreateResult {
	var res CreateResult

	reqBody, err := opts.ToNodeCreateMap()
	if err != nil {
		res.Err = err
		return res
	}

	resp, err := client.Request("POST", rootURL(client, loadBalancerID), gophercloud.RequestOpts{
		JSONBody:     &reqBody,
		JSONResponse: &res.Body,
		OkCodes:      []int{202},
	})
	if err != nil {
		res.Err = err
		return res
	}

	pr, err := pagination.PageResultFrom(resp)
	if err != nil {
		res.Err = err
		return res
	}

	return CreateResult{pagination.SinglePageBase(pr)}
}

// BulkDelete is the operation responsible for batch deleting multiple nodes in
// a single operation. It accepts a slice of integer IDs and will remove them
// from the load balancer. The maximum limit is 10 node removals at once.
func BulkDelete(c *gophercloud.ServiceClient, loadBalancerID int, nodeIDs []int) DeleteResult {
	var res DeleteResult

	if len(nodeIDs) > 10 || len(nodeIDs) == 0 {
		res.Err = errors.New("You must provide a minimum of 1 and a maximum of 10 node IDs")
		return res
	}

	url := rootURL(c, loadBalancerID)
	url += gophercloud.IDSliceToQueryString("id", nodeIDs)

	_, res.Err = c.Request("DELETE", url, gophercloud.RequestOpts{
		OkCodes: []int{202},
	})

	return res
}

// Get is the operation responsible for showing details for a single node.
func Get(c *gophercloud.ServiceClient, lbID, nodeID int) GetResult {
	var res GetResult

	_, res.Err = c.Request("GET", resourceURL(c, lbID, nodeID), gophercloud.RequestOpts{
		JSONResponse: &res.Body,
		OkCodes:      []int{200},
	})

	return res
}

// UpdateOptsBuilder represents a type that can be converted into a JSON-like
// map structure.
type UpdateOptsBuilder interface {
	ToNodeUpdateMap() (map[string]interface{}, error)
}

// UpdateOpts represent the options for updating an existing node.
type UpdateOpts struct {
	// Optional - the condition of the node. See the consts in Results.go.
	Condition Condition

	// Optional - the type of the node. See the consts in Results.go.
	Type Type

	// Optional - a pointer to an integer between 0 and 100.
	Weight *int
}

// ToNodeUpdateMap converts an options struct into a JSON-like map.
func (opts UpdateOpts) ToNodeUpdateMap() (map[string]interface{}, error) {
	node := make(map[string]interface{})

	if opts.Condition != "" {
		node["condition"] = opts.Condition
	}
	if opts.Weight != nil {
		if weightErr := validateWeight(opts.Weight); weightErr != nil {
			return node, weightErr
		}
		node["weight"] = &opts.Weight
	}
	if opts.Type != "" {
		node["type"] = opts.Type
	}

	return map[string]interface{}{"node": node}, nil
}

// Update is the operation responsible for updating an existing node. A node's
// IP, port, and status are immutable attributes and cannot be modified.
func Update(c *gophercloud.ServiceClient, lbID, nodeID int, opts UpdateOptsBuilder) UpdateResult {
	var res UpdateResult

	reqBody, err := opts.ToNodeUpdateMap()
	if err != nil {
		res.Err = err
		return res
	}

	_, res.Err = c.Request("PUT", resourceURL(c, lbID, nodeID), gophercloud.RequestOpts{
		JSONBody: &reqBody,
		OkCodes:  []int{202},
	})

	return res
}

// Delete is the operation responsible for permanently deleting a node.
func Delete(c *gophercloud.ServiceClient, lbID, nodeID int) DeleteResult {
	var res DeleteResult
	_, res.Err = c.Request("DELETE", resourceURL(c, lbID, nodeID), gophercloud.RequestOpts{
		OkCodes: []int{202},
	})
	return res
}

// ListEventsOptsBuilder allows extensions to add additional parameters to the
// List request.
type ListEventsOptsBuilder interface {
	ToEventsListQuery() (string, error)
}

// ListEventsOpts allows the filtering and sorting of paginated collections through
// the API.
type ListEventsOpts struct {
	Marker string `q:"marker"`
	Limit  int    `q:"limit"`
}

// ToEventsListQuery formats a ListOpts into a query string.
func (opts ListEventsOpts) ToEventsListQuery() (string, error) {
	q, err := gophercloud.BuildQueryString(opts)
	if err != nil {
		return "", err
	}
	return q.String(), nil
}

// ListEvents is the operation responsible for listing all the events
// associated with the activity between the node and the load balancer. The
// events report errors found with the node. The detailedMessage provides the
// detailed reason for the error.
func ListEvents(client *gophercloud.ServiceClient, loadBalancerID int, opts ListEventsOptsBuilder) pagination.Pager {
	url := eventsURL(client, loadBalancerID)

	if opts != nil {
		query, err := opts.ToEventsListQuery()
		if err != nil {
			return pagination.Pager{Err: err}
		}
		url += query
	}

	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
		return NodeEventPage{pagination.SinglePageBase(r)}
	})
}
