package nmlite

import (
	"bytes"
	"fmt"
	"html/template"
	"os"
	"strings"

	"github.com/jetkvm/kvm/internal/network/types"
	"github.com/jetkvm/kvm/internal/sync"
	"github.com/jetkvm/kvm/pkg/nmlite/link"
	"github.com/rs/zerolog"
)

const (
	resolvConfPath     = "/etc/resolv.conf"
	resolvConfFileMode = 0644
	resolvConfTemplate = `# the resolv.conf file is managed by JetKVM
# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN

{{ if .searchList }}
search {{ join .searchList " " }}
{{- end -}}
{{ if .domain }}
domain {{ .domain }}
{{- end -}}
{{ range $ns, $comment := .nameservers }}
nameserver {{ printf "%s" $ns }} # {{ join $comment ", " }}
{{- end }}
`
)

var (
	tplFuncMap = template.FuncMap{
		"join": strings.Join,
	}
)

// ResolvConfManager manages the resolv.conf file
type ResolvConfManager struct {
	logger *zerolog.Logger
	mu     sync.Mutex
	conf   *types.ResolvConf

	hostname string
	domain   string
}

// NewResolvConfManager creates a new resolv.conf manager
func NewResolvConfManager(logger *zerolog.Logger) *ResolvConfManager {
	if logger == nil {
		// Create a no-op logger if none provided
		logger = &zerolog.Logger{}
	}

	return &ResolvConfManager{
		logger: logger,
		mu:     sync.Mutex{},
		conf: &types.ResolvConf{
			ConfigIPv4: make(map[string]types.InterfaceResolvConf),
			ConfigIPv6: make(map[string]types.InterfaceResolvConf),
		},
	}
}

// SetInterfaceConfig sets the resolv.conf configuration for a specific interface
func (rcm *ResolvConfManager) SetInterfaceConfig(iface string, family int, config types.InterfaceResolvConf) error {
	// DO NOT USE defer HERE, rcm.update() also locks the mutex
	rcm.mu.Lock()
	switch family {
	case link.AfInet:
		rcm.conf.ConfigIPv4[iface] = config
	case link.AfInet6:
		rcm.conf.ConfigIPv6[iface] = config
	default:
		rcm.mu.Unlock()
		return fmt.Errorf("invalid family: %d", family)
	}
	rcm.mu.Unlock()

	if err := rcm.reconcileHostname(); err != nil {
		return fmt.Errorf("failed to reconcile hostname: %w", err)
	}

	return rcm.update()
}

// SetConfig sets the resolv.conf configuration
func (rcm *ResolvConfManager) SetConfig(resolvConf *types.ResolvConf) error {
	if resolvConf == nil {
		return fmt.Errorf("resolvConf cannot be nil")
	}

	rcm.mu.Lock()
	rcm.conf = resolvConf
	defer rcm.mu.Unlock()

	return rcm.update()
}

// Reconcile reconciles the resolv.conf configuration
func (rcm *ResolvConfManager) Reconcile() error {
	if err := rcm.reconcileHostname(); err != nil {
		return fmt.Errorf("failed to reconcile hostname: %w", err)
	}
	return rcm.update()
}

// Update updates the resolv.conf file
func (rcm *ResolvConfManager) update() error {
	rcm.mu.Lock()
	defer rcm.mu.Unlock()

	rcm.logger.Debug().Msg("updating resolv.conf")

	// Generate resolv.conf content
	content, err := rcm.generateResolvConf(rcm.conf)
	if err != nil {
		return fmt.Errorf("failed to generate resolv.conf: %w", err)
	}

	// Check if the file is the same
	if _, err := os.Stat(resolvConfPath); err == nil {
		existingContent, err := os.ReadFile(resolvConfPath)
		if err != nil {
			rcm.logger.Warn().Err(err).Msg("failed to read existing resolv.conf")
		}

		if bytes.Equal(existingContent, content) {
			rcm.logger.Debug().Msg("resolv.conf is the same, skipping write")
			return nil
		}
	}

	// Write to file
	if err := os.WriteFile(resolvConfPath, content, resolvConfFileMode); err != nil {
		return fmt.Errorf("failed to write resolv.conf: %w", err)
	}

	rcm.logger.Info().
		Interface("config", rcm.conf).
		Msg("resolv.conf updated successfully")

	return nil
}

type configMap map[string][]string

func mergeConfig(nameservers *configMap, searchList *configMap, config *types.InterfaceResolvConfMap) {
	localNameservers := *nameservers
	localSearchList := *searchList

	for ifname, iface := range *config {
		comment := ifname
		if iface.Source != "" {
			comment += fmt.Sprintf(" (%s)", iface.Source)
		}

		for _, ip := range iface.NameServers {
			ns := ip.String()
			if _, ok := localNameservers[ns]; !ok {
				localNameservers[ns] = []string{}
			}
			localNameservers[ns] = append(localNameservers[ns], comment)
		}

		for _, search := range iface.SearchList {
			search = strings.Trim(search, ".")
			if _, ok := localSearchList[search]; !ok {
				localSearchList[search] = []string{}
			}
			localSearchList[search] = append(localSearchList[search], comment)
		}
	}

	*nameservers = localNameservers
	*searchList = localSearchList
}

// generateResolvConf generates resolv.conf content
func (rcm *ResolvConfManager) generateResolvConf(conf *types.ResolvConf) ([]byte, error) {
	tmpl, err := template.New("resolv.conf").Funcs(tplFuncMap).Parse(resolvConfTemplate)
	if err != nil {
		return nil, fmt.Errorf("failed to parse template: %w", err)
	}

	// merge the nameservers and searchList
	nameservers := configMap{}
	searchList := configMap{}

	mergeConfig(&nameservers, &searchList, &conf.ConfigIPv4)
	mergeConfig(&nameservers, &searchList, &conf.ConfigIPv6)

	flattenedSearchList := []string{}
	for search := range searchList {
		flattenedSearchList = append(flattenedSearchList, search)
	}

	var buf bytes.Buffer
	if err := tmpl.Execute(&buf, map[string]any{
		"nameservers": nameservers,
		"searchList":  flattenedSearchList,
	}); err != nil {
		return nil, fmt.Errorf("failed to execute template: %w", err)
	}

	return buf.Bytes(), nil
}
