package govultr

import (
	"context"
	"fmt"
	"net/http"

	"github.com/google/go-querystring/query"
)

const lbPath = "/v2/load-balancers"

// LoadBalancerService is the interface to interact with the server endpoints on the Vultr API
// Link : https://www.vultr.com/api/#tag/load-balancer
type LoadBalancerService interface {
	Create(ctx context.Context, createReq *LoadBalancerReq) (*LoadBalancer, error)
	Get(ctx context.Context, ID string) (*LoadBalancer, error)
	Update(ctx context.Context, ID string, updateReq *LoadBalancerReq) error
	Delete(ctx context.Context, ID string) error
	List(ctx context.Context, options *ListOptions) ([]LoadBalancer, *Meta, error)
	CreateForwardingRule(ctx context.Context, ID string, rule *ForwardingRule) (*ForwardingRule, error)
	GetForwardingRule(ctx context.Context, ID string, ruleID string) (*ForwardingRule, error)
	DeleteForwardingRule(ctx context.Context, ID string, RuleID string) error
	ListForwardingRules(ctx context.Context, ID string, options *ListOptions) ([]ForwardingRule, *Meta, error)
	ListFirewallRules(ctx context.Context, ID string, options *ListOptions) ([]LBFirewallRule, *Meta, error)
	GetFirewallRule(ctx context.Context, ID string, ruleID string) (*LBFirewallRule, error)
}

// LoadBalancerHandler handles interaction with the server methods for the Vultr API
type LoadBalancerHandler struct {
	client *Client
}

// LoadBalancer represent the structure of a load balancer
type LoadBalancer struct {
	ID              string           `json:"id,omitempty"`
	DateCreated     string           `json:"date_created,omitempty"`
	Region          string           `json:"region,omitempty"`
	Label           string           `json:"label,omitempty"`
	Status          string           `json:"status,omitempty"`
	IPV4            string           `json:"ipv4,omitempty"`
	IPV6            string           `json:"ipv6,omitempty"`
	Instances       []string         `json:"instances,omitempty"`
	HealthCheck     *HealthCheck     `json:"health_check,omitempty"`
	GenericInfo     *GenericInfo     `json:"generic_info,omitempty"`
	SSLInfo         *bool            `json:"has_ssl,omitempty"`
	ForwardingRules []ForwardingRule `json:"forwarding_rules,omitempty"`
	FirewallRules   []LBFirewallRule `json:"firewall_rules,omitempty"`
}

// LoadBalancerReq gives options for creating or updating a load balancer
type LoadBalancerReq struct {
	Region             string           `json:"region,omitempty"`
	Label              string           `json:"label,omitempty"`
	Instances          []string         `json:"instances"`
	HealthCheck        *HealthCheck     `json:"health_check,omitempty"`
	StickySessions     *StickySessions  `json:"sticky_session,omitempty"`
	ForwardingRules    []ForwardingRule `json:"forwarding_rules,omitempty"`
	SSL                *SSL             `json:"ssl,omitempty"`
	SSLRedirect        *bool            `json:"ssl_redirect,omitempty"`
	ProxyProtocol      *bool            `json:"proxy_protocol,omitempty"`
	BalancingAlgorithm string           `json:"balancing_algorithm,omitempty"`
	FirewallRules      []LBFirewallRule `json:"firewall_rules"`
	// Deprecated:  PrivateNetwork should no longer be used. Instead, use VPC.
	PrivateNetwork *string `json:"private_network,omitempty"`
	VPC            *string `json:"vpc,omitempty"`
}

// InstanceList represents instances that are attached to your load balancer
type InstanceList struct {
	InstanceList []string
}

// HealthCheck represents your health check configuration for your load balancer.
type HealthCheck struct {
	Protocol           string `json:"protocol,omitempty"`
	Port               int    `json:"port,omitempty"`
	Path               string `json:"path,omitempty"`
	CheckInterval      int    `json:"check_interval,omitempty"`
	ResponseTimeout    int    `json:"response_timeout,omitempty"`
	UnhealthyThreshold int    `json:"unhealthy_threshold,omitempty"`
	HealthyThreshold   int    `json:"healthy_threshold,omitempty"`
}

// GenericInfo represents generic configuration of your load balancer
type GenericInfo struct {
	BalancingAlgorithm string          `json:"balancing_algorithm,omitempty"`
	SSLRedirect        *bool           `json:"ssl_redirect,omitempty"`
	StickySessions     *StickySessions `json:"sticky_sessions,omitempty"`
	ProxyProtocol      *bool           `json:"proxy_protocol,omitempty"`
	// Deprecated:  PrivateNetwork should no longer be used. Instead, use VPC.
	PrivateNetwork string `json:"private_network,omitempty"`
	VPC            string `json:"vpc,omitempty"`
}

