func (p *LinodeProvider) ApplyChanges(changes *plan.Changes) error {
	recordsByZoneID := make(map[string][]*linodego.DomainRecord)

	zones, err := p.fetchZones()

	if err != nil {
		return err
	}

	zonesByID := make(map[string]*linodego.Domain)

	zoneNameIDMapper := zoneIDName{}

	for _, z := range zones {
		zoneNameIDMapper.Add(strconv.Itoa(z.ID), z.Domain)
		zonesByID[strconv.Itoa(z.ID)] = z
	}

	// Fetch records for each zone
	for _, zone := range zones {
		records, err := p.fetchRecords(zone.ID)

		if err != nil {
			return err
		}

		recordsByZoneID[strconv.Itoa(zone.ID)] = append(recordsByZoneID[strconv.Itoa(zone.ID)], records...)
	}

	createsByZone := endpointsByZone(zoneNameIDMapper, changes.Create)
	updatesByZone := endpointsByZone(zoneNameIDMapper, changes.UpdateNew)
	deletesByZone := endpointsByZone(zoneNameIDMapper, changes.Delete)

	var linodeCreates []*LinodeChangeCreate
	var linodeUpdates []*LinodeChangeUpdate
	var linodeDeletes []*LinodeChangeDelete

	// Generate Creates
	for zoneID, creates := range createsByZone {
		zone := zonesByID[zoneID]

		if len(creates) == 0 {
			log.WithFields(log.Fields{
				"zoneID":   zoneID,
				"zoneName": zone.Domain,
			}).Debug("Skipping Zone, no creates found.")
			continue
		}

		records := recordsByZoneID[zoneID]

		for _, ep := range creates {
			matchedRecords := getRecordID(records, zone, ep)

			if len(matchedRecords) != 0 {
				log.WithFields(log.Fields{
					"zoneID":     zoneID,
					"zoneName":   zone.Domain,
					"dnsName":    ep.DNSName,
					"recordType": ep.RecordType,
				}).Warn("Records found which should not exist")
			}

			recordType, err := convertRecordType(ep.RecordType)

			if err != nil {
				return err
			}

			for _, target := range ep.Targets {
				linodeCreates = append(linodeCreates, &LinodeChangeCreate{
					Domain: zone,
					Options: linodego.DomainRecordCreateOptions{
						Target:   target,
						Name:     getStrippedRecordName(zone, ep),
						Type:     recordType,
						Weight:   getWeight(),
						Port:     getPort(),
						Priority: getPriority(),
						TTLSec:   int(ep.RecordTTL),
					},
				})
			}
		}
	}

	// Generate Updates
	for zoneID, updates := range updatesByZone {
		zone := zonesByID[zoneID]

		if len(updates) == 0 {
			log.WithFields(log.Fields{
				"zoneID":   zoneID,
				"zoneName": zone.Domain,
			}).Debug("Skipping Zone, no updates found.")
			continue
		}

		records := recordsByZoneID[zoneID]

		for _, ep := range updates {
			matchedRecords := getRecordID(records, zone, ep)

			if len(matchedRecords) == 0 {
				log.WithFields(log.Fields{
					"zoneID":     zoneID,
					"dnsName":    ep.DNSName,
					"zoneName":   zone.Domain,
					"recordType": ep.RecordType,
				}).Warn("Update Records not found.")
			}

			recordType, err := convertRecordType(ep.RecordType)

			if err != nil {
				return err
			}

			matchedRecordsByTarget := make(map[string]*linodego.DomainRecord)

			for _, record := range matchedRecords {
				matchedRecordsByTarget[record.Target] = record
			}

			for _, target := range ep.Targets {
				if record, ok := matchedRecordsByTarget[target]; ok {
					log.WithFields(log.Fields{
						"zoneID":     zoneID,
						"dnsName":    ep.DNSName,
						"zoneName":   zone.Domain,
						"recordType": ep.RecordType,
						"target":     target,
					}).Warn("Updating Existing Target")

					linodeUpdates = append(linodeUpdates, &LinodeChangeUpdate{
						Domain:       zone,
						DomainRecord: record,
						Options: linodego.DomainRecordUpdateOptions{
							Target:   target,
							Name:     getStrippedRecordName(zone, ep),
							Type:     recordType,
							Weight:   getWeight(),
							Port:     getPort(),
							Priority: getPriority(),
							TTLSec:   int(ep.RecordTTL),
						},
					})

					delete(matchedRecordsByTarget, target)
				} else {
					// Record did not previously exist, create new 'target'
					log.WithFields(log.Fields{
						"zoneID":     zoneID,
						"dnsName":    ep.DNSName,
						"zoneName":   zone.Domain,
						"recordType": ep.RecordType,
						"target":     target,
					}).Warn("Creating New Target")

					linodeCreates = append(linodeCreates, &LinodeChangeCreate{
						Domain: zone,
						Options: linodego.DomainRecordCreateOptions{
							Target:   target,
							Name:     getStrippedRecordName(zone, ep),
							Type:     recordType,
							Weight:   getWeight(),
							Port:     getPort(),
							Priority: getPriority(),
							TTLSec:   int(ep.RecordTTL),
						},
					})
				}
			}

			// Any remaining records have been removed, delete them
			for _, record := range matchedRecordsByTarget {
				log.WithFields(log.Fields{
					"zoneID":     zoneID,
					"dnsName":    ep.DNSName,
					"zoneName":   zone.Domain,
					"recordType": ep.RecordType,
					"target":     record.Target,
				}).Warn("Deleting Target")

				linodeDeletes = append(linodeDeletes, &LinodeChangeDelete{
					Domain:       zone,
					DomainRecord: record,
				})
			}

		}
	}

	// Generate Deletes
	for zoneID, deletes := range deletesByZone {
		zone := zonesByID[zoneID]

		if len(deletes) == 0 {
			log.WithFields(log.Fields{
				"zoneID":   zoneID,
				"zoneName": zone.Domain,
			}).Debug("Skipping Zone, no deletes found.")
			continue
		}

		records := recordsByZoneID[zoneID]

		for _, ep := range deletes {
			matchedRecords := getRecordID(records, zone, ep)

			if len(matchedRecords) == 0 {
				log.WithFields(log.Fields{
					"zoneID":     zoneID,
					"dnsName":    ep.DNSName,
					"zoneName":   zone.Domain,
					"recordType": ep.RecordType,
				}).Warn("Records to Delete not found.")
			}

			for _, record := range matchedRecords {
				linodeDeletes = append(linodeDeletes, &LinodeChangeDelete{
					Domain:       zone,
					DomainRecord: record,
				})
			}
		}
	}

	return p.submitChanges(LinodeChanges{
		Creates: linodeCreates,
		Deletes: linodeDeletes,
		Updates: linodeUpdates,
	})
}
func (p *LinodeProvider) ApplyChanges(changes *plan.Changes) error {
	recordsByZoneID := make(map[string][]*linodego.DomainRecord)

	zones, err := p.fetchZones()

	if err != nil {
		return err
	}

	zonesByID := make(map[string]*linodego.Domain)

	zoneNameIDMapper := zoneIDName{}

	for _, z := range zones {
		zoneNameIDMapper.Add(strconv.Itoa(z.ID), z.Domain)
		zonesByID[strconv.Itoa(z.ID)] = z
	}

	// Fetch records for each zone
	for _, zone := range zones {
		records, err := p.fetchRecords(zone.ID)

		if err != nil {
			return err
		}

		recordsByZoneID[strconv.Itoa(zone.ID)] = append(recordsByZoneID[strconv.Itoa(zone.ID)], records...)
	}

	createsByZone := endpointsByZone(zoneNameIDMapper, changes.Create)
	updatesByZone := endpointsByZone(zoneNameIDMapper, changes.UpdateNew)
	deletesByZone := endpointsByZone(zoneNameIDMapper, changes.Delete)

	var linodeCreates []*LinodeChangeCreate
	var linodeUpdates []*LinodeChangeUpdate
	var linodeDeletes []*LinodeChangeDelete

	// Generate Creates
	for zoneID, creates := range createsByZone {
		zone := zonesByID[zoneID]

		if len(creates) == 0 {
			log.WithFields(log.Fields{
				"zoneID":   zoneID,
				"zoneName": zone.Domain,
			}).Debug("Skipping Zone, no creates found.")
			continue
		}

		records := recordsByZoneID[zoneID]

		for _, ep := range creates {
			matchedRecords := getRecordID(records, zone, ep)

			if len(matchedRecords) != 0 {
				log.WithFields(log.Fields{
					"zoneID":     zoneID,
					"zoneName":   zone.Domain,
					"dnsName":    ep.DNSName,
					"recordType": ep.RecordType,
				}).Warn("Records found which should not exist")
			}

			recordType, err := convertRecordType(ep.RecordType)

			if err != nil {
				return err
			}

			for _, target := range ep.Targets {
				linodeCreates = append(linodeCreates, &LinodeChangeCreate{
					Domain: zone,
					Options: linodego.DomainRecordCreateOptions{
						Target:   target,
						Name:     getStrippedRecordName(zone, ep),
						Type:     recordType,
						Weight:   getWeight(),
						Port:     getPort(),
						Priority: getPriority(),
						TTLSec:   int(ep.RecordTTL),
					},
				})
			}
		}
	}

	// Generate Updates
	for zoneID, updates := range updatesByZone {
		zone := zonesByID[zoneID]

		if len(updates) == 0 {
			log.WithFields(log.Fields{
				"zoneID":   zoneID,
				"zoneName": zone.Domain,
			}).Debug("Skipping Zone, no updates found.")
			continue
		}

		records := recordsByZoneID[zoneID]

		for _, ep := range updates {
			matchedRecords := getRecordID(records, zone, ep)

			if len(matchedRecords) == 0 {
				log.WithFields(log.Fields{
					"zoneID":     zoneID,
					"dnsName":    ep.DNSName,
					"zoneName":   zone.Domain,
					"recordType": ep.RecordType,
				}).Warn("Update Records not found.")
			}

			recordType, err := convertRecordType(ep.RecordType)

			if err != nil {
				return err
			}

			matchedRecordsByTarget := make(map[string]*linodego.DomainRecord)

			for _, record := range matchedRecords {
				matchedRecordsByTarget[record.Target] = record
			}

			for _, target := range ep.Targets {
				if record, ok := matchedRecordsByTarget[target]; ok {
					log.WithFields(log.Fields{
						"zoneID":     zoneID,
						"dnsName":    ep.DNSName,
						"zoneName":   zone.Domain,
						"recordType": ep.RecordType,
						"target":     target,
					}).Warn("Updating Existing Target")

					linodeUpdates = append(linodeUpdates, &LinodeChangeUpdate{
						Domain:       zone,
						DomainRecord: record,
						Options: linodego.DomainRecordUpdateOptions{
							Target:   target,
							Name:     getStrippedRecordName(zone, ep),
							Type:     recordType,
							Weight:   getWeight(),
							Port:     getPort(),
							Priority: getPriority(),
							TTLSec:   int(ep.RecordTTL),
						},
					})

					delete(matchedRecordsByTarget, target)
				} else {
					// Record did not previously exist, create new 'target'
					log.WithFields(log.Fields{
						"zoneID":     zoneID,
						"dnsName":    ep.DNSName,
						"zoneName":   zone.Domain,
						"recordType": ep.RecordType,
						"target":     target,
					}).Warn("Creating New Target")

					linodeCreates = append(linodeCreates, &LinodeChangeCreate{
						Domain: zone,
						Options: linodego.DomainRecordCreateOptions{
							Target:   target,
							Name:     getStrippedRecordName(zone, ep),
							Type:     recordType,
							Weight:   getWeight(),
							Port:     getPort(),
							Priority: getPriority(),
							TTLSec:   int(ep.RecordTTL),
						},
					})
				}
			}

			// Any remaining records have been removed, delete them
			for _, record := range matchedRecordsByTarget {
				log.WithFields(log.Fields{
					"zoneID":     zoneID,
					"dnsName":    ep.DNSName,
					"zoneName":   zone.Domain,
					"recordType": ep.RecordType,
					"target":     record.Target,
				}).Warn("Deleting Target")

				linodeDeletes = append(linodeDeletes, &LinodeChangeDelete{
					Domain:       zone,
					DomainRecord: record,
				})
			}

		}
	}

	// Generate Deletes
	for zoneID, deletes := range deletesByZone {
		zone := zonesByID[zoneID]

		if len(deletes) == 0 {
			log.WithFields(log.Fields{
				"zoneID":   zoneID,
				"zoneName": zone.Domain,
			}).Debug("Skipping Zone, no deletes found.")
			continue
		}

		records := recordsByZoneID[zoneID]

		for _, ep := range deletes {
			matchedRecords := getRecordID(records, zone, ep)

			if len(matchedRecords) == 0 {
				log.WithFields(log.Fields{
					"zoneID":     zoneID,
					"dnsName":    ep.DNSName,
					"zoneName":   zone.Domain,
					"recordType": ep.RecordType,
				}).Warn("Records to Delete not found.")
			}

			for _, record := range matchedRecords {
				linodeDeletes = append(linodeDeletes, &LinodeChangeDelete{
					Domain:       zone,
					DomainRecord: record,
				})
			}
		}
	}

	return p.submitChanges(LinodeChanges{
		Creates: linodeCreates,
		Deletes: linodeDeletes,
		Updates: linodeUpdates,
	})
}
// ApplyChanges applies a given set of changes in a given zone.