// Package arp implements the arp_discovery scanner module.
// It sends ARP requests across configured subnets and records
// responding hosts (IP + MAC) in the shared SQLite store.
package arp

import (
	"context"
	"fmt"
	"log/slog"
	"net"
	"net/netip"
	"time"

	"github.com/mdlayher/arp"

	"github.com/cereda-systems/nexus-scanner/internal/config"
	"github.com/cereda-systems/nexus-scanner/internal/db"
)

// Module performs ARP discovery on configured subnets.
type Module struct {
	site  string
	cfg   config.ARPConfig
	store *db.Store
	log   *slog.Logger
}

// New returns a new ARP discovery module.
func New(site string, cfg config.ARPConfig, store *db.Store) *Module {
	return &Module{
		site:  site,
		cfg:   cfg,
		store: store,
		log:   slog.With("module", "arp_discovery"),
	}
}

func (m *Module) Name() string            { return "arp_discovery" }
func (m *Module) Interval() time.Duration { return m.cfg.Interval }

// Run executes one full ARP scan cycle across all configured subnets.
func (m *Module) Run(ctx context.Context) error {
	scanID, err := m.store.BeginScan(m.Name())
	if err != nil {
		return fmt.Errorf("begin scan record: %w", err)
	}

	scanErr := m.scanAllSubnets(ctx)

	if err := m.store.EndScan(scanID, scanErr); err != nil {
		m.log.Error("failed to record scan end", "err", err)
	}

	return scanErr
}

func (m *Module) scanAllSubnets(ctx context.Context) error {
	iface, err := net.InterfaceByName(m.cfg.Interface)
	if err != nil {
		return fmt.Errorf("interface %q: %w", m.cfg.Interface, err)
	}

	client, err := arp.Dial(iface)
	if err != nil {
		return fmt.Errorf("arp dial on %s: %w", m.cfg.Interface, err)
	}
	defer client.Close()

	for _, subnet := range m.cfg.Subnets {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
		}

		if err := m.scanSubnet(ctx, client, subnet); err != nil {
			// Log and continue — one failed subnet should not abort others.
			m.log.Error("subnet scan failed", "subnet", subnet, "err", err)
		}
	}
	return nil
}

func (m *Module) scanSubnet(ctx context.Context, client *arp.Client, cidr string) error {
	prefix, err := netip.ParsePrefix(cidr)
	if err != nil {
		return fmt.Errorf("parse prefix %q: %w", cidr, err)
	}
	prefix = prefix.Masked()

	targets := hostsInPrefix(prefix)
	m.log.Info("scanning subnet", "subnet", cidr, "targets", len(targets))

	// Send all ARP requests first, then collect responses.
	// Set a deadline so Read() returns after silence.
	if err := client.SetDeadline(time.Now().Add(3 * time.Second)); err != nil {
		return fmt.Errorf("set deadline: %w", err)
	}

	for _, ip := range targets {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
		}

		if err := client.Request(ip); err != nil {
			m.log.Debug("arp request failed", "ip", ip, "err", err)
		}

		// Small delay to avoid flooding the network.
		time.Sleep(2 * time.Millisecond)
	}

	// Collect all responses until the deadline fires.
	found := 0
	for {
		pkt, _, err := client.Read()
		if err != nil {
			// Deadline reached — done reading.
			break
		}

		now := time.Now().UTC()
		host := db.Host{
			IP:        pkt.SenderIP.String(),
			MAC:       pkt.SenderHardwareAddr.String(),
			Site:      m.site,
			FirstSeen: now,
			LastSeen:  now,
		}

		if err := m.store.UpsertHost(host); err != nil {
			m.log.Error("upsert host", "ip", host.IP, "err", err)
			continue
		}

		m.log.Info("host discovered", "ip", host.IP, "mac", host.MAC)
		found++
	}

	m.log.Info("subnet scan done", "subnet", cidr, "found", found)
	return nil
}

// hostsInPrefix returns all usable host addresses in a prefix,
// excluding the network address and the broadcast address.
//
// Example: 192.168.0.0/24 → 192.168.0.1 … 192.168.0.254
func hostsInPrefix(prefix netip.Prefix) []netip.Addr {
	var addrs []netip.Addr

	// Start at network address + 1 (skip network address itself).
	addr := prefix.Masked().Addr().Next()

	for prefix.Contains(addr) {
		next := addr.Next()
		// Stop before the broadcast address (last address in prefix).
		if !prefix.Contains(next) {
			break
		}
		addrs = append(addrs, addr)
		addr = next
	}

	return addrs
}
