package gtm

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"slices"
	"strings"

	"github.com/akamai/AkamaiOPEN-edgegrid-golang/v13/pkg/edgegriderr"
	"github.com/akamai/AkamaiOPEN-edgegrid-golang/v13/pkg/session"
	validation "github.com/go-ozzo/ozzo-validation/v4"
)

var allowedSchemes = []string{"http", "https", "mailto"}

type (
	// TrafficTarget struct contains information about where to direct data center traffic
	TrafficTarget struct {
		DatacenterID int      `json:"datacenterId"`
		Enabled      bool     `json:"enabled"`
		Weight       float64  `json:"weight,omitempty"`
		Servers      []string `json:"servers,omitempty"`
		Name         string   `json:"name,omitempty"`
		HandoutCName string   `json:"handoutCName,omitempty"`
		Precedence   *int     `json:"precedence,omitempty"`
	}

	// HTTPHeader struct contains HTTP headers to send if the testObjectProtocol is http or https
	HTTPHeader struct {
		Name  string `json:"name"`
		Value string `json:"value"`
	}

	// LivenessTest contains configuration of liveness tests to determine whether your servers respond to requests
	LivenessTest struct {
		Name                          string       `json:"name"`
		ErrorPenalty                  float64      `json:"errorPenalty,omitempty"`
		PeerCertificateVerification   bool         `json:"peerCertificateVerification"`
		TestInterval                  int          `json:"testInterval,omitempty"`
		TestObject                    string       `json:"testObject,omitempty"`
		Links                         []Link       `json:"links,omitempty"`
		RequestString                 string       `json:"requestString,omitempty"`
		ResponseString                string       `json:"responseString,omitempty"`
		HTTPError3xx                  bool         `json:"httpError3xx"`
		HTTPError4xx                  bool         `json:"httpError4xx"`
		HTTPError5xx                  bool         `json:"httpError5xx"`
		HTTPMethod                    *string      `json:"httpMethod"`
		HTTPRequestBody               *string      `json:"httpRequestBody"`
		Disabled                      bool         `json:"disabled"`
		TestObjectProtocol            string       `json:"testObjectProtocol,omitempty"`
		TestObjectPassword            string       `json:"testObjectPassword,omitempty"`
		TestObjectPort                int          `json:"testObjectPort,omitempty"`
		SSLClientPrivateKey           string       `json:"sslClientPrivateKey,omitempty"`
		SSLClientCertificate          string       `json:"sslClientCertificate,omitempty"`
		Pre2023SecurityPosture        bool         `json:"pre2023SecurityPosture"`
		DisableNonstandardPortWarning bool         `json:"disableNonstandardPortWarning"`
		HTTPHeaders                   []HTTPHeader `json:"httpHeaders,omitempty"`
		TestObjectUsername            string       `json:"testObjectUsername,omitempty"`
		TestTimeout                   float32      `json:"testTimeout,omitempty"`
		TimeoutPenalty                float64      `json:"timeoutPenalty,omitempty"`
		AnswersRequired               bool         `json:"answersRequired"`
		ResourceType                  string       `json:"resourceType,omitempty"`
		RecursionRequested            bool         `json:"recursionRequested"`
		AlternateCACertificates       []string     `json:"alternateCACertificates"`
	}

	// StaticRRSet contains static recordset
	StaticRRSet struct {
		Type  string   `json:"type"`
		TTL   int      `json:"ttl"`
		Rdata []string `json:"rdata"`
	}

	// Property represents a GTM property
	Property struct {
		Name                           string                          `json:"name"`
		Type                           string                          `json:"type"`
		IPv6                           bool                            `json:"ipv6"`
		ScoreAggregationType           string                          `json:"scoreAggregationType"`
		StickinessBonusPercentage      int                             `json:"stickinessBonusPercentage,omitempty"`
		StickinessBonusConstant        int                             `json:"stickinessBonusConstant,omitempty"`
		HealthThreshold                float64                         `json:"healthThreshold,omitempty"`
		UseComputedTargets             bool                            `json:"useComputedTargets"`
		BackupIP                       string                          `json:"backupIp,omitempty"`
		BalanceByDownloadScore         bool                            `json:"balanceByDownloadScore"`
		StaticTTL                      int                             `json:"staticTTL,omitempty"`
		StaticRRSets                   []StaticRRSet                   `json:"staticRRSets,omitempty"`
		LastModified                   string                          `json:"lastModified"`
		UnreachableThreshold           float64                         `json:"unreachableThreshold,omitempty"`
		MinLiveFraction                float64                         `json:"minLiveFraction,omitempty"`
		HealthMultiplier               float64                         `json:"healthMultiplier,omitempty"`
		DynamicTTL                     int                             `json:"dynamicTTL,omitempty"`
		MaxUnreachablePenalty          int                             `json:"maxUnreachablePenalty,omitempty"`
		MapName                        string                          `json:"mapName,omitempty"`
		HandoutLimit                   int                             `json:"handoutLimit"`
		HandoutMode                    string                          `json:"handoutMode"`
		FailoverDelay                  int                             `json:"failoverDelay,omitempty"`
		BackupCName                    string                          `json:"backupCName,omitempty"`
		FailbackDelay                  int                             `json:"failbackDelay,omitempty"`
		LoadImbalancePercentage        float64                         `json:"loadImbalancePercentage,omitempty"`
		HealthMax                      float64                         `json:"healthMax,omitempty"`
		GhostDemandReporting           bool                            `json:"ghostDemandReporting"`
		Comments                       string                          `json:"comments,omitempty"`
		CName                          string                          `json:"cname,omitempty"`
		WeightedHashBitsForIPv4        int                             `json:"weightedHashBitsForIPv4,omitempty"`
		WeightedHashBitsForIPv6        int                             `json:"weightedHashBitsForIPv6,omitempty"`
		TrafficTargets                 []TrafficTarget                 `json:"trafficTargets,omitempty"`
		Links                          []Link                          `json:"links,omitempty"`
		LivenessTests                  []LivenessTest                  `json:"livenessTests,omitempty"`
		StateChangeNotificationWebhook *StateChangeNotificationWebhook `json:"stateChangeNotificationWebhook,omitempty"`
	}

	// StateChangeNotificationWebhook defines a webhook to notify external systems when datacenter or server state changes occur.
	StateChangeNotificationWebhook struct {
		URL    *string `json:"url"`
		Format Format  `json:"format"`
	}

	// PropertyRequest contains request parameters
	PropertyRequest struct {
		Property   *Property
		DomainName string
	}

	// PropertyList contains a list of property items
	PropertyList struct {
		PropertyItems []Property `json:"items"`
	}
	// GetPropertyRequest contains request parameters for GetProperty
	GetPropertyRequest struct {
		DomainName   string
		PropertyName string
	}

	// GetPropertyResponse contains the response data from GetProperty operation
	GetPropertyResponse Property

	// ListPropertiesRequest contains request parameters for ListProperties
	ListPropertiesRequest struct {
		DomainName string
	}

	// CreatePropertyRequest contains request parameters for CreateProperty
	CreatePropertyRequest PropertyRequest

	// CreatePropertyResponse contains the response data from CreateProperty operation
	CreatePropertyResponse struct {
		Resource *Property       `json:"resource"`
		Status   *ResponseStatus `json:"status"`
	}

	// UpdatePropertyRequest contains request parameters for UpdatePropertyResponse
	UpdatePropertyRequest PropertyRequest

	// UpdatePropertyResponse contains the response data from UpdatePropertyResponse operation
	UpdatePropertyResponse struct {
		Resource *Property       `json:"resource"`
		Status   *ResponseStatus `json:"status"`
	}

	// DeletePropertyRequest contains request parameters for DeleteProperty
	DeletePropertyRequest struct {
		DomainName   string
		PropertyName string
	}

	// DeletePropertyResponse contains the response data from DeleteProperty operation
	DeletePropertyResponse struct {
		Resource *Property       `json:"resource"`
		Status   *ResponseStatus `json:"status"`
	}

	// Format represents format of the message that’s sent to the application or service.
	Format string
)

