package papi

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"regexp"
	"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"
)

type (
	// GetEdgeHostnamesRequest contains query params used for listing edge hostnames
	GetEdgeHostnamesRequest struct {
		ContractID string
		GroupID    string
		Options    []string
	}

	// GetEdgeHostnameRequest contains path and query params used to fetch specific edge hostname
	GetEdgeHostnameRequest struct {
		EdgeHostnameID string
		ContractID     string
		GroupID        string
		Options        []string
	}

	// GetEdgeHostnamesResponse contains data received by calling GetEdgeHostnames or GetEdgeHostname
	GetEdgeHostnamesResponse struct {
		AccountID     string            `json:"accountId"`
		ContractID    string            `json:"contractId"`
		GroupID       string            `json:"groupId"`
		EdgeHostnames EdgeHostnameItems `json:"edgeHostnames"`
		EdgeHostname  EdgeHostnameGetItem
	}

	// EdgeHostnameItems contains a list of EdgeHostnames
	EdgeHostnameItems struct {
		Items []EdgeHostnameGetItem `json:"items"`
	}

	// EdgeHostnameGetItem contains GET details for edge hostname
	EdgeHostnameGetItem struct {
		ID                  string    `json:"edgeHostnameId"`
		Domain              string    `json:"edgeHostnameDomain"`
		ProductID           string    `json:"productId"`
		DomainPrefix        string    `json:"domainPrefix"`
		DomainSuffix        string    `json:"domainSuffix"`
		Status              string    `json:"status,omitempty"`
		Secure              bool      `json:"secure"`
		IPVersionBehavior   string    `json:"ipVersionBehavior"`
		UseCases            []UseCase `json:"useCases,omitempty"`
		HTTPSServiceBinding *string   `json:"httpsServiceBinding"`
	}

	// UseCase contains UseCase data
	UseCase struct {
		Option  string `json:"option"`
		Type    string `json:"type"`
		UseCase string `json:"useCase"`
	}

	// CreateEdgeHostnameRequest contains query params and body required for creation of new edge hostname
	CreateEdgeHostnameRequest struct {
		ContractID   string
		GroupID      string
		Options      []string
		EdgeHostname EdgeHostnameCreate
	}

	// EdgeHostnameCreate contains body of edge hostname POST request
	EdgeHostnameCreate struct {
		ProductID           string    `json:"productId"`
		DomainPrefix        string    `json:"domainPrefix"`
		DomainSuffix        string    `json:"domainSuffix"`
		Secure              bool      `json:"secure,omitempty"`
		SecureNetwork       string    `json:"secureNetwork,omitempty"`
		SlotNumber          int       `json:"slotNumber,omitempty"`
		IPVersionBehavior   string    `json:"ipVersionBehavior"`
		CertEnrollmentID    int       `json:"certEnrollmentId,omitempty"`
		UseCases            []UseCase `json:"useCases,omitempty"`
		HTTPSServiceBinding string    `json:"httpsServiceBinding,omitempty"`
	}

	// CreateEdgeHostnameResponse contains a link returned after creating new edge hostname and DI of this hostname
	CreateEdgeHostnameResponse struct {
		EdgeHostnameLink string `json:"edgeHostnameLink"`
		EdgeHostnameID   string `json:"-"`
	}
)

var (
	// domainPrefixPatterns maps domain suffixes to their respective regex patterns for validating domain prefixes
	domainPrefixPatterns = map[string]*regexp.Regexp{
		"akamaized.net": regexp.MustCompile(AkamaizedNetDomainRegexPattern),
		"default":       regexp.MustCompile(DefaultEHDomainRegexPattern),
	}
)

const (
	// EHSecureNetworkStandardTLS constant
	EHSecureNetworkStandardTLS = "STANDARD_TLS"
	// EHSecureNetworkSharedCert constant
	EHSecureNetworkSharedCert = "SHARED_CERT"
	// EHSecureNetworkEnhancedTLS constant
	EHSecureNetworkEnhancedTLS = "ENHANCED_TLS"

	// EHIPVersionV4 constant
	EHIPVersionV4 = "IPV4"
	// EHIPVersionV6Performance constant
	EHIPVersionV6Performance = "IPV6_PERFORMANCE"
	// EHIPVersionV6Compliance constant
	EHIPVersionV6Compliance = "IPV6_COMPLIANCE"

	// UseCaseGlobal constant
	UseCaseGlobal = "GLOBAL"

	minDomainPrefixLength          = 1
	minDomainPrefixLengthAkamaized = 4

	// AkamaizedNetDomainRegexPattern pattern for akamaized.net domain prefix validation
	AkamaizedNetDomainRegexPattern = `^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])$`
	// DefaultEHDomainRegexPattern pattern for default edge hostname domain prefix validation
	DefaultEHDomainRegexPattern = `^[A-Za-z0-9](?:[A-Za-z0-9_-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9_-]*[A-Za-z0-9])?)*\.?$`
)

