package infomaniak

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

	dnsv2 "codeberg.org/miekg/dns"
	"github.com/DNSControl/dnscontrol/v5/models"
	"github.com/DNSControl/dnscontrol/v5/pkg/diff2"
	"github.com/DNSControl/dnscontrol/v5/pkg/providers"
)

// infomaniakProvider is the handle for operations.
type infomaniakProvider struct {
	apiToken string // the account access token
}

var features = providers.DocumentationNotes{
	// The default for unlisted capabilities is 'Cannot'.
	// See providers/capabilities.go for the entire list of capabilities.
	providers.CanUseCAA:   providers.Can(),
	providers.CanUseDNAME: providers.Can(),
	providers.CanUseDS:    providers.Can(),
	providers.CanUseSSHFP: providers.Can(),
	providers.CanUseTLSA:  providers.Can(),
	providers.CanUseSRV:   providers.Can(),
	// providers.DocCreateDomains: providers.Can(),
}

func newInfomaniak(m map[string]string, message json.RawMessage) (providers.DNSServiceProvider, error) {
	api := &infomaniakProvider{}
	api.apiToken = m["token"]
	if api.apiToken == "" {
		return nil, errors.New("missing Infomaniak personal access token")
	}

	return api, nil
}

func init() {
	const providerName = "INFOMANIAK"
	const providerMaintainer = "@jbelien"
	fns := providers.DspFuncs{
		Initializer:   newInfomaniak,
		RecordAuditor: AuditRecords,
	}
	providers.RegisterDomainServiceProviderType(providerName, fns, features)
	providers.RegisterMaintainer(providerName, providerMaintainer)
}

func (p *infomaniakProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
	zone, err := p.getDNSZone(domain)
	if err != nil {
		return nil, err
	}

	return models.ToNameservers(zone.Nameservers)
}

// addTrailingDot adds a trailing dot if it's missing.
func addTrailingDot(target string) string {
	if target == "" || target == "." {
		return target
	}
	if !strings.HasSuffix(target, ".") {
		return target + "."
	}
	return target
}

// toRecordConfig converts a DNS record from Infomaniak API to RecordConfig.
func toRecordConfig(dc *models.DomainConfig, r dnsRecord) (*models.RecordConfig, error) {
	// Handle the source/label - Infomaniak uses empty string or "." for apex
	label := r.Source
	if label == "" || label == "." {
		label = "@"
	}
	// Parse the target based on record type
	rtype := r.Type
	target := r.Target
	ttl := uint32(r.TTL)

	var rc *models.RecordConfig
	var err error
	switch rtype {
	// case "A", "AAAA":
	// 	rc, err = dc.NewRecordConfig(label, ttl, rtype, target)

	case "CNAME", "NS", "DNAME":
		rc, err = dc.NewRecordConfig(label, ttl, rtype, addTrailingDot(target))

	case "MX":
		// Infomaniak returns MX as "priority target" (e.g., "5 mta-gw.infomaniak.ch")
		rc, err = dc.NewRecordConfigParse(label, ttl, rtype, addTrailingDot(target))

	case "TXT":
		// Infomaniak returns TXT in RFC 1035 presentation format: one or more
		// quoted character-strings. Values longer than 255 octets come back
		// split into multiple `"..." "..."` strings, so parse them (naively
		// stripping the outer quotes leaves the interior `" "` and breaks the
		// round-trip). See issue #4748.
		rc, err = dc.NewRecordConfigParse(label, ttl, rtype, target)

	case "SRV":
		// Infomaniak returns SRV as "priority weight port target"
		rc, err = dc.NewRecordConfigParse(label, ttl, rtype, addTrailingDot(target))

	case "CAA":
		// Infomaniak returns CAA as "flags tag value" (e.g., "0 issue letsencrypt.org")
		rc, err = dc.NewRecordConfigParse(label, ttl, rtype, target)

	case "DS":
		// Infomaniak returns DS as "keytag algorithm digesttype digest"
		// Note: Infomaniak may split long digest data with spaces, so we need to rejoin them
		parts := strings.Fields(target)
		if len(parts) >= 4 {
			// Rejoin all parts after the first 3 (keytag, algorithm, digesttype) as the digest
			digest := strings.Join(parts[3:], "")
			target = fmt.Sprintf("%s %s %s %s", parts[0], parts[1], parts[2], digest)
		}
		rc, err = dc.NewRecordConfigParse(label, ttl, rtype, target)

	case "SSHFP":
		// Infomaniak returns SSHFP as "algorithm fingerprint_type fingerprint"
		// Note: Infomaniak may split long fingerprint data with spaces, so we need to rejoin them
		parts := strings.Fields(target)
		if len(parts) >= 3 {
			// Rejoin all parts after the first 2 (algorithm, fingerprint_type) as the fingerprint
			fingerprint := strings.Join(parts[2:], "")
			target = fmt.Sprintf("%s %s %s", parts[0], parts[1], fingerprint)
		}
		rc, err = dc.NewRecordConfigParse(label, ttl, rtype, target)

	case "TLSA":
		// Infomaniak returns TLSA as "usage selector matching_type certificate"
		// Note: Infomaniak may split long certificate data with spaces, so we need to rejoin them
		parts := strings.Fields(target)
		if len(parts) >= 4 {
			// Rejoin all parts after the first 3 (usage, selector, matching_type) as the certificate
			certificate := strings.Join(parts[3:], "")
			target = fmt.Sprintf("%s %s %s %s", parts[0], parts[1], parts[2], certificate)
		}
		rc, err = dc.NewRecordConfigParse(label, ttl, rtype, target)

	default:
		rc, err = dc.NewRecordConfig(label, ttl, rtype, target)
	}

	if err != nil {
		return nil, fmt.Errorf("unparsable record type=%q target=%q received from Infomaniak: %w", rtype, target, err)
	}
	rc.Original = r

	return rc, nil
}

