package godo

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

const (
	cachePath             = "cache"
	dropletsPath          = "droplets"
	forwardingRulesPath   = "forwarding_rules"
	loadBalancersBasePath = "/v2/load_balancers"
)

const (
	// Load Balancer types
	LoadBalancerTypeGlobal          = "GLOBAL"
	LoadBalancerTypeRegional        = "REGIONAL"
	LoadBalancerTypeRegionalNetwork = "REGIONAL_NETWORK"

	// Load Balancer network types
	LoadBalancerNetworkTypeExternal = "EXTERNAL"
	LoadBalancerNetworkTypeInternal = "INTERNAL"

	// Load Balancer network_stack types
	LoadBalancerNetworkStackIPv4      = "IPV4"
	LoadBalancerNetworkStackDualstack = "DUALSTACK"

	// Supported TLS Cipher policies
	LoadBalancerTLSCipherPolicyDefault = "DEFAULT"
	LoadBalancerTLSCipherPolicyStrong  = "STRONG"
)

// LoadBalancersService is an interface for managing load balancers with the DigitalOcean API.
// See: https://docs.digitalocean.com/reference/api/api-reference/#tag/Load-Balancers
type LoadBalancersService interface {
	Get(context.Context, string) (*LoadBalancer, *Response, error)
	List(context.Context, *ListOptions) ([]LoadBalancer, *Response, error)
	ListByNames(context.Context, []string, *ListOptions) ([]LoadBalancer, *Response, error)
	ListByUUIDs(context.Context, []string, *ListOptions) ([]LoadBalancer, *Response, error)
	Create(context.Context, *LoadBalancerRequest) (*LoadBalancer, *Response, error)
	Update(ctx context.Context, lbID string, lbr *LoadBalancerRequest) (*LoadBalancer, *Response, error)
	Delete(ctx context.Context, lbID string) (*Response, error)
	AddDroplets(ctx context.Context, lbID string, dropletIDs ...int) (*Response, error)
	RemoveDroplets(ctx context.Context, lbID string, dropletIDs ...int) (*Response, error)
	AddForwardingRules(ctx context.Context, lbID string, rules ...ForwardingRule) (*Response, error)
	RemoveForwardingRules(ctx context.Context, lbID string, rules ...ForwardingRule) (*Response, error)
	PurgeCache(ctx context.Context, lbID string) (*Response, error)
}

// LoadBalancer represents a DigitalOcean load balancer configuration.
// Tags can only be provided upon the creation of a Load Balancer.
type LoadBalancer struct {
	ID   string `json:"id,omitempty"`
	Name string `json:"name,omitempty"`
	IP   string `json:"ip,omitempty"`
	IPv6 string `json:"ipv6,omitempty"`
	// SizeSlug is mutually exclusive with SizeUnit. Only one should be specified
	SizeSlug string `json:"size,omitempty"`
	// SizeUnit is mutually exclusive with SizeSlug. Only one should be specified
	SizeUnit                     uint32           `json:"size_unit,omitempty"`
	Type                         string           `json:"type,omitempty"`
	Algorithm                    string           `json:"algorithm,omitempty"`
	Status                       string           `json:"status,omitempty"`
	Created                      string           `json:"created_at,omitempty"`
	ForwardingRules              []ForwardingRule `json:"forwarding_rules,omitempty"`
	HealthCheck                  *HealthCheck     `json:"health_check,omitempty"`
	StickySessions               *StickySessions  `json:"sticky_sessions,omitempty"`
	Region                       *Region          `json:"region,omitempty"`
	DropletIDs                   []int            `json:"droplet_ids,omitempty"`
	Tag                          string           `json:"tag,omitempty"`
	Tags                         []string         `json:"tags,omitempty"`
	RedirectHttpToHttps          bool             `json:"redirect_http_to_https,omitempty"`
	EnableProxyProtocol          bool             `json:"enable_proxy_protocol,omitempty"`
	EnableBackendKeepalive       bool             `json:"enable_backend_keepalive,omitempty"`
	VPCUUID                      string           `json:"vpc_uuid,omitempty"`
	DisableLetsEncryptDNSRecords *bool            `json:"disable_lets_encrypt_dns_records,omitempty"`
	ValidateOnly                 bool             `json:"validate_only,omitempty"`
	ProjectID                    string           `json:"project_id,omitempty"`
	HTTPIdleTimeoutSeconds       *uint64          `json:"http_idle_timeout_seconds,omitempty"`
	Firewall                     *LBFirewall      `json:"firewall,omitempty"`
	Domains                      []*LBDomain      `json:"domains,omitempty"`
	GLBSettings                  *GLBSettings     `json:"glb_settings,omitempty"`
	TargetLoadBalancerIDs        []string         `json:"target_load_balancer_ids,omitempty"`
	Network                      string           `json:"network,omitempty"`
	NetworkStack                 string           `json:"network_stack,omitempty"`
	TLSCipherPolicy              string           `json:"tls_cipher_policy,omitempty"`
}

