package dnsmonitor

import (
	"errors"
	"net"
	"strings"

	"github.com/miekg/dns"

	"github.com/safing/portmaster/base/database"
	"github.com/safing/portmaster/base/log"
	"github.com/safing/portmaster/service/compat"
	"github.com/safing/portmaster/service/integration"
	"github.com/safing/portmaster/service/mgr"
	"github.com/safing/portmaster/service/network/netutils"
	"github.com/safing/portmaster/service/resolver"
)

var ResolverInfo = resolver.ResolverInfo{
	Name: "SystemResolver",
	Type: resolver.ServerTypeMonitor,
}

type DNSMonitor struct {
	instance instance
	mgr      *mgr.Manager

	listener *Listener
}

// Manager returns the module manager.
func (dl *DNSMonitor) Manager() *mgr.Manager {
	return dl.mgr
}

// Start starts the module.
func (dl *DNSMonitor) Start() error {
	// Initialize dns event listener
	var err error
	dl.listener, err = newListener(dl)
	if err != nil {
		log.Warningf("dnsmonitor: failed to start dns listener: %s", err)
	}

	return nil
}

// Stop stops the module.
func (dl *DNSMonitor) Stop() error {
	if dl.listener != nil {
		err := dl.listener.stop()
		if err != nil {
			log.Errorf("dnsmonitor: failed to close listener: %s", err)
		}
	}
	return nil
}

// Flush flushes the buffer forcing all events to be processed.
func (dl *DNSMonitor) Flush() error {
	return dl.listener.flush()
}

func saveDomain(domain string, ips []net.IP, cnames map[string]string, profileScope string) {
	fqdn := dns.Fqdn(domain)
	// Create new record for this IP.
	record := resolver.ResolvedDomain{
		Domain:            fqdn,
		Resolver:          &ResolverInfo,
		DNSRequestContext: &resolver.DNSRequestContext{},
		Expires:           0,
	}

	// Process cnames
	record.AddCNAMEs(cnames)

	// Add to cache
	saveIPsInCache(ips, profileScope, record)
}

func New(instance instance) (*DNSMonitor, error) {
	// Initialize module
	m := mgr.New("DNSMonitor")
	module := &DNSMonitor{
		mgr:      m,
		instance: instance,
	}

	return module, nil
}

type instance interface {
	OSIntegration() *integration.OSIntegration
}

func processIfSelfCheckDomain(fqdn string) bool {
	// Check for compat check dns request.
	if strings.HasSuffix(fqdn, compat.DNSCheckInternalDomainScope) {
		subdomain := strings.TrimSuffix(fqdn, compat.DNSCheckInternalDomainScope)
		_ = compat.SubmitDNSCheckDomain(subdomain)
		log.Infof("dnsmonitor: self-check domain received")
		// No need to parse the answer.
		return true
	}

	return false
}

// saveIPsInCache saves the provided ips in the dns cashe assoseted with the record Domain and CNAMEs.
func saveIPsInCache(ips []net.IP, profileID string, record resolver.ResolvedDomain) {
	// Package IPs and CNAMEs into IPInfo structs.
	for _, ip := range ips {
		// Never save domain attributions for localhost IPs.
		if netutils.GetIPScope(ip) == netutils.HostLocal {
			continue
		}

		ipString := ip.String()
		info, err := resolver.GetIPInfo(profileID, ipString)
		if err != nil {
			if !errors.Is(err, database.ErrNotFound) {
				log.Errorf("dnsmonitor: failed to search for IP info record: %s", err)
			}

			info = &resolver.IPInfo{
				IP:        ipString,
				ProfileID: profileID,
			}
		}

		// Add the new record to the resolved domains for this IP and scope.
		info.AddDomain(record)

		// Save if the record is new or has been updated.
		if err := info.Save(); err != nil {
			log.Errorf("dnsmonitor: failed to save IP info record: %s", err)
		}
	}
}