package main

import (
	"context"
	"log/slog"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/cereda-systems/nexus-scanner/internal/config"
	"github.com/cereda-systems/nexus-scanner/internal/db"
	"github.com/cereda-systems/nexus-scanner/internal/module"
	arpmod      "github.com/cereda-systems/nexus-scanner/internal/modules/arp"
	admod       "github.com/cereda-systems/nexus-scanner/internal/modules/adsync"
	dnsmod      "github.com/cereda-systems/nexus-scanner/internal/modules/dnsreverse"
	vendormod   "github.com/cereda-systems/nexus-scanner/internal/modules/macvendor"
	reportermod "github.com/cereda-systems/nexus-scanner/internal/modules/nexusreporter"
	portscanmod "github.com/cereda-systems/nexus-scanner/internal/modules/portscan"
	sitemon     "github.com/cereda-systems/nexus-scanner/internal/modules/sitemon"
	snmpmod     "github.com/cereda-systems/nexus-scanner/internal/modules/snmpmod"
	sysarpmod   "github.com/cereda-systems/nexus-scanner/internal/modules/sysarp"
	"github.com/cereda-systems/nexus-scanner/internal/scheduler"
	"github.com/cereda-systems/nexus-scanner/internal/web"
)

var version = "dev"

func main() {
	cfgPath := "/etc/nexus-scanner/config.yaml"
	if len(os.Args) > 1 {
		cfgPath = os.Args[1]
	}

	cfg, err := config.Load(cfgPath)
	if err != nil {
		slog.Error("failed to load config", "path", cfgPath, "err", err)
		os.Exit(1)
	}

	// Log buffer captures entries for the /logs web page.
	logBuf := web.NewLogBuffer(500)

	level := slog.LevelInfo
	if cfg.LogLevel == "debug" {
		level = slog.LevelDebug
	}
	inner := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level})
	handler := web.NewSlogHandler(inner, logBuf)
	slog.SetDefault(slog.New(handler))

	slog.Info("nexus-scanner starting", "version", version, "site", cfg.Site)

	store, err := db.Open(cfg.DB.Path)
	if err != nil {
		slog.Error("failed to open database", "path", cfg.DB.Path, "err", err)
		os.Exit(1)
	}
	defer store.Close()

	registry := module.NewRegistry()

	// sys_arp: reads the OS ARP cache — works on Windows, Linux and macOS
	// without raw-socket privileges. Always enabled.
	registry.Register(sysarpmod.New(cfg.Site, store))
	slog.Info("module registered", "module", "sys_arp")

	// dns_reverse: enriches known hosts with reverse-DNS hostnames.
	registry.Register(dnsmod.New(cfg.Site, store))
	slog.Info("module registered", "module", "dns_reverse")

	// mac_vendor: assigns OUI-based vendor names to known hosts (runs once at startup).
	registry.Register(vendormod.New(cfg.Site, store))
	slog.Info("module registered", "module", "mac_vendor")

	// ad_sync: queries Active Directory for all computer objects.
	if cfg.Modules.ADSync.Enabled {
		registry.Register(admod.New(cfg.Site, cfg.Modules.ADSync, store))
		slog.Info("module registered", "module", "ad_sync",
			"server", cfg.Modules.ADSync.Server,
			"search_base", cfg.Modules.ADSync.SearchBase,
		)
	}

	// site_monitoring: availability checks (ping, http, tcp) — always enabled,
	// checks are managed via the web UI.
	cfg.Modules.SiteMonitoring.Enabled = true
	registry.Register(sitemon.New(cfg.Site, cfg.Modules.SiteMonitoring, cfg.Alert, *cfg, store))
	slog.Info("module registered", "module", "site_monitoring",
		"interval", cfg.Modules.SiteMonitoring.Interval)

	// port_scan: TCP port scanner for all online hosts.
	cfg.Modules.PortScan.Enabled = true
	registry.Register(portscanmod.New(cfg.Site, cfg.Modules.PortScan, store))
	slog.Info("module registered", "module", "port_scan",
		"interval", cfg.Modules.PortScan.Interval)

	// snmp: polls SNMP targets for device info (managed via web UI).
	cfg.Modules.SNMP.Enabled = true
	registry.Register(snmpmod.New(cfg.Site, cfg.Modules.SNMP, store))
	slog.Info("module registered", "module", "snmp",
		"interval", cfg.Modules.SNMP.Interval)

	// nexus_reporter: pushes changed assets to IT Nexus API.
	if cfg.Nexus.URL != "" && cfg.Nexus.APIKey != "" {
		cfg.Modules.NexusReporter.Enabled = true
		registry.Register(reportermod.New(cfg.Site, cfg.Modules.NexusReporter, *cfg, store))
		slog.Info("module registered", "module", "nexus_reporter")
	}

	// arp_discovery: full ARP sweep via raw sockets (Linux only).
	if cfg.Modules.ARPDiscovery.Enabled {
		registry.Register(arpmod.New(cfg.Site, cfg.Modules.ARPDiscovery, store))
		slog.Info("module registered", "module", "arp_discovery",
			"interval", cfg.Modules.ARPDiscovery.Interval,
			"subnets", cfg.Modules.ARPDiscovery.Subnets,
		)
	}

	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer stop()

	// Offline-Sweeper: marks hosts as offline when not seen for OfflineAfter duration.
	go func() {
		ticker := time.NewTicker(5 * time.Minute)
		defer ticker.Stop()
		for {
			select {
			case <-ctx.Done():
				return
			case <-ticker.C:
				n, err := store.MarkStaleHostsOffline(cfg.OfflineAfter)
				if err != nil {
					slog.Error("offline sweep", "err", err)
				} else if n > 0 {
					slog.Info("hosts marked offline", "count", n, "threshold", cfg.OfflineAfter)
				}
			}
		}
	}()

	sched := scheduler.New(registry)
	go sched.Run(ctx)

	srv := web.NewServer(cfgPath, cfg, store, registry, logBuf)
	go func() {
		if err := srv.ListenAndServe(ctx); err != nil {
			slog.Error("web server stopped", "err", err)
			stop()
		}
	}()

	slog.Info("nexus-scanner ready", "addr", cfg.Web.Addr)
	<-ctx.Done()
	slog.Info("nexus-scanner shutting down")
}