// String creates a human-readable description of a LoadBalancer.
func (l LoadBalancer) String() string {
	return Stringify(l)
}

// URN returns the load balancer ID in a valid DO API URN form.
func (l LoadBalancer) URN() string {
	return ToURN("LoadBalancer", l.ID)
}

// AsRequest creates a LoadBalancerRequest that can be submitted to Update with the current values of the LoadBalancer.
// Modifying the returned LoadBalancerRequest will not modify the original LoadBalancer.
func (l LoadBalancer) AsRequest() *LoadBalancerRequest {
	r := LoadBalancerRequest{
		Name:                         l.Name,
		Algorithm:                    l.Algorithm,
		SizeSlug:                     l.SizeSlug,
		SizeUnit:                     l.SizeUnit,
		Type:                         l.Type,
		ForwardingRules:              append([]ForwardingRule(nil), l.ForwardingRules...),
		DropletIDs:                   append([]int(nil), l.DropletIDs...),
		Tag:                          l.Tag,
		RedirectHttpToHttps:          l.RedirectHttpToHttps,
		EnableProxyProtocol:          l.EnableProxyProtocol,
		EnableBackendKeepalive:       l.EnableBackendKeepalive,
		VPCUUID:                      l.VPCUUID,
		DisableLetsEncryptDNSRecords: l.DisableLetsEncryptDNSRecords,
		ValidateOnly:                 l.ValidateOnly,
		ProjectID:                    l.ProjectID,
		HTTPIdleTimeoutSeconds:       l.HTTPIdleTimeoutSeconds,
		TargetLoadBalancerIDs:        append([]string(nil), l.TargetLoadBalancerIDs...),
		Network:                      l.Network,
		NetworkStack:                 l.NetworkStack,
		TLSCipherPolicy:              l.TLSCipherPolicy,
	}

	if l.DisableLetsEncryptDNSRecords != nil {
		*r.DisableLetsEncryptDNSRecords = *l.DisableLetsEncryptDNSRecords
	}

	if l.HealthCheck != nil {
		r.HealthCheck = &HealthCheck{}
		*r.HealthCheck = *l.HealthCheck
	}

	if l.StickySessions != nil {
		r.StickySessions = &StickySessions{}
		*r.StickySessions = *l.StickySessions
	}

	if l.Region != nil {
		r.Region = l.Region.Slug
	}

	if l.Firewall != nil {
		r.Firewall = l.Firewall.deepCopy()
	}

	for _, domain := range l.Domains {
		lbDomain := &LBDomain{}
		*lbDomain = *domain
		lbDomain.VerificationErrorReasons = append([]string(nil), domain.VerificationErrorReasons...)
		lbDomain.SSLValidationErrorReasons = append([]string(nil), domain.SSLValidationErrorReasons...)
		r.Domains = append(r.Domains, lbDomain)
	}

	if l.GLBSettings != nil {
		r.GLBSettings = l.GLBSettings.deepCopy()
	}

	return &r
}

