package dnsserver

import (
	"fmt"
	"net"
	"time"

	"github.com/coredns/caddy"
	"github.com/coredns/caddy/caddyfile"
	"github.com/coredns/coredns/plugin"
	"github.com/coredns/coredns/plugin/pkg/parse"
	"github.com/coredns/coredns/plugin/pkg/transport"

	"github.com/miekg/dns"
)

const serverType = "dns"

func init() {
	caddy.RegisterServerType(serverType, caddy.ServerType{
		Directives: func() []string { return Directives },
		DefaultInput: func() caddy.Input {
			return caddy.CaddyfileInput{
				Filepath:       "Corefile",
				Contents:       []byte(".:" + Port + " {\nwhoami\nlog\n}\n"),
				ServerTypeName: serverType,
			}
		},
		NewContext: newContext,
	})
}

func newContext(_i *caddy.Instance) caddy.Context {
	return &dnsContext{keysToConfigs: make(map[string]*Config)}
}

type dnsContext struct {
	keysToConfigs map[string]*Config

	// configs is the master list of all site configs.
	configs []*Config
}

func (h *dnsContext) saveConfig(key string, cfg *Config) {
	h.configs = append(h.configs, cfg)
	h.keysToConfigs[key] = cfg
}

// Compile-time check to ensure dnsContext implements the caddy.Context interface
var _ caddy.Context = &dnsContext{}

// InspectServerBlocks make sure that everything checks out before
// executing directives and otherwise prepares the directives to
// be parsed and executed.
func (h *dnsContext) InspectServerBlocks(_sourceFile string, serverBlocks []caddyfile.ServerBlock) ([]caddyfile.ServerBlock, error) {
	// Normalize and check all the zone names and check for duplicates
	for ib, s := range serverBlocks {
		// Walk the s.Keys and expand any reverse address in their proper DNS in-addr zones. If the expansions leads for
		// more than one reverse zone, replace the current value and add the rest to s.Keys.
		zoneAddrs := []zoneAddr{}
		for ik, k := range s.Keys {
			trans, k1 := parse.Transport(k) // get rid of any dns:// or other scheme.
			hosts, port, err := plugin.SplitHostPort(k1)
			// We need to make this a fully qualified domain name to catch all errors here and not later when
			// plugin.Normalize is called again on these strings, with the prime difference being that the domain
			// name is fully qualified. This was found by fuzzing where "ȶ" is deemed OK, but "ȶ." is not (might be a
			// bug in miekg/dns actually). But here we were checking ȶ, which is OK, and later we barf in ȶ. leading to
			// "index out of range".
			for ih := range hosts {
				_, _, err := plugin.SplitHostPort(dns.Fqdn(hosts[ih]))
				if err != nil {
					return nil, err
				}
			}
			if err != nil {
				return nil, err
			}

			if port == "" {
				switch trans {
				case transport.DNS:
					port = Port
				case transport.TLS:
					port = transport.TLSPort
				case transport.QUIC:
					port = transport.QUICPort
				case transport.GRPC:
					port = transport.GRPCPort
				case transport.HTTPS:
					port = transport.HTTPSPort
				case transport.HTTPS3:
					port = transport.HTTPSPort
				}
			}

			if len(hosts) > 1 {
				s.Keys[ik] = hosts[0] + ":" + port // replace for the first
				for _, h := range hosts[1:] {      // add the rest
					s.Keys = append(s.Keys, h+":"+port)
				}
			}
			for i := range hosts {
				zoneAddrs = append(zoneAddrs, zoneAddr{Zone: plugin.Name(hosts[i]).Normalize(), Port: port, Transport: trans})
			}
		}

		serverBlocks[ib].Keys = s.Keys // important to save back the new keys that are potentially created here.

		var firstConfigInBlock *Config

		for ik := range s.Keys {
			za := zoneAddrs[ik]
			s.Keys[ik] = za.String()
			// Save the config to our master list, and key it for lookups.
			cfg := &Config{
				Zone:        za.Zone,
				ListenHosts: []string{""},
				Port:        za.Port,
				Transport:   za.Transport,
			}

			// Set reference to the first config in the current block.
			// This is used later by MakeServers to share a single plugin list
			// for all zones in a server block.
			if ik == 0 {
				firstConfigInBlock = cfg
			}
			cfg.firstConfigInBlock = firstConfigInBlock

			keyConfig := keyForConfig(ib, ik)
			h.saveConfig(keyConfig, cfg)
		}
	}
	return serverBlocks, nil
}

