package joker

import (
	"fmt"
	"net/url"
	"strconv"
	"strings"

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

// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
func (api *jokerProvider) GetZoneRecords(dc *models.DomainConfig) (models.Records, error) {
	domain := dc.Name

	params := url.Values{}
	params.Set("domain", domain)

	_, body, err := api.makeRequest("dns-zone-get", params)
	if err != nil {
		return nil, err
	}

	records, err := api.parseZoneRecords(domain, body)
	if err != nil {
		return nil, err
	}

	return records, nil
}

// parseZoneLine parses a zone file line while preserving quoted strings.
func parseZoneLine(line string) []string {
	var parts []string
	var current strings.Builder
	inQuotes := false
	escaped := false

	for _, r := range line {
		if escaped {
			current.WriteRune(r)
			escaped = false
			continue
		}

		if r == '\\' {
			escaped = true
			current.WriteRune(r)
			continue
		}

		if r == '"' {
			inQuotes = !inQuotes
			current.WriteRune(r)
			continue
		}

		if !inQuotes && (r == ' ' || r == '\t') {
			// Skip multiple consecutive spaces
			if current.Len() > 0 {
				parts = append(parts, current.String())
				current.Reset()
			}
			continue
		}

		current.WriteRune(r)
	}

	// Add the final part if any
	if current.Len() > 0 {
		parts = append(parts, current.String())
	}

	return parts
}

// parseZoneRecords parses Joker zone format into RecordConfig format.
func (api *jokerProvider) parseZoneRecords(domain, zoneData string) (models.Records, error) {
	var records models.Records
	dc, err := models.NewDomainConfig(domain)
	if err != nil {
		return nil, err
	}

	lines := strings.SplitSeq(strings.TrimSpace(zoneData), "\n")
	for line := range lines {
		line = strings.TrimSpace(line)
		if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "$") {
			continue
		}

		// Parse the line while preserving quoted strings
		parts := parseZoneLine(line)
		if len(parts) < 4 {
			continue
		}

		label := parts[0]
		recordType := parts[1]
		priority := parts[2]

		// For TXT records, we need to handle quoted content specially
		var target string
		var ttl uint32 = 300

		if recordType == "TXT" {
			// Use parseZoneLine to handle proper quoting
			if len(parts) >= 4 {
				// For TXT, the content is in parts[3] and may be quoted
				target = parts[3]
				// Remove surrounding quotes if present
				if strings.HasPrefix(target, "\"") && strings.HasSuffix(target, "\"") && len(target) > 1 {
					target = target[1 : len(target)-1]
				}
				// Properly handle escaped characters - reverse the generation order
				target = strings.ReplaceAll(target, "\\\"", "\"")
				target = strings.ReplaceAll(target, "\\\\", "\\")
				// Parse TTL if present
				if len(parts) >= 5 {
					if ttlParsed, err := strconv.ParseUint(parts[4], 10, 32); err == nil {
						ttl = uint32(ttlParsed)
					}
				}
			}
		} else {
			target = parts[3]
			// Default TTL if not specified in zone record
			if len(parts) >= 5 {
				if ttlParsed, err := strconv.ParseUint(parts[4], 10, 32); err == nil {
					ttl = uint32(ttlParsed)
				}
			}
		}

		// Handle different record types
		var rc *models.RecordConfig
		var err error
		switch recordType {
		case "A", "AAAA":
			rc, err = dc.NewRecordConfig(label, ttl, recordType, target)
		case "CNAME", "NS":
			// Ensure CNAME targets are fully qualified
			if !strings.HasSuffix(target, ".") {
				target = target + "."
			}
			rc, err = dc.NewRecordConfig(label, ttl, recordType, target)
		case "TXT":
			// TXT target is already extracted without quotes in the parsing above
			rc, err = dc.NewRecordConfig(label, ttl, recordType, target)
		case "MX":
			// Ensure MX targets are fully qualified
			if !strings.HasSuffix(target, ".") {
				target = target + "."
			}
			rc, err = dc.NewRecordConfig(label, ttl, recordType, priority, target)
		case "SRV":
			var srvPriority, srvWeight, srvPort string
			// SRV format: priority/weight target:port
			if strings.Contains(priority, "/") {
				priorityParts := strings.Split(priority, "/")
				if len(priorityParts) == 2 {
					srvPriority = priorityParts[0]
					srvWeight = priorityParts[1]
				}
			}
			if strings.Contains(target, ":") {
				targetParts := strings.Split(target, ":")
				if len(targetParts) == 2 {
					srvPort = targetParts[1]
					srvTarget := targetParts[0]
					// Ensure SRV targets are fully qualified
					if !strings.HasSuffix(srvTarget, ".") {
						srvTarget = srvTarget + "."
					}
					rc, err = dc.NewRecordConfig(label, ttl, recordType, srvPriority, srvWeight, srvPort, srvTarget)
				}
			}
		case "CAA":
			// CAA format: flags tag value [ttl]
			if len(parts) >= 4 {
				flags := priority // priority field contains flags for CAA
				tag := parts[3]   // tag field
				// Value might be quoted
				value := ""
				if len(parts) >= 5 {
					value = parts[4]
					// Remove surrounding quotes if present
					if len(value) > 1 && strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") {
						value = value[1 : len(value)-1]
					}
					// Parse TTL from the end if present (position 5)
					if len(parts) >= 6 {
						if ttlParsed, err := strconv.ParseUint(parts[5], 10, 32); err == nil {
							ttl = uint32(ttlParsed)
						}
					}
				}
				rc, err = dc.NewRecordConfig(label, ttl, recordType, flags, tag, value)
			}
		case "NAPTR":
			var order, preference string
			var flags, service, regexp string
			// NAPTR format for Joker: order/preference replacement ttl 0 0 "flags" "service" "regex"
			if len(parts) >= 9 {
				// Parse order/preference from priority field (parts[2])
				if strings.Contains(priority, "/") {
					priorityParts := strings.Split(priority, "/")
					if len(priorityParts) == 2 {
						order = priorityParts[0]
						preference = priorityParts[1]
					}
				}
				// Replacement is in position 3
				if len(parts) > 3 {
					target = parts[3]
				}
				// Parse TTL from position 4
				if len(parts) >= 5 {
					if ttlParsed, err := strconv.ParseUint(parts[4], 10, 32); err == nil {
						ttl = uint32(ttlParsed)
					}
				}
				// Parse flags, service, and regex from positions 7, 8, 9
				if len(parts) > 7 {
					flags = strings.Trim(parts[7], "\"")
				}
				if len(parts) > 8 {
					service = strings.Trim(parts[8], "\"")
				}
				if len(parts) > 9 {
					regexp = strings.Trim(parts[9], "\"")
				}
				// Ensure NAPTR targets are fully qualified if they're not empty or "."
				if target != "" && target != "." && !strings.HasSuffix(target, ".") {
					target = target + "."
				}
				rc, err = dc.NewRecordConfig(label, ttl, recordType, order, preference, flags, service, regexp, target)
			}
		default:
			// Skip unsupported record types
			continue
		}
		if err != nil || rc == nil {
			continue
		}

		records = append(records, rc)
	}

	return records, nil
}