// ForwardingRule represents load balancer forwarding rules.
type ForwardingRule struct {
	EntryProtocol  string `json:"entry_protocol,omitempty"`
	EntryPort      int    `json:"entry_port,omitempty"`
	TargetProtocol string `json:"target_protocol,omitempty"`
	TargetPort     int    `json:"target_port,omitempty"`
	CertificateID  string `json:"certificate_id,omitempty"`
	TlsPassthrough bool   `json:"tls_passthrough,omitempty"`
}

// String creates a human-readable description of a ForwardingRule.
func (f ForwardingRule) String() string {
	return Stringify(f)
}

// HealthCheck represents optional load balancer health check rules.
type HealthCheck struct {
	Protocol               string `json:"protocol,omitempty"`
	Port                   int    `json:"port,omitempty"`
	Path                   string `json:"path,omitempty"`
	CheckIntervalSeconds   int    `json:"check_interval_seconds,omitempty"`
	ResponseTimeoutSeconds int    `json:"response_timeout_seconds,omitempty"`
	HealthyThreshold       int    `json:"healthy_threshold,omitempty"`
	UnhealthyThreshold     int    `json:"unhealthy_threshold,omitempty"`
	ProxyProtocol          *bool  `json:"proxy_protocol,omitempty"`
}

// String creates a human-readable description of a HealthCheck.
func (h HealthCheck) String() string {
	return Stringify(h)
}

// StickySessions represents optional load balancer session affinity rules.
type StickySessions struct {
	Type             string `json:"type,omitempty"`
	CookieName       string `json:"cookie_name,omitempty"`
	CookieTtlSeconds int    `json:"cookie_ttl_seconds,omitempty"`
}

// String creates a human-readable description of a StickySessions instance.
func (s StickySessions) String() string {
	return Stringify(s)
}

// LBFirewall holds the allow and deny rules for a loadbalancer's firewall.
// Currently, allow and deny rules support cidrs and ips.
// Please use the helper methods (IPSourceFirewall/CIDRSourceFirewall) to format the allow/deny rules.
type LBFirewall struct {
	Allow []string `json:"allow,omitempty"`
	Deny  []string `json:"deny,omitempty"`
}

func (lbf *LBFirewall) deepCopy() *LBFirewall {
	return &LBFirewall{
		Allow: append([]string(nil), lbf.Allow...),
		Deny:  append([]string(nil), lbf.Deny...),
	}
}

// IPSourceFirewall takes an IP (string) and returns a formatted ip source firewall rule
func IPSourceFirewall(ip string) string { return fmt.Sprintf("ip:%s", ip) }

// CIDRSourceFirewall takes a CIDR notation IP address and prefix length string
// like "192.0.2.0/24" and returns a formatted cidr source firewall rule
func CIDRSourceFirewall(cidr string) string { return fmt.Sprintf("cidr:%s", cidr) }

// String creates a human-readable description of an LBFirewall instance.
func (f LBFirewall) String() string {
	return Stringify(f)
}

