package property

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/akamai/AkamaiOPEN-edgegrid-golang/v12/pkg/hapi"
	"github.com/akamai/AkamaiOPEN-edgegrid-golang/v12/pkg/papi"
	"github.com/akamai/terraform-provider-akamai/v9/pkg/common/str"
	"github.com/akamai/terraform-provider-akamai/v9/pkg/common/tf"
	"github.com/akamai/terraform-provider-akamai/v9/pkg/common/timeouts"
	"github.com/akamai/terraform-provider-akamai/v9/pkg/log"
	"github.com/akamai/terraform-provider-akamai/v9/pkg/meta"
	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

const (
	retriesMax                     = 15
	minDomainPrefixLength          = 1
	minDomainPrefixLengthAkamaized = 4
)

const (
	changeRequestStatusPending   = "PENDING"
	changeRequestStatusSucceeded = "SUCCEEDED"
	changeRequestStatusFailed    = "FAILED"
	changeRequestStatusIgnored   = "IGNORED"
)

var (
	// EgdeHostnamePollInterval is the interval for polling an edgehostname creation or deletion
	EgdeHostnamePollInterval = time.Minute

	// GetEdgeHostnamePollInterval is the interval for polling an edgehostname after creation.
	GetEdgeHostnamePollInterval = time.Second * 10

	// errContextTerminated is returned when the context is terminated while waiting for an edge hostname to be ready.
	errContextTerminated = errors.New("get edge hostname context terminated")

	// EdgeHostnameReadTimeout is the timeout for fetching an edge hostname in the Read context.
	EdgeHostnameReadTimeout = time.Minute * 1

	// domainPrefixPatterns maps domain suffixes to their respective regex patterns for validating domain prefixes
	domainPrefixPatterns = map[string]*regexp.Regexp{
		"akamaized.net": regexp.MustCompile(`^[A-Za-z]([A-Za-z0-9-]*[A-Za-z0-9])?$`),
		"default":       regexp.MustCompile(`^[A-Za-z]([A-Za-z0-9.-]*[A-Za-z0-9])?(\.)?$`),
	}
)

var defaultEdgeHostnameTimeout = time.Minute * 45

func resourceSecureEdgeHostName() *schema.Resource {
	return &schema.Resource{
		CustomizeDiff: customdiff.All(
			validateImmutableFields,
		),
		CreateContext: resourceSecureEdgeHostNameCreate,
		ReadContext:   resourceSecureEdgeHostNameRead,
		UpdateContext: resourceSecureEdgeHostNameUpdate,
		DeleteContext: resourceSecureEdgeHostNameDelete,
		Importer: &schema.ResourceImporter{
			StateContext: resourceSecureEdgeHostNameImport,
		},
		Schema: akamaiSecureEdgeHostNameSchema,
		Timeouts: &schema.ResourceTimeout{
			Default: &defaultEdgeHostnameTimeout,
		},
	}
}

var akamaiSecureEdgeHostNameSchema = map[string]*schema.Schema{
	"product_id": {
		Type:      schema.TypeString,
		Optional:  true,
		Computed:  true,
		StateFunc: addPrefixToState("prd_"),
	},
	"contract_id": {
		Type:      schema.TypeString,
		Required:  true,
		StateFunc: addPrefixToState("ctr_"),
	},
	"group_id": {
		Type:      schema.TypeString,
		Required:  true,
		StateFunc: addPrefixToState("grp_"),
	},
	"edge_hostname": {
		Type:             schema.TypeString,
		Required:         true,
		ForceNew:         true,
		DiffSuppressFunc: diffSuppressEdgeHostname,
		ValidateDiagFunc: tf.IsNotBlank,
		StateFunc:        appendDefaultSuffixToEdgeHostname,
	},
	"ttl": {
		Type:        schema.TypeInt,
		Optional:    true,
		Description: "The time to live, or number of seconds to keep an edge hostname assigned to a map or target. If not provided default value for product is used.",
	},
	"ip_behavior": {
		Type:     schema.TypeString,
		Required: true,
	},
	"status_update_email": {
		Type:        schema.TypeList,
		Optional:    true,
		Elem:        &schema.Schema{Type: schema.TypeString},
		Description: "Email address that should receive updates on the IP behavior update request.",
	},
	"certificate": {
		Type:     schema.TypeInt,
		Optional: true,
		ForceNew: true,
	},
	"use_cases": {
		Type:             schema.TypeString,
		Optional:         true,
		ForceNew:         true,
		DiffSuppressFunc: suppressEdgeHostnameUseCases,
		Description:      "A JSON encoded list of use cases",
	},
	"timeouts": {
		Type:        schema.TypeList,
		Optional:    true,
		MaxItems:    1,
		Description: "Enables to set timeout for processing",
		Elem: &schema.Resource{
			Schema: map[string]*schema.Schema{
				"default": {
					Type:             schema.TypeString,
					Optional:         true,
					ValidateDiagFunc: timeouts.ValidateDurationFormat,
				},
			},
		},
	},
}

func resourceSecureEdgeHostNameCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
	meta := meta.Must(m)
	logger := meta.Log("PAPI", "resourceSecureEdgeHostNameCreate")

	client := Client(meta)

	edgeHostname, err := tf.GetStringValue("edge_hostname", d)
	if err != nil {
		return diag.FromErr(err)
	}

	diags := validateDomainPrefix(edgeHostname)
	if diags.HasError() {
		return diags
	}

	groupID, err := tf.GetStringValue("group_id", d)
	if err != nil {
		return diag.FromErr(err)
	}
	groupID = str.AddPrefix(groupID, "grp_")
	if err := d.Set("group_id", groupID); err != nil {
		return diag.FromErr(fmt.Errorf("%w: %s", tf.ErrValueSet, err.Error()))
	}

	contractID, err := tf.GetStringValue("contract_id", d)
	if err != nil {
		return diag.FromErr(err)
	}
	contractID = str.AddPrefix(contractID, "ctr_")
	if err := d.Set("contract_id", contractID); err != nil {
		return diag.FromErr(fmt.Errorf("%w: %s", tf.ErrValueSet, err.Error()))
	}

	logger.Debugf("Edgehostnames GROUP = %v", groupID)
	logger.Debugf("Edgehostnames CONTRACT = %v", contractID)

	// Schema no longer guarantees that product_id is set, this field is required only for creation
	productID, err := tf.GetStringValue("product_id", d)
	if err != nil {
		return diag.Errorf("`product_id` must be specified for creation")
	}
	productID = str.AddPrefix(productID, "prd_")
	if err := d.Set("product_id", productID); err != nil {
		return diag.FromErr(fmt.Errorf("%w: %s", tf.ErrValueSet, err.Error()))
	}

	edgeHostnames, err := client.GetEdgeHostnames(ctx, papi.GetEdgeHostnamesRequest{
		ContractID: contractID,
		GroupID:    groupID,
	})
	if err != nil {
		return diag.FromErr(err)
	}

	newHostname := papi.EdgeHostnameCreate{}
	newHostname.ProductID = productID
	newHostname.DomainSuffix, newHostname.SecureNetwork = parseEdgeHostname(edgeHostname)
	newHostname.DomainPrefix = strings.TrimSuffix(edgeHostname, "."+newHostname.DomainSuffix)
	// ip_behavior is required value in schema.
	newHostname.IPVersionBehavior = strings.ToUpper(d.Get("ip_behavior").(string))

	for _, h := range edgeHostnames.EdgeHostnames.Items {
		if h.DomainPrefix == newHostname.DomainPrefix && h.DomainSuffix == newHostname.DomainSuffix {
			return diag.Errorf("edgehostname '%s' already exists", edgeHostname)
		}
	}
	certEnrollmentID, err := tf.GetIntValue("certificate", d)
	if err != nil {
		if !errors.Is(err, tf.ErrNotFound) {
			return diag.FromErr(err)
		}
		if newHostname.SecureNetwork == papi.EHSecureNetworkEnhancedTLS {
			return diag.FromErr(fmt.Errorf("a certificate enrollment ID is required for Enhanced TLS edge hostnames with 'edgekey.net' suffix"))
		}
	}
	newHostname.CertEnrollmentID = certEnrollmentID
	newHostname.SlotNumber = certEnrollmentID

	useCasesJSON, err := tf.GetStringValue("use_cases", d)
	if err != nil && !errors.Is(err, tf.ErrNotFound) {
		return diag.FromErr(err)
	}
	if useCasesJSON != "" {
		var useCases []papi.UseCase
		if err := json.Unmarshal([]byte(useCasesJSON), &useCases); err != nil {
			return diag.Errorf("error while un-marshaling use cases JSON: %s", err)
		}
		newHostname.UseCases = useCases
	}

	logger.Debugf("Creating new edge hostname: %#v", newHostname)
	hostname, err := client.CreateEdgeHostname(ctx, papi.CreateEdgeHostnameRequest{
		EdgeHostname: newHostname,
		ContractID:   contractID,
		GroupID:      groupID,
	})
	if err != nil {
		return diag.FromErr(err)
	}

	edgeHostnameKey := edgeHostnameKey{
		edgeHostnameID: hostname.EdgeHostnameID,
		contractID:     contractID,
		groupID:        groupID,
	}

	if _, err := waitUntilEdgeHostnameReady(ctx, meta, client, edgeHostnameKey); err != nil {
		return diag.FromErr(err)
	}

	if d.HasChange("ttl") {
		edgeHostnameID, err := strconv.Atoi(strings.TrimPrefix(hostname.EdgeHostnameID, "ehn_"))
		if err != nil {
			return diag.FromErr(err)
		}
		hapiClient := HapiClient(meta)
		_, err = waitForHAPIPropagation(ctx, hapiClient, edgeHostnameID)
		if err != nil {
			return diag.FromErr(err)
		}

		ttl, err := tf.GetIntValueAsInt64("ttl", d)
		if err != nil {
			return diag.FromErr(err)
		}
		patches := []patch{{
			value: strconv.FormatInt(ttl, 10),
			field: "ttl",
			path:  "/ttl",
		}}
		diagnostics := patchEdgeHostname(ctx, d, meta, edgeHostnameID, patches)
		if diagnostics != nil {
			return diagnostics
		}
	}

	d.SetId(hostname.EdgeHostnameID)
	return resourceSecureEdgeHostNameRead(ctx, d, meta)
}

