/*
Copyright 2017 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package alibabacloud

import (
	"context"
	"fmt"
	"os"
	"slices"
	"sort"
	"strings"
	"sync"
	"time"

	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests"
	"github.com/aliyun/alibaba-cloud-sdk-go/services/alidns"
	"github.com/aliyun/alibaba-cloud-sdk-go/services/pvtz"
	"github.com/denverdino/aliyungo/metadata"
	"github.com/goccy/go-yaml"
	log "github.com/sirupsen/logrus"

	"sigs.k8s.io/external-dns/pkg/apis/externaldns"

	"sigs.k8s.io/external-dns/endpoint"
	"sigs.k8s.io/external-dns/plan"
	"sigs.k8s.io/external-dns/provider"
)

const (
	defaultTTL                              = 600
	defaultAlibabaCloudPrivateZoneRecordTTL = 60
	defaultAlibabaCloudPageSize             = 50
	nullHostAlibabaCloud                    = "@"
	pVTZDoamin                              = "pvtz.aliyuncs.com"
	defaultAlibabaCloudRequestScheme        = "https"
)

// AlibabaCloudDNSAPI is a minimal implementation of DNS API that we actually use, used primarily for unit testing.
// See https://help.aliyun.com/document_detail/29739.html for descriptions of all of its methods.
type AlibabaCloudDNSAPI interface {
	AddDomainRecord(request *alidns.AddDomainRecordRequest) (*alidns.AddDomainRecordResponse, error)
	DeleteDomainRecord(request *alidns.DeleteDomainRecordRequest) (*alidns.DeleteDomainRecordResponse, error)
	UpdateDomainRecord(request *alidns.UpdateDomainRecordRequest) (*alidns.UpdateDomainRecordResponse, error)
	DescribeDomainRecords(request *alidns.DescribeDomainRecordsRequest) (*alidns.DescribeDomainRecordsResponse, error)
	DescribeDomains(request *alidns.DescribeDomainsRequest) (*alidns.DescribeDomainsResponse, error)
}

// AlibabaCloudPrivateZoneAPI is a minimal implementation of Private Zone API that we actually use, used primarily for unit testing.
// See https://help.aliyun.com/document_detail/66234.html for descriptions of all of its methods.
type AlibabaCloudPrivateZoneAPI interface {
	AddZoneRecord(request *pvtz.AddZoneRecordRequest) (*pvtz.AddZoneRecordResponse, error)
	DeleteZoneRecord(request *pvtz.DeleteZoneRecordRequest) (*pvtz.DeleteZoneRecordResponse, error)
	UpdateZoneRecord(request *pvtz.UpdateZoneRecordRequest) (*pvtz.UpdateZoneRecordResponse, error)
	DescribeZoneRecords(request *pvtz.DescribeZoneRecordsRequest) (*pvtz.DescribeZoneRecordsResponse, error)
	DescribeZones(request *pvtz.DescribeZonesRequest) (*pvtz.DescribeZonesResponse, error)
	DescribeZoneInfo(request *pvtz.DescribeZoneInfoRequest) (*pvtz.DescribeZoneInfoResponse, error)
}

// AlibabaCloudProvider implements the DNS provider for Alibaba Cloud.
type AlibabaCloudProvider struct {
	provider.BaseProvider
	domainFilter         *endpoint.DomainFilter
	zoneIDFilter         provider.ZoneIDFilter // Private Zone only
	MaxChangeCount       int
	EvaluateTargetHealth bool
	AssumeRole           string
	vpcID                string // Private Zone only
	dryRun               bool
	dnsClient            AlibabaCloudDNSAPI
	pvtzClient           AlibabaCloudPrivateZoneAPI
	privateZone          bool
	clientLock           sync.RWMutex
	nextExpire           time.Time
}

type alibabaCloudConfig struct {
	RegionID        string    `json:"regionId"        yaml:"regionId"`
	AccessKeyID     string    `json:"accessKeyId"     yaml:"accessKeyId"`
	AccessKeySecret string    `json:"accessKeySecret" yaml:"accessKeySecret"`
	VPCID           string    `json:"vpcId"           yaml:"vpcId"`
	RoleName        string    `json:"-"               yaml:"-"` // For ECS RAM role only
	StsToken        string    `json:"-"               yaml:"-"`
	ExpireTime      time.Time `json:"-"               yaml:"-"`
}

// New creates an Alibaba Cloud provider from the given configuration.
func New(_ context.Context, cfg *externaldns.Config, domainFilter *endpoint.DomainFilter) (provider.Provider, error) {
	return newProvider(cfg.AlibabaCloudConfigFile, domainFilter, provider.NewZoneIDFilter(cfg.ZoneIDFilter), cfg.AlibabaCloudZoneType, cfg.DryRun)
}

// newAlibabaCloudProvider creates a new Alibaba Cloud provider.
//
// Returns the provider or an error if a provider could not be created.
func newProvider(configFile string, domainFilter *endpoint.DomainFilter, zoneIDFileter provider.ZoneIDFilter, zoneType string, dryRun bool) (*AlibabaCloudProvider, error) {
	cfg := alibabaCloudConfig{}
	if configFile != "" {
		contents, err := os.ReadFile(configFile)
		if err != nil {
			return nil, fmt.Errorf("failed to read Alibaba Cloud config file '%s': %w", configFile, err)
		}
		err = yaml.Unmarshal(contents, &cfg)
		if err != nil {
			return nil, fmt.Errorf("failed to parse Alibaba Cloud config file '%s': %w", configFile, err)
		}
	} else {
		var tmpError error
		cfg, tmpError = getCloudConfigFromStsToken()
		if tmpError != nil {
			return nil, fmt.Errorf("failed to getCloudConfigFromStsToken: %w", tmpError)
		}
	}

	// Public DNS service
	var dnsClient AlibabaCloudDNSAPI
	var err error

	if cfg.RoleName == "" {
		dnsClient, err = alidns.NewClientWithAccessKey(
			cfg.RegionID,
			cfg.AccessKeyID,
			cfg.AccessKeySecret,
		)
	} else {
		dnsClient, err = alidns.NewClientWithStsToken(
			cfg.RegionID,
			cfg.AccessKeyID,
			cfg.AccessKeySecret,
			cfg.StsToken,
		)
	}

	if err != nil {
		return nil, fmt.Errorf("failed to create Alibaba Cloud DNS client: %w", err)
	}

	// Private DNS service
	var pvtzClient AlibabaCloudPrivateZoneAPI
	if cfg.RoleName == "" {
		pvtzClient, err = pvtz.NewClientWithAccessKey(
			"cn-hangzhou", // The Private Zone location is fixed
			cfg.AccessKeyID,
			cfg.AccessKeySecret,
		)
	} else {
		pvtzClient, err = pvtz.NewClientWithStsToken(
			cfg.RegionID,
			cfg.AccessKeyID,
			cfg.AccessKeySecret,
			cfg.StsToken,
		)
	}

	if err != nil {
		return nil, err
	}

	provider := &AlibabaCloudProvider{
		domainFilter: domainFilter,
		zoneIDFilter: zoneIDFileter,
		vpcID:        cfg.VPCID,
		dryRun:       dryRun,
		dnsClient:    dnsClient,
		pvtzClient:   pvtzClient,
		privateZone:  zoneType == "private",
	}

	if cfg.RoleName != "" {
		provider.setNextExpire(cfg.ExpireTime)
		go provider.refreshStsToken(1 * time.Second)
	}
	return provider, nil
}

func getCloudConfigFromStsToken() (alibabaCloudConfig, error) {
	cfg := alibabaCloudConfig{}
	// Load config from Metadata Service
	m := metadata.NewMetaData(nil)
	roleName := ""
	var err error
	if roleName, err = m.RoleName(); err != nil {
		return cfg, fmt.Errorf("failed to get role name from Metadata Service: %w", err)
	}
	vpcID, err := m.VpcID()
	if err != nil {
		return cfg, fmt.Errorf("failed to get VPC ID from Metadata Service: %w", err)
	}
	regionID, err := m.Region()
	if err != nil {
		return cfg, fmt.Errorf("failed to get Region ID from Metadata Service: %w", err)
	}
	role, err := m.RamRoleToken(roleName)
	if err != nil {
		return cfg, fmt.Errorf("failed to get STS Token from Metadata Service: %w", err)
	}
	cfg.RegionID = regionID
	cfg.RoleName = roleName
	cfg.VPCID = vpcID
	cfg.AccessKeyID = role.AccessKeyId
	cfg.AccessKeySecret = role.AccessKeySecret
	cfg.StsToken = role.SecurityToken
	cfg.ExpireTime = role.Expiration
	return cfg, nil
}

func (p *AlibabaCloudProvider) getDNSClient() AlibabaCloudDNSAPI {
	p.clientLock.RLock()
	defer p.clientLock.RUnlock()
	return p.dnsClient
}

func (p *AlibabaCloudProvider) getPvtzClient() AlibabaCloudPrivateZoneAPI {
	p.clientLock.RLock()
	defer p.clientLock.RUnlock()
	return p.pvtzClient
}

func (p *AlibabaCloudProvider) setNextExpire(expireTime time.Time) {
	p.clientLock.Lock()
	defer p.clientLock.Unlock()
	p.nextExpire = expireTime
}

func (p *AlibabaCloudProvider) refreshStsToken(sleepTime time.Duration) {
	for {
		time.Sleep(sleepTime)
		now := time.Now()
		utcLocation, err := time.LoadLocation("")
		if err != nil {
			log.Errorf("Get utc time error %v", err)
			continue
		}
		nowTime := now.In(utcLocation)
		p.clientLock.RLock()
		sleepTime = p.nextExpire.Sub(nowTime)
		p.clientLock.RUnlock()
		log.Infof("Distance expiration time %v", sleepTime)
		if sleepTime < 10*time.Minute {
			sleepTime = time.Second * 1
		} else {
			sleepTime = 9 * time.Minute
			log.Info("Next fetch sts sleep interval : ", sleepTime.String())
			continue
		}
		cfg, err := getCloudConfigFromStsToken()
		if err != nil {
			log.Errorf("Failed to getCloudConfigFromStsToken: %v", err)
			continue
		}
		dnsClient, err := alidns.NewClientWithStsToken(
			cfg.RegionID,
			cfg.AccessKeyID,
			cfg.AccessKeySecret,
			cfg.StsToken,
		)
		if err != nil {
			log.Errorf("Failed to new client with sts token %v", err)
			continue
		}
		pvtzClient, err := pvtz.NewClientWithStsToken(
			cfg.RegionID,
			cfg.AccessKeyID,
			cfg.AccessKeySecret,
			cfg.StsToken,
		)
		if err != nil {
			log.Errorf("Failed to new client with sts token %v", err)
			continue
		}
		log.Infof("Refresh client from sts token, next expire time %v", cfg.ExpireTime)
		p.clientLock.Lock()
		p.dnsClient = dnsClient
		p.pvtzClient = pvtzClient
		p.nextExpire = cfg.ExpireTime
		p.clientLock.Unlock()
	}
}

// Records gets the current records.
//
// Returns the current records or an error if the operation failed.
func (p *AlibabaCloudProvider) Records(_ context.Context) ([]*endpoint.Endpoint, error) {
	if p.privateZone {
		return p.privateZoneRecords()
	} else {
		return p.recordsForDNS()
	}
}

// ApplyChanges applies the given changes.
//
// Returns nil if the operation was successful or an error if the operation failed.
func (p *AlibabaCloudProvider) ApplyChanges(_ context.Context, changes *plan.Changes) error {
	if changes == nil || len(changes.Create)+len(changes.Delete)+len(changes.UpdateNew) == 0 {
		// No op
		return nil
	}

	if p.privateZone {
		return p.applyChangesForPrivateZone(changes)
	}
	return p.applyChangesForDNS(changes)
}

func (p *AlibabaCloudProvider) getDNSName(rr, domain string) string {
	if rr == nullHostAlibabaCloud {
		return domain
	}
	return rr + "." + domain
}

// recordsForDNS gets the current records.
//
// Returns the current records or an error if the operation failed.
func (p *AlibabaCloudProvider) recordsForDNS() ([]*endpoint.Endpoint, error) {
	records, err := p.records()
	if err != nil {
		return nil, err
	}
	endpoints := make([]*endpoint.Endpoint, 0, len(records))
	for _, recordList := range p.groupRecords(records) {
		name := p.getDNSName(recordList[0].RR, recordList[0].DomainName)
		recordType := recordList[0].Type
		ttl := recordList[0].TTL

		var targets []string
		for _, record := range recordList {
			target := record.Value
			if recordType == "TXT" {
				target = p.unescapeTXTRecordValue(target)
			}
			targets = append(targets, target)
		}
		ep := endpoint.NewEndpointWithTTL(name, recordType, endpoint.TTL(ttl), targets...)
		endpoints = append(endpoints, ep)
	}
	return endpoints, nil
}

func getNextPageNumber(pageNumber, totalCount int64) int64 {
	if pageNumber*defaultAlibabaCloudPageSize >= totalCount {
		return 0
	}
	return pageNumber + 1
}

func (p *AlibabaCloudProvider) getRecordKey(record alidns.Record) string {
	if record.RR == nullHostAlibabaCloud {
		return record.Type + ":" + record.DomainName
	}
	return record.Type + ":" + record.RR + "." + record.DomainName
}

func (p *AlibabaCloudProvider) getRecordKeyByEndpoint(endpoint *endpoint.Endpoint) string {
	return endpoint.RecordType + ":" + endpoint.DNSName
}

func (p *AlibabaCloudProvider) groupRecords(records []alidns.Record) map[string][]alidns.Record {
	endpointMap := make(map[string][]alidns.Record)
	for _, record := range records {
		key := p.getRecordKey(record)

		recordList := endpointMap[key]
		endpointMap[key] = append(recordList, record)
	}
	return endpointMap
}

func (p *AlibabaCloudProvider) records() ([]alidns.Record, error) {
	log.Infof("Retrieving Alibaba Cloud DNS Domain Records")
	var results []alidns.Record
	hostedZoneDomains, err := p.getDomainList()
	if err != nil {
		return results, fmt.Errorf("getting domain list: %w", err)
	}
	if !p.domainFilter.IsConfigured() {
		for _, zoneDomain := range hostedZoneDomains {
			domainRecords, err := p.getDomainRecords(zoneDomain)
			if err != nil {
				return nil, fmt.Errorf("getDomainRecords %q: %w", zoneDomain, err)
			}
			results = append(results, domainRecords...)
		}
	} else {
		for _, domainName := range p.domainFilter.Filters {
			_, domainName = p.splitDNSName(domainName, hostedZoneDomains)
			tmpResults, err := p.getDomainRecords(domainName)
			if err != nil {
				log.Errorf("getDomainRecords %s error %v", domainName, err)
				continue
			}
			results = append(results, tmpResults...)
		}
	}
	log.Infof("Found %d Alibaba Cloud DNS record(s).", len(results))
	return results, nil
}

func (p *AlibabaCloudProvider) getDomainList() ([]string, error) {
	var domainNames []string
	request := alidns.CreateDescribeDomainsRequest()
	request.PageSize = requests.NewInteger(defaultAlibabaCloudPageSize)
	request.PageNumber = "1"
	request.Scheme = defaultAlibabaCloudRequestScheme
	for {
		resp, err := p.dnsClient.DescribeDomains(request)
		if err != nil {
			log.Errorf("Failed to describe domains for Alibaba Cloud DNS: %v", err)
			return nil, err
		}
		for _, tmpDomain := range resp.Domains.Domain {
			domainNames = append(domainNames, tmpDomain.DomainName)
		}
		nextPage := getNextPageNumber(resp.PageNumber, resp.TotalCount)
		if nextPage == 0 {
			break
		} else {
			request.PageNumber = requests.NewInteger64(nextPage)
		}
	}
	return domainNames, nil
}

func (p *AlibabaCloudProvider) getDomainRecords(domainName string) ([]alidns.Record, error) {
	var results []alidns.Record
	request := alidns.CreateDescribeDomainRecordsRequest()
	request.DomainName = domainName
	request.PageSize = requests.NewInteger(defaultAlibabaCloudPageSize)
	request.PageNumber = "1"
	request.Scheme = defaultAlibabaCloudRequestScheme
	for {
		response, err := p.getDNSClient().DescribeDomainRecords(request)
		if err != nil {
			log.Errorf("Failed to describe domain records for Alibaba Cloud DNS: %v", err)
			return nil, err
		}

		for _, record := range response.DomainRecords.Record {
			domainName := record.RR + "." + record.DomainName
			recordType := record.Type

			if !p.domainFilter.Match(domainName) {
				continue
			}
			if !provider.SupportedRecordType(recordType) {
				continue
			}
			// TODO filter Locked record
			results = append(results, record)
		}
		nextPage := getNextPageNumber(response.PageNumber, response.TotalCount)
		if nextPage == 0 {
			break
		} else {
			request.PageNumber = requests.NewInteger64(nextPage)
		}
	}

	return results, nil
}

func (p *AlibabaCloudProvider) applyChangesForDNS(changes *plan.Changes) error {
	log.Infof("ApplyChanges to Alibaba Cloud DNS: %++v", *changes)

	records, err := p.records()
	if err != nil {
		return err
	}

	recordMap := p.groupRecords(records)

	hostedZoneDomains, err := p.getDomainList()
	if err != nil {
		return fmt.Errorf("getting domain list: %w", err)
	}

	p.createRecords(changes.Create, hostedZoneDomains)
	p.deleteRecords(recordMap, changes.Delete)
	p.updateRecords(recordMap, changes.UpdateNew, hostedZoneDomains)
	return nil
}

func (p *AlibabaCloudProvider) escapeTXTRecordValue(value string) string {
	// For unsupported chars
	return value
}

func (p *AlibabaCloudProvider) unescapeTXTRecordValue(value string) string {
	if strings.HasPrefix(value, "heritage=") {
		return fmt.Sprintf("\"%s\"", strings.ReplaceAll(value, ";", ","))
	}
	return value
}

func (p *AlibabaCloudProvider) createRecord(endpoint *endpoint.Endpoint, target string, hostedZoneDomains []string) error {
	if len(hostedZoneDomains) == 0 {
		log.Errorf("Failed to create %s record named '%s' to '%s' for Alibaba Cloud DNS: zone not found",
			endpoint.RecordType, endpoint.DNSName, target)
		return fmt.Errorf("zone not found")
	}

	rr, domain := p.splitDNSName(endpoint.DNSName, hostedZoneDomains)

	if domain == "" {
		log.Errorf("Failed to create %s record named '%s' to '%s' for Alibaba Cloud DNS: no corresponding DNS zone found for this domain '%s'",
			endpoint.RecordType, endpoint.DNSName, target, endpoint.DNSName)
		return fmt.Errorf("no corresponding DNS zone found for this domain")
	}

	request := alidns.CreateAddDomainRecordRequest()
	request.DomainName = domain
	request.Type = endpoint.RecordType
	request.RR = rr
	request.Scheme = defaultAlibabaCloudRequestScheme

	ttl := int(endpoint.RecordTTL)
	if ttl != 0 {
		request.TTL = requests.NewInteger(ttl)
	}

	if endpoint.RecordType == "TXT" {
		target = p.escapeTXTRecordValue(target)
	}

	request.Value = target

	if p.dryRun {
		log.Infof("Dry run: Create %s record named '%s' to '%s' with ttl %d for Alibaba Cloud DNS", endpoint.RecordType, endpoint.DNSName, target, ttl)
		return nil
	}

	response, err := p.getDNSClient().AddDomainRecord(request)
	if err == nil {
		log.Infof("Create %s record named '%s' to '%s' with ttl %d for Alibaba Cloud DNS: Record ID=%s", endpoint.RecordType, endpoint.DNSName, target, ttl, response.RecordId)
	} else {
		log.Errorf("Failed to create %s record named '%s' to '%s' with ttl %d for Alibaba Cloud DNS: %v", endpoint.RecordType, endpoint.DNSName, target, ttl, err)
	}
	return err
}

func (p *AlibabaCloudProvider) createRecords(endpoints []*endpoint.Endpoint, hostedZoneDomains []string) {
	for _, endpoint := range endpoints {
		for _, target := range endpoint.Targets {
			p.createRecord(endpoint, target, hostedZoneDomains)
		}
	}
}

func (p *AlibabaCloudProvider) deleteRecord(recordID string) error {
	if p.dryRun {
		log.Infof("Dry run: Delete record id '%s' in Alibaba Cloud DNS", recordID)
		return nil
	}

	request := alidns.CreateDeleteDomainRecordRequest()
	request.RecordId = recordID
	request.Scheme = defaultAlibabaCloudRequestScheme
	response, err := p.getDNSClient().DeleteDomainRecord(request)
	if err == nil {
		log.Infof("Delete record id %s in Alibaba Cloud DNS", response.RecordId)
	} else {
		log.Errorf("Failed to delete record '%s' in Alibaba Cloud DNS: %v", response.RecordId, err)
	}
	return err
}

func (p *AlibabaCloudProvider) updateRecord(record alidns.Record, endpoint *endpoint.Endpoint) error {
	request := alidns.CreateUpdateDomainRecordRequest()
	request.RecordId = record.RecordId
	request.RR = record.RR
	request.Type = record.Type
	request.Value = record.Value
	request.Scheme = defaultAlibabaCloudRequestScheme
	ttl := int(endpoint.RecordTTL)
	if ttl != 0 {
		request.TTL = requests.NewInteger(ttl)
	}
	response, err := p.getDNSClient().UpdateDomainRecord(request)
	if err == nil {
		log.Infof("Update record id '%s' in Alibaba Cloud DNS", response.RecordId)
	} else {
		log.Errorf("Failed to update record '%s' in Alibaba Cloud DNS: %v", response.RecordId, err)
	}
	return err
}

func (p *AlibabaCloudProvider) deleteRecords(recordMap map[string][]alidns.Record, endpoints []*endpoint.Endpoint) {
	for _, endpoint := range endpoints {
		key := p.getRecordKeyByEndpoint(endpoint)
		records := recordMap[key]
		found := false
		for _, record := range records {
			value := record.Value
			if record.Type == "TXT" {
				value = p.unescapeTXTRecordValue(value)
			}

			if slices.Contains(endpoint.Targets, value) {
				p.deleteRecord(record.RecordId)
				found = true
			}
		}
		if !found {
			log.Errorf("Failed to find %s record named '%s' to delete for Alibaba Cloud DNS", endpoint.RecordType, endpoint.DNSName)
		}
	}
}

func (p *AlibabaCloudProvider) equals(record alidns.Record, endpoint *endpoint.Endpoint) bool {
	ttl1 := record.TTL
	if ttl1 == defaultTTL {
		ttl1 = 0
	}

	ttl2 := int64(endpoint.RecordTTL)
	if ttl2 == defaultTTL {
		ttl2 = 0
	}

	return ttl1 == ttl2
}

func (p *AlibabaCloudProvider) updateRecords(recordMap map[string][]alidns.Record, endpoints []*endpoint.Endpoint, hostedZoneDomains []string) {
	for _, endpoint := range endpoints {
		key := p.getRecordKeyByEndpoint(endpoint)
		records := recordMap[key]
		for _, record := range records {
			value := record.Value
			if record.Type == "TXT" {
				value = p.unescapeTXTRecordValue(value)
			}
			found := false
			for _, target := range endpoint.Targets {
				// Find matched record to delete
				if value == target {
					found = true
				}
			}
			if found {
				if !p.equals(record, endpoint) {
					// Update record
					p.updateRecord(record, endpoint)
				}
			} else {
				p.deleteRecord(record.RecordId)
			}
		}
		for _, target := range endpoint.Targets {
			if endpoint.RecordType == "TXT" {
				target = p.escapeTXTRecordValue(target)
			}
			found := false
			for _, record := range records {
				// Find matched record to delete
				if record.Value == target {
					found = true
				}
			}
			if !found {
				p.createRecord(endpoint, target, hostedZoneDomains)
			}
		}
	}
}

func (p *AlibabaCloudProvider) splitDNSName(dnsName string, hostedZoneDomains []string) (string, string) {
	name := strings.TrimSuffix(dnsName, ".")

	// sort zones by dot count; make sure subdomains sort earlier
	sort.Slice(hostedZoneDomains, func(i, j int) bool {
		return strings.Count(hostedZoneDomains[i], ".") > strings.Count(hostedZoneDomains[j], ".")
	})

	var rr, domain string

	for _, filter := range hostedZoneDomains {
		if strings.HasSuffix(name, "."+filter) {
			rr = name[0 : len(name)-len(filter)-1]
			domain = filter
			break
		} else if name == filter {
			domain = filter
			rr = ""
		}
	}

	if rr == "" {
		rr = nullHostAlibabaCloud
	}
	return rr, domain
}

func (p *AlibabaCloudProvider) matchVPC(zoneID string) bool {
	request := pvtz.CreateDescribeZoneInfoRequest()
	request.ZoneId = zoneID
	request.Domain = pVTZDoamin
	request.Scheme = defaultAlibabaCloudRequestScheme
	response, err := p.getPvtzClient().DescribeZoneInfo(request)
	if err != nil {
		log.Errorf("Failed to describe zone info %s in Alibaba Cloud DNS: %v", zoneID, err)
		return false
	}
	foundVPC := false
	for _, vpc := range response.BindVpcs.Vpc {
		if vpc.VpcId == p.vpcID {
			foundVPC = true
			break
		}
	}
	return foundVPC
}

func (p *AlibabaCloudProvider) privateZones() ([]pvtz.Zone, error) {
	var zones []pvtz.Zone

	request := pvtz.CreateDescribeZonesRequest()
	request.PageSize = requests.NewInteger(defaultAlibabaCloudPageSize)
	request.PageNumber = "1"
	request.Domain = pVTZDoamin
	request.Scheme = defaultAlibabaCloudRequestScheme
	for {
		response, err := p.getPvtzClient().DescribeZones(request)
		if err != nil {
			log.Errorf("Failed to describe zones in Alibaba Cloud DNS: %v", err)
			return nil, err
		}
		for _, zone := range response.Zones.Zone {
			log.Infof("PrivateZones zone: %++v", zone)

			if !p.zoneIDFilter.Match(zone.ZoneId) {
				continue
			}
			if !p.domainFilter.Match(zone.ZoneName) {
				continue
			}
			if !p.matchVPC(zone.ZoneId) {
				continue
			}
			zones = append(zones, zone)
		}
		nextPage := getNextPageNumber(int64(response.PageNumber), int64(response.TotalItems))
		if nextPage == 0 {
			break
		} else {
			request.PageNumber = requests.NewInteger64(nextPage)
		}
	}
	return zones, nil
}

type alibabaPrivateZone struct {
	pvtz.Zone
	records []pvtz.Record
}

func (p *AlibabaCloudProvider) getPrivateZones() (map[string]*alibabaPrivateZone, error) {
	log.Infof("Retrieving Alibaba Cloud Private Zone records")

	result := make(map[string]*alibabaPrivateZone)
	recordsCount := 0

	zones, err := p.privateZones()
	if err != nil {
		return nil, err
	}

	for _, zone := range zones {
		request := pvtz.CreateDescribeZoneRecordsRequest()
		request.ZoneId = zone.ZoneId
		request.PageSize = requests.NewInteger(defaultAlibabaCloudPageSize)
		request.PageNumber = "1"
		request.Domain = pVTZDoamin
		request.Scheme = defaultAlibabaCloudRequestScheme
		var records []pvtz.Record

		for {
			response, err := p.getPvtzClient().DescribeZoneRecords(request)
			if err != nil {
				log.Errorf("Failed to describe zone record '%s' in Alibaba Cloud DNS: %v", zone.ZoneId, err)
				return nil, err
			}

			for _, record := range response.Records.Record {
				recordType := record.Type

				if !provider.SupportedRecordType(recordType) {
					continue
				}

				// TODO filter Locked
				records = append(records, record)
			}
			nextPage := getNextPageNumber(int64(response.PageNumber), int64(response.TotalItems))
			if nextPage == 0 {
				break
			} else {
				request.PageNumber = requests.NewInteger64(nextPage)
			}
		}

		privateZone := alibabaPrivateZone{
			Zone:    zone,
			records: records,
		}
		recordsCount += len(records)
		result[zone.ZoneName] = &privateZone
	}
	log.Infof("Found %d Alibaba Cloud Private Zone record(s).", recordsCount)
	return result, nil
}

func (p *AlibabaCloudProvider) groupPrivateZoneRecords(zone *alibabaPrivateZone) map[string][]pvtz.Record {
	endpointMap := make(map[string][]pvtz.Record)

	for _, record := range zone.records {
		key := record.Type + ":" + record.Rr
		recordList := endpointMap[key]
		endpointMap[key] = append(recordList, record)
	}

	return endpointMap
}

// recordsForPrivateZone gets the current records.
//
// Returns the current records or an error if the operation failed.
func (p *AlibabaCloudProvider) privateZoneRecords() ([]*endpoint.Endpoint, error) {
	zones, err := p.getPrivateZones()
	if err != nil {
		return nil, err
	}

	endpoints := make([]*endpoint.Endpoint, 0)

	for _, zone := range zones {
		recordMap := p.groupPrivateZoneRecords(zone)
		for _, recordList := range recordMap {
			name := p.getDNSName(recordList[0].Rr, zone.ZoneName)
			recordType := recordList[0].Type
			ttl := recordList[0].Ttl
			if ttl == defaultAlibabaCloudPrivateZoneRecordTTL {
				ttl = 0
			}
			var targets []string
			for _, record := range recordList {
				target := record.Value
				if recordType == "TXT" {
					target = p.unescapeTXTRecordValue(target)
				}
				targets = append(targets, target)
			}
			ep := endpoint.NewEndpointWithTTL(name, recordType, endpoint.TTL(ttl), targets...)
			endpoints = append(endpoints, ep)
		}
	}
	return endpoints, nil
}

func (p *AlibabaCloudProvider) createPrivateZoneRecord(zones map[string]*alibabaPrivateZone, endpoint *endpoint.Endpoint, target string) error {
	rr, domain := p.splitDNSName(endpoint.DNSName, keys(zones))
	zone := zones[domain]
	if zone == nil {
		err := fmt.Errorf("failed to find private zone '%s'", domain)
		log.Errorf("Failed to create %s record named '%s' to '%s' for Alibaba Cloud Private Zone: %v", endpoint.RecordType, endpoint.DNSName, target, err)
		return err
	}

	request := pvtz.CreateAddZoneRecordRequest()
	request.ZoneId = zone.ZoneId
	request.Type = endpoint.RecordType
	request.Rr = rr
	request.Domain = pVTZDoamin
	request.Scheme = defaultAlibabaCloudRequestScheme

	ttl := int(endpoint.RecordTTL)
	if ttl != 0 {
		request.Ttl = requests.NewInteger(ttl)
	}

	if endpoint.RecordType == "TXT" {
		target = p.escapeTXTRecordValue(target)
	}

	request.Value = target

	if p.dryRun {
		log.Infof("Dry run: Create %s record named '%s' to '%s' with ttl %d for Alibaba Cloud Private Zone", endpoint.RecordType, endpoint.DNSName, target, ttl)
		return nil
	}

	response, err := p.getPvtzClient().AddZoneRecord(request)
	if err == nil {
		log.Infof("Create %s record named '%s' to '%s' with ttl %d for Alibaba Cloud Private Zone: Record ID=%d", endpoint.RecordType, endpoint.DNSName, target, ttl, response.RecordId)
	} else {
		log.Errorf("Failed to create %s record named '%s' to '%s' with ttl %d for Alibaba Cloud Private Zone: %v", endpoint.RecordType, endpoint.DNSName, target, ttl, err)
	}
	return err
}

func (p *AlibabaCloudProvider) createPrivateZoneRecords(zones map[string]*alibabaPrivateZone, endpoints []*endpoint.Endpoint) {
	for _, endpoint := range endpoints {
		for _, target := range endpoint.Targets {
			_ = p.createPrivateZoneRecord(zones, endpoint, target)
		}
	}
}

func (p *AlibabaCloudProvider) deletePrivateZoneRecord(recordID int64) error {
	if p.dryRun {
		log.Infof("Dry run: Delete record id '%d' in Alibaba Cloud Private Zone", recordID)
	}

	request := pvtz.CreateDeleteZoneRecordRequest()
	request.RecordId = requests.NewInteger64(recordID)
	request.Domain = pVTZDoamin
	request.Scheme = defaultAlibabaCloudRequestScheme

	response, err := p.getPvtzClient().DeleteZoneRecord(request)
	if err == nil {
		log.Infof("Delete record id '%d' in Alibaba Cloud Private Zone", response.RecordId)
	} else {
		log.Errorf("Failed to delete record %d in Alibaba Cloud Private Zone: %v", response.RecordId, err)
	}
	return err
}

func (p *AlibabaCloudProvider) deletePrivateZoneRecords(zones map[string]*alibabaPrivateZone, endpoints []*endpoint.Endpoint) {
	zoneNames := keys(zones)
	for _, endpoint := range endpoints {
		rr, domain := p.splitDNSName(endpoint.DNSName, zoneNames)

		zone := zones[domain]
		if zone == nil {
			log.Errorf("Failed to delete %s record named '%s' for Alibaba Cloud Private Zone: failed to find private zone '%s'", endpoint.RecordType, endpoint.DNSName, domain)
			continue
		}
		found := false
		for _, record := range zone.records {
			if rr == record.Rr && endpoint.RecordType == record.Type {
				value := record.Value
				if record.Type == "TXT" {
					value = p.unescapeTXTRecordValue(value)
				}
				if slices.Contains(endpoint.Targets, value) {
					p.deletePrivateZoneRecord(record.RecordId)
					found = true
				}
			}
		}
		if !found {
			log.Errorf("Failed to find %s record named '%s' to delete for Alibaba Cloud Private Zone", endpoint.RecordType, endpoint.DNSName)
		}
	}
}

// ApplyChanges applies the given changes.
//
// Returns nil if the operation was successful or an error if the operation failed.
func (p *AlibabaCloudProvider) applyChangesForPrivateZone(changes *plan.Changes) error {
	log.Infof("ApplyChanges to Alibaba Cloud Private Zone: %++v", *changes)

	zones, err := p.getPrivateZones()
	if err != nil {
		return err
	}

	for zoneName, zone := range zones {
		log.Debugf("%s: %++v", zoneName, zone)
	}

	p.createPrivateZoneRecords(zones, changes.Create)
	p.deletePrivateZoneRecords(zones, changes.Delete)
	p.updatePrivateZoneRecords(zones, changes.UpdateNew)
	return nil
}

func (p *AlibabaCloudProvider) updatePrivateZoneRecord(record pvtz.Record, endpoint *endpoint.Endpoint) error {
	request := pvtz.CreateUpdateZoneRecordRequest()
	request.RecordId = requests.NewInteger64(record.RecordId)
	request.Rr = record.Rr
	request.Type = record.Type
	request.Value = record.Value
	request.Domain = pVTZDoamin
	request.Scheme = defaultAlibabaCloudRequestScheme
	ttl := int(endpoint.RecordTTL)
	if ttl != 0 {
		request.Ttl = requests.NewInteger(ttl)
	}
	response, err := p.getPvtzClient().UpdateZoneRecord(request)
	if err == nil {
		log.Infof("Update record id '%d' in Alibaba Cloud Private Zone", response.RecordId)
	} else {
		log.Errorf("Failed to update record '%d' in Alibaba Cloud Private Zone: %v", response.RecordId, err)
	}
	return err
}

func (p *AlibabaCloudProvider) equalsPrivateZone(record pvtz.Record, endpoint *endpoint.Endpoint) bool {
	ttl1 := record.Ttl
	if ttl1 == defaultAlibabaCloudPrivateZoneRecordTTL {
		ttl1 = 0
	}

	ttl2 := int(endpoint.RecordTTL)
	if ttl2 == defaultAlibabaCloudPrivateZoneRecordTTL {
		ttl2 = 0
	}

	return ttl1 == ttl2
}

func (p *AlibabaCloudProvider) updatePrivateZoneRecords(zones map[string]*alibabaPrivateZone, endpoints []*endpoint.Endpoint) {
	zoneNames := keys(zones)
	for _, endpoint := range endpoints {
		rr, domain := p.splitDNSName(endpoint.DNSName, zoneNames)
		zone := zones[domain]
		if zone == nil {
			log.Errorf("Failed to update %s record named '%s' for Alibaba Cloud Private Zone: failed to find private zone '%s'", endpoint.RecordType, endpoint.DNSName, domain)
			continue
		}

		for _, record := range zone.records {
			if record.Rr != rr || record.Type != endpoint.RecordType {
				continue
			}
			value := record.Value
			if record.Type == "TXT" {
				value = p.unescapeTXTRecordValue(value)
			}
			found := slices.Contains(endpoint.Targets, value)
			if found {
				if !p.equalsPrivateZone(record, endpoint) {
					// Update record
					p.updatePrivateZoneRecord(record, endpoint)
				}
			} else {
				p.deletePrivateZoneRecord(record.RecordId)
			}
		}
		for _, target := range endpoint.Targets {
			if endpoint.RecordType == "TXT" {
				target = p.escapeTXTRecordValue(target)
			}
			found := false
			for _, record := range zone.records {
				if record.Rr != rr || record.Type != endpoint.RecordType {
					continue
				}
				// Find matched record to delete
				if record.Value == target {
					found = true
					break
				}
			}
			if !found {
				p.createPrivateZoneRecord(zones, endpoint, target)
			}
		}
	}
}

func keys[T any](value map[string]T) []string {
	var results []string
	for k := range value {
		results = append(results, k)
	}
	return results
}
