package cscglobal

import (
	"fmt"
	"strings"

	"github.com/StackExchange/dnscontrol/v3/models"
	"github.com/StackExchange/dnscontrol/v3/pkg/diff"
)

// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
func (client *providerClient) GetZoneRecords(domain string) (models.Records, error) {
	records, err := client.getZoneRecordsAll(domain)
	if err != nil {
		return nil, err
	}

	// Convert them to DNScontrol's native format:

	existingRecords := []*models.RecordConfig{}

	// Option 1: One long list.  If your provider returns one long list,
	// convert each one to RecordType like this:
	// for _, rr := range records {
	// 	existingRecords = append(existingRecords, nativeToRecord(rr, domain))
	//}

	// Option 2: Grouped records. Sometimes the provider returns one item per
	// label. Each item contains a list of all the records at that label.
	// You'll need to split them out into one RecordConfig for each record.  An
	// example of this is the ROUTE53 provider.
	// for _, rg := range records {
	// 	for _, rr := range rg {
	// 		existingRecords = append(existingRecords, nativeToRecords(rg, rr, domain)...)
	// 	}
	// }

	// Option 3: Something else.  In this case, we get a big massive structure
	// which needs to be broken up.  Still, we're generating a list of
	// RecordConfig structures.
	defaultTTL := records.Soa.TTL
	for _, rr := range records.A {
		existingRecords = append(existingRecords, nativeToRecordA(rr, domain, defaultTTL))
	}
	for _, rr := range records.Cname {
		existingRecords = append(existingRecords, nativeToRecordCNAME(rr, domain, defaultTTL))
	}
	for _, rr := range records.Aaaa {
		existingRecords = append(existingRecords, nativeToRecordAAAA(rr, domain, defaultTTL))
	}
	for _, rr := range records.Txt {
		existingRecords = append(existingRecords, nativeToRecordTXT(rr, domain, defaultTTL))
	}
	for _, rr := range records.Mx {
		existingRecords = append(existingRecords, nativeToRecordMX(rr, domain, defaultTTL))
	}
	for _, rr := range records.Ns {
		existingRecords = append(existingRecords, nativeToRecordNS(rr, domain, defaultTTL))
	}
	for _, rr := range records.Srv {
		existingRecords = append(existingRecords, nativeToRecordSRV(rr, domain, defaultTTL))
	}
	for _, rr := range records.Caa {
		existingRecords = append(existingRecords, nativeToRecordCAA(rr, domain, defaultTTL))
	}

	return existingRecords, nil
}

func (client *providerClient) GetNameservers(domain string) ([]*models.Nameserver, error) {
	nss, err := client.getNameservers(domain)
	if err != nil {
		return nil, err
	}
	return models.ToNameservers(nss)
}

// GetDomainCorrections get the current and existing records,
// post-process them, and generate corrections.
// NB(tlim): This function should be exactly the same in all DNS providers.  Once
// all providers do this, we can eliminate it and use a Go interface instead.
func (client *providerClient) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
	existing, err := client.GetZoneRecords(dc.Name)
	if err != nil {
		return nil, err
	}
	models.PostProcessRecords(existing)
	//txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records

	clean := PrepFoundRecords(existing)
	PrepDesiredRecords(dc)
	return client.GenerateDomainCorrections(dc, clean)
}

// PrepFoundRecords munges any records to make them compatible with
// this provider. Usually this is a no-op.
func PrepFoundRecords(recs models.Records) models.Records {
	// If there are records that need to be modified, removed, etc. we
	// do it here.  Usually this is a no-op.
	return recs
}

// PrepDesiredRecords munges any records to best suit this provider.
func PrepDesiredRecords(dc *models.DomainConfig) {
	// Sort through the dc.Records, eliminate any that can't be
	// supported; modify any that need adjustments to work with the
	// provider.  We try to do minimal changes otherwise it gets
	// confusing.

	dc.Punycode()
}