type edgeHostnameKey struct {
	edgeHostnameID string
	contractID     string
	groupID        string
}

// waitUntilEdgeHostnameReady waits until the edge hostname is available in the GetEdgeHostname response.
// Note: After creation, the edge hostname can be in one of three states: missing (not yet available even though creation was successful), CREATED or PENDING.
// This function waits only if the edge hostname is present in the GetEdgeHostname response, it does not matter if it is in CREATED or PENDING status.
func waitUntilEdgeHostnameReady(ctx context.Context, meta meta.Meta, client papi.PAPI, edgeHostnameKey edgeHostnameKey) (*papi.GetEdgeHostnamesResponse, error) {
	logger := meta.Log("PAPI", "waitUntilEdgeHostnameReady")

	for {
		resp, err := client.GetEdgeHostname(ctx, papi.GetEdgeHostnameRequest{
			EdgeHostnameID: edgeHostnameKey.edgeHostnameID,
			ContractID:     edgeHostnameKey.contractID,
			GroupID:        edgeHostnameKey.groupID,
		})
		if err == nil {
			logger.Debugf("edge hostname %s is now available in GetEdgeHostname response: %v, continuing", edgeHostnameKey.edgeHostnameID, resp)
			return resp, nil
		}

		// Retries called from GetEdgeHostname may detect timeout exceeded which was still not in the ctx.Done check below.
		if ctx.Err() != nil {
			logger.Debugf("context terminated while waiting for the edge hostname %s to appear in the response: %s", edgeHostnameKey.edgeHostnameID, ctx.Err())
			return nil, fmt.Errorf("%w: %s", errContextTerminated, ctx.Err())
		}

		if !errors.Is(err, papi.ErrNotFound) {
			logger.Debugf("received unexpected error while getting edge hostname %s: %v", edgeHostnameKey.edgeHostnameID, err.Error())
			return nil, err
		}

		logger.Debugf("edge hostname is %s not found yet, waiting %s before retrying", edgeHostnameKey.edgeHostnameID, GetEdgeHostnamePollInterval)

		select {
		case <-time.After(GetEdgeHostnamePollInterval):
			logger.Debugf("retrying check for edge hostname %s", edgeHostnameKey.edgeHostnameID)
		case <-ctx.Done():
			logger.Debugf("context terminated while waiting for the edge hostname %s to appear in the response: %s", edgeHostnameKey.edgeHostnameID, ctx.Err())
			return nil, fmt.Errorf("%w: %s", errContextTerminated, ctx.Err())
		}
	}
}