// StickySessions represents cookie for your load balancer
type StickySessions struct {
	CookieName string `json:"cookie_name,omitempty"`
}

// ForwardingRules represent a list of forwarding rules
type ForwardingRules struct {
	ForwardRuleList []ForwardingRule `json:"forwarding_rules,omitempty"`
}

// ForwardingRule represent a single forwarding rule
type ForwardingRule struct {
	RuleID           string `json:"id,omitempty"`
	FrontendProtocol string `json:"frontend_protocol,omitempty"`
	FrontendPort     int    `json:"frontend_port,omitempty"`
	BackendProtocol  string `json:"backend_protocol,omitempty"`
	BackendPort      int    `json:"backend_port,omitempty"`
}

// LBFirewallRule represent a single firewall rule
type LBFirewallRule struct {
	RuleID string `json:"id,omitempty"`
	Port   int    `json:"port,omitempty"`
	IPType string `json:"ip_type,omitempty"`
	Source string `json:"source,omitempty"`
}

// SSL represents valid SSL config
type SSL struct {
	PrivateKey  string `json:"private_key,omitempty"`
	Certificate string `json:"certificate,omitempty"`
	Chain       string `json:"chain,omitempty"`
}

type lbsBase struct {
	LoadBalancers []LoadBalancer `json:"load_balancers"`
	Meta          *Meta          `json:"meta"`
}

type lbBase struct {
	LoadBalancer *LoadBalancer `json:"load_balancer"`
}

type lbRulesBase struct {
	ForwardingRules []ForwardingRule `json:"forwarding_rules"`
	Meta            *Meta            `json:"meta"`
}

type lbRuleBase struct {
	ForwardingRule *ForwardingRule `json:"forwarding_rule"`
}

type lbFWRulesBase struct {
	FirewallRules []LBFirewallRule `json:"firewall_rules"`
	Meta          *Meta            `json:"meta"`
}

type lbFWRuleBase struct {
	FirewallRule *LBFirewallRule `json:"firewall_rule"`
}

// Create a load balancer
func (l *LoadBalancerHandler) Create(ctx context.Context, createReq *LoadBalancerReq) (*LoadBalancer, error) {
	req, err := l.client.NewRequest(ctx, http.MethodPost, lbPath, createReq)
	if err != nil {
		return nil, err
	}

	var lb = new(lbBase)
	if err = l.client.DoWithContext(ctx, req, &lb); err != nil {
		return nil, err
	}

	return lb.LoadBalancer, nil
}

// Get a load balancer
func (l *LoadBalancerHandler) Get(ctx context.Context, ID string) (*LoadBalancer, error) {
	uri := fmt.Sprintf("%s/%s", lbPath, ID)
	req, err := l.client.NewRequest(ctx, http.MethodGet, uri, nil)
	if err != nil {
		return nil, err
	}

	var lb = new(lbBase)
	if err = l.client.DoWithContext(ctx, req, lb); err != nil {
		return nil, err
	}

	return lb.LoadBalancer, nil
}

// Update updates your your load balancer
func (l *LoadBalancerHandler) Update(ctx context.Context, ID string, updateReq *LoadBalancerReq) error {
	uri := fmt.Sprintf("%s/%s", lbPath, ID)
	req, err := l.client.NewRequest(ctx, http.MethodPatch, uri, updateReq)
	if err != nil {
		return err
	}

	return l.client.DoWithContext(ctx, req, nil)
}

// Delete a load balancer subscription.
func (l *LoadBalancerHandler) Delete(ctx context.Context, ID string) error {
	uri := fmt.Sprintf("%s/%s", lbPath, ID)
	req, err := l.client.NewRequest(ctx, http.MethodDelete, uri, nil)
	if err != nil {
		return err
	}

	return l.client.DoWithContext(ctx, req, nil)
}

// List all load balancer subscriptions on the current account.
func (l *LoadBalancerHandler) List(ctx context.Context, options *ListOptions) ([]LoadBalancer, *Meta, error) {
	req, err := l.client.NewRequest(ctx, http.MethodGet, lbPath, nil)
	if err != nil {
		return nil, nil, err
	}

	newValues, err := query.Values(options)
	if err != nil {
		return nil, nil, err
	}

	req.URL.RawQuery = newValues.Encode()

	lbs := new(lbsBase)
	if err = l.client.DoWithContext(ctx, req, &lbs); err != nil {
		return nil, nil, err
	}

	return lbs.LoadBalancers, lbs.Meta, nil
}