// LoadBalancerRequest represents the configuration to be applied to an existing or a new load balancer.
type LoadBalancerRequest struct {
	Name      string `json:"name,omitempty"`
	Algorithm string `json:"algorithm,omitempty"`
	Region    string `json:"region,omitempty"`
	// SizeSlug is mutually exclusive with SizeUnit. Only one should be specified
	SizeSlug string `json:"size,omitempty"`
	// SizeUnit is mutually exclusive with SizeSlug. Only one should be specified
	SizeUnit                     uint32           `json:"size_unit,omitempty"`
	Type                         string           `json:"type,omitempty"`
	ForwardingRules              []ForwardingRule `json:"forwarding_rules,omitempty"`
	HealthCheck                  *HealthCheck     `json:"health_check,omitempty"`
	StickySessions               *StickySessions  `json:"sticky_sessions,omitempty"`
	DropletIDs                   []int            `json:"droplet_ids,omitempty"`
	Tag                          string           `json:"tag,omitempty"`
	Tags                         []string         `json:"tags,omitempty"`
	RedirectHttpToHttps          bool             `json:"redirect_http_to_https,omitempty"`
	EnableProxyProtocol          bool             `json:"enable_proxy_protocol,omitempty"`
	EnableBackendKeepalive       bool             `json:"enable_backend_keepalive,omitempty"`
	VPCUUID                      string           `json:"vpc_uuid,omitempty"`
	DisableLetsEncryptDNSRecords *bool            `json:"disable_lets_encrypt_dns_records,omitempty"`
	ValidateOnly                 bool             `json:"validate_only,omitempty"`
	ProjectID                    string           `json:"project_id,omitempty"`
	HTTPIdleTimeoutSeconds       *uint64          `json:"http_idle_timeout_seconds,omitempty"`
	Firewall                     *LBFirewall      `json:"firewall,omitempty"`
	Domains                      []*LBDomain      `json:"domains,omitempty"`
	GLBSettings                  *GLBSettings     `json:"glb_settings,omitempty"`
	TargetLoadBalancerIDs        []string         `json:"target_load_balancer_ids,omitempty"`
	Network                      string           `json:"network,omitempty"`
	NetworkStack                 string           `json:"network_stack,omitempty"`
	TLSCipherPolicy              string           `json:"tls_cipher_policy,omitempty"`
}

// String creates a human-readable description of a LoadBalancerRequest.
func (l LoadBalancerRequest) String() string {
	return Stringify(l)
}

type forwardingRulesRequest struct {
	Rules []ForwardingRule `json:"forwarding_rules,omitempty"`
}

func (l forwardingRulesRequest) String() string {
	return Stringify(l)
}

type dropletIDsRequest struct {
	IDs []int `json:"droplet_ids,omitempty"`
}

func (l dropletIDsRequest) String() string {
	return Stringify(l)
}

// LBDomain defines domain names required to ingress traffic to a Global LB
type LBDomain struct {
	// Name defines the domain fqdn
	Name string `json:"name"`
	// IsManaged indicates if the domain is DO-managed
	IsManaged bool `json:"is_managed"`
	// CertificateID indicates ID of a TLS certificate
	CertificateID string `json:"certificate_id,omitempty"`
	// Status indicates the domain validation status
	Status string `json:"status,omitempty"`
	// VerificationErrorReasons indicates any domain verification errors
	VerificationErrorReasons []string `json:"verification_error_reasons,omitempty"`
	// SSLValidationErrorReasons indicates any domain SSL validation errors
	SSLValidationErrorReasons []string `json:"ssl_validation_error_reasons,omitempty"`
}

// String creates a human-readable description of a LBDomain
func (d LBDomain) String() string {
	return Stringify(d)
}

// GLBSettings define settings for configuring a Global LB
type GLBSettings struct {
	// TargetProtocol is the outgoing traffic protocol.
	TargetProtocol string `json:"target_protocol"`
	// EntryPort is the outgoing traffic port.
	TargetPort uint32 `json:"target_port"`
	// CDNSettings is the CDN configurations
	CDN *CDNSettings `json:"cdn"`
	// RegionPriorities embeds regional priority information for regional active-passive failover policy
	RegionPriorities map[string]uint32 `json:"region_priorities,omitempty"`
	// FailoverThreshold embeds failover threshold percentage for regional active-passive failover policy
	FailoverThreshold uint32 `json:"failover_threshold,omitempty"`
}

// String creates a human-readable description of a GLBSettings
func (s GLBSettings) String() string {
	return Stringify(s)
}