// Validate validates CreateEdgeHostnameRequest
func (eh CreateEdgeHostnameRequest) Validate() error {
	errs := validation.Errors{
		"ContractID":   validation.Validate(eh.ContractID, validation.Required),
		"GroupID":      validation.Validate(eh.GroupID, validation.Required),
		"EdgeHostname": validation.Validate(eh.EdgeHostname),
	}
	return edgegriderr.ParseValidationErrors(errs)
}

// Validate validates EdgeHostnameCreate
func (eh EdgeHostnameCreate) Validate() error {
	return validation.Errors{
		"DomainPrefix": validation.Validate(eh.DomainPrefix, validation.Required),
		"DomainSuffix": validation.Validate(eh.DomainSuffix, validation.Required,
			validation.When(eh.SecureNetwork == EHSecureNetworkStandardTLS, validation.In("edgesuite.net")),
			validation.When(eh.SecureNetwork == EHSecureNetworkSharedCert, validation.In("akamaized.net")),
			validation.When(eh.SecureNetwork == EHSecureNetworkEnhancedTLS, validation.In("edgekey.net")),
		),
		"ProductID":           validation.Validate(eh.ProductID, validation.Required),
		"CertEnrollmentID":    validation.Validate(eh.CertEnrollmentID, validation.Required.When(eh.SecureNetwork == EHSecureNetworkEnhancedTLS)),
		"IPVersionBehavior":   validation.Validate(eh.IPVersionBehavior, validation.Required, validation.In(EHIPVersionV4, EHIPVersionV6Performance, EHIPVersionV6Compliance)),
		"SecureNetwork":       validation.Validate(eh.SecureNetwork, validation.In(EHSecureNetworkStandardTLS, EHSecureNetworkSharedCert, EHSecureNetworkEnhancedTLS)),
		"UseCases":            validation.Validate(eh.UseCases),
		"HTTPSServiceBinding": validation.Validate(eh.HTTPSServiceBinding, validation.In("H2", "H3", "H2_AND_H3")),
	}.Filter()
}

// Validate validates UseCase
func (uc UseCase) Validate() error {
	return validation.Errors{
		"Option":  validation.Validate(uc.Option, validation.Required),
		"Type":    validation.Validate(uc.Type, validation.Required, validation.In(UseCaseGlobal)),
		"UseCase": validation.Validate(uc.UseCase, validation.Required),
	}.Filter()
}

// Validate validates GetEdgeHostnamesRequest
func (eh GetEdgeHostnamesRequest) Validate() error {
	return validation.Errors{
		"ContractID": validation.Validate(eh.ContractID, validation.Required),
		"GroupID":    validation.Validate(eh.GroupID, validation.Required),
	}.Filter()
}

// Validate validates GetEdgeHostnameRequest
func (eh GetEdgeHostnameRequest) Validate() error {
	return validation.Errors{
		"EdgeHostnameID": validation.Validate(eh.EdgeHostnameID, validation.Required),
		"ContractID":     validation.Validate(eh.ContractID, validation.Required),
		"GroupID":        validation.Validate(eh.GroupID, validation.Required),
	}.Filter()
}

var (
	// ErrGetEdgeHostnames represents error when fetching edge hostnames fails
	ErrGetEdgeHostnames = errors.New("fetching edge hostnames")
	// ErrGetEdgeHostname represents error when fetching edge hostname fails
	ErrGetEdgeHostname = errors.New("fetching edge hostname")
	// ErrCreateEdgeHostname represents error when creating edge hostname fails
	ErrCreateEdgeHostname = errors.New("creating edge hostname")
)

// GetEdgeHostnames id used to list edge hostnames for provided group and contract IDs
func (p *papi) GetEdgeHostnames(ctx context.Context, params GetEdgeHostnamesRequest) (*GetEdgeHostnamesResponse, error) {
	logger := p.Log(ctx)
	logger.Debug("GetEdgeHostnames")

	if err := params.Validate(); err != nil {
		return nil, fmt.Errorf("%s: %w: %s", ErrGetEdgeHostnames, ErrStructValidation, err)
	}
	uri := url.URL{Path: "/papi/v1/edgehostnames"}
	q := url.Values{}
	q.Set("groupId", params.GroupID)
	q.Set("contractId", params.ContractID)
	if len(params.Options) > 0 {
		q.Set("options", strings.Join(params.Options, ","))
	}
	uri.RawQuery = q.Encode()

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil)
	if err != nil {
		return nil, fmt.Errorf("%w: failed to create request: %s", ErrGetEdgeHostnames, err)
	}

	var edgeHostnames GetEdgeHostnamesResponse
	resp, err := p.Exec(req, &edgeHostnames)
	if err != nil {
		return nil, fmt.Errorf("%w: request failed: %s", ErrGetEdgeHostnames, err)
	}
	defer session.CloseResponseBody(resp)

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("%s: %w", ErrGetEdgeHostnames, p.Error(resp))
	}

	return &edgeHostnames, nil
}