// MakeServers uses the newly-created siteConfigs to create and return a list of server instances.
func (h *dnsContext) MakeServers() ([]caddy.Server, error) {
	// Copy parameters from first config in the block to all other config in the same block
	propagateConfigParams(h.configs)

	// we must map (group) each config to a bind address
	groups, err := groupConfigsByListenAddr(h.configs)
	if err != nil {
		return nil, err
	}

	// then we create a server for each group
	var servers []caddy.Server
	for addr, group := range groups {
		serversForGroup, err := makeServersForGroup(addr, group)
		if err != nil {
			return nil, err
		}
		servers = append(servers, serversForGroup...)
	}

	// For each server config, check for View Filter plugins
	for _, c := range h.configs {
		// Add filters in the plugin.cfg order for consistent filter func evaluation order.
		for _, d := range Directives {
			if vf, ok := c.registry[d].(Viewer); ok {
				if c.ViewName != "" {
					return nil, fmt.Errorf("multiple views defined in server block")
				}
				c.ViewName = vf.ViewName()
				c.FilterFuncs = append(c.FilterFuncs, vf.Filter)
			}
		}
	}

	// Verify that there is no overlap on the zones and listen addresses
	// for unfiltered server configs
	errValid := h.validateZonesAndListeningAddresses()
	if errValid != nil {
		return nil, errValid
	}

	return servers, nil
}

// AddPlugin adds a plugin to a site's plugin stack.
func (c *Config) AddPlugin(m plugin.Plugin) {
	c.Plugin = append(c.Plugin, m)
}

// AllowOpcode permits a non-default DNS opcode to reach this config's plugin chain
// on UDP, TCP, and DNS-over-TLS listeners. Plugins should call it during setup.
// The listener still requires exactly one question, and configs that do not opt in
// continue to reject the opcode.
func (c *Config) AllowOpcode(opcode int) {
	if c.allowedOpcodes == nil {
		c.allowedOpcodes = make(map[int]struct{})
	}
	c.allowedOpcodes[opcode] = struct{}{}
}

// registerHandler adds a handler to a site's handler registration. Handlers
//
//	use this to announce that they exist to other plugin.
func (c *Config) registerHandler(h plugin.Handler) {
	if c.registry == nil {
		c.registry = make(map[string]plugin.Handler)
	}

	// Just overwrite...
	c.registry[h.Name()] = h
}

// Handler returns the plugin handler that has been added to the config under its name.
// This is useful to inspect if a certain plugin is active in this server.
// Note that this is order dependent and the order is defined in directives.go, i.e. if your plugin
// comes before the plugin you are checking; it will not be there (yet).
func (c *Config) Handler(name string) plugin.Handler {
	if c.registry == nil {
		return nil
	}
	if h, ok := c.registry[name]; ok {
		return h
	}
	return nil
}

// Handlers returns a slice of plugins that have been registered. This can be used to
// inspect and interact with registered plugins but cannot be used to remove or add plugins.
// Note that this is order dependent and the order is defined in directives.go, i.e. if your plugin
// comes before the plugin you are checking; it will not be there (yet).
func (c *Config) Handlers() []plugin.Handler {
	if c.registry == nil {
		return nil
	}
	hs := make([]plugin.Handler, 0, len(c.registry))
	for _, k := range Directives {
		registry := c.Handler(k)
		if registry != nil {
			hs = append(hs, registry)
		}
	}
	return hs
}

func (h *dnsContext) validateZonesAndListeningAddresses() error {
	//Validate Zone and addresses
	checker := newOverlapZone()
	for _, conf := range h.configs {
		for _, h := range conf.ListenHosts {
			// Validate the overlapping of ZoneAddr
			akey := zoneAddr{Transport: conf.Transport, Zone: conf.Zone, Address: h, Port: conf.Port}
			var existZone, overlapZone *zoneAddr
			if len(conf.FilterFuncs) > 0 {
				// This config has filters (e.g. view plugin). It is allowed to
				// share a zone/port with an unfiltered server block, so we only
				// check without registering and skip the "already defined" error.
				_, overlapZone = checker.check(akey)
			} else {
				// This config has no filters. Check for overlap with other
				// unfiltered configs and register the zone.
				existZone, overlapZone = checker.registerAndCheck(akey)
			}
			if existZone != nil {
				return fmt.Errorf("cannot serve %s - it is already defined", akey.String())
			}
			if overlapZone != nil {
				return fmt.Errorf("cannot serve %s - zone overlap listener capacity with %v", akey.String(), overlapZone.String())
			}
		}
	}
	return nil
}

