package dhcpd

import (
	"encoding/json"
	"fmt"
	"net"
	"net/netip"
	"os"
	"path/filepath"
	"time"

	"github.com/AdguardTeam/golibs/errors"
	"github.com/AdguardTeam/golibs/log"
)

const (
	// leaseExpireStatic is used to define the Expiry field for static
	// leases.
	//
	// Deprecated:  Remove it when migration of DHCP leases will be not needed.
	leaseExpireStatic = 1

	// dbFilename contains saved leases.
	//
	// Deprecated:  Use dataFilename.
	dbFilename = "leases.db"
)

// leaseJSON is the structure of stored lease in a legacy database.
//
// Deprecated:  Use [dbLease].
type leaseJSON struct {
	HWAddr   []byte `json:"mac"`
	IP       []byte `json:"ip"`
	Hostname string `json:"host"`
	Expiry   int64  `json:"exp"`
}

// readOldDB reads the old database from the given path.
func readOldDB(path string) (leases []*leaseJSON, err error) {
	// #nosec G304 -- Trust this path, since it's taken from the old file name
	// relative to the working directory and should generally be considered
	// safe.
	file, err := os.Open(path)
	if errors.Is(err, os.ErrNotExist) {
		// Nothing to migrate.
		return nil, nil
	} else if err != nil {
		// Don't wrap the error since it's informative enough as is.
		return nil, err
	}
	defer func() { err = errors.WithDeferred(err, file.Close()) }()

	leases = []*leaseJSON{}
	err = json.NewDecoder(file).Decode(&leases)
	if err != nil {
		return nil, fmt.Errorf("decoding old db: %w", err)
	}

	return leases, nil
}

// migrateDB migrates stored leases if necessary.
func migrateDB(conf *ServerConfig) (err error) {
	defer func() { err = errors.Annotate(err, "migrating db: %w") }()

	oldLeasesPath := filepath.Join(conf.WorkDir, dbFilename)
	dataDirPath := filepath.Join(conf.DataDir, dataFilename)

	oldLeases, err := readOldDB(oldLeasesPath)
	if err != nil {
		// Don't wrap the error since it's informative enough as is.
		return err
	} else if oldLeases == nil {
		// Nothing to migrate.
		return nil
	}

	leases := make([]*dbLease, 0, len(oldLeases))
	for _, l := range oldLeases {
		l.IP = normalizeIP(l.IP)
		ip, ok := netip.AddrFromSlice(l.IP)
		if !ok {
			log.Info("dhcp: invalid IP: %s", l.IP)

			continue
		}

		leases = append(leases, &dbLease{
			Expiry:   time.Unix(l.Expiry, 0).Format(time.RFC3339),
			Hostname: l.Hostname,
			HWAddr:   net.HardwareAddr(l.HWAddr).String(),
			IP:       ip,
			IsStatic: l.Expiry == leaseExpireStatic,
		})
	}

	err = writeDB(dataDirPath, leases)
	if err != nil {
		// Don't wrap the error since an annotation deferred already.
		return err
	}

	return os.Remove(oldLeasesPath)
}

// normalizeIP converts the given IP address to IPv4 if it's IPv4-mapped IPv6,
// or leaves it as is otherwise.
func normalizeIP(ip net.IP) (normalized net.IP) {
	normalized = ip.To4()
	if normalized != nil {
		return normalized
	}

	return ip
}