// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

package main

import (
	"context"
	"encoding/hex"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/signal"
	"strconv"
	"syscall"
	"time"

	"github.com/MeshCore-Beacon/beacon-server/db"
	_ "github.com/MeshCore-Beacon/beacon-server/docs"
	"github.com/MeshCore-Beacon/beacon-server/internal/api"
	"github.com/MeshCore-Beacon/beacon-server/internal/api/router"
	"github.com/MeshCore-Beacon/beacon-server/internal/background"
	"github.com/MeshCore-Beacon/beacon-server/internal/cache"
	"github.com/MeshCore-Beacon/beacon-server/internal/config"
	"github.com/MeshCore-Beacon/beacon-server/internal/hub"
	"github.com/MeshCore-Beacon/beacon-server/internal/iatadb"
	"github.com/MeshCore-Beacon/beacon-server/internal/ingest"
	"github.com/MeshCore-Beacon/beacon-server/internal/keystore"
	"github.com/MeshCore-Beacon/beacon-server/internal/presence"
	"github.com/MeshCore-Beacon/beacon-server/internal/scopestore"

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5/pgxpool"
	"github.com/joho/godotenv"
)

// version is set at build time via -ldflags "-X main.version=vX.Y.Z".
// Falls back to "dev" when built without the flag.
var version = "dev"

//	@title			MeshCore Beacon API
//	@version		1.6.0
//	@description	MeshCore network observation backend. Ingests LoRa packets from MQTT brokers, stores in PostgreSQL, and streams live events via WebSocket.
//	@termsOfService	https://github.com/MeshCore-Beacon/beacon-server

//	@contact.name	MeshCore Beacon
//	@contact.url	https://github.com/MeshCore-Beacon/beacon-server

//	@license.name	AGPL-3-or-later

//	@host		localhost:8080
//	@BasePath	/api/v1

//	@schemes	http https