func validateDomainPrefix(edgeHostname string) diag.Diagnostics {

	domainSuffix, _ := parseEdgeHostname(edgeHostname)
	domainPrefix := strings.TrimSuffix(edgeHostname, "."+domainSuffix)
	domainPrefixLen := len(domainPrefix)

	minLen := minDomainPrefixLength
	if domainSuffix == "akamaized.net" {
		minLen = minDomainPrefixLengthAkamaized
	}

	if domainPrefixLen < minLen || domainPrefixLen > 63 {
		return diag.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 diag.Errorf(`A prefix for the edge hostname with the "akamaized.net" suffix must begin with a letter, end with a letter or digit, and contain only letters, digits, and hyphens, for example, abc-def, or abc-123.`)
		}
		return diag.Errorf(`A prefix for the edge hostname with the "%s" suffix must begin with a letter, end with a letter, digit, or dot, and contain only letters, digits, dots, and hyphens, for example, abc-def.123.456., or abc.123-def.`, domainSuffix)
	}
	return nil
}

func resourceSecureEdgeHostNameRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
	meta := meta.Must(m)
	logger := meta.Log("PAPI", "resourceSecureEdgeHostNameRead")

	client := Client(meta)

	groupID, err := tf.GetStringValue("group_id", d)
	if err != nil {
		return diag.FromErr(err)
	}
	groupID = str.AddPrefix(groupID, "grp_")
	if err := d.Set("group_id", groupID); err != nil {
		return diag.FromErr(fmt.Errorf("%w: %s", tf.ErrValueSet, err.Error()))
	}

	contractID, err := tf.GetStringValue("contract_id", d)
	if err != nil {
		return diag.FromErr(err)
	}
	contractID = str.AddPrefix(contractID, "ctr_")
	// set contract/contract_id into ResourceData
	if err := d.Set("contract_id", contractID); err != nil {
		return diag.FromErr(fmt.Errorf("%w: %s", tf.ErrValueSet, err.Error()))
	}

	// Schema guarantees product_id/product are strings and one or the other is set
	var productID string
	if got, ok := d.GetOk("product_id"); ok {
		productID = got.(string)
	}
	productID = str.AddPrefix(productID, "prd_")
	if err := d.Set("product_id", productID); err != nil {
		return diag.FromErr(fmt.Errorf("%w: %s", tf.ErrValueSet, err.Error()))
	}

	logger.Debugf("Edgehostnames GROUP = %v", groupID)
	logger.Debugf("Edgehostnames CONTRACT = %v", contractID)

	edgeHostnameResp, err := client.GetEdgeHostname(ctx, papi.GetEdgeHostnameRequest{
		EdgeHostnameID: d.Id(),
		ContractID:     contractID,
		GroupID:        groupID,
	})
	if err != nil {
		if errors.Is(err, papi.ErrNotFound) {
			logger.Debugf("edge hostname %s not found in read", d.Id())
			// If the edge hostname is not found in Read, wait one minute to check if it's a problem with database inconsistency on API side.
			ctx, cancel := context.WithTimeout(ctx, EdgeHostnameReadTimeout)
			defer cancel()

			edgeHostnameResp, err = waitUntilEdgeHostnameReady(ctx, meta, client, edgeHostnameKey{
				edgeHostnameID: d.Id(),
				contractID:     contractID,
				groupID:        groupID,
			})
			if err != nil {
				// If after one minute the edge hostname is still not found, remove it from the state,
				// as most probably it was deleted outside of terraform.
				if errors.Is(err, errContextTerminated) {
					logger.Info("edge hostname was deleted outside of terraform")
					d.SetId("")
					return nil
				}
				return diag.FromErr(fmt.Errorf("error waiting for edge hostname %s to be ready: %w", d.Id(), err))
			}
			// If the edge hostname is returned within one minute, continue processing.
			logger.Debugf("edge hostname %s recovered after receiving 404", d.Id())
		} else {
			// Return any other error as is.
			return diag.FromErr(err)
		}
	}

	foundEdgeHostname := edgeHostnameResp.EdgeHostname
	useCasesJSON, err := useCases2JSON(foundEdgeHostname.UseCases)
	if err != nil {
		return diag.FromErr(err)
	}
	if err := d.Set("use_cases", string(useCasesJSON)); err != nil {
		return diag.FromErr(fmt.Errorf("%w: %s", tf.ErrValueSet, err.Error()))
	}

	if err := d.Set("edge_hostname", foundEdgeHostname.Domain); err != nil {
		return diag.FromErr(fmt.Errorf("%w: %s", tf.ErrValueSet, err.Error()))
	}

	if err := d.Set("ip_behavior", foundEdgeHostname.IPVersionBehavior); err != nil {
		return diag.FromErr(fmt.Errorf("%w: %s", tf.ErrValueSet, err.Error()))
	}

	_, err = tf.GetIntValueAsInt64("ttl", d)
	if err != nil && !errors.Is(err, tf.ErrNotFound) {
		return diag.FromErr(err)
	}
	if err == nil {
		edgeHostnameID, err := strconv.Atoi(strings.TrimPrefix(foundEdgeHostname.ID, "ehn_"))
		if err != nil {
			return diag.FromErr(err)
		}

		hapiClient := HapiClient(meta)
		// in theory this call is redundant, added here as safeguard
		_, err = waitForHAPIPropagation(ctx, hapiClient, edgeHostnameID)
		if err != nil {
			return diag.FromErr(err)
		}
		hostname, err := hapiClient.GetEdgeHostname(ctx, edgeHostnameID)
		if err != nil {
			return diag.FromErr(err)
		}
		ttl := hostname.TTL
		if hostname.UseDefaultTTL {
			ttl = 0
		}
		if err := d.Set("ttl", ttl); err != nil {
			return diag.FromErr(fmt.Errorf("%w: %s", tf.ErrValueSet, err.Error()))
		}
	}
	return nil
}

func resourceSecureEdgeHostNameUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
	meta := meta.Must(m)
	logger := meta.Log("PAPI", "resourceSecureEdgeHostNameUpdate")

	if !d.HasChangeExcept("timeouts") {
		logger.Debug("Only timeouts were updated, skipping")
		return nil
	}

	patches := make([]patch, 0, 2)
	if d.HasChange("ip_behavior") {
		ipBehavior, err := tf.GetStringValue("ip_behavior", d)
		if err != nil {
			return diag.FromErr(err)
		}
		// IPV6_COMPLIANCE type has to mapped to IPV6_IPV4_DUALSTACK which is only accepted value by HAPI client
		if ipBehavior == papi.EHIPVersionV6Compliance {
			ipBehavior = "IPV6_IPV4_DUALSTACK"
		}
		patches = append(patches, patch{
			value: ipBehavior,
			field: "ip_behavior",
			path:  "/ipVersionBehavior",
		})
	}

	if d.HasChange("ttl") {
		ttl, err := tf.GetIntValueAsInt64("ttl", d)
		if err != nil {
			return diag.FromErr(err)
		}
		patches = append(patches, patch{
			value: strconv.FormatInt(ttl, 10),
			field: "ttl",
			path:  "/ttl",
		})
	}

	if len(patches) > 0 {
		edgeHostnameIDString := d.Id()
		edgeHostnameID, err := strconv.Atoi(strings.TrimPrefix(edgeHostnameIDString, "ehn_"))
		if err != nil {
			return diag.FromErr(err)
		}

		diagnostics := patchEdgeHostname(ctx, d, meta, edgeHostnameID, patches)
		if diagnostics != nil {
			return diagnostics
		}
	}

	return resourceSecureEdgeHostNameRead(ctx, d, m)
}

type patch struct {
	value string
	field string
	path  string
}