const (
	// JSONCompact is a JSON format where the message is a single line and does not contain line spaces.
	JSONCompact Format = "json-compact"
	// JSONPretty is a JSON format where the message contains line spaces and is more readable.
	JSONPretty Format = "json-pretty"
	// SlackMarkdown is a Markdown format based on Slack's allowed syntax.
	SlackMarkdown Format = "slack-mrkdwn"
)

var (
	// ErrGetProperty is returned when GetProperty fails.
	ErrGetProperty = errors.New("get property")
	// ErrListProperties is returned when ListProperties fails.
	ErrListProperties = errors.New("list properties")
	// ErrCreateProperty is returned when CreateProperty fails.
	ErrCreateProperty = errors.New("create Property")
	// ErrUpdateProperty is returned when UpdateProperty fails
	ErrUpdateProperty = errors.New("update Property")
	// ErrDeleteProperty is returned when DeleteProperty fails
	ErrDeleteProperty = errors.New("delete Property")
)

// Validate validates GetPropertyRequest
func (r GetPropertyRequest) Validate() error {
	return edgegriderr.ParseValidationErrors(validation.Errors{
		"DomainName":   validation.Validate(r.DomainName, validation.Required),
		"PropertyName": validation.Validate(r.PropertyName, validation.Required),
	})
}