func (s GLBSettings) deepCopy() *GLBSettings {
	settings := &GLBSettings{
		TargetProtocol:    s.TargetProtocol,
		TargetPort:        s.TargetPort,
		RegionPriorities:  s.RegionPriorities,
		FailoverThreshold: s.FailoverThreshold,
	}
	if s.CDN != nil {
		settings.CDN = &CDNSettings{IsEnabled: s.CDN.IsEnabled}
	}
	return settings
}

// CDNSettings define CDN settings for a Global LB
type CDNSettings struct {
	// IsEnabled is the caching enabled flag
	IsEnabled bool `json:"is_enabled"`
}

// String creates a human-readable description of a CDNSettings
func (c CDNSettings) String() string {
	return Stringify(c)
}

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

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

// LoadBalancersServiceOp handles communication with load balancer-related methods of the DigitalOcean API.
type LoadBalancersServiceOp struct {
	client *Client
}

var _ LoadBalancersService = &LoadBalancersServiceOp{}

// Get an existing load balancer by its identifier.
func (l *LoadBalancersServiceOp) Get(ctx context.Context, lbID string) (*LoadBalancer, *Response, error) {
	path := fmt.Sprintf("%s/%s", loadBalancersBasePath, lbID)

	req, err := l.client.NewRequest(ctx, http.MethodGet, path, nil)
	if err != nil {
		return nil, nil, err
	}

	root := new(loadBalancerRoot)
	resp, err := l.client.Do(ctx, req, root)
	if err != nil {
		return nil, resp, err
	}

	return root.LoadBalancer, resp, err
}

// List load balancers, with optional pagination.
func (l *LoadBalancersServiceOp) List(ctx context.Context, opt *ListOptions) ([]LoadBalancer, *Response, error) {
	path, err := addOptions(loadBalancersBasePath, opt)
	if err != nil {
		return nil, nil, err
	}

	req, err := l.client.NewRequest(ctx, http.MethodGet, path, nil)
	if err != nil {
		return nil, nil, err
	}

	root := new(loadBalancersRoot)
	resp, err := l.client.Do(ctx, req, root)
	if err != nil {
		return nil, resp, err
	}
	if l := root.Links; l != nil {
		resp.Links = l
	}
	if m := root.Meta; m != nil {
		resp.Meta = m
	}

	return root.LoadBalancers, resp, err
}

// ListByNames lists load balancers filtered by resource names, with optional pagination.
func (l *LoadBalancersServiceOp) ListByNames(ctx context.Context, names []string, opt *ListOptions) ([]LoadBalancer, *Response, error) {
	path, err := addOptions(loadBalancersBasePath, opt)
	if err != nil {
		return nil, nil, err
	}

	req, err := l.client.NewRequest(ctx, http.MethodGet, path, nil)
	if err != nil {
		return nil, nil, err
	}

	q := req.URL.Query()
	for _, name := range names {
		q.Add("names", name)
	}
	req.URL.RawQuery = q.Encode()

	root := new(loadBalancersRoot)
	resp, err := l.client.Do(ctx, req, root)
	if err != nil {
		return nil, resp, err
	}
	if l := root.Links; l != nil {
		resp.Links = l
	}
	if m := root.Meta; m != nil {
		resp.Meta = m
	}

	return root.LoadBalancers, resp, err
}

// ListByUUIDs lists load balancers filtered by resource UUIDs, with optional pagination.
func (l *LoadBalancersServiceOp) ListByUUIDs(ctx context.Context, uuids []string, opt *ListOptions) ([]LoadBalancer, *Response, error) {
	path, err := addOptions(loadBalancersBasePath, opt)
	if err != nil {
		return nil, nil, err
	}

	req, err := l.client.NewRequest(ctx, http.MethodGet, path, nil)
	if err != nil {
		return nil, nil, err
	}

	q := req.URL.Query()
	for _, uuid := range uuids {
		q.Add("uuids", uuid)
	}
	req.URL.RawQuery = q.Encode()

	root := new(loadBalancersRoot)
	resp, err := l.client.Do(ctx, req, root)
	if err != nil {
		return nil, resp, err
	}
	if l := root.Links; l != nil {
		resp.Links = l
	}
	if m := root.Meta; m != nil {
		resp.Meta = m
	}

	return root.LoadBalancers, resp, err
}

