package ovh

import (
	"encoding/json"
	"fmt"
	"sort"
	"strings"

	"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"
	"github.com/ovh/go-ovh/ovh"
)

type ovhProvider struct {
	client *ovh.Client
	zones  map[string]bool
}

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

func newOVH(m map[string]string, metadata json.RawMessage) (*ovhProvider, error) {
	appKey, appSecretKey, consumerKey := m["app-key"], m["app-secret-key"], m["consumer-key"]

	c, err := ovh.NewClient(ovh.OvhEU, appKey, appSecretKey, consumerKey)
	if c == nil {
		return nil, err
	}

	ovh := &ovhProvider{client: c}
	if err := ovh.fetchZones(); err != nil {
		return nil, err
	}
	return ovh, nil
}

func newDsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
	return newOVH(conf, metadata)
}

func newReg(conf map[string]string) (providers.Registrar, error) {
	return newOVH(conf, nil)
}

func init() {
	fns := providers.DspFuncs{
		Initializer:   newDsp,
		RecordAuditor: AuditRecords,
	}
	providers.RegisterRegistrarType("OVH", newReg)
	providers.RegisterDomainServiceProviderType("OVH", fns, features)
}

func (c *ovhProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
	_, ok := c.zones[domain]
	if !ok {
		return nil, fmt.Errorf("'%s' not a zone in ovh account", domain)
	}

	ns, err := c.fetchNS(domain)
	if err != nil {
		return nil, err
	}

	return models.ToNameservers(ns)
}

type errNoExist struct {
	domain string
}

func (e errNoExist) Error() string {
	return fmt.Sprintf("Domain %s not found in your ovh account", e.domain)
}

// ListZones lists the zones on this account.
func (c *ovhProvider) ListZones() (zones []string, err error) {
	for zone := range c.zones {
		zones = append(zones, zone)
	}
	return
}

// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
func (c *ovhProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
	if !c.zones[domain] {
		return nil, errNoExist{domain}
	}

	records, err := c.fetchRecords(domain)
	if err != nil {
		return nil, err
	}

	var actual models.Records
	for _, r := range records {
		rec, err := nativeToRecord(r, domain)
		if err != nil {
			return nil, err
		}
		if rec != nil {
			actual = append(actual, rec)
		}
	}

	return actual, nil
}

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

	var corrections []*models.Correction
	var err error
	if !diff2.EnableDiff2 {
		corrections, err = c.getDiff1DomainCorrections(dc, actual)
	} else {
		corrections, err = c.getDiff2DomainCorrections(dc, actual)
	}

	if err != nil {
		return nil, err
	}

	if len(corrections) > 0 {
		corrections = append(corrections, &models.Correction{
			Msg: "REFRESH zone " + dc.Name,
			F: func() error {
				return c.refreshZone(dc.Name)
			},
		})
	}

	return corrections, nil
}

func (c *ovhProvider) getDiff1DomainCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, error) {
	var corrections []*models.Correction

	differ := diff.New(dc)
	_, create, delete, modify, err := differ.IncrementalDiff(actual)
	if err != nil {
		return nil, err
	}

	for _, del := range delete {
		rec := del.Existing.Original.(*Record)
		corrections = append(corrections, &models.Correction{
			Msg: del.String(),
			F:   c.deleteRecordFunc(rec.ID, dc.Name),
		})
	}

	for _, cre := range create {
		rec := cre.Desired
		corrections = append(corrections, &models.Correction{
			Msg: cre.String(),
			F:   c.createRecordFunc(rec, dc.Name),
		})
	}

	for _, mod := range modify {
		oldR := mod.Existing.Original.(*Record)
		newR := mod.Desired
		corrections = append(corrections, &models.Correction{
			Msg: mod.String(),
			F:   c.updateRecordFunc(oldR, newR, dc.Name),
		})
	}
	return corrections, nil
}

func (c *ovhProvider) getDiff2DomainCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, error) {
	var corrections []*models.Correction
	instructions, err := diff2.ByRecord(actual, dc, nil)
	if err != nil {
		return nil, err
	}

	for _, inst := range instructions {
		switch inst.Type {
		case diff2.REPORT:
			corrections = append(corrections, &models.Correction{Msg: inst.MsgsJoined})
		case diff2.CHANGE:
			corrections = append(corrections, &models.Correction{
				Msg: inst.Msgs[0],
				F:   c.updateRecordFunc(inst.Old[0].Original.(*Record), inst.New[0], dc.Name),
			})
		case diff2.CREATE:
			corrections = append(corrections, &models.Correction{
				Msg: inst.Msgs[0],
				F:   c.createRecordFunc(inst.New[0], dc.Name),
			})
		case diff2.DELETE:
			rec := inst.Old[0].Original.(*Record)
			corrections = append(corrections, &models.Correction{
				Msg: inst.Msgs[0],
				F:   c.deleteRecordFunc(rec.ID, dc.Name),
			})
		default:
			panic(fmt.Sprintf("unhandled inst.Type %s", inst.Type))
		}
	}
	return corrections, nil
}

func nativeToRecord(r *Record, origin string) (*models.RecordConfig, error) {
	if r.FieldType == "SOA" {
		return nil, nil
	}
	rec := &models.RecordConfig{
		TTL:      uint32(r.TTL),
		Original: r,
	}

	rtype := r.FieldType

	// ovh uses a custom type for SPF, DKIM and DMARC
	if rtype == "SPF" || rtype == "DKIM" || rtype == "DMARC" {
		rtype = "TXT"
	}

	rec.SetLabel(r.SubDomain, origin)
	if err := rec.PopulateFromString(rtype, r.Target, origin); err != nil {
		return nil, fmt.Errorf("unparsable record received from ovh: %w", err)
	}

	// ovh default is 3600
	if rec.TTL == 0 {
		rec.TTL = 3600
	}

	return rec, nil
}

func (c *ovhProvider) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {

	// get the actual in-use nameservers
	actualNs, err := c.fetchRegistrarNS(dc.Name)
	if err != nil {
		return nil, err
	}

	// get the actual used ones + the configured one through dnscontrol
	expectedNs := []string{}
	for _, d := range dc.Nameservers {
		expectedNs = append(expectedNs, d.Name)
	}

	sort.Strings(actualNs)
	actual := strings.Join(actualNs, ",")

	sort.Strings(expectedNs)
	expected := strings.Join(expectedNs, ",")

	// check if we need to change something
	if actual != expected {
		return []*models.Correction{
			{
				Msg: fmt.Sprintf("Change Nameservers from '%s' to '%s'", actual, expected),
				F: func() error {
					err := c.updateNS(dc.Name, expectedNs)
					if err != nil {
						return err
					}
					return nil
				}},
		}, nil
	}

	return nil, nil
}