// propagateConfigParams copies the necessary parameters from first config in the block
// to all other config in the same block. Doing this results in zones
// sharing the same plugin instances and settings as other zones in
// the same block.
func propagateConfigParams(configs []*Config) {
	for _, c := range configs {
		c.Plugin = c.firstConfigInBlock.Plugin
		c.ListenHosts = c.firstConfigInBlock.ListenHosts
		c.Debug = c.firstConfigInBlock.Debug
		c.Stacktrace = c.firstConfigInBlock.Stacktrace
		c.NumSockets = c.firstConfigInBlock.NumSockets

		// Fork TLSConfig for each encrypted connection
		c.TLSConfig = c.firstConfigInBlock.TLSConfig.Clone()
		c.ReadTimeout = c.firstConfigInBlock.ReadTimeout
		c.WriteTimeout = c.firstConfigInBlock.WriteTimeout
		c.IdleTimeout = c.firstConfigInBlock.IdleTimeout
		c.MaxTCPQueries = c.firstConfigInBlock.MaxTCPQueries
		c.TsigSecret = c.firstConfigInBlock.TsigSecret
		c.allowedOpcodes = c.firstConfigInBlock.allowedOpcodes

		// Propagate HTTPRequestValidateFunc so that custom path validators work in
		// multi-transport blocks. Otherwise HTTPS 404s on non-"/dns-query" paths.
		c.HTTPRequestValidateFunc = c.firstConfigInBlock.HTTPRequestValidateFunc

		// Propagate UDPDecorateWriterFunc so a decorator configured once in a
		// server block applies to the block's UDP listener(s).
		c.UDPDecorateWriterFunc = c.firstConfigInBlock.UDPDecorateWriterFunc
	}
}

// groupConfigsByListenAddr groups site configs by their listen
// (bind) address, so sites that use the same listener can be served
// on the same server instance. The return value maps the listen
// address (what you pass into net.Listen) to the list of site configs.
// This function does NOT vet the configs to ensure they are compatible.
func groupConfigsByListenAddr(configs []*Config) (map[string][]*Config, error) {
	groups := make(map[string][]*Config)
	for _, conf := range configs {
		for _, h := range conf.ListenHosts {
			addr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(h, conf.Port))
			if err != nil {
				return nil, err
			}
			addrstr := conf.Transport + "://" + addr.String()
			groups[addrstr] = append(groups[addrstr], conf)
		}
	}

	return groups, nil
}

// makeServersForGroup creates servers for a specific transport and group.
// It creates as many servers as specified in the NumSockets configuration.
// If the NumSockets param is not specified, one server is created by default.
func makeServersForGroup(addr string, group []*Config) ([]caddy.Server, error) {
	// that is impossible, but better to check
	if len(group) == 0 {
		return nil, fmt.Errorf("no configs for group defined")
	}
	// create one server by default if no NumSockets specified
	numSockets := 1
	if group[0].NumSockets > 0 {
		numSockets = group[0].NumSockets
	}

	var servers []caddy.Server
	for range numSockets {
		// switch on addr
		switch tr, _ := parse.Transport(addr); tr {
		case transport.DNS:
			s, err := NewServer(addr, group)
			if err != nil {
				return nil, err
			}
			servers = append(servers, s)

		case transport.TLS:
			s, err := NewServerTLS(addr, group)
			if err != nil {
				return nil, err
			}
			servers = append(servers, s)

		case transport.QUIC:
			s, err := NewServerQUIC(addr, group)
			if err != nil {
				return nil, err
			}
			servers = append(servers, s)

		case transport.GRPC:
			s, err := NewServergRPC(addr, group)
			if err != nil {
				return nil, err
			}
			servers = append(servers, s)

		case transport.HTTPS:
			s, err := NewServerHTTPS(addr, group)
			if err != nil {
				return nil, err
			}
			servers = append(servers, s)

		case transport.HTTPS3:
			s, err := NewServerHTTPS3(addr, group)
			if err != nil {
				return nil, err
			}
			servers = append(servers, s)
		}
	}
	return servers, nil
}

// DefaultPort is the default port.
const DefaultPort = transport.Port

// These "soft defaults" are configurable by
// command line flags, etc.
var (
	// Port is the port we listen on by default.
	Port = DefaultPort

	// GracefulTimeout is the maximum duration of a graceful shutdown.
	GracefulTimeout time.Duration
)