// GetEdgeHostname id used to fetch edge hostname with given ID for provided group and contract IDs
func (p *papi) GetEdgeHostname(ctx context.Context, params GetEdgeHostnameRequest) (*GetEdgeHostnamesResponse, error) {
	logger := p.Log(ctx)
	logger.Debug("GetEdgeHostname")

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

	uri, err := url.Parse(fmt.Sprintf("/papi/v1/edgehostnames/%s", params.EdgeHostnameID))
	if err != nil {
		return nil, fmt.Errorf("%w: failed to parse url: %s", ErrGetEdgeHostname, err)
	}
	q := url.Values{}
	q.Set("groupId", params.GroupID)
	q.Set("contractId", params.ContractID)
	if len(params.Options) > 0 {
		q.Set("options", strings.Join(params.Options, ","))
	}
	uri.RawQuery = q.Encode()

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil)
	if err != nil {
		return nil, fmt.Errorf("%w: failed to create request: %s", ErrGetEdgeHostname, err)
	}

	var edgeHostname GetEdgeHostnamesResponse
	resp, err := p.Exec(req, &edgeHostname)
	if err != nil {
		return nil, fmt.Errorf("%w: request failed: %s", ErrGetEdgeHostname, err)
	}
	defer session.CloseResponseBody(resp)

	if resp.StatusCode != http.StatusOK {
		return nil, p.Error(resp)
	}
	if len(edgeHostname.EdgeHostnames.Items) == 0 {
		return nil, fmt.Errorf("%s: %w: EdgeHostnameID: %s", ErrGetEdgeHostname, ErrNotFound, params.EdgeHostnameID)
	}
	edgeHostname.EdgeHostname = edgeHostname.EdgeHostnames.Items[0]

	return &edgeHostname, nil
}

// CreateEdgeHostname id used to create new edge hostname for provided group and contract IDs
func (p *papi) CreateEdgeHostname(ctx context.Context, params CreateEdgeHostnameRequest) (*CreateEdgeHostnameResponse, error) {
	logger := p.Log(ctx)
	logger.Debug("CreateEdgeHostname")

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

	if err := validateDomainPrefix(params.EdgeHostname.DomainPrefix, params.EdgeHostname.DomainSuffix); err != nil {
		return nil, err
	}

	uri := url.URL{Path: "/papi/v1/edgehostnames"}
	q := url.Values{}
	q.Set("groupId", params.GroupID)
	q.Set("contractId", params.ContractID)
	if len(params.Options) > 0 {
		q.Set("options", strings.Join(params.Options, ","))
	}
	uri.RawQuery = q.Encode()

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri.String(), nil)
	if err != nil {
		return nil, fmt.Errorf("%w: failed to create request: %s", ErrCreateEdgeHostname, err)
	}

	var createResponse CreateEdgeHostnameResponse
	resp, err := p.Exec(req, &createResponse, params.EdgeHostname)
	if err != nil {
		return nil, fmt.Errorf("%w: request failed: %s", ErrCreateEdgeHostname, err)
	}
	defer session.CloseResponseBody(resp)

	if resp.StatusCode != http.StatusCreated {
		return nil, fmt.Errorf("%s: %w", ErrCreateEdgeHostname, p.Error(resp))
	}
	id, err := ResponseLinkParse(createResponse.EdgeHostnameLink)
	if err != nil {
		return nil, fmt.Errorf("%s: %w: %s", ErrCreateEdgeHostname, ErrInvalidResponseLink, err)
	}
	createResponse.EdgeHostnameID = id
	return &createResponse, nil
}

func validateDomainPrefix(domainPrefix, domainSuffix string) error {
	domainPrefixLen := len(domainPrefix)
	minLen := minDomainPrefixLength
	if domainSuffix == "akamaized.net" {
		minLen = minDomainPrefixLengthAkamaized
	}

	if domainPrefixLen < minLen || domainPrefixLen > 63 {
		return fmt.Errorf(`the edge hostname prefix must be at least %d character(s) and no more than 63 characters for "%s" suffix; you provided %d character(s)`, minLen, domainSuffix, domainPrefixLen)
	}

	pattern, exists := domainPrefixPatterns[domainSuffix]
	if !exists {
		pattern = domainPrefixPatterns["default"]
	}

	if !pattern.MatchString(domainPrefix) {
		if domainSuffix == "akamaized.net" {
			return fmt.Errorf("a prefix for the edge hostname with the \"akamaized.net\" suffix must begin and end with a letter or digit, and contain only letters, digits, and hyphens, for example, abc-def, or 1abc-123")
		}
		return fmt.Errorf("a prefix for the edge hostname with the \"%s\" suffix must begin with a letter or digit, end with a letter, digit or dot, and contain only letters, digits, dots, and hyphens, for example, abc-def.123.456., or 1abc.123-def", domainSuffix)
	}
	return nil
}