// Create a new load balancer with a given configuration.
func (l *LoadBalancersServiceOp) Create(ctx context.Context, lbr *LoadBalancerRequest) (*LoadBalancer, *Response, error) {
	req, err := l.client.NewRequest(ctx, http.MethodPost, loadBalancersBasePath, lbr)
	if err != nil {
		return nil, nil, err
	}

	root := new(loadBalancerRoot)
	resp, err := l.client.Do(ctx, req, root)
	if err != nil {
		return nil, resp, err
	}

	return root.LoadBalancer, resp, err
}

// Update an existing load balancer with new configuration.
func (l *LoadBalancersServiceOp) Update(ctx context.Context, lbID string, lbr *LoadBalancerRequest) (*LoadBalancer, *Response, error) {
	path := fmt.Sprintf("%s/%s", loadBalancersBasePath, lbID)

	req, err := l.client.NewRequest(ctx, "PUT", path, lbr)
	if err != nil {
		return nil, nil, err
	}

	root := new(loadBalancerRoot)
	resp, err := l.client.Do(ctx, req, root)
	if err != nil {
		return nil, resp, err
	}

	return root.LoadBalancer, resp, err
}

// Delete a load balancer by its identifier.
func (l *LoadBalancersServiceOp) Delete(ctx context.Context, ldID string) (*Response, error) {
	path := fmt.Sprintf("%s/%s", loadBalancersBasePath, ldID)

	req, err := l.client.NewRequest(ctx, http.MethodDelete, path, nil)
	if err != nil {
		return nil, err
	}

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

// AddDroplets adds droplets to a load balancer.
func (l *LoadBalancersServiceOp) AddDroplets(ctx context.Context, lbID string, dropletIDs ...int) (*Response, error) {
	path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, lbID, dropletsPath)

	req, err := l.client.NewRequest(ctx, http.MethodPost, path, &dropletIDsRequest{IDs: dropletIDs})
	if err != nil {
		return nil, err
	}

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

// RemoveDroplets removes droplets from a load balancer.
func (l *LoadBalancersServiceOp) RemoveDroplets(ctx context.Context, lbID string, dropletIDs ...int) (*Response, error) {
	path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, lbID, dropletsPath)

	req, err := l.client.NewRequest(ctx, http.MethodDelete, path, &dropletIDsRequest{IDs: dropletIDs})
	if err != nil {
		return nil, err
	}

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

// AddForwardingRules adds forwarding rules to a load balancer.
func (l *LoadBalancersServiceOp) AddForwardingRules(ctx context.Context, lbID string, rules ...ForwardingRule) (*Response, error) {
	path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, lbID, forwardingRulesPath)

	req, err := l.client.NewRequest(ctx, http.MethodPost, path, &forwardingRulesRequest{Rules: rules})
	if err != nil {
		return nil, err
	}

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

// RemoveForwardingRules removes forwarding rules from a load balancer.
func (l *LoadBalancersServiceOp) RemoveForwardingRules(ctx context.Context, lbID string, rules ...ForwardingRule) (*Response, error) {
	path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, lbID, forwardingRulesPath)

	req, err := l.client.NewRequest(ctx, http.MethodDelete, path, &forwardingRulesRequest{Rules: rules})
	if err != nil {
		return nil, err
	}

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

// PurgeCache purges the CDN cache of a global load balancer by its identifier.
func (l *LoadBalancersServiceOp) PurgeCache(ctx context.Context, ldID string) (*Response, error) {
	path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, ldID, cachePath)

	req, err := l.client.NewRequest(ctx, http.MethodDelete, path, nil)
	if err != nil {
		return nil, err
	}

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