// Package dns handles DNS configuration pushed by an OpenVPN server.
//
// Backends:
//   - Linux: ApplyResolved (systemd-resolved via D-Bus) or /etc/resolv.conf.
//     See dns_linux.go.
//   - macOS (CLI): networksetup(8) or /etc/resolv.conf.
//     See dns_darwin.go.
//
// On the GUI path (NEPacketTunnelProvider) the OS applies DNS via
// setTunnelNetworkSettings; Apply/Revert are never called.
//
// Reference: openvpn3-core client/dns_option.hpp, systemd resolved.conf(5)
package dns

import (
	"bufio"
	"fmt"
	"net"
	"os"
	"strings"
)

// Config holds the DNS servers and search domains pushed by the server.
type Config struct {
	// Servers is the list of DNS server addresses, in preference order.
	Servers []net.IP
	// SearchDomains is the list of DNS search domains.
	// May be empty.
	SearchDomains []string
}

// ParsePushReply extracts DNS options from a PUSH_REPLY control message.
//
// The message may contain any number of "dhcp-option DNS <ip>" and
// "dhcp-option DOMAIN <domain>" directives.  All other directives are
// silently ignored.
func ParsePushReply(msg string) (*Config, error) {
	msg = strings.TrimPrefix(msg, "PUSH_REPLY,")
	msg = strings.TrimRight(msg, "\x00")

	cfg := &Config{}

	for _, field := range strings.Split(msg, ",") {
		field = strings.TrimSpace(field)
		if field == "" {
			continue
		}
		parts := strings.Fields(field)
		if len(parts) < 3 {
			continue
		}
		if !strings.EqualFold(parts[0], "dhcp-option") {
			continue
		}
		switch strings.ToUpper(parts[1]) {
		case "DNS":
			ip := net.ParseIP(parts[2])
			if ip == nil {
				return nil, fmt.Errorf("dns: dhcp-option DNS: invalid IP %q", parts[2])
			}
			cfg.Servers = append(cfg.Servers, ip.To4())
		case "DOMAIN":
			cfg.SearchDomains = append(cfg.SearchDomains, parts[2])
		}
	}

	return cfg, nil
}

// ResolvConfPath is the path written by ApplyResolvConf.
// Tests may override this to a temporary file.
var ResolvConfPath = "/etc/resolv.conf"

// ApplyResolvConf writes the DNS configuration to ResolvConfPath.
func ApplyResolvConf(cfg *Config) error {
	if cfg == nil || len(cfg.Servers) == 0 {
		return nil
	}

	var sb strings.Builder
	sb.WriteString("# Generated by go-openlawsvpn — do not edit\n")
	if len(cfg.SearchDomains) > 0 {
		sb.WriteString("search " + strings.Join(cfg.SearchDomains, " ") + "\n")
	}
	for _, srv := range cfg.Servers {
		sb.WriteString("nameserver " + srv.String() + "\n")
	}

	if err := os.WriteFile(ResolvConfPath, []byte(sb.String()), 0644); err != nil {
		return fmt.Errorf("dns: write %s: %w", ResolvConfPath, err)
	}
	return nil
}

// RestoreResolvConf restores /etc/resolv.conf from the backup at backupPath.
func RestoreResolvConf(backupPath string) error {
	if backupPath == "" {
		return nil
	}
	data, err := os.ReadFile(backupPath)
	if err != nil {
		return fmt.Errorf("dns: read backup %s: %w", backupPath, err)
	}
	if err := os.WriteFile(ResolvConfPath, data, 0644); err != nil {
		return fmt.Errorf("dns: restore %s: %w", ResolvConfPath, err)
	}
	return os.Remove(backupPath)
}

// BackupResolvConf saves the current /etc/resolv.conf to backupPath.
func BackupResolvConf(backupPath string) error {
	data, err := os.ReadFile(ResolvConfPath)
	if err != nil {
		data = []byte{}
	}
	if err := os.WriteFile(backupPath, data, 0600); err != nil {
		return fmt.Errorf("dns: backup %s → %s: %w", ResolvConfPath, backupPath, err)
	}
	return nil
}

// Backend identifies which DNS backend was activated by Apply.
// Stored by the caller so Revert uses the matching cleanup path.
type Backend int

const (
	// BackendNone means no DNS configuration was applied (no servers pushed).
	BackendNone Backend = iota
	// BackendResolved means the platform-native resolver API was used
	// (systemd-resolved on Linux; networksetup on macOS).
	BackendResolved
	// BackendResolvConf means ApplyResolvConf (/etc/resolv.conf) was used.
	BackendResolvConf
)

// IsManaged reports whether path (typically /etc/resolv.conf) was written by
// go-openlawsvpn (i.e. the first line is the managed-file comment).
func IsManaged(path string) (bool, error) {
	f, err := os.Open(path)
	if err != nil {
		return false, err
	}
	defer f.Close()
	scanner := bufio.NewScanner(f)
	if scanner.Scan() {
		return strings.HasPrefix(scanner.Text(), "# Generated by go-openlawsvpn"), nil
	}
	return false, scanner.Err()
}