// fixTTLs enforces minimum TTL requirements for Joker provider.
// NAPTR and SVC records can have TTL=0, others need >= 300.
func fixTTLs(records models.Records) {
	for _, rc := range records {
		if rc.TTL != 0 && rc.TTL < 300 {
			if rc.Type != "NAPTR" && rc.Type != "SVC" {
				rc.TTL = 300
			}
		}
	}
}

// GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records.
func (api *jokerProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) {
	// Apply TTL fixes to desired records before comparison
	fixTTLs(dc.Records)

	result, err := diff2.ByZone(existingRecords, dc, nil)
	if err != nil {
		return nil, 0, err
	}

	if !result.HasChanges {
		return []*models.Correction{}, result.ActualChangeCount, nil
	}

	msg := fmt.Sprintf("Zone update for %s\n%s", dc.Name, strings.Join(result.Msgs, "\n"))

	correction := &models.Correction{
		Msg: msg,
		F: func() error {
			return api.updateZoneRecords(dc.Name, result.DesiredPlus)
		},
	}

	return []*models.Correction{correction}, result.ActualChangeCount, nil
}

// updateZoneRecords replaces the entire zone with new records.
func (api *jokerProvider) updateZoneRecords(domain string, records models.Records) error {
	zoneData := api.recordsToZoneFormat(domain, records)

	// Joker API doesn't support empty zones (returns Status-Code: 2400)
	// If the zone would be empty, we need to skip the update
	if zoneData == "" {
		// Return success for empty zone updates since Joker maintains minimal DNS infrastructure
		// (SOA, NS records) automatically and doesn't allow completely empty zones
		return nil
	}

	params := url.Values{}
	params.Set("domain", domain)
	params.Set("zone", zoneData)

	_, _, err := api.makeRequest("dns-zone-put", params)
	return err
}