// fromRecordConfig converts a RecordConfig to the API format for creation.
func fromRecordConfig(rc *models.RecordConfig) *dnsRecordCreate {
	// Get the label - Infomaniak uses empty string for apex
	label := rc.GetLabel()
	if label == "@" {
		label = ""
	}

	// Get the target in the format expected by Infomaniak API
	var target string
	switch rc.TypeNum {
	// case "A":
	// 	target = rc.AsA().Addr.String()
	// case "AAAA":
	// 	target = rc.AsAAAA().Addr.String()

	case dnsv2.TypeCNAME:
		target = strings.TrimSuffix(rc.AsCNAME().Target, ".")
	case dnsv2.TypeNS:
		target = strings.TrimSuffix(rc.AsNS().Ns, ".")
	case dnsv2.TypeDNAME:
		target = strings.TrimSuffix(rc.AsDNAME().Target, ".")

	case dnsv2.TypeMX:
		// Format: "priority target" (without trailing dot)
		f := rc.AsMX()
		target = fmt.Sprintf("%d %s", f.Preference, strings.TrimSuffix(f.Mx, "."))

	case dnsv2.TypeTXT:
		target = rc.AsTXT().String()

	case dnsv2.TypeSRV:
		// Format: "priority weight port target" (without trailing dot)
		f := rc.AsSRV()
		target = fmt.Sprintf("%d %d %d %s", f.Priority, f.Weight, f.Port, strings.TrimSuffix(f.Target, "."))

	// case "CAA":
	// 	// Format: "flags tag value"
	// 	target = rc.GetRDATA().String()

	// case "DS":
	// 	// Format: "keytag algorithm digesttype digest"
	// 	target = rc.GetRDATA().String()

	// case "SSHFP":
	// 	// Format: "algorithm fingerprint_type fingerprint"
	// 	target = rc.GetRDATA().String()

	// case "TLSA":
	// 	// Format: "usage selector matching_type certificate"
	// 	target = rc.GetRDATA().String()

	// TODO(): If tests pass, removed the above comments.

	default:
		target = rc.GetRDATA().String()
	}

	return &dnsRecordCreate{
		Source: label,
		Type:   rc.Type,
		TTL:    int64(rc.TTL),
		Target: target,
	}
}