// GetDomainCorrections gets existing records, diffs them against existing, and returns corrections.
func (client *providerClient) GenerateDomainCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, error) {

	// Read foundRecords:
	foundRecords, err := client.GetZoneRecords(dc.Name)
	if err != nil {
		return nil, fmt.Errorf("c.GetDNSZoneRecords(%v) failed: %v", dc.Name, err)
	}

	// Normalize
	models.PostProcessRecords(foundRecords)
	//txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records

	differ := diff.New(dc)
	_, creates, dels, modifications, err := differ.IncrementalDiff(foundRecords)
	if err != nil {
		return nil, err
	}

	// How to generate corrections?

	// (1) Most providers take individual deletes, creates, and
	// modifications:

	// // Generate changes.
	//	corrections := []*models.Correction{}
	//	for _, del := range dels {
	//		corrections = append(corrections, client.deleteRec(client.dnsserver, dc.Name, del))
	//	}
	//	for _, cre := range creates {
	//		corrections = append(corrections, client.createRec(client.dnsserver, dc.Name, cre)...)
	//	}
	//	for _, m := range modifications {
	//		corrections = append(corrections, client.modifyRec(client.dnsserver, dc.Name, m))
	//	}
	//	return corrections, nil

	// (2) Some providers upload the entire zone every time.  Look at
	// GetDomainCorrections for BIND and NAMECHEAP for inspiration.

	// (3) Others do something entirely different. Like CSCGlobal:

	// CSCGlobal has a unique API.  A list of edits is sent in one API
	// call. Edits aren't permitted if an existing edit is being
	// processed. Therefore, before we do an edit we block until the
	// previous edit is done executing.

	var edits []zoneResourceRecordEdit
	var descriptions []string
	for _, del := range dels {
		edits = append(edits, makePurge(dc.Name, del))
		descriptions = append(descriptions, del.String())
	}
	for _, cre := range creates {
		edits = append(edits, makeAdd(dc.Name, cre))
		descriptions = append(descriptions, cre.String())
	}
	for _, m := range modifications {
		edits = append(edits, makeEdit(dc.Name, m))
		descriptions = append(descriptions, m.String())
	}
	corrections := []*models.Correction{}
	if len(edits) > 0 {
		c := &models.Correction{
			Msg: "\t" + strings.Join(descriptions, "\n\t"),
			F: func() error {
				// CSCGlobal's API only permits one pending update at a time.
				// Therefore we block until any outstanding updates are done.
				// We also clear out any failures, since (and I can't believe
				// I'm writing this) any time something fails, the failure has
				// to be cleared out with an additional API call.

				err := client.clearRequests(dc.Name)
				if err != nil {
					return err
				}
				return client.sendZoneEditRequest(dc.Name, edits)
			},
		}
		corrections = append(corrections, c)
	}
	return corrections, nil
}

func makePurge(domainname string, cor diff.Correlation) zoneResourceRecordEdit {
	var existingTarget string

	switch cor.Existing.Type {
	case "TXT":
		existingTarget = strings.Join(cor.Existing.TxtStrings, "")
	default:
		existingTarget = cor.Existing.GetTargetField()
	}

	zer := zoneResourceRecordEdit{
		Action:       "PURGE",
		RecordType:   cor.Existing.Type,
		CurrentKey:   cor.Existing.Name,
		CurrentValue: existingTarget,
	}

	if cor.Existing.Type == "CAA" {
		var tagValue = cor.Existing.CaaTag
		//printer.Printf("DEBUG: CAA TAG = %q\n", tagValue)
		zer.CurrentTag = &tagValue
	}

	return zer
}

func makeAdd(domainname string, cre diff.Correlation) zoneResourceRecordEdit {
	rec := cre.Desired

	var recTarget string
	switch rec.Type {
	case "TXT":
		recTarget = strings.Join(rec.TxtStrings, "")
	default:
		recTarget = rec.GetTargetField()
	}

	zer := zoneResourceRecordEdit{
		Action:     "ADD",
		RecordType: rec.Type,
		NewKey:     rec.Name,
		NewValue:   recTarget,
		NewTTL:     rec.TTL,
	}

	switch rec.Type {
	case "CAA":
		var tagValue = rec.CaaTag
		var flagValue = rec.CaaFlag
		zer.NewTag = &tagValue
		zer.NewFlag = &flagValue
	case "MX":
		zer.NewPriority = rec.MxPreference
	case "SRV":
		zer.NewPriority = rec.SrvPriority
		zer.NewWeight = rec.SrvWeight
		zer.NewPort = rec.SrvPort
	case "TXT":
		zer.NewValue = strings.Join(rec.TxtStrings, "")
	default: // "A", "CNAME", "NS"
		// Nothing to do.
	}

	return zer
}

func makeEdit(domainname string, m diff.Correlation) zoneResourceRecordEdit {
	old, rec := m.Existing, m.Desired
	// TODO: Assert that old.Type == rec.Type
	// TODO: Assert that old.Name == rec.Name

	var oldTarget, recTarget string
	switch old.Type {
	case "TXT":
		oldTarget = strings.Join(old.TxtStrings, "")
		recTarget = strings.Join(rec.TxtStrings, "")
	default:
		oldTarget = old.GetTargetField()
		recTarget = rec.GetTargetField()
	}

	zer := zoneResourceRecordEdit{
		Action:       "EDIT",
		RecordType:   old.Type,
		CurrentKey:   old.Name,
		CurrentValue: oldTarget,
	}
	if oldTarget != recTarget {
		zer.NewValue = recTarget
	}
	if old.TTL != rec.TTL {
		zer.NewTTL = rec.TTL
	}

	switch old.Type {
	case "CAA":
		var tagValue = old.CaaTag
		zer.CurrentTag = &tagValue
		if old.CaaTag != rec.CaaTag || old.CaaFlag != rec.CaaFlag || old.TTL != rec.TTL {
			// If anything changed, we need to update both tag and flag.
			zer.NewTag = &(rec.CaaTag)
			zer.NewFlag = &(rec.CaaFlag)
		}
	case "MX":
		if old.MxPreference != rec.MxPreference {
			zer.NewPriority = rec.MxPreference
		}
	case "SRV":
		zer.NewWeight = rec.SrvWeight
		zer.NewPort = rec.SrvPort
		zer.NewPriority = rec.SrvPriority
	default: // "A", "CNAME", "NS", "TXT"
		// Nothing to do.
	}

	return zer
}