// CreateForwardingRule will create a new forwarding rule for your load balancer subscription.
// Note the RuleID will be returned in the ForwardingRule struct
func (l *LoadBalancerHandler) CreateForwardingRule(ctx context.Context, ID string, rule *ForwardingRule) (*ForwardingRule, error) {
	uri := fmt.Sprintf("%s/%s/forwarding-rules", lbPath, ID)
	req, err := l.client.NewRequest(ctx, http.MethodPost, uri, rule)
	if err != nil {
		return nil, err
	}

	fwRule := new(lbRuleBase)
	if err = l.client.DoWithContext(ctx, req, fwRule); err != nil {
		return nil, err
	}

	return fwRule.ForwardingRule, nil
}

// GetForwardingRule will get a forwarding rule from your load balancer subscription.
func (l *LoadBalancerHandler) GetForwardingRule(ctx context.Context, ID string, ruleID string) (*ForwardingRule, error) {
	uri := fmt.Sprintf("%s/%s/forwarding-rules/%s", lbPath, ID, ruleID)
	req, err := l.client.NewRequest(ctx, http.MethodGet, uri, nil)
	if err != nil {
		return nil, err
	}

	fwRule := new(lbRuleBase)
	if err = l.client.DoWithContext(ctx, req, fwRule); err != nil {
		return nil, err
	}

	return fwRule.ForwardingRule, nil
}

// ListForwardingRules lists all forwarding rules for a load balancer subscription
func (l *LoadBalancerHandler) ListForwardingRules(ctx context.Context, ID string, options *ListOptions) ([]ForwardingRule, *Meta, error) {
	uri := fmt.Sprintf("%s/%s/forwarding-rules", lbPath, ID)
	req, err := l.client.NewRequest(ctx, http.MethodGet, uri, nil)
	if err != nil {
		return nil, nil, err
	}

	newValues, err := query.Values(options)
	if err != nil {
		return nil, nil, err
	}

	req.URL.RawQuery = newValues.Encode()

	fwRules := new(lbRulesBase)
	if err = l.client.DoWithContext(ctx, req, &fwRules); err != nil {
		return nil, nil, err
	}

	return fwRules.ForwardingRules, fwRules.Meta, nil
}

// DeleteForwardingRule removes a forwarding rule from a load balancer subscription
func (l *LoadBalancerHandler) DeleteForwardingRule(ctx context.Context, ID string, RuleID string) error {
	uri := fmt.Sprintf("%s/%s/forwarding-rules/%s", lbPath, ID, RuleID)
	req, err := l.client.NewRequest(ctx, http.MethodDelete, uri, nil)
	if err != nil {
		return err
	}

	return l.client.DoWithContext(ctx, req, nil)
}

// GetFirewallRule will get a firewall rule from your load balancer subscription.
func (l *LoadBalancerHandler) GetFirewallRule(ctx context.Context, ID string, ruleID string) (*LBFirewallRule, error) {
	uri := fmt.Sprintf("%s/%s/firewall-rules/%s", lbPath, ID, ruleID)
	req, err := l.client.NewRequest(ctx, http.MethodGet, uri, nil)
	if err != nil {
		return nil, err
	}

	fwRule := new(lbFWRuleBase)
	if err = l.client.DoWithContext(ctx, req, fwRule); err != nil {
		return nil, err
	}

	return fwRule.FirewallRule, nil
}

// ListFirewallRules lists all firewall rules for a load balancer subscription
func (l *LoadBalancerHandler) ListFirewallRules(ctx context.Context, ID string, options *ListOptions) ([]LBFirewallRule, *Meta, error) {
	uri := fmt.Sprintf("%s/%s/firewall-rules", lbPath, ID)
	req, err := l.client.NewRequest(ctx, http.MethodGet, uri, nil)
	if err != nil {
		return nil, nil, err
	}

	newValues, err := query.Values(options)
	if err != nil {
		return nil, nil, err
	}

	req.URL.RawQuery = newValues.Encode()

	fwRules := new(lbFWRulesBase)
	if err = l.client.DoWithContext(ctx, req, &fwRules); err != nil {
		return nil, nil, err
	}

	return fwRules.FirewallRules, fwRules.Meta, nil
}
