// Package dhcpd provides a DHCP server.
package dhcpd

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

	"github.com/AdguardTeam/AdGuardHome/internal/dhcpsvc"
	"github.com/AdguardTeam/golibs/logutil/slogutil"
	"github.com/AdguardTeam/golibs/timeutil"
)

const (
	// DefaultDHCPLeaseTTL is the default time-to-live for leases.
	DefaultDHCPLeaseTTL = uint32(timeutil.Day / time.Second)

	// DefaultDHCPTimeoutICMP is the default timeout for waiting ICMP responses.
	DefaultDHCPTimeoutICMP = 1000
)

// Currently used defaults for ifaceDNSAddrs.
const (
	defaultMaxAttempts int           = 10
	defaultBackoff     time.Duration = 500 * time.Millisecond
)

// OnLeaseChangedT is a callback for lease changes.
type OnLeaseChangedT func(flags int)

// flags for onLeaseChanged()
const (
	LeaseChangedAdded = iota
	LeaseChangedAddedStatic
	LeaseChangedRemovedStatic
	LeaseChangedRemovedAll

	LeaseChangedDBStore
)

// GetLeasesFlags are the flags for GetLeases.
type GetLeasesFlags uint8

// GetLeasesFlags values
const (
	LeasesDynamic GetLeasesFlags = 0b01
	LeasesStatic  GetLeasesFlags = 0b10

	LeasesAll = LeasesDynamic | LeasesStatic
)

// Interface is the DHCP server that deals with both IP address families.
type Interface interface {
	Start(ctx context.Context) (err error)
	Stop() (err error)

	// Enabled returns true if the DHCP server is running.
	//
	// TODO(e.burkov):  Currently, we need this method to determine whether the
	// local domain suffix should be considered while resolving A/AAAA requests.
	// This is because other parts of the code aren't aware of the DNS suffixes
	// in DHCP clients names and caller is responsible for trimming it.  This
	// behavior should be changed in the future.
	Enabled() (ok bool)

	// Leases returns all the leases in the database.
	Leases() (leases []*dhcpsvc.Lease)

	// MacByIP returns the MAC address of a client with ip.  It returns nil if
	// there is no such client, due to an assumption that a DHCP client must
	// always have a HardwareAddr.
	MACByIP(ip netip.Addr) (mac net.HardwareAddr)

	// HostByIP returns the hostname of the DHCP client with the given IP
	// address.  The address will be netip.Addr{} if there is no such client,
	// due to an assumption that a DHCP client must always have an IP address.
	HostByIP(ip netip.Addr) (host string)

	// IPByHost returns the IP address of the DHCP client with the given
	// hostname.  The address will be netip.Addr{} if there is no such client,
	// due to an assumption that a DHCP client must always have an IP address.
	IPByHost(host string) (ip netip.Addr)

	WriteDiskConfig(c *ServerConfig)
}

// server is the DHCP service that handles DHCPv4, DHCPv6, and HTTP API.
type server struct {
	srv4 DHCPServer
	srv6 DHCPServer

	// TODO(a.garipov): Either create a separate type for the internal config or
	// just put the config values into Server.
	conf *ServerConfig

	// Called when the leases DB is modified
	onLeaseChanged []OnLeaseChangedT
}

// type check
var _ Interface = (*server)(nil)

// Create initializes and returns the DHCP server handling both address
// families.  It also registers the corresponding HTTP API endpoints.
func Create(ctx context.Context, conf *ServerConfig) (s *server, err error) {
	s = &server{
		conf: &ServerConfig{
			Logger:             conf.Logger,
			CommandConstructor: conf.CommandConstructor,
			ConfModifier:       conf.ConfModifier,

			HTTPRegister: conf.HTTPRegister,

			Enabled:       conf.Enabled,
			InterfaceName: conf.InterfaceName,

			LocalDomainName: conf.LocalDomainName,

			dbFilePath: filepath.Join(conf.DataDir, dataFilename),
		},
	}

	// TODO(e.burkov):  Don't register handlers, see TODO on
	// [aghhttp.RegisterFunc].
	s.registerHandlers()

	v4Enabled, v6Enabled, err := s.setServers(ctx, conf)
	if err != nil {
		// Don't wrap the error, because it's informative enough as is.
		return nil, err
	}

	s.conf.Conf4 = conf.Conf4
	s.conf.Conf6 = conf.Conf6

	if s.conf.Enabled && !v4Enabled && !v6Enabled {
		return nil, fmt.Errorf("neither dhcpv4 nor dhcpv6 srv is configured")
	}

	// Migrate leases db if needed.
	err = migrateDB(conf)
	if err != nil {
		// Don't wrap the error since it's informative enough as is.
		return nil, err
	}

	// Don't delay database loading until the DHCP server is started,
	// because we need static leases functionality available beforehand.
	err = s.dbLoad()
	if err != nil {
		return nil, fmt.Errorf("loading db: %w", err)
	}

	return s, nil
}