// Validate validates ListPropertiesRequest
func (r ListPropertiesRequest) Validate() error {
	return edgegriderr.ParseValidationErrors(validation.Errors{
		"DomainName": validation.Validate(r.DomainName, validation.Required),
	})
}

// Validate validates CreatePropertyRequest
func (r CreatePropertyRequest) Validate() error {
	return edgegriderr.ParseValidationErrors(validation.Errors{
		"DomainName": validation.Validate(r.DomainName, validation.Required),
		"Property":   validation.Validate(r.Property, validation.Required),
	})
}

// Validate validates StateChangeNotificationWebhook
func (s StateChangeNotificationWebhook) Validate() error {
	return edgegriderr.ParseValidationErrors(validation.Errors{
		"URL":    validation.Validate(s.URL, validation.Required, validation.Length(0, 1024), validation.By(validScheme)),
		"Format": validation.Validate(s.Format, validation.In(JSONCompact, JSONPretty, SlackMarkdown)),
	})
}

// Validate validates UpdatePropertyRequest
func (r UpdatePropertyRequest) Validate() error {
	return edgegriderr.ParseValidationErrors(validation.Errors{
		"DomainName": validation.Validate(r.DomainName, validation.Required),
		"Property":   validation.Validate(r.Property, validation.Required),
	})
}

// Validate validates DeletePropertyRequest
func (r DeletePropertyRequest) Validate() error {
	return edgegriderr.ParseValidationErrors(validation.Errors{
		"DomainName":   validation.Validate(r.DomainName, validation.Required),
		"PropertyName": validation.Validate(r.PropertyName, validation.Required),
	})
}

// Validate validates Property
func (p *Property) Validate() error {
	return edgegriderr.ParseValidationErrors(validation.Errors{
		"Name":                           validation.Validate(p.Name, validation.Required),
		"Type":                           validation.Validate(p.Type, validation.Required),
		"ScoreAggregationTypes":          validation.Validate(p.ScoreAggregationType, validation.Required),
		"HandoutMode":                    validation.Validate(p.HandoutMode, validation.Required),
		"TrafficTargets":                 validation.Validate(p.TrafficTargets, validation.When(p.Type == "ranked-failover", validation.By(validateRankedFailoverTrafficTargets))),
		"StateChangeNotificationWebhook": validation.Validate(p.StateChangeNotificationWebhook),
	})
}

// validateRankedFailoverTrafficTargets validates traffic targets when property type is 'ranked-failover'
func validateRankedFailoverTrafficTargets(value interface{}) error {
	tt := value.([]TrafficTarget)
	if len(tt) == 0 {
		return fmt.Errorf("no traffic targets are enabled")
	}
	precedenceCounter := map[int]int{}
	minPrecedence := 256
	for _, t := range tt {
		if t.Precedence == nil {
			precedenceCounter[0]++
			minPrecedence = 0
		} else {
			if *t.Precedence > 255 || *t.Precedence < 0 {
				return fmt.Errorf("'Precedence' value has to be between 0 and 255")
			}
			precedenceCounter[*t.Precedence]++
			if *t.Precedence < minPrecedence {
				minPrecedence = *t.Precedence
			}
		}
	}
	if precedenceCounter[minPrecedence] > 1 {
		return fmt.Errorf("property cannot have multiple primary traffic targets (targets with lowest precedence)")
	}

	return nil
}

func validScheme(value interface{}) error {
	str, ok := toString(value)
	if !ok || str == "" {
		return nil
	}

	parsed, err := url.Parse(str)
	if err != nil {
		return fmt.Errorf("invalid URL: %w", err)
	}

	scheme := strings.ToLower(parsed.Scheme)
	if !slices.Contains(allowedSchemes, scheme) {
		return fmt.Errorf(
			"invalid URL scheme: %s. Only %s are allowed",
			parsed.Scheme,
			strings.Join(allowedSchemes, ", "),
		)
	}
	return nil
}

// toString attempts to convert a value to a string.
func toString(value interface{}) (string, bool) {
	switch v := value.(type) {
	case string:
		return v, true
	case *string:
		if v == nil {
			return "", false
		}
		return *v, true
	default:
		return "", false
	}
}

