package luadns

import (
	"encoding/json"
	"fmt"

	"github.com/StackExchange/dnscontrol/v4/models"
	"github.com/StackExchange/dnscontrol/v4/pkg/diff"
	"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
	"github.com/StackExchange/dnscontrol/v4/providers"
)

/*

LuaDNS API DNS provider:

Info required in `creds.json`:
   - email
   - apikey
*/

var features = providers.DocumentationNotes{
	providers.CanGetZones:            providers.Can(),
	providers.CanUseAlias:            providers.Can(),
	providers.CanUseCAA:              providers.Can(),
	providers.CanUseLOC:              providers.Cannot(),
	providers.CanUsePTR:              providers.Can(),
	providers.CanUseSRV:              providers.Can(),
	providers.CanUseSSHFP:            providers.Can(),
	providers.CanUseTLSA:             providers.Can(),
	providers.DocCreateDomains:       providers.Can(),
	providers.DocDualHost:            providers.Can(),
	providers.DocOfficiallySupported: providers.Cannot(),
}

func init() {
	fns := providers.DspFuncs{
		Initializer:   NewLuaDNS,
		RecordAuditor: AuditRecords,
	}
	providers.RegisterDomainServiceProviderType("LUADNS", fns, features)
}

// NewLuaDNS creates the provider.
func NewLuaDNS(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
	l := &luadnsProvider{}
	l.creds.email, l.creds.apikey = m["email"], m["apikey"]
	if l.creds.email == "" || l.creds.apikey == "" {
		return nil, fmt.Errorf("missing LuaDNS email or apikey")
	}

	// Get a domain to validate authentication
	if err := l.fetchDomainList(); err != nil {
		return nil, err
	}

	return l, nil
}

// GetNameservers returns the nameservers for a domain.
func (l *luadnsProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
	if len(l.nameserversNames) == 0 {
		l.fetchAvailableNameservers()
	}
	return models.ToNameserversStripTD(l.nameserversNames)
}

// ListZones returns a list of the DNS zones.
func (l *luadnsProvider) ListZones() ([]string, error) {
	if err := l.fetchDomainList(); err != nil {
		return nil, err
	}
	zones := make([]string, 0, len(l.domainIndex))
	for d := range l.domainIndex {
		zones = append(zones, d)
	}
	return zones, nil
}

// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
func (l *luadnsProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
	domainID, err := l.getDomainID(domain)
	if err != nil {
		return nil, err
	}
	records, err := l.getRecords(domainID)
	if err != nil {
		return nil, err
	}
	existingRecords := make([]*models.RecordConfig, len(records))
	for i := range records {
		existingRecords[i] = nativeToRecord(domain, &records[i])
	}
	return existingRecords, nil
}

// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
func (l *luadnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, records models.Records) ([]*models.Correction, error) {

	checkNS(dc)

	domainID, err := l.getDomainID(dc.Name)
	if err != nil {
		return nil, err
	}

	var corrections []*models.Correction
	var corrs []*models.Correction
	if !diff2.EnableDiff2 {
		differ := diff.New(dc)
		_, create, del, mod, err := differ.IncrementalDiff(records)
		if err != nil {
			return nil, err
		}

		corrections := []*models.Correction{}
		for _, d := range del {
			corrs := l.makeDeleteCorrection(d.Existing, domainID, d.String())
			corrections = append(corrections, corrs...)
		}

		for _, d := range create {
			corrs := l.makeCreateCorrection(d.Desired, domainID, d.String())
			corrections = append(corrections, corrs...)
		}

		for _, d := range mod {
			corrs := l.makeChangeCorrection(d.Existing, d.Desired, domainID, d.String())
			corrections = append(corrections, corrs...)
		}

		return corrections, nil
	}

	changes, err := diff2.ByRecord(records, dc, nil)
	if err != nil {
		return nil, err
	}

	for _, change := range changes {
		msg := change.Msgs[0]
		switch change.Type {
		case diff2.REPORT:
			corrs = []*models.Correction{{Msg: change.MsgsJoined}}
		case diff2.CREATE:
			corrs = l.makeCreateCorrection(change.New[0], domainID, msg)
		case diff2.CHANGE:
			corrs = l.makeChangeCorrection(change.Old[0], change.New[0], domainID, msg)
		case diff2.DELETE:
			corrs = l.makeDeleteCorrection(change.Old[0], domainID, msg)
		default:
			panic(fmt.Sprintf("unhandled inst.Type %s", change.Type))
		}
		corrections = append(corrections, corrs...)
	}
	return corrections, nil
}

func (l *luadnsProvider) makeCreateCorrection(newrec *models.RecordConfig, domainID uint32, msg string) []*models.Correction {
	req := recordsToNative(newrec)
	return []*models.Correction{{
		Msg: msg,
		F: func() error {
			return l.createRecord(domainID, req)
		},
	}}
}

func (l *luadnsProvider) makeChangeCorrection(oldrec *models.RecordConfig, newrec *models.RecordConfig, domainID uint32, msg string) []*models.Correction {
	recordID := oldrec.Original.(*domainRecord).ID
	req := recordsToNative(newrec)
	return []*models.Correction{{
		Msg: fmt.Sprintf("%s, LuaDNS ID: %d", msg, recordID),
		F: func() error {
			return l.modifyRecord(domainID, recordID, req)
		},
	}}
}

func (l *luadnsProvider) makeDeleteCorrection(deleterec *models.RecordConfig, domainID uint32, msg string) []*models.Correction {
	recordID := deleterec.Original.(*domainRecord).ID
	return []*models.Correction{{
		Msg: fmt.Sprintf("%s, LuaDNS ID: %d", msg, recordID),
		F: func() error {
			return l.deleteRecord(domainID, recordID)
		},
	}}
}

// EnsureZoneExists creates a zone if it does not exist
func (l *luadnsProvider) EnsureZoneExists(domain string) error {
	if l.domainIndex == nil {
		if err := l.fetchDomainList(); err != nil {
			return err
		}
	}
	if _, ok := l.domainIndex[domain]; ok {
		return nil
	}
	return l.createDomain(domain)
}
