package dhcpd

import (
	"fmt"
	"net"
	"time"

	"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
	"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
	"github.com/AdguardTeam/golibs/errors"
	"github.com/AdguardTeam/golibs/netutil"
)

// ServerConfig is the configuration for the DHCP server.  The order of YAML
// fields is important, since the YAML configuration file follows it.
type ServerConfig struct {
	// Called when the configuration is changed by HTTP request
	ConfigModified func() `yaml:"-"`

	// Register an HTTP handler
	HTTPRegister aghhttp.RegisterFunc `yaml:"-"`

	Enabled       bool   `yaml:"enabled"`
	InterfaceName string `yaml:"interface_name"`

	// LocalDomainName is the domain name used for DHCP hosts.  For example,
	// a DHCP client with the hostname "myhost" can be addressed as "myhost.lan"
	// when LocalDomainName is "lan".
	LocalDomainName string `yaml:"local_domain_name"`

	Conf4 V4ServerConf `yaml:"dhcpv4"`
	Conf6 V6ServerConf `yaml:"dhcpv6"`

	WorkDir    string `yaml:"-"`
	DBFilePath string `yaml:"-"`
}

// DHCPServer - DHCP server interface
type DHCPServer interface {
	// ResetLeases resets leases.
	ResetLeases(leases []*Lease) (err error)
	// GetLeases returns deep clones of the current leases.
	GetLeases(flags GetLeasesFlags) (leases []*Lease)
	// AddStaticLease - add a static lease
	AddStaticLease(l *Lease) (err error)
	// RemoveStaticLease - remove a static lease
	RemoveStaticLease(l *Lease) (err error)
	// FindMACbyIP - find a MAC address by IP address in the currently active DHCP leases
	FindMACbyIP(ip net.IP) net.HardwareAddr

	// WriteDiskConfig4 - copy disk configuration
	WriteDiskConfig4(c *V4ServerConf)
	// WriteDiskConfig6 - copy disk configuration
	WriteDiskConfig6(c *V6ServerConf)

	// Start - start server
	Start() (err error)
	// Stop - stop server
	Stop() (err error)
	getLeasesRef() []*Lease
}

// V4ServerConf - server configuration
type V4ServerConf struct {
	Enabled       bool   `yaml:"-" json:"-"`
	InterfaceName string `yaml:"-" json:"-"`

	GatewayIP  net.IP `yaml:"gateway_ip" json:"gateway_ip"`
	SubnetMask net.IP `yaml:"subnet_mask" json:"subnet_mask"`
	// broadcastIP is the broadcasting address pre-calculated from the
	// configured gateway IP and subnet mask.
	broadcastIP net.IP

	// The first & the last IP address for dynamic leases
	// Bytes [0..2] of the last allowed IP address must match the first IP
	RangeStart net.IP `yaml:"range_start" json:"range_start"`
	RangeEnd   net.IP `yaml:"range_end" json:"range_end"`

	LeaseDuration uint32 `yaml:"lease_duration" json:"lease_duration"` // in seconds

	// IP conflict detector: time (ms) to wait for ICMP reply
	// 0: disable
	ICMPTimeout uint32 `yaml:"icmp_timeout_msec" json:"-"`

	// Custom Options.
	//
	// Option with arbitrary hexadecimal data:
	//     DEC_CODE hex HEX_DATA
	// where DEC_CODE is a decimal DHCPv4 option code in range [1..255]
	//
	// Option with IP data (only 1 IP is supported):
	//     DEC_CODE ip IP_ADDR
	Options []string `yaml:"options" json:"-"`

	ipRange *ipRange

	leaseTime  time.Duration // the time during which a dynamic lease is considered valid
	dnsIPAddrs []net.IP      // IPv4 addresses to return to DHCP clients as DNS server addresses

	// subnet contains the DHCP server's subnet.  The IP is the IP of the
	// gateway.
	subnet *net.IPNet

	// notify is a way to signal to other components that leases have been
	// changed.  notify must be called outside of locked sections, since the
	// clients might want to get the new data.
	//
	// TODO(a.garipov): This is utter madness and must be refactored.  It just
	// begs for deadlock bugs and other nastiness.
	notify func(uint32)
}

// errNilConfig is an error returned by validation method if the config is nil.
const errNilConfig errors.Error = "nil config"

// ensureV4 returns a 4-byte version of ip.  An error is returned if the passed
// ip is not an IPv4.
func ensureV4(ip net.IP) (ip4 net.IP, err error) {
	if ip == nil {
		return nil, fmt.Errorf("%v is not an IP address", ip)
	}

	ip4 = ip.To4()
	if ip4 == nil {
		return nil, fmt.Errorf("%v is not an IPv4 address", ip)
	}

	return ip4, nil
}

// Validate returns an error if c is not a valid configuration.
//
// TODO(e.burkov):  Don't set the config fields when the server itself will stop
// containing the config.
func (c *V4ServerConf) Validate() (err error) {
	defer func() { err = errors.Annotate(err, "dhcpv4: %w") }()

	if c == nil {
		return errNilConfig
	}

	var gatewayIP net.IP
	gatewayIP, err = ensureV4(c.GatewayIP)
	if err != nil {
		// Don't wrap an errors since it's inforative enough as is and there is
		// an annotation deferred already.
		return err
	}

	if c.SubnetMask == nil {
		return fmt.Errorf("invalid subnet mask: %v", c.SubnetMask)
	}

	subnetMask := net.IPMask(netutil.CloneIP(c.SubnetMask.To4()))
	c.subnet = &net.IPNet{
		IP:   gatewayIP,
		Mask: subnetMask,
	}
	c.broadcastIP = aghnet.BroadcastFromIPNet(c.subnet)

	c.ipRange, err = newIPRange(c.RangeStart, c.RangeEnd)
	if err != nil {
		// Don't wrap an errors since it's inforative enough as is and there is
		// an annotation deferred already.
		return err
	}

	if c.ipRange.contains(gatewayIP) {
		return fmt.Errorf("gateway ip %v in the ip range: %v-%v",
			gatewayIP,
			c.RangeStart,
			c.RangeEnd,
		)
	}

	if !c.subnet.Contains(c.RangeStart) {
		return fmt.Errorf("range start %v is outside network %v",
			c.RangeStart,
			c.subnet,
		)
	}

	if !c.subnet.Contains(c.RangeEnd) {
		return fmt.Errorf("range end %v is outside network %v",
			c.RangeEnd,
			c.subnet,
		)
	}

	return nil
}

// V6ServerConf - server configuration
type V6ServerConf struct {
	Enabled       bool   `yaml:"-" json:"-"`
	InterfaceName string `yaml:"-" json:"-"`

	// The first IP address for dynamic leases
	// The last allowed IP address ends with 0xff byte
	RangeStart net.IP `yaml:"range_start" json:"range_start"`

	LeaseDuration uint32 `yaml:"lease_duration" json:"lease_duration"` // in seconds

	RASLAACOnly  bool `yaml:"ra_slaac_only" json:"-"`  // send ICMPv6.RA packets without MO flags
	RAAllowSLAAC bool `yaml:"ra_allow_slaac" json:"-"` // send ICMPv6.RA packets with MO flags

	ipStart    net.IP        // starting IP address for dynamic leases
	leaseTime  time.Duration // the time during which a dynamic lease is considered valid
	dnsIPAddrs []net.IP      // IPv6 addresses to return to DHCP clients as DNS server addresses

	// Server calls this function when leases data changes
	notify func(uint32)
}