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, *http.Response, error)
	Get(ctx context.Context, lbID string) (*LoadBalancer, *http.Response, error)
	Update(ctx context.Context, lbID string, updateReq *LoadBalancerReq) error
	Delete(ctx context.Context, lbID string) error
	List(ctx context.Context, options *ListOptions) ([]LoadBalancer, *Meta, *http.Response, error)

	DeleteSSL(ctx context.Context, lbID string) error
	DeleteAutoSSL(ctx context.Context, lbID string) error

	CreateForwardingRule(ctx context.Context, lbID string, rule *ForwardingRule) (*ForwardingRule, *http.Response, error)
	GetForwardingRule(ctx context.Context, lbID string, ruleID string) (*ForwardingRule, *http.Response, error)
	DeleteForwardingRule(ctx context.Context, lbID string, RuleID string) error
	ListForwardingRules(ctx context.Context, lbID string, options *ListOptions) ([]ForwardingRule, *Meta, *http.Response, error)

	CreateFirewallRules(ctx context.Context, lbID string, fwRule []LBFirewallRule) error
	GetFirewallRule(ctx context.Context, lbID string, ruleID string) (*LBFirewallRule, *http.Response, error)
	DeleteFirewallRule(ctx context.Context, lbID, fwRuleID string) error
	ListFirewallRules(ctx context.Context, lbID string, options *ListOptions) ([]LBFirewallRule, *Meta, *http.Response, 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"`
	Nodes           int              `json:"nodes,omitempty"`
	HealthCheck     *HealthCheck     `json:"health_check,omitempty"`
	GenericInfo     *GenericInfo     `json:"generic_info,omitempty"`
	SSLInfo         *bool            `json:"has_ssl,omitempty"`
	AutoSSL         *AutoSSL         `json:"auto_ssl,omitempty"`
	HTTP2           *bool            `json:"http2,omitempty"`
	HTTP3           *bool            `json:"http3,omitempty"`
	ForwardingRules []ForwardingRule `json:"forwarding_rules,omitempty"`
	FirewallRules   []LBFirewallRule `json:"firewall_rules,omitempty"`
	GlobalRegions   []string         `json:"global_regions,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,omitempty"`
	Nodes              int              `json:"nodes,omitempty"`
	HealthCheck        *HealthCheck     `json:"health_check,omitempty"`
	StickySessions     *StickySessions  `json:"sticky_session,omitempty"`
	ForwardingRules    []ForwardingRule `json:"forwarding_rules,omitempty"`
	SSL                *SSL             `json:"ssl,omitempty"`
	AutoSSL            *AutoSSL         `json:"auto_ssl,omitempty"`
	SSLRedirect        *bool            `json:"ssl_redirect,omitempty"`
	HTTP2              *bool            `json:"http2,omitempty"`
	HTTP3              *bool            `json:"http3,omitempty"`
	ProxyProtocol      *bool            `json:"proxy_protocol,omitempty"`
	BalancingAlgorithm string           `json:"balancing_algorithm,omitempty"`
	FirewallRules      []LBFirewallRule `json:"firewall_rules,omitempty"`
	Timeout            int              `json:"timeout,omitempty"`
	VPC                *string          `json:"vpc,omitempty"`
	GlobalRegions      []string         `json:"global_regions,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"`
	Timeout            int             `json:"timeout,omitempty"`
	SSLRedirect        *bool           `json:"ssl_redirect,omitempty"`
	StickySessions     *StickySessions `json:"sticky_sessions,omitempty"`
	ProxyProtocol      *bool           `json:"proxy_protocol,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"`
}

// LBFirewallRules represents a list of firewall rules.  This is only used when
// sending the create firewall rules request
type LBFirewallRules struct {
	Rules []LBFirewallRule `json:"firewall_rules"`
}

// SSL represents valid SSL config
type SSL struct {
	PrivateKey     string `json:"private_key,omitempty"`
	Certificate    string `json:"certificate,omitempty"`
	Chain          string `json:"chain,omitempty"`
	PrivateKeyB64  string `json:"private_key_b64,omitempty"`
	CertificateB64 string `json:"certificate_b64,omitempty"`
	ChainB64       string `json:"chain_b64,omitempty"`
}

// AutoSSL represents valid AutoSSL config
type AutoSSL struct {
	DomainZone string `json:"domain_zone"`
	DomainSub  string `json:"domain_sub,omitempty"`
	Domain     string `json:"domain,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, *http.Response, error) {
	req, err := l.client.NewRequest(ctx, http.MethodPost, lbPath, createReq)
	if err != nil {
		return nil, nil, err
	}

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

	return lb.LoadBalancer, resp, nil
}

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

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

	return lb.LoadBalancer, resp, nil
}

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

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

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

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

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

	req.URL.RawQuery = newValues.Encode()

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

	return lbs.LoadBalancers, lbs.Meta, resp, 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, lbID string, rule *ForwardingRule) (*ForwardingRule, *http.Response, error) { //nolint:lll
	uri := fmt.Sprintf("%s/%s/forwarding-rules", lbPath, lbID)
	req, err := l.client.NewRequest(ctx, http.MethodPost, uri, rule)
	if err != nil {
		return nil, nil, err
	}

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

	return fwRule.ForwardingRule, resp, nil
}

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

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

	return fwRule.ForwardingRule, resp, nil
}

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

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

	req.URL.RawQuery = newValues.Encode()

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

	return fwRules.ForwardingRules, fwRules.Meta, resp, nil
}

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

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

// CreateFirewallRules will create firewall rules on your load balancer
func (l *LoadBalancerHandler) CreateFirewallRules(ctx context.Context, lbID string, fwRule []LBFirewallRule) error {
	uri := fmt.Sprintf("%s/%s/firewall-rules", lbPath, lbID)
	req, err := l.client.NewRequest(ctx, http.MethodPost, uri, LBFirewallRules{Rules: fwRule})
	if err != nil {
		return err
	}

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

	return nil
}

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

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

	return fwRule.FirewallRule, resp, nil
}

// DeleteFirewallRule will delete a firewall rule from your load balancer
func (l *LoadBalancerHandler) DeleteFirewallRule(ctx context.Context, lbID, fwRuleID string) error {
	uri := fmt.Sprintf("%s/%s/firewall-rules/%s", lbPath, lbID, fwRuleID)
	req, err := l.client.NewRequest(ctx, http.MethodDelete, uri, nil)
	if err != nil {
		return err
	}

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

	return nil
}

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

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

	req.URL.RawQuery = newValues.Encode()

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

	return fwRules.FirewallRules, fwRules.Meta, resp, nil
}

// DeleteSSL removes the SSL configuration from a load balancer subscription.
func (l *LoadBalancerHandler) DeleteSSL(ctx context.Context, lbID string) error {
	uri := fmt.Sprintf("%s/%s/ssl", lbPath, lbID)
	req, err := l.client.NewRequest(ctx, http.MethodDelete, uri, nil)
	if err != nil {
		return err
	}

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

// DeleteAutoSSL removes the AutoSSL configuration from a load balancer subscription.
func (l *LoadBalancerHandler) DeleteAutoSSL(ctx context.Context, lbID string) error {
	uri := fmt.Sprintf("%s/%s/auto_ssl", lbPath, lbID)
	req, err := l.client.NewRequest(ctx, http.MethodDelete, uri, nil)
	if err != nil {
		return err
	}

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