// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

// Package resolvconffile parses & serializes /etc/resolv.conf-style files.
//
// It's a leaf package so both net/dns and net/dns/resolver can depend
// on it and we can unify a handful of implementations.
//
// The package is verbosely named to disambiguate it from resolvconf
// the daemon, which Tailscale also supports.
package resolvconffile

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"net/netip"
	"os"
	"strings"

	"tailscale.com/util/dnsname"
)

// Path is the canonical location of resolv.conf.
const Path = "/etc/resolv.conf"

// Config represents a resolv.conf(5) file.
type Config struct {
	// Nameservers are the IP addresses of the nameservers to use.
	Nameservers []netip.Addr

	// SearchDomains are the domain suffixes to use when expanding
	// single-label name queries. SearchDomains is additive to
	// whatever non-Tailscale search domains the OS has.
	SearchDomains []dnsname.FQDN
}

// Write writes c to w. It does so in one Write call.
func (c *Config) Write(w io.Writer) error {
	buf := new(bytes.Buffer)
	io.WriteString(buf, "# resolv.conf(5) file generated by tailscale\n")
	io.WriteString(buf, "# For more info, see https://tailscale.com/s/resolvconf-overwrite\n")
	io.WriteString(buf, "# DO NOT EDIT THIS FILE BY HAND -- CHANGES WILL BE OVERWRITTEN\n\n")
	for _, ns := range c.Nameservers {
		io.WriteString(buf, "nameserver ")
		io.WriteString(buf, ns.String())
		io.WriteString(buf, "\n")
	}
	if len(c.SearchDomains) > 0 {
		io.WriteString(buf, "search")
		for _, domain := range c.SearchDomains {
			io.WriteString(buf, " ")
			io.WriteString(buf, domain.WithoutTrailingDot())
		}
		io.WriteString(buf, "\n")
	}
	_, err := w.Write(buf.Bytes())
	return err
}

// Parse parses a resolv.conf file from r.
func Parse(r io.Reader) (*Config, error) {
	config := new(Config)
	scanner := bufio.NewScanner(r)
	for scanner.Scan() {
		line := scanner.Text()
		line, _, _ = strings.Cut(line, "#") // remove any comments
		line = strings.TrimSpace(line)

		if s, ok := strings.CutPrefix(line, "nameserver"); ok {
			nameserver := strings.TrimSpace(s)
			if len(nameserver) == len(s) {
				return nil, fmt.Errorf("missing space after \"nameserver\" in %q", line)
			}
			ip, err := netip.ParseAddr(nameserver)
			if err != nil {
				return nil, err
			}
			config.Nameservers = append(config.Nameservers, ip)
			continue
		}

		if s, ok := strings.CutPrefix(line, "search"); ok {
			domains := strings.TrimSpace(s)
			if len(domains) == len(s) {
				// No leading space?!
				return nil, fmt.Errorf("missing space after \"search\" in %q", line)
			}
			for len(domains) > 0 {
				domain := domains
				i := strings.IndexAny(domain, " \t")
				if i != -1 {
					domain = domain[:i]
					domains = strings.TrimSpace(domains[i+1:])
				} else {
					domains = ""
				}
				fqdn, err := dnsname.ToFQDN(domain)
				if err != nil {
					return nil, fmt.Errorf("parsing search domain %q in %q: %w", domain, line, err)
				}
				config.SearchDomains = append(config.SearchDomains, fqdn)
			}
		}
	}
	return config, nil
}

// ParseFile parses the named resolv.conf file.
func ParseFile(name string) (*Config, error) {
	fi, err := os.Stat(name)
	if err != nil {
		return nil, err
	}
	if n := fi.Size(); n > 10<<10 {
		return nil, fmt.Errorf("unexpectedly large %q file: %d bytes", name, n)
	}
	all, err := os.ReadFile(name)
	if err != nil {
		return nil, err
	}
	return Parse(bytes.NewReader(all))
}
