package luadns

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strings"

	api "github.com/luadns/luadns-go"
	"golang.org/x/time/rate"

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

/*

LuaDNS API DNS provider:

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

var features = providers.DocumentationNotes{
	// The default for unlisted capabilities is 'Cannot'.
	// See providers/capabilities.go for the entire list of capabilities.
	providers.CanGetZones:            providers.Can(),
	providers.CanConcur:              providers.Can(),
	providers.CanUseAlias:            providers.Can(),
	providers.CanUseCAA:              providers.Can(),
	providers.CanUseHTTPS:            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() {
	const providerName = "LUADNS"
	const providerMaintainer = "@riku22"
	fns := providers.DspFuncs{
		Initializer:   NewLuaDNS,
		RecordAuditor: AuditRecords,
	}
	providers.RegisterDomainServiceProviderType(providerName, fns, features)
	providers.RegisterMaintainer(providerName, providerMaintainer)
}

type luadnsProvider struct {
	provider    *api.Client
	ctx         context.Context
	rateLimiter *rate.Limiter
	nameServers []string
	zones       []*api.Zone
}

// NewLuaDNS creates the provider.
func NewLuaDNS(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
	if m["email"] == "" || m["apikey"] == "" {
		return nil, errors.New("missing LuaDNS email or apikey")
	}
	ctx := context.Background()
	rateLimiter := rate.NewLimiter(4, 1)
	provider := api.NewClient(m["email"], m["apikey"])
	user, err := provider.Me(ctx)
	if err != nil {
		return nil, err
	}
	l := &luadnsProvider{
		provider:    provider,
		ctx:         ctx,
		rateLimiter: rateLimiter,
		nameServers: user.NameServers,
	}
	return l, nil
}

// GetNameservers returns the nameservers for a domain.
func (l *luadnsProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
	return models.ToNameserversStripTD(l.nameServers)
}

// ListZones returns a list of the DNS zones.
func (l *luadnsProvider) ListZones() ([]string, error) {
	if err := l.fetchDomainList(); err != nil {
		return nil, err
	}
	zoneList := make([]string, 0, len(l.zones))
	for _, d := range l.zones {
		zoneList = append(zoneList, d.Name)
	}
	return zoneList, 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) {
	zone, err := l.getZone(domain)
	if err != nil {
		return nil, err
	}
	records, err := l.getRecords(zone)
	if err != nil {
		return nil, err
	}
	existingRecords := make([]*models.RecordConfig, len(records))
	for i := range records {
		newr, err := nativeToRecord(domain, records[i])
		if err != nil {
			return nil, err
		}
		existingRecords[i] = newr
	}
	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, int, error) {
	var corrections []*models.Correction

	checkNS(dc)

	zone, err := l.getZone(dc.Name)
	if err != nil {
		return nil, 0, err
	}

	var corrs []*models.Correction

	changes, actualChangeCount, err := diff2.ByRecord(records, dc, nil)
	if err != nil {
		return nil, 0, 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], zone, msg)
		case diff2.CHANGE:
			corrs = l.makeChangeCorrection(change.Old[0], change.New[0], zone, msg)
		case diff2.DELETE:
			corrs = l.makeDeleteCorrection(change.Old[0], zone, msg)
		default:
			panic(fmt.Sprintf("unhandled inst.Type %s", change.Type))
		}
		corrections = append(corrections, corrs...)
	}
	return corrections, actualChangeCount, nil
}

func (l *luadnsProvider) makeCreateCorrection(newrec *models.RecordConfig, zone *api.Zone, msg string) []*models.Correction {
	req := recordsToNative(newrec)
	return []*models.Correction{{
		Msg: msg,
		F: func() error {
			if err := l.rateLimiter.Wait(l.ctx); err != nil {
				return err
			}
			_, err := l.provider.CreateRecord(l.ctx, zone, req)
			if err != nil {
				return err
			}
			return nil
		},
	}}
}

func (l *luadnsProvider) makeChangeCorrection(oldrec *models.RecordConfig, newrec *models.RecordConfig, zone *api.Zone, msg string) []*models.Correction {
	recordID := oldrec.Original.(*api.Record).ID
	req := recordsToNative(newrec)
	return []*models.Correction{{
		Msg: fmt.Sprintf("%s, LuaDNS ID: %d", msg, recordID),
		F: func() error {
			if err := l.rateLimiter.Wait(l.ctx); err != nil {
				return err
			}
			_, err := l.provider.UpdateRecord(l.ctx, zone, recordID, req)
			if err != nil {
				return err
			}
			return nil
		},
	}}
}

func (l *luadnsProvider) makeDeleteCorrection(deleterec *models.RecordConfig, zone *api.Zone, msg string) []*models.Correction {
	recordID := deleterec.Original.(*api.Record).ID
	return []*models.Correction{{
		Msg: fmt.Sprintf("%s, LuaDNS ID: %d", msg, recordID),
		F: func() error {
			if err := l.rateLimiter.Wait(l.ctx); err != nil {
				return err
			}
			_, err := l.provider.DeleteRecord(l.ctx, zone, recordID)
			if err != nil {
				return err
			}
			return nil
		},
	}}
}

// EnsureZoneExists creates a zone if it does not exist
func (l *luadnsProvider) EnsureZoneExists(domain string, metadata map[string]string) error {
	if l.zones == nil {
		if err := l.fetchDomainList(); err != nil {
			return err
		}
	}
	if err := l.rateLimiter.Wait(l.ctx); err != nil {
		return err
	}
	zone, err := l.provider.CreateZone(l.ctx, &api.Zone{Name: domain})
	if err != nil {
		return err
	}
	l.zones = append(l.zones, zone)
	return nil
}

func (l *luadnsProvider) fetchDomainList() error {
	if err := l.rateLimiter.Wait(l.ctx); err != nil {
		return err
	}
	zones, err := l.provider.ListZones(l.ctx, &api.ListParams{})
	if err != nil {
		return err
	}
	l.zones = zones
	return nil
}

func (l *luadnsProvider) getZone(name string) (*api.Zone, error) {
	if l.zones == nil {
		if err := l.fetchDomainList(); err != nil {
			return nil, err
		}
	}
	for i := range l.zones {
		if l.zones[i].Name == name {
			return l.zones[i], nil
		}
	}
	return nil, fmt.Errorf("'%s' not a zone in luadns account", name)
}

func (l *luadnsProvider) getRecords(zone *api.Zone) ([]*api.Record, error) {
	if err := l.rateLimiter.Wait(l.ctx); err != nil {
		return nil, err
	}
	records, err := l.provider.ListRecords(l.ctx, zone, &api.ListParams{})
	if err != nil {
		return nil, err
	}
	var newRecords []*api.Record
	for _, rec := range records {
		if rec.Type == "SOA" {
			continue
		}
		newRecords = append(newRecords, rec)
	}
	return newRecords, nil
}

func nativeToRecord(domain string, r *api.Record) (*models.RecordConfig, error) {
	rc := &models.RecordConfig{
		Type:     r.Type,
		TTL:      r.TTL,
		Original: r,
	}
	rc.SetLabelFromFQDN(r.Name, domain)
	var err error
	switch rtype := rc.Type; rtype {
	case "TXT":
		err = rc.SetTargetTXT(r.Content)
	default:
		err = rc.PopulateFromString(rtype, r.Content, domain)
	}
	return rc, err
}

func recordsToNative(rc *models.RecordConfig) *api.Record {
	r := &api.Record{
		Name: rc.GetLabelFQDN() + ".",
		Type: rc.Type,
		TTL:  rc.TTL,
	}
	switch rtype := rc.Type; rtype {
	case "TXT":
		r.Content = rc.GetTargetTXTJoined()
	case "HTTPS":
		content := fmt.Sprintf("%d %s %s", rc.SvcPriority, rc.GetTargetField(), rc.SvcParams)
		if rc.SvcParams == "" {
			content = content[:len(content)-1]
		}
		r.Content = content
	default:
		r.Content = rc.GetTargetCombined()
	}
	return r
}

func checkNS(dc *models.DomainConfig) {
	newList := make([]*models.RecordConfig, 0, len(dc.Records))
	for _, rec := range dc.Records {
		// LuaDNS does not support changing the TTL of the default nameservers, so forcefully change the TTL to 86400.
		if rec.Type == "NS" && strings.HasSuffix(rec.GetTargetField(), ".luadns.net.") && rec.TTL != 86400 {
			rec.TTL = 86400
		}
		newList = append(newList, rec)
	}
	dc.Records = newList
}
