package cli

import (
	"net"
	"net/netip"
	"path/filepath"
	"time"

	"github.com/fsnotify/fsnotify"

	"github.com/Control-D-Inc/ctrld/internal/resolvconffile"
)

// parseResolvConfNameservers reads the resolv.conf file and returns the nameservers found.
// Returns nil if no nameservers are found.
// This function parses the system DNS configuration to understand current nameserver settings
func (p *prog) parseResolvConfNameservers(path string) ([]string, error) {
	return resolvconffile.NameserversFromFile(path)
}

// watchResolvConf watches any changes to /etc/resolv.conf file,
// and reverting to the original config set by ctrld.
// This ensures that DNS settings are not overridden by other applications or system processes
func (p *prog) watchResolvConf(iface *net.Interface, ns []netip.Addr, setDnsFn func(iface *net.Interface, ns []netip.Addr) error) {
	resolvConfPath := "/etc/resolv.conf"
	// Evaluating symbolics link to watch the target file that /etc/resolv.conf point to.
	// This handles systems where resolv.conf is a symlink to another location
	if rp, _ := filepath.EvalSymlinks(resolvConfPath); rp != "" {
		resolvConfPath = rp
	}
	p.Debug().Msgf("Start watching %s file", resolvConfPath)
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		p.Warn().Err(err).Msg("Could not create watcher for /etc/resolv.conf")
		return
	}
	defer watcher.Close()

	// We watch /etc instead of /etc/resolv.conf directly,
	// see: https://github.com/fsnotify/fsnotify#watching-a-file-doesnt-work-well
	// This is necessary because some systems don't properly notify on file changes
	watchDir := filepath.Dir(resolvConfPath)
	if err := watcher.Add(watchDir); err != nil {
		p.Warn().Err(err).Msgf("Could not add %s to watcher list", watchDir)
		return
	}

	for {
		select {
		case <-p.dnsWatcherStopCh:
			return
		case <-p.stopCh:
			p.Debug().Msgf("Stopping watcher for %s", resolvConfPath)
			return
		case event, ok := <-watcher.Events:
			if p.recoveryRunning.Load() {
				return
			}
			if !ok {
				return
			}
			if event.Name != resolvConfPath { // skip if not /etc/resolv.conf changes.
				continue
			}
			if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) {
				p.Debug().Msgf("/etc/resolv.conf changes detected, reading changes...")

				// Convert expected nameservers to strings for comparison
				// This allows us to detect when the resolv.conf has been modified
				expectedNS := make([]string, len(ns))
				for i, addr := range ns {
					expectedNS[i] = addr.String()
				}

				var foundNS []string
				var err error

				maxRetries := 1
				for retry := 0; retry < maxRetries; retry++ {
					foundNS, err = p.parseResolvConfNameservers(resolvConfPath)
					if err != nil {
						p.Error().Err(err).Msg("Failed to read resolv.conf content")
						break
					}

					// If we found nameservers, break out of retry loop
					// This handles cases where the file is being written but not yet complete
					if len(foundNS) > 0 {
						break
					}

					// Only retry if we found no nameservers
					// This handles temporary file states during updates
					if retry < maxRetries-1 {
						p.Debug().Msgf("resolv.conf has no nameserver entries, retry %d/%d in 2 seconds", retry+1, maxRetries)
						select {
						case <-p.stopCh:
							return
						case <-p.dnsWatcherStopCh:
							return
						case <-time.After(2 * time.Second):
							continue
						}
					} else {
						p.Debug().Msg("resolv.conf remained empty after all retries")
					}
				}

				// If we found nameservers, check if they match what we expect
				if len(foundNS) > 0 {
					// Check if the nameservers match exactly what we expect
					matches := len(foundNS) == len(expectedNS)
					if matches {
						for i := range foundNS {
							if foundNS[i] != expectedNS[i] {
								matches = false
								break
							}
						}
					}

					p.Debug().
						Strs("found", foundNS).
						Strs("expected", expectedNS).
						Bool("matches", matches).
						Msg("checking nameservers")

					// Only revert if the nameservers don't match
					if !matches {
						if err := watcher.Remove(watchDir); err != nil {
							p.Error().Err(err).Msg("Failed to pause watcher")
							continue
						}

						if err := setDnsFn(iface, ns); err != nil {
							p.Error().Err(err).Msg("Failed to revert /etc/resolv.conf changes")
						}

						if err := watcher.Add(watchDir); err != nil {
							p.Error().Err(err).Msg("Failed to continue running watcher")
							return
						}
					}
				}
			}
		case err, ok := <-watcher.Errors:
			if !ok {
				return
			}
			p.Error().Err(err).Msg("Could not get event for /etc/resolv.conf")
		}
	}
}