// recordsToZoneFormat converts RecordConfig records to Joker zone format.
func (api *jokerProvider) recordsToZoneFormat(_ string, records models.Records) string {
	var lines []string

	for _, rc := range records {
		label := rc.Name
		if label == "" {
			label = "@"
		}

		// TODO(tlim): The <target> can probably be generated by rc.GetRDATA().String().
		// Using that would simplify many of the cases. I've modified the easy ones.

		// Joker format: <label> <type> <pri> <target> <ttl> (valid-from/valid-to omitted when 0)
		switch rc.TypeNum {
		case dnsv2.TypeA, dnsv2.TypeAAAA:
			line := fmt.Sprintf("%s %s 0 %s %d", label, rc.Type, rc.GetRDATA().String(), rc.TTL)
			lines = append(lines, line)
		case dnsv2.TypeCNAME:
			line := fmt.Sprintf("%s %s 0 %s %d", label, rc.Type, rc.AsCNAME().Target, rc.TTL)
			lines = append(lines, line)
		case dnsv2.TypeNS:
			line := fmt.Sprintf("%s %s 0 %s %d", label, rc.Type, rc.AsNS().Ns, rc.TTL)
			lines = append(lines, line)
		case dnsv2.TypeMX:
			f := rc.AsMX()
			line := fmt.Sprintf("%s %s %d %s %d", label, rc.Type, f.Preference, f.Mx, rc.TTL)
			lines = append(lines, line)
		case dnsv2.TypeTXT:
			// Fix TXT record escaping - escape backslashes first, then quotes
			content := rc.GetTargetTXTJoined()
			content = strings.ReplaceAll(content, "\\", "\\\\")
			content = strings.ReplaceAll(content, "\"", "\\\"")
			line := fmt.Sprintf("%s %s 0 \"%s\" %d", label, rc.Type, content, rc.TTL)

			// TODO(tlim): Possible replacement.
			// t := dnsrdatav2.TXT{Txt: []string{rc.GetTargetTXTJoined()}}
			// ts := t.String()
			// line := fmt.Sprintf("%s TXT 0 \"%s\" %d", label, ts, rc.TTL)

			lines = append(lines, line)
		case dnsv2.TypeSRV:
			f := rc.AsSRV()
			target := fmt.Sprintf("%s:%d", f.Target, f.Port)
			priority := fmt.Sprintf("%d/%d", f.Priority, f.Weight)
			line := fmt.Sprintf("%s %s %s %s %d", label, rc.Type, priority, target, rc.TTL)
			lines = append(lines, line)
		case dnsv2.TypeCAA:
			f := rc.AsCAA()
			line := fmt.Sprintf("%s %s %d %s \"%s\" %d", label, rc.Type, f.Flag, f.Tag, f.Value, rc.TTL)
			lines = append(lines, line)
		case dnsv2.TypeNAPTR:
			// NAPTR format for Joker: order/preference replacement ttl 0 0 "flags" "service" "regex"
			f := rc.AsNAPTR()
			priority := fmt.Sprintf("%d/%d", f.Order, f.Preference)
			line := fmt.Sprintf("%s %s %s %s %d 0 0 \"%s\" \"%s\" \"%s\"",
				label, rc.Type, priority, f.Replacement, rc.TTL,
				f.Flags, f.Service, f.Regexp)
			lines = append(lines, line)
		}
	}

	// If no records, return empty zone rather than completely empty string
	// Some DNS providers require at least a minimal zone structure
	if len(lines) == 0 {
		return ""
	}

	return strings.Join(lines, "\n")
}