// @tag.name			IATAs
// @tag.description	Airport/location codes that group observers and packets
// @tag.name			Regions
// @tag.description	Super-regions grouping multiple IATAs
// @tag.name			Observers
// @tag.description	MeshCore MQTT observers (gateways)
// @tag.name			Nodes
// @tag.description	MeshCore radio nodes
// @tag.name			Packets
// @tag.description	LoRa packets heard by observers
// @tag.name			Channels
// @tag.description	MeshCore group text channels
// @tag.name			Messages
// @tag.description	Decrypted channel messages
// @tag.name			Brokers
// @tag.description	MQTT broker connection status
// @tag.name			Stats
// @tag.description	Network statistics and time series
func main() {
	log.Printf("beacon version %s", version)
	_ = godotenv.Load()
	addr := os.Getenv("LISTEN_ADDR")
	if addr == "" {
		addr = ":8080"
	}

	configPath := os.Getenv("CONFIG_PATH")
	if configPath == "" {
		configPath = "config.yaml"
	}

	cfg, err := config.Load(configPath)
	if err != nil {
		log.Fatalf("failed to load config: %v", err)
	}

	resolved := config.Resolve(cfg)

	log.Printf("config: loaded — %s", resolved)

	// ── Hub ──────────────────────────────────────────────────────────────────
	h := hub.New()
	go h.Run()

	// ── MQTT ingest workers ──────────────────────────────────────────────────
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	pool, err := pgxpool.New(ctx, getEnv("POSTGRES_DSN"))
	if err != nil {
		log.Fatalf("failed to connect to postgres at %s: %v", os.Getenv("POSTGRES_DSN_HOST"), err)
	}
	defer pool.Close()

	if err := db.RunMigrations(ctx, pool); err != nil {
		log.Fatalf("migrations failed: %v", err)
	}

	store := db.New(pool, resolved.ClockDriftThreshold, resolved.NodeStaleThreshold)

	// ── Presence write coalescing ────────────────────────────────────────────
	// Ingest writes go through the coalescer; reads keep using the store.
	coalescer := presence.New(store, resolved.PresenceFlushInterval, resolved.PresencePacketTTL)
	go coalescer.Run(ctx)

	// ── Redis cache layer ────────────────────────────────────────────────────
	var reader api.Reader = store
	if redisAddr := os.Getenv("REDIS_ADDR"); redisAddr != "" {
		redisClient := cache.NewClient(
			redisAddr,
			os.Getenv("REDIS_PASSWORD"),
			func() int {
				if db := os.Getenv("REDIS_DB"); db != "" {
					n, err := strconv.Atoi(db)
					if err == nil {
						return n
					}
				}
				return 0
			}(),
		)
		if err := redisClient.Ping(ctx); err != nil {
			log.Printf("warning: redis unavailable at %s, caching disabled: %v", redisAddr, err)
		} else {
			ttls := cache.ResolveTTLs(cfg.Cache)
			reader = cache.NewCachedReader(store, redisClient, ttls)
			defer redisClient.Close()
			log.Printf("cache: Redis connected at %s (stats=%s reference=%s nodes=%s observers=%s)",
				redisAddr, ttls.Stats, ttls.Reference, ttls.Nodes, ttls.Observers)
		}
	}

	// ── Seed config data ─────────────────────────────────────────────────────
	if err := config.Seed(ctx, cfg, store); err != nil {
		log.Fatalf("failed to seed config: %v", err)
	}

	// ── Build transport scope keystore ───────────────────────────────────────
	scopes := scopestore.New()
	scopeEntries, err := store.GetTransportScopes(ctx)
	if err != nil {
		log.Fatalf("failed to load transport scopes: %v", err)
	}
	scopes.Load(scopeEntries)
	log.Printf("loaded %d transport scopes", len(scopeEntries))

	// ── Build channel keystore ──────────────────────────────────────────────
	entries := make(map[string][]keystore.Entry)

	// Hashtag-derived channels: secret = SHA256("#tag")[:16], hash = SHA256(secret)[0]
	for _, tag := range cfg.ChannelKeys.Hashtags {
		secret, channelHash, fingerprint := keystore.DeriveHashtagKey(tag)
		hashHex := fmt.Sprintf("%02x", channelHash)
		entry := keystore.Entry{
			Key:         secret,
			Fingerprint: fingerprint,
			Hashtag:     tag,
			Name:        "#" + tag,
		}
		if !keystore.EntryExists(entries[hashHex], entry) {
			entries[hashHex] = append(entries[hashHex], entry)
			log.Printf("config: loaded hashtag channel #%s (hash=%s)", tag, hashHex)
		}
	}

	// Explicit keys: hash provided directly, key is hex-encoded
	for hashHex, keyCfg := range cfg.ChannelKeys.Keys {
		key, err := hex.DecodeString(keyCfg.Key)
		if err != nil {
			log.Printf("warning: invalid channel key for hash %s, skipping: %v", hashHex, err)
			continue
		}
		entry := keystore.Entry{
			Key:         key,
			Fingerprint: keystore.Fingerprint(key),
			Name:        keyCfg.Name,
		}
		if !keystore.EntryExists(entries[hashHex], entry) {
			entries[hashHex] = append(entries[hashHex], entry)
			log.Printf("config: loaded explicit channel key for hash %s name=%q", hashHex, keyCfg.Name)
		}
	}

	keys := keystore.NewMapKeyStore(entries)

	// ── Backfill channel messages ────────────────────────────────────────────
	// Packets whose channel key wasn't yet configured at ingest time were stored as
	// hash-only channels and never decrypted. Retry them now against the keystore we just
	// built, so adding a channel key to the config surfaces its history on the next boot
	// instead of leaving it stranded in the DB indefinitely.
	if n, err := ingest.BackfillChannelMessages(ctx, store, keys); err != nil {
		log.Printf("config: channel message backfill failed: %v", err)
	} else if n > 0 {
		log.Printf("config: backfilled %d previously-undecrypted channel message(s)", n)
	}

	// ── Build geographic ingest filter ───────────────────────────────────────────────────────────
	allowedIATAs := iatadb.BuildAllowedSet(cfg.Ingest.AllowCountries, cfg.Ingest.AllowContinents)
	if allowedIATAs != nil {
		log.Printf("config: ingest filter active — %d allowed IATAs (countries=%v continents=%v)",
			len(allowedIATAs), cfg.Ingest.AllowCountries, cfg.Ingest.AllowContinents)
	} else {
		log.Printf("config: ingest filter inactive — accepting all IATAs")
	}

	broker1 := ingest.New(
		ingest.Config{
			BrokerName:          "mqtt1",
			URL:                 getEnv("MQTT_BROKER_1_URL"),
			Username:            getEnv("MQTT_BROKER_1_USERNAME"),
			Password:            getEnv("MQTT_BROKER_1_PASSWORD"),
			TelemetryResolution: resolved.TelemetryResolution,
			AllowedIATAs:        allowedIATAs,
		},
		coalescer,
		h,
		keys,
		scopes,
	)

	broker2 := ingest.New(
		ingest.Config{
			BrokerName:          "mqtt2",
			URL:                 getEnv("MQTT_BROKER_2_URL"),
			Username:            getEnv("MQTT_BROKER_2_USERNAME"),
			Password:            getEnv("MQTT_BROKER_2_PASSWORD"),
			TelemetryResolution: resolved.TelemetryResolution,
			AllowedIATAs:        allowedIATAs,
		},
		coalescer,
		h,
		keys,
		scopes,
	)

	if cr, ok := reader.(*cache.CachedReader); ok {
		broker1.SetCacheInvalidators(cr.InvalidateNode, cr.InvalidateObserver)
		broker2.SetCacheInvalidators(cr.InvalidateNode, cr.InvalidateObserver)
	}

	go broker1.Start(ctx)
	go broker2.Start(ctx)

	tasks := []background.Task{
		background.ViewRefreshTask(store, resolved.ViewRefreshInterval),
		background.CleanupTask(store, resolved.TelemetryRetention, resolved.PacketRetention, resolved.NodeDeleteAfter, resolved.CleanupInterval),
		background.ReconfirmTask(store, resolved.RouteRetention, resolved.RouteGrace, int64(resolved.RouteMinObservations), resolved.ReconfirmInterval),
	}
	if resolved.ObserverDeleteAfter > 0 {
		var onDelete func(context.Context, uuid.UUID)
		if cr, ok := reader.(*cache.CachedReader); ok {
			onDelete = cr.InvalidateObserver
		}
		tasks = append(tasks, background.ObserverCleanupTask(coalescer, resolved.ObserverDeleteAfter, resolved.CleanupInterval, onDelete))
	}
	scheduler := background.New(tasks)
	go scheduler.Start(ctx)

	// ── HTTP server ──────────────────────────────────────────────────────────
	r := router.New(h, reader, []*ingest.Worker{broker1, broker2}, resolved.MaxConnsPerIP, cfg.CORS)

	srv := &http.Server{
		Addr:    addr,
		Handler: r,
	}

	go func() {
		fmt.Printf("Beacon listening on %s\n", addr)
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("server error: %v", err)
		}
	}()

	// ── Graceful shutdown ────────────────────────────────────────────────────
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit

	log.Println("shutting down...")
	cancel() // stops ingest workers
	shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer shutdownCancel()
	if err := srv.Shutdown(shutdownCtx); err != nil {
		log.Printf("server shutdown error: %v", err)
	}
	coalescer.Flush(shutdownCtx)
}

// getEnv returns the value of an env var and logs a warning if it is unset.
// Callers that require the value to be non-empty should fatal themselves;
// ingest workers tolerate missing broker config and will fail on connect instead.
func getEnv(key string) string {
	v := os.Getenv(key)
	if v == "" {
		log.Printf("warning: %s is not set", key)
	}
	return v
}
