// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package dns

import (
	"bytes"
	"errors"
	"os"

	"go4.org/mem"
	"tailscale.com/types/logger"
	"tailscale.com/util/mak"
)

func NewOSConfigurator(logf logger.Logf, ifName string) (OSConfigurator, error) {
	return &darwinConfigurator{logf: logf, ifName: ifName}, nil
}

// darwinConfigurator is the tailscaled-on-macOS DNS OS configurator that
// maintains the Split DNS nameserver entries pointing MagicDNS DNS suffixes
// to 100.100.100.100 using the macOS /etc/resolver/$SUFFIX files.
type darwinConfigurator struct {
	logf   logger.Logf
	ifName string
}

func (c *darwinConfigurator) Close() error {
	c.removeResolverFiles(func(domain string) bool { return true })
	return nil
}

func (c *darwinConfigurator) SupportsSplitDNS() bool {
	return true
}

func (c *darwinConfigurator) SetDNS(cfg OSConfig) error {
	var buf bytes.Buffer
	buf.WriteString(macResolverFileHeader)
	for i, ip := range cfg.Nameservers {
		if i == 0 {
			buf.WriteString("nameserver ")
		} else {
			buf.WriteString(" ")
		}
		buf.WriteString(ip.String())
	}
	buf.WriteString("\n")

	if err := os.MkdirAll("/etc/resolver", 0755); err != nil {
		return err
	}

	var keep map[string]bool

	// Add a dummy file to /etc/resolver with a "search ..." directive if we have
	// search suffixes to add.
	if len(cfg.SearchDomains) > 0 {
		const searchFile = "search.tailscale" // fake DNS suffix+TLD to put our search
		mak.Set(&keep, searchFile, true)
		var sbuf bytes.Buffer
		sbuf.WriteString(macResolverFileHeader)
		sbuf.WriteString("search")
		for _, d := range cfg.SearchDomains {
			sbuf.WriteString(" ")
			sbuf.WriteString(string(d.WithoutTrailingDot()))
		}
		sbuf.WriteString("\n")
		if err := os.WriteFile("/etc/resolver/"+searchFile, sbuf.Bytes(), 0644); err != nil {
			return err
		}
	}

	for _, d := range cfg.MatchDomains {
		fileBase := string(d.WithoutTrailingDot())
		mak.Set(&keep, fileBase, true)
		fullPath := "/etc/resolver/" + fileBase

		if err := os.WriteFile(fullPath, buf.Bytes(), 0644); err != nil {
			return err
		}
	}
	return c.removeResolverFiles(func(domain string) bool { return !keep[domain] })
}

func (c *darwinConfigurator) GetBaseConfig() (OSConfig, error) {
	return OSConfig{}, errors.New("[unexpected] unreachable")
}

const macResolverFileHeader = "# Added by tailscaled\n"

// removeResolverFiles deletes all files in /etc/resolver for which the shouldDelete
// func returns true.
func (c *darwinConfigurator) removeResolverFiles(shouldDelete func(domain string) bool) error {
	dents, err := os.ReadDir("/etc/resolver")
	if os.IsNotExist(err) {
		return nil
	}
	if err != nil {
		return err
	}
	for _, de := range dents {
		if !de.Type().IsRegular() {
			continue
		}
		name := de.Name()
		if !shouldDelete(name) {
			continue
		}
		fullPath := "/etc/resolver/" + name
		contents, err := os.ReadFile(fullPath)
		if err != nil {
			if os.IsNotExist(err) { // race?
				continue
			}
			return err
		}
		if !mem.HasPrefix(mem.B(contents), mem.S(macResolverFileHeader)) {
			continue
		}
		if err := os.Remove(fullPath); err != nil {
			return err
		}
	}
	return nil
}