// setServers updates DHCPv4 and DHCPv6 servers created from the provided
// configuration conf.  It returns the status of both the DHCPv4 and the DHCPv6
// servers, which is always false for corresponding server on any error.
func (s *server) setServers(
	ctx context.Context,
	conf *ServerConfig,
) (v4Enabled, v6Enabled bool, err error) {
	v4conf := conf.Conf4
	v4conf.Logger = s.conf.Logger.With("ip_version", "4")
	v4conf.InterfaceName = s.conf.InterfaceName
	v4conf.notify = s.onNotify
	v4conf.Enabled = s.conf.Enabled && v4conf.RangeStart.IsValid()

	s.srv4, err = v4Create(&v4conf)
	if err != nil {
		if v4conf.Enabled {
			return false, false, fmt.Errorf("creating dhcpv4 srv: %w", err)
		}

		s.conf.Logger.WarnContext(ctx, "creating dhcpv4 server", slogutil.KeyError, err)
	}

	v6conf := conf.Conf6
	v6conf.Logger = s.conf.Logger.With("ip_version", "6")
	v6conf.InterfaceName = s.conf.InterfaceName
	v6conf.notify = s.onNotify
	v6conf.Enabled = s.conf.Enabled && len(v6conf.RangeStart) != 0

	s.srv6, err = v6Create(v6conf)
	if err != nil {
		return v4conf.Enabled, false, fmt.Errorf("creating dhcpv6 srv: %w", err)
	}

	return v4conf.Enabled, v6conf.Enabled, nil
}

// Enabled returns true when the server is enabled.
func (s *server) Enabled() (ok bool) {
	return s.conf.Enabled
}

// resetLeases resets all leases in the lease database.
func (s *server) resetLeases() (err error) {
	err = s.srv4.ResetLeases(nil)
	if err != nil {
		return err
	}

	if s.srv6 != nil {
		err = s.srv6.ResetLeases(nil)
		if err != nil {
			return err
		}
	}

	return s.dbStore()
}

// server calls this function after DB is updated
func (s *server) onNotify(flags uint32) {
	if flags == LeaseChangedDBStore {
		err := s.dbStore()
		if err != nil {
			// TODO(s.chzhen):  Pass context.
			ctx := context.TODO()
			s.conf.Logger.ErrorContext(ctx, "updating db", slogutil.KeyError, err)
		}

		return
	}

	s.notify(int(flags))
}

func (s *server) notify(flags int) {
	for _, f := range s.onLeaseChanged {
		f(flags)
	}
}

// WriteDiskConfig - write configuration
func (s *server) WriteDiskConfig(c *ServerConfig) {
	c.Enabled = s.conf.Enabled
	c.InterfaceName = s.conf.InterfaceName
	c.LocalDomainName = s.conf.LocalDomainName

	s.srv4.WriteDiskConfig4(&c.Conf4)
	s.srv6.WriteDiskConfig6(&c.Conf6)
}

// Start will listen on port 67 and serve DHCP requests.
func (s *server) Start(ctx context.Context) (err error) {
	err = s.srv4.Start(ctx)
	if err != nil {
		return err
	}

	err = s.srv6.Start(ctx)
	if err != nil {
		return err
	}

	return nil
}

// Stop closes the listening UDP socket
func (s *server) Stop() (err error) {
	err = s.srv4.Stop()
	if err != nil {
		return err
	}

	err = s.srv6.Stop()
	if err != nil {
		return err
	}

	return nil
}

// Leases returns the list of active DHCP leases.
func (s *server) Leases() (leases []*dhcpsvc.Lease) {
	return append(s.srv4.GetLeases(LeasesAll), s.srv6.GetLeases(LeasesAll)...)
}

// MACByIP returns a MAC address by the IP address of its lease, if there is
// one.
func (s *server) MACByIP(ip netip.Addr) (mac net.HardwareAddr) {
	if ip.Is4() {
		return s.srv4.FindMACbyIP(ip)
	}

	return s.srv6.FindMACbyIP(ip)
}

// HostByIP implements the [Interface] interface for *server.
//
// TODO(e.burkov):  Implement this method for DHCPv6.
func (s *server) HostByIP(ip netip.Addr) (host string) {
	if ip.Is4() {
		return s.srv4.HostByIP(ip)
	}

	return ""
}

// IPByHost implements the [Interface] interface for *server.
//
// TODO(e.burkov):  Implement this method for DHCPv6.
func (s *server) IPByHost(host string) (ip netip.Addr) {
	return s.srv4.IPByHost(host)
}

// AddStaticLease - add static v4 lease
func (s *server) AddStaticLease(l *dhcpsvc.Lease) error {
	return s.srv4.AddStaticLease(l)
}