func patchEdgeHostname(ctx context.Context, d *schema.ResourceData, meta meta.Meta, edgeHostnameID int, patches []patch) diag.Diagnostics {
	logger := meta.Log("PAPI", "patchEdgeHostname")

	edgeHostname, err := tf.GetStringValue("edge_hostname", d)
	if err != nil {
		return diag.FromErr(err)
	}
	dnsZone, _ := parseEdgeHostname(edgeHostname)
	emails, err := tf.GetListValue("status_update_email", d)
	if err != nil && !errors.Is(err, tf.ErrNotFound) {
		return diag.FromErr(err)
	}

	l := len(patches)
	body := make([]hapi.UpdateEdgeHostnameRequestBody, 0, l)
	fields := make([]string, 0, l)
	comments := make([]string, 0, l)
	for _, p := range patches {
		logger.Debugf("Proceeding to update %s for %s", p.field, edgeHostname)
		body = append(body, hapi.UpdateEdgeHostnameRequestBody{

			Op:    "replace",
			Path:  p.path,
			Value: p.value,
		})
		comments = append(comments, fmt.Sprintf("change %s to %s", p.path, p.value))
		fields = append(fields, p.field)
	}
	req := hapi.UpdateEdgeHostnameRequest{
		DNSZone:    dnsZone,
		RecordName: strings.ReplaceAll(edgeHostname, "."+dnsZone, ""),
		Comments:   strings.Join(comments, "; "),
		Body:       body,
	}

	if len(emails) != 0 {
		statusUpdateEmails := make([]string, len(emails))
		for i, email := range emails {
			statusUpdateEmails[i] = email.(string)
		}
		req.StatusUpdateEmail = statusUpdateEmails
	}

	hapiClient := HapiClient(meta)
	_, err = waitForHAPIPropagation(ctx, hapiClient, edgeHostnameID)
	if err != nil {
		return diag.FromErr(err)
	}
	resp, err := hapiClient.UpdateEdgeHostname(ctx, req)
	if err != nil {
		if err2 := tf.RestoreOldValues(d, fields); err2 != nil {
			return diag.Errorf(`%s failed. No changes were written to server: 
%s

Failed to restore previous local schema values. The schema will remain in tainted state:
%s`, hapi.ErrUpdateEdgeHostname, err.Error(), err2.Error())
		}
		return diag.FromErr(err)
	}

	if err = waitForChange(ctx, hapiClient, resp.ChangeID); err != nil {
		return diag.FromErr(err)
	}
	return nil
}

func waitForHAPIPropagation(ctx context.Context, hapiClient hapi.HAPI, edgeHostnameID int) (*hapi.GetEdgeHostnameResponse, error) {
	retries := 0
	for {
		select {
		case <-time.After(EgdeHostnamePollInterval):
			resp, err := hapiClient.GetEdgeHostname(ctx, edgeHostnameID)
			if resp == nil && err != nil {
				var target = &hapi.Error{}
				if !errors.As(err, &target) {
					return nil, fmt.Errorf("error has unexpected type: %T", err)
				}
				if target.Status != 200 {
					retries++
					if retries > retriesMax {
						return nil, fmt.Errorf("reached max number of retries: %d", retries-1)
					}
					continue
				}
			}

			return resp, nil

		case <-ctx.Done():
			return nil, fmt.Errorf("get edge hostname context terminated: %s", ctx.Err())
		}
	}
}

func waitForChange(ctx context.Context, client hapi.HAPI, changeID int) error {
	for {
		change, err := client.GetChangeRequest(ctx, hapi.GetChangeRequest{
			ChangeID: changeID,
		})
		if err != nil {
			return err
		}
		if change.Status == changeRequestStatusPending {
			select {
			case <-time.After(time.Second * 10):
			case <-ctx.Done():
				return ctx.Err()
			}
			continue
		}
		if change.Status == changeRequestStatusSucceeded {
			return nil
		}
		return fmt.Errorf("unexpected change status: %s", change.Status)
	}
}

func resourceSecureEdgeHostNameDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
	meta := meta.Must(m)
	logger := meta.Log("PAPI", "resourceSecureEdgeHostNameDelete")

	edgeHostnameID, err := strconv.Atoi(strings.TrimPrefix(d.Id(), "ehn_"))
	if err != nil {
		return diag.FromErr(err)
	}

	hapiClient := HapiClient(meta)
	hostname, err := waitForHAPIPropagation(ctx, hapiClient, edgeHostnameID)
	if err != nil {
		return diag.FromErr(err)
	}

	emails, err := tf.GetListValue("status_update_email", d)
	if err != nil && !errors.Is(err, tf.ErrNotFound) {
		return diag.FromErr(err)
	}

	statusUpdateEmail := make([]string, 0)
	for _, email := range emails {
		statusUpdateEmail = append(statusUpdateEmail, email.(string))
	}

	deleteEdgeHostname, err := hapiClient.DeleteEdgeHostname(ctx, hapi.DeleteEdgeHostnameRequest{
		DNSZone:           hostname.DNSZone,
		RecordName:        hostname.RecordName,
		StatusUpdateEmail: statusUpdateEmail,
		Comments:          hostname.Comments,
	})
	if err != nil {
		return diag.FromErr(err)
	}

	for deleteInProgress := true; deleteInProgress; {
		select {
		case <-time.After(EgdeHostnamePollInterval):
			deleteStatus, err := hapiClient.GetChangeRequest(ctx, hapi.GetChangeRequest{ChangeID: deleteEdgeHostname.ChangeID})
			if err != nil {
				return diag.FromErr(err)
			}

			switch deleteStatus.Status {
			case changeRequestStatusSucceeded:
				deleteInProgress = false
				continue
			case changeRequestStatusFailed, changeRequestStatusIgnored:
				return diag.Diagnostics{diag.Diagnostic{
					Severity: diag.Error,
					Summary:  fmt.Sprintf("edgehostname deletion request got status %s", deleteStatus.Status),
					Detail:   deleteStatus.StatusMessage,
				}}
			case changeRequestStatusPending:
				logger.Debugf("edgehostname %d deletion is not yet ready, waiting for another attempt", edgeHostnameID)
			}
		case <-ctx.Done():
			return diag.FromErr(fmt.Errorf("delete edge hostname context terminated: %s", ctx.Err()))
		}
	}

	logger.Info("edge hostname was deleted successfully")
	d.SetId("")
	return nil
}

// resourceSecureEdgeHostNameImport accepts the following import ID:
// EdgehostNameID,contractID,groupID[,productID]
// productID is optional and needs to be specified if the resource config contains the product_id
// attribute (which is not the case for the configuration generated by export done with Akamai
// CLI tools).
func resourceSecureEdgeHostNameImport(ctx context.Context, d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) {
	meta := meta.Must(m)
	client := Client(meta)
	logger := meta.Log("PAPI", "resourceSecureEdgeHostNameImport")

	parts := strings.Split(d.Id(), ",")
	if len(parts) < 3 || len(parts) > 4 {
		return nil, fmt.Errorf("expected import identifier with format: "+
			`"EdgehostNameID,contractID,groupID[,productID]". Got: %q`, d.Id())
	}
	var productID string
	if len(parts) == 4 {
		if len(parts[3]) == 0 {
			return nil, fmt.Errorf("productID is empty for the import ID=%q", d.Id())
		}
		productID = str.AddPrefix(parts[3], "prd_")
		logger.Debugf("Setting product_id=%s", productID)
	}

	edgehostID := parts[0]
	contractID := str.AddPrefix(parts[1], "ctr_")
	groupID := str.AddPrefix(parts[2], "grp_")

	edgehostnameDetails, err := client.GetEdgeHostname(ctx, papi.GetEdgeHostnameRequest{
		EdgeHostnameID: edgehostID,
		ContractID:     contractID,
		GroupID:        groupID,
	})
	if err != nil {
		return nil, err
	}
	if len(productID) == 0 {
		productID = edgehostnameDetails.EdgeHostname.ProductID
	}

	if err := d.Set("contract_id", edgehostnameDetails.ContractID); err != nil {
		return nil, fmt.Errorf("%w: %s", tf.ErrValueSet, err.Error())
	}
	if err := d.Set("group_id", edgehostnameDetails.GroupID); err != nil {
		return nil, fmt.Errorf("%w: %s", tf.ErrValueSet, err.Error())
	}
	if err := d.Set("edge_hostname", edgehostnameDetails.EdgeHostname.Domain); err != nil {
		return nil, fmt.Errorf("%w: %s", tf.ErrValueSet, err.Error())
	}
	if err := d.Set("product_id", productID); err != nil {
		return nil, fmt.Errorf("%w: %s", tf.ErrValueSet, err.Error())
	}

	hapiClient := HapiClient(meta)
	edgeHostnameID, err := str.GetIntID(edgehostID, "ehn_")
	if err != nil {
		return nil, err
	}
	edgeHostnameResp, err := hapiClient.GetEdgeHostname(ctx, edgeHostnameID)
	if err != nil {
		return nil, fmt.Errorf("error getting edge hostname with id '%d': it may not be ready in HAPI yet: %s", edgeHostnameID, err)
	}

	// get certificate id when network is ENHANCED-TLS
	if edgeHostnameResp.SecurityType == "ENHANCED-TLS" {
		certificate, err := hapiClient.GetCertificate(ctx, hapi.GetCertificateRequest{
			DNSZone:    edgeHostnameResp.DNSZone,
			RecordName: edgeHostnameResp.RecordName,
		})
		if err != nil {
			if !errors.Is(err, hapi.ErrNotFound) {
				return nil, err
			}
		} else {
			certificateID, err := strconv.ParseInt(certificate.CertificateID, 10, 64)
			if err != nil {
				return nil, err
			}
			if err := d.Set("certificate", certificateID); err != nil {
				return nil, fmt.Errorf("%w: %s", tf.ErrValueSet, err.Error())
			}
		}
	}

	if !edgeHostnameResp.UseDefaultTTL {
		err := d.Set("ttl", edgeHostnameResp.TTL)
		if err != nil {
			return nil, fmt.Errorf("%w: %s", tf.ErrValueSet, err.Error())
		}
	}

	d.SetId(edgehostID)

	return []*schema.ResourceData{d}, nil
}

