// On-disk database for lease table

package dhcpd

import (
	"encoding/json"
	"io/ioutil"
	"net"
	"os"
	"time"

	"github.com/AdguardTeam/golibs/file"
	"github.com/AdguardTeam/golibs/log"
	"github.com/krolaw/dhcp4"
)

const dbFilename = "leases.db"

type leaseJSON struct {
	HWAddr   []byte `json:"mac"`
	IP       []byte `json:"ip"`
	Hostname string `json:"host"`
	Expiry   int64  `json:"exp"`
}

// Safe version of dhcp4.IPInRange()
func ipInRange(start, stop, ip net.IP) bool {
	if len(start) != len(stop) ||
		len(start) != len(ip) {
		return false
	}
	return dhcp4.IPInRange(start, stop, ip)
}

// Load lease table from DB
func (s *Server) dbLoad() {
	s.leases = nil
	s.IPpool = make(map[[4]byte]net.HardwareAddr)
	dynLeases := []*Lease{}
	staticLeases := []*Lease{}

	data, err := ioutil.ReadFile(s.conf.DBFilePath)
	if err != nil {
		if !os.IsNotExist(err) {
			log.Error("DHCP: can't read file %s: %v", s.conf.DBFilePath, err)
		}
		return
	}

	obj := []leaseJSON{}
	err = json.Unmarshal(data, &obj)
	if err != nil {
		log.Error("DHCP: invalid DB: %v", err)
		return
	}

	numLeases := len(obj)
	for i := range obj {

		if obj[i].Expiry != leaseExpireStatic &&
			!ipInRange(s.leaseStart, s.leaseStop, obj[i].IP) {

			log.Tracef("Skipping a lease with IP %v: not within current IP range", obj[i].IP)
			continue
		}

		lease := Lease{
			HWAddr:   obj[i].HWAddr,
			IP:       obj[i].IP,
			Hostname: obj[i].Hostname,
			Expiry:   time.Unix(obj[i].Expiry, 0),
		}

		if obj[i].Expiry == leaseExpireStatic {
			staticLeases = append(staticLeases, &lease)
		} else {
			dynLeases = append(dynLeases, &lease)
		}
	}

	s.leases = normalizeLeases(staticLeases, dynLeases)

	for _, lease := range s.leases {
		s.reserveIP(lease.IP, lease.HWAddr)
	}

	log.Info("DHCP: loaded %d (%d) leases from DB", len(s.leases), numLeases)
}

// Skip duplicate leases
// Static leases have a priority over dynamic leases
func normalizeLeases(staticLeases, dynLeases []*Lease) []*Lease {
	leases := []*Lease{}
	index := map[string]int{}

	for i, lease := range staticLeases {
		_, ok := index[lease.HWAddr.String()]
		if ok {
			continue // skip the lease with the same HW address
		}
		index[lease.HWAddr.String()] = i
		leases = append(leases, lease)
	}

	for i, lease := range dynLeases {
		_, ok := index[lease.HWAddr.String()]
		if ok {
			continue // skip the lease with the same HW address
		}
		index[lease.HWAddr.String()] = i
		leases = append(leases, lease)
	}

	return leases
}

// Store lease table in DB
func (s *Server) dbStore() {
	var leases []leaseJSON

	for i := range s.leases {
		if s.leases[i].Expiry.Unix() == 0 {
			continue
		}
		lease := leaseJSON{
			HWAddr:   s.leases[i].HWAddr,
			IP:       s.leases[i].IP,
			Hostname: s.leases[i].Hostname,
			Expiry:   s.leases[i].Expiry.Unix(),
		}
		leases = append(leases, lease)
	}

	data, err := json.Marshal(leases)
	if err != nil {
		log.Error("json.Marshal: %v", err)
		return
	}

	err = file.SafeWrite(s.conf.DBFilePath, data)
	if err != nil {
		log.Error("DHCP: can't store lease table on disk: %v  filename: %s",
			err, s.conf.DBFilePath)
		return
	}
	log.Info("DHCP: stored %d leases in DB", len(leases))
}