// toRecordUpdate converts a RecordConfig to the API format for updating.
func toRecordUpdate(rc *models.RecordConfig) *dnsRecordUpdate {
	// Get the target in the format expected by Infomaniak API
	var target string
	switch rc.TypeNum {
	case dnsv2.TypeA:
		target = rc.AsA().Addr.String()
	case dnsv2.TypeAAAA:
		target = rc.AsAAAA().Addr.String()

	case dnsv2.TypeCNAME:
		// Remove trailing dot for the API
		target = strings.TrimSuffix(rc.AsCNAME().Target, ".")
	case dnsv2.TypeNS:
		target = strings.TrimSuffix(rc.AsNS().Ns, ".")
	case dnsv2.TypeDNAME:
		target = strings.TrimSuffix(rc.AsDNAME().Target, ".")

	case dnsv2.TypeMX:
		// Format: "priority target" (without trailing dot)
		f := rc.AsMX()
		target = fmt.Sprintf("%d %s", f.Preference, strings.TrimSuffix(f.Mx, "."))

	case dnsv2.TypeTXT:
		target = rc.GetTargetTXTJoined()

	case dnsv2.TypeSRV:
		// Format: "priority weight port target" (without trailing dot)
		f := rc.AsSRV()
		target = fmt.Sprintf("%d %d %d %s", f.Priority, f.Weight, f.Port, strings.TrimSuffix(f.Target, "."))

	case dnsv2.TypeCAA:
		// Format: "flags tag value"
		f := rc.AsCAA()
		target = fmt.Sprintf("%d %s %s", f.Flag, f.Tag, f.Value)

	case dnsv2.TypeDS:
		// Format: "keytag algorithm digesttype digest"
		f := rc.AsDS()
		target = f.String()

	case dnsv2.TypeSSHFP:
		// Format: "algorithm fingerprint_type fingerprint"
		f := rc.AsSSHFP()
		target = f.String()
		// If that doesn't work, try this:
		//target = fmt.Sprintf("%d %d %s", f.Algorithm, f.Type, f.FingerPrint)

	case dnsv2.TypeTLSA:
		// Format: "usage selector matching_type certificate"
		f := rc.AsTLSA()
		target = fmt.Sprintf("%d %d %d %s", f.Usage, f.Selector, f.MatchingType, f.Certificate)

	default:
		target = rc.GetRDATA().String()
	}

	return &dnsRecordUpdate{
		TTL:    int64(rc.TTL),
		Target: target,
	}
}

func (p *infomaniakProvider) GetZoneRecords(dc *models.DomainConfig) (models.Records, error) {
	domain := dc.Name

	records, err := p.getDNSRecords(domain)
	if err != nil {
		return nil, err
	}

	cleanRecords := make(models.Records, 0, len(records))

	for _, r := range records {
		recConfig, err := toRecordConfig(dc, r)
		if err != nil {
			return nil, err
		}
		cleanRecords = append(cleanRecords, recConfig)
	}

	return cleanRecords, nil
}

func (p *infomaniakProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) {
	var corrections []*models.Correction
	domain := dc.Name

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

	for _, change := range changes {
		switch change.Type {
		case diff2.REPORT:
			corrections = append(corrections, &models.Correction{Msg: change.MsgsJoined})
		case diff2.CHANGE:
			oldRec := change.Old[0].Original.(dnsRecord)
			newRec := change.New[0]
			corrections = append(corrections, &models.Correction{
				Msg: change.MsgsJoined,
				F: func() error {
					_, err := p.updateDNSRecord(domain, fmt.Sprintf("%v", oldRec.ID), toRecordUpdate(newRec))
					return err
				},
			})

		case diff2.CREATE:
			rec := change.New[0]
			corrections = append(corrections, &models.Correction{
				Msg: change.MsgsJoined,
				F: func() error {
					_, err := p.createDNSRecord(domain, fromRecordConfig(rec))
					return err
				},
			})

		case diff2.DELETE:
			rec := change.Old[0].Original.(dnsRecord)
			corrections = append(corrections, &models.Correction{
				Msg: change.MsgsJoined,
				F: func() error {
					return p.deleteDNSRecord(domain, fmt.Sprintf("%v", rec.ID))
				},
			})
		}
	}

	return corrections, actualChangeCount, nil
}