func diffSuppressEdgeHostname(_, oldVal, newVal string, _ *schema.ResourceData) bool {
	oldVal = strings.ToLower(oldVal)
	newVal = strings.ToLower(newVal)

	if oldVal == newVal {
		return true
	}

	if !(strings.HasSuffix(newVal, "edgekey.net") || strings.HasSuffix(newVal, "edgesuite.net") ||
		strings.HasSuffix(newVal, "akamaized.net")) {
		return oldVal == fmt.Sprintf("%s.edgesuite.net", newVal)
	}
	return false
}

func suppressEdgeHostnameUseCases(_, oldVal, newVal string, _ *schema.ResourceData) bool {
	logger := log.Get("PAPI", "suppressEdgeHostnameUseCases")
	if oldVal == newVal {
		return true
	}
	var oldUseCases, newUseCases []papi.UseCase
	if err := json.Unmarshal([]byte(oldVal), &oldUseCases); err != nil {
		logger.Errorf("Unable to unmarshal 'old' use cases: %s", err)
		return false
	}
	if err := json.Unmarshal([]byte(newVal), &newUseCases); err != nil {
		logger.Errorf("Unable to unmarshal 'new' use cases: %s", err)
		return false
	}

	diff := make(map[papi.UseCase]int)
	for _, useCase := range oldUseCases {
		diff[useCase]++
	}

	for _, useCase := range newUseCases {
		if _, ok := diff[useCase]; !ok {
			return false
		}

		diff[useCase]--
		if diff[useCase] == 0 {
			delete(diff, useCase)
		}
	}

	return len(diff) == 0
}

// appendDefaultSuffixToEdgeHostname is a StateFunc which appends ".edgesuite.net" to an edge hostname if none of the supported prefixes were provided
// It is used in order to retain idempotency when "edge_hostname" value is used as output
func appendDefaultSuffixToEdgeHostname(i interface{}) string {
	name := strings.ToLower(i.(string))
	if !(strings.HasSuffix(name, "edgekey.net") || strings.HasSuffix(name, "edgesuite.net") || strings.HasSuffix(name, "akamaized.net")) {
		name = fmt.Sprintf("%s.edgesuite.net", name)
	}
	return name
}

func useCases2JSON(useCases []papi.UseCase) ([]byte, error) {
	if len(useCases) == 0 {
		return []byte{}, nil
	}
	return json.MarshalIndent(useCases, "", "  ")
}

func parseEdgeHostname(hostname string) (string, string) {
	switch {
	case strings.HasSuffix(hostname, ".edgesuite.net"):
		return "edgesuite.net", papi.EHSecureNetworkStandardTLS
	case strings.HasSuffix(hostname, ".edgekey.net"):
		return "edgekey.net", papi.EHSecureNetworkEnhancedTLS
	case strings.HasSuffix(hostname, ".akamaized.net"):
		return "akamaized.net", papi.EHSecureNetworkSharedCert
	}
	return "edgesuite.net", ""
}

func validateImmutableFields(_ context.Context, diff *schema.ResourceDiff, _ interface{}) error {
	if diff.Id() != "" {
		oldValue, newValue := diff.GetChange("product_id")
		o := oldValue.(string)
		n := newValue.(string)

		if diff.HasChange("certificate") || str.AddPrefix(o, "prd_") != str.AddPrefix(n, "prd_") {
			return fmt.Errorf("error: Changes to non-updatable fields 'product_id' and 'certificate' are not permitted")
		}
	}
	return nil
}