func (g *gtm) ListProperties(ctx context.Context, params ListPropertiesRequest) ([]Property, error) {
	logger := g.Log(ctx)
	logger.Debug("ListProperties")

	if err := params.Validate(); err != nil {
		return nil, fmt.Errorf("%s: %w: %s", ErrListProperties, ErrStructValidation, err)
	}

	getURL := fmt.Sprintf("/config-gtm/v1/domains/%s/properties", params.DomainName)
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, getURL, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create ListProperties request: %w", err)
	}
	setVersionHeader(req, schemaVersion)

	var result PropertyList
	resp, err := g.Exec(req, &result)
	if err != nil {
		return nil, fmt.Errorf("ListProperties request failed: %w", err)
	}
	defer session.CloseResponseBody(resp)

	if resp.StatusCode != http.StatusOK {
		return nil, g.Error(resp)
	}

	return result.PropertyItems, nil
}

func (g *gtm) GetProperty(ctx context.Context, params GetPropertyRequest) (*GetPropertyResponse, error) {
	logger := g.Log(ctx)
	logger.Debug("GetProperty")

	if err := params.Validate(); err != nil {
		return nil, fmt.Errorf("%s: %w: %s", ErrGetProperty, ErrStructValidation, err)
	}

	getURL := fmt.Sprintf("/config-gtm/v1/domains/%s/properties/%s", params.DomainName, params.PropertyName)
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, getURL, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create GetProperty request: %w", err)
	}
	setVersionHeader(req, schemaVersion)

	var result GetPropertyResponse
	resp, err := g.Exec(req, &result)
	if err != nil {
		return nil, fmt.Errorf("GetProperty request failed: %w", err)
	}
	defer session.CloseResponseBody(resp)

	if resp.StatusCode != http.StatusOK {
		return nil, g.Error(resp)
	}

	return &result, nil
}

func (g *gtm) CreateProperty(ctx context.Context, params CreatePropertyRequest) (*CreatePropertyResponse, error) {
	logger := g.Log(ctx)
	logger.Debug("CreateProperty")

	if err := params.Validate(); err != nil {
		return nil, fmt.Errorf("%s: %w: %s", ErrCreateProperty, ErrStructValidation, err)
	}

	putURL := fmt.Sprintf("/config-gtm/v1/domains/%s/properties/%s", params.DomainName, params.Property.Name)
	req, err := http.NewRequestWithContext(ctx, http.MethodPut, putURL, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create Property request: %w", err)
	}
	setVersionHeader(req, schemaVersion)

	var result CreatePropertyResponse
	resp, err := g.Exec(req, &result, params.Property)
	if err != nil {
		return nil, fmt.Errorf("property request failed: %w", err)
	}
	defer session.CloseResponseBody(resp)

	if resp.StatusCode != http.StatusCreated {
		return nil, g.Error(resp)
	}

	return &result, nil
}

func (g *gtm) UpdateProperty(ctx context.Context, params UpdatePropertyRequest) (*UpdatePropertyResponse, error) {
	logger := g.Log(ctx)
	logger.Debug("UpdateProperty")

	if err := params.Validate(); err != nil {
		return nil, fmt.Errorf("%s: %w: %s", ErrUpdateProperty, ErrStructValidation, err)
	}

	putURL := fmt.Sprintf("/config-gtm/v1/domains/%s/properties/%s", params.DomainName, params.Property.Name)
	req, err := http.NewRequestWithContext(ctx, http.MethodPut, putURL, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create Property request: %w", err)
	}
	setVersionHeader(req, schemaVersion)

	var result UpdatePropertyResponse
	resp, err := g.Exec(req, &result, params.Property)
	if err != nil {
		return nil, fmt.Errorf("property request failed: %w", err)
	}
	defer session.CloseResponseBody(resp)

	if resp.StatusCode != http.StatusOK {
		return nil, g.Error(resp)
	}

	return &result, nil
}

func (g *gtm) DeleteProperty(ctx context.Context, params DeletePropertyRequest) (*DeletePropertyResponse, error) {
	logger := g.Log(ctx)
	logger.Debug("DeleteProperty")

	if err := params.Validate(); err != nil {
		return nil, fmt.Errorf("%s: %w: %s", ErrDeleteProperty, ErrStructValidation, err)
	}

	delURL := fmt.Sprintf("/config-gtm/v1/domains/%s/properties/%s", params.DomainName, params.PropertyName)
	req, err := http.NewRequestWithContext(ctx, http.MethodDelete, delURL, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create Property request: %w", err)
	}
	setVersionHeader(req, schemaVersion)

	var result DeletePropertyResponse
	resp, err := g.Exec(req, &result)
	if err != nil {
		return nil, fmt.Errorf("DeleteProperty request failed: %w", err)
	}
	defer session.CloseResponseBody(resp)

	if resp.StatusCode != http.StatusOK {
		return nil, g.Error(resp)
	}

	return &result, nil
}
