package main

import (
	"context"
	"crypto/tls"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"net"
	"net/http"
	"os"
	"os/signal"
	"path/filepath"
	"strings"
	"sync"
	"syscall"
	"time"

	"golang.org/x/term"

	"vocat/internal/auth"
	"vocat/internal/config"
	"vocat/internal/developer"
	"vocat/internal/device"
	"vocat/internal/exportproxy"
	"vocat/internal/extensions"
	"vocat/internal/httpsmode"
	"vocat/internal/loghub"
	"vocat/internal/modem"
	"vocat/internal/pcsc"
	"vocat/internal/server"
	"vocat/internal/store"
	"vocat/internal/update"
	"vocat/internal/vowifi"
	"vocat/internal/vowifi/ike"
	"vocat/internal/vowifi/ims"
	"vocat/internal/vowifi/integration"
	vowifiruntime "vocat/internal/vowifi/runtime"
	"vocat/web"
)

func main() {
	logs := loghub.New(slog.NewJSONHandler(os.Stdout, nil), 2000)
	logger := slog.New(logs)

	args := os.Args[1:]
	switch subcommand, rest := splitSubcommand(args); subcommand {
	case "":
		// No subcommand: TTY+root → interactive menu (operator on the host);
		// otherwise run the server. systemd runs vocat with stdin=/dev/null
		// (non-TTY) so the unit keeps starting the server unchanged. Non-root
		// on a TTY also falls through to the server rather than erroring on
		// runMenu's root requirement.
		if term.IsTerminal(int(os.Stdin.Fd())) && os.Geteuid() == 0 {
			if err := runMenu(logger); err != nil {
				logger.Error("menu failed", "error", err)
				os.Exit(1)
			}
		} else {
			if err := run(logger, logs); err != nil {
				logger.Error("server stopped", "error", err)
				os.Exit(1)
			}
		}
	case "serve":
		// Explicit foreground server. Use this when vocat with no arguments
		// would otherwise enter the menu (root on a TTY) but a server is wanted.
		if err := run(logger, logs); err != nil {
			logger.Error("server stopped", "error", err)
			os.Exit(1)
		}
	case "version", "-v", "--version":
		runVersion()
	case "update":
		if err := update.Run(logger, rest); err != nil {
			logger.Error("update failed", "error", err)
			os.Exit(1)
		}
	case "menu":
		if err := runMenu(logger); err != nil {
			logger.Error("menu failed", "error", err)
			os.Exit(1)
		}
	case "develop":
		// Hidden subcommand: intentionally not listed in printUsage or the
		// interactive menu. It toggles the developer-mode flag that gates the
		// entire plugin/extension system; the flag takes effect on next start.
		if err := runDevelop(rest, logger); err != nil {
			logger.Error("develop failed", "error", err)
			os.Exit(2)
		}
	case "bootstrap-admin":
		// Installer-only command. The password is read from stdin so it never
		// appears in argv, an environment file, or process listings.
		if err := runBootstrapAdmin(rest); err != nil {
			logger.Error("bootstrap admin failed", "error", err)
			os.Exit(1)
		}
	case "help", "-h", "--help":
		printUsage(os.Stdout)
	default:
		fmt.Fprintf(os.Stderr, "vocat: unknown subcommand %q\n\n", subcommand)
		printUsage(os.Stderr)
		os.Exit(2)
	}
}

// splitSubcommand returns the first non-flag token as the subcommand and the
// remaining args. An empty arg list yields ("", nil) → server mode.
func splitSubcommand(args []string) (string, []string) {
	if len(args) == 0 {
		return "", nil
	}
	return args[0], args[1:]
}

func run(logger *slog.Logger, logs *loghub.Hub) error {
	cfg, err := config.Load()
	if err != nil {
		return fmt.Errorf("load configuration: %w", err)
	}
	instanceLock, err := lockServerInstance(cfg.DatabasePath)
	if err != nil {
		return err
	}
	defer instanceLock.Close()
	startupContext, cancelStartup := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancelStartup()

	database, err := store.Open(startupContext, cfg.DatabasePath)
	if err != nil {
		return err
	}
	defer database.Close()
	developerEnabled := isDeveloperEnabled(startupContext, database)
	pluginRoot := filepath.Join(filepath.Dir(cfg.DatabasePath), "plugins")
	legacyExportProxyConfig := filepath.Join(pluginRoot, exportproxy.ReservedID, "data", "configs.json")
	if !developerEnabled {
		if err := developer.ResetExperimental(startupContext, database); err != nil {
			return fmt.Errorf("reset disabled developer settings: %w", err)
		}
		if err := exportproxy.RemoveLegacyConfig(legacyExportProxyConfig); err != nil {
			return fmt.Errorf("remove legacy export proxy configuration: %w", err)
		}
	}
	httpsManager, err := httpsmode.New(
		startupContext,
		database,
		filepath.Join(filepath.Dir(cfg.DatabasePath), "tls"),
		cfg.Address,
	)
	if err != nil {
		return fmt.Errorf("configure self-signed HTTPS: %w", err)
	}

	// The plugin/extension system is gated behind a hidden developer-mode flag.
	// When off (the default) the manager is never created and the server receives
	// a nil Extensions handle, so every /extensions* and /plugin-assets/* route
	// returns 503/404 and the SPA hides the plugin surface.
	var extensionManager *extensions.Manager
	var exportProxyManager *exportproxy.Manager
	if developerEnabled {
		exportProxyManager, err = exportproxy.New(startupContext, database, logger, legacyExportProxyConfig)
		if err != nil {
			return fmt.Errorf("create built-in export proxy: %w", err)
		}
		defer exportProxyManager.Close()
		extensionManager, err = extensions.NewManager(
			pluginRoot,
			logger,
		)
		if err != nil {
			return fmt.Errorf("create plugin manager: %w", err)
		}
		defer extensionManager.Close()
	} else {
		logger.Info("developer mode is off; plugin system disabled")
	}

	authService, err := auth.New(database, auth.Options{
		SessionTTL: cfg.SessionTTL,
	})
	if err != nil {
		return err
	}
	if _, adminErr := database.CurrentAdmin(startupContext); adminErr != nil {
		if errors.Is(adminErr, store.ErrNotFound) {
			return errors.New("administrator is not initialized; run vocat bootstrap-admin before starting the service")
		}
		return fmt.Errorf("read administrator: %w", adminErr)
	}

	cardReaders := pcsc.New()
	deviceManager, err := device.NewManager(device.Options{CardReaders: cardReaders})
	if err != nil {
		return fmt.Errorf("create device manager: %w", err)
	}
	if err := deviceManager.Start(startupContext); err != nil {
		logger.Warn("device discovery is not available at startup", "error", err)
	}
	if err := provisionDiscoveredDevices(startupContext, database, deviceManager); err != nil {
		logger.Warn("automatic first-run device provisioning failed", "error", err)
	}
	configureDeviceBackends(startupContext, logger, database, deviceManager)
	restoreDefaultCellularRadios(startupContext, logger, database, deviceManager)
	defer func() {
		stopContext, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		if err := deviceManager.Stop(stopContext); err != nil {
			logger.Warn("stop device manager", "error", err)
		}
	}()
	pollContext, cancelPolling := context.WithCancel(context.Background())
	defer cancelPolling()
	go pollDeviceSnapshots(pollContext, logger, database, deviceManager)
	go restoreConfiguredCellularData(pollContext, logger, database, deviceManager)
	go collectCellularTraffic(pollContext, logger, database)
	go persistLogsToStore(pollContext, logger, logs, database)
	if !developerEnabled {
		go disableAllDeveloperCellularData(pollContext, logger, database, deviceManager)
	} else {
		go watchDeveloperDisable(pollContext, logger, database, deviceManager, exportProxyManager, legacyExportProxyConfig)
	}

	vowifiManager, err := configureVoWiFiRuntime(
		startupContext,
		logger,
		database,
		deviceManager,
		cardReaders,
	)
	if err != nil {
		return fmt.Errorf("configure VoWiFi runtime: %w", err)
	}
	go reconcileCardPolicies(pollContext, logger, database, deviceManager, vowifiManager)
	defer func() {
		stopContext, cancel := context.WithTimeout(context.Background(), 15*time.Second)
		defer cancel()
		if err := vowifiManager.Close(stopContext); err != nil {
			logger.Warn("stop VoWiFi runtime", "error", err)
		}
	}()

	handler, err := server.New(server.Options{
		Store:               database,
		Auth:                authService,
		Devices:             deviceManager,
		VoWiFi:              vowifiManager,
		Logs:                logs,
		Assets:              web.Dist,
		Logger:              logger,
		SecureCookies:       cfg.SecureCookies,
		MaxRequestBodyBytes: cfg.MaxRequestBodyBytes,
		Extensions:          extensionManager,
		ExportProxy:         exportProxyManager,
		DeveloperEnabled:    developerEnabled,
		UpdateRepository:    strings.TrimSpace(os.Getenv("VOCAT_REPO")),
		UpdateToken:         strings.TrimSpace(os.Getenv("GITHUB_TOKEN")),
		HTTPS:               httpsManager,
	})
	if err != nil {
		return err
	}
	go handler.StartLogRetentionLoop(pollContext, time.Minute)
	go handler.StartSMSSyncLoop(pollContext, 15*time.Second)
	handler.StartTelegramBot(pollContext)
	handler.StartSMSNotificationDispatchers(pollContext)
	handler.StartAutomaticTasks(pollContext)

	serverConfig := func(handler http.Handler) *http.Server {
		return &http.Server{
			Addr:              cfg.Address,
			Handler:           handler,
			ReadHeaderTimeout: 5 * time.Second,
			ReadTimeout:       15 * time.Second,
			WriteTimeout:      30 * time.Second,
			IdleTimeout:       90 * time.Second,
			MaxHeaderBytes:    1 << 20,
		}
	}
	plainHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if httpsManager.Enabled() {
			host := strings.TrimSpace(r.Host)
			if host == "" {
				host = cfg.Address
			}
			http.Redirect(w, r, "https://"+host+r.URL.RequestURI(), http.StatusPermanentRedirect)
			return
		}
		handler.ServeHTTP(w, r)
	})
	plainServer := serverConfig(plainHandler)
	tlsServer := serverConfig(handler)
	baseListener, err := net.Listen("tcp", cfg.Address)
	if err != nil {
		return fmt.Errorf("listen on %s: %w", cfg.Address, err)
	}
	protocolMux := httpsmode.NewMultiplexer(baseListener, httpsManager)

	signalContext, stopSignals := signal.NotifyContext(
		context.Background(),
		os.Interrupt,
		syscall.SIGTERM,
	)
	defer stopSignals()

	serverError := make(chan error, 2)
	go func() {
		logger.Info("HTTP server listening", "address", cfg.Address, "self_signed_https", httpsManager.Enabled())
		err := plainServer.Serve(protocolMux.Plain())
		if errors.Is(err, http.ErrServerClosed) {
			err = nil
		}
		serverError <- err
	}()
	go func() {
		err := tlsServer.Serve(tls.NewListener(protocolMux.TLS(), httpsManager.TLSConfig()))
		if errors.Is(err, http.ErrServerClosed) {
			err = nil
		}
		serverError <- err
	}()

	select {
	case err := <-serverError:
		_ = protocolMux.Close()
		return err
	case <-signalContext.Done():
		logger.Info("shutdown signal received")
	}
	// Long-lived SSE and polling handlers use this context. Stop them before
	// http.Server.Shutdown so they do not consume the entire graceful-shutdown
	// deadline while waiting for a stream that is intentionally still active.
	cancelPolling()

	shutdownContext, cancelShutdown := context.WithTimeout(
		context.Background(),
		cfg.ShutdownTimeout,
	)
	defer cancelShutdown()
	shutdownErrors := make(chan error, 2)
	go func() { shutdownErrors <- plainServer.Shutdown(shutdownContext) }()
	go func() { shutdownErrors <- tlsServer.Shutdown(shutdownContext) }()
	time.Sleep(10 * time.Millisecond)
	_ = protocolMux.Close()
	for range 2 {
		if err := <-shutdownErrors; err != nil {
			_ = plainServer.Close()
			_ = tlsServer.Close()
			return fmt.Errorf("graceful HTTP shutdown: %w", err)
		}
	}
	return nil
}

func configureDeviceBackends(
	ctx context.Context,
	logger *slog.Logger,
	database *store.Store,
	manager *device.Manager,
) {
	configs, err := database.ListDevices(ctx)
	if err != nil {
		logger.Warn("configure device backends: list devices", "error", err)
		return
	}
	mapper := integration.ATMapper{Store: database, Devices: manager}
	for _, config := range configs {
		entry, mapErr := mapper.Get(config.ID)
		if mapErr != nil {
			continue
		}
		if config.DeviceType == store.DeviceTypeUSBSIMReader {
			if err := manager.SetSIMPin(entry.ID, config.SIMPIN); err != nil {
				logger.Warn("configure USB SIM reader", "device_id", config.ID, "error", err)
			}
			continue
		}
		if err := manager.SetBackend(entry.ID, config.DeviceBackend); err != nil {
			logger.Warn("configure device backend", "device_id", config.ID, "backend", config.DeviceBackend, "error", err)
		}
	}
}

// restoreDefaultCellularRadios applies an explicitly saved cellular policy
// after restart. Missing policies remain RF-off and are claimed by the safe
// default policy; there is no automatic cellular fallback.
func restoreDefaultCellularRadios(
	ctx context.Context,
	logger *slog.Logger,
	database *store.Store,
	manager *device.Manager,
) {
	configs, err := database.ListDevices(ctx)
	if err != nil {
		logger.Warn("startup cellular recovery: list devices", "error", err)
		return
	}
	mapper := integration.ATMapper{Store: database, Devices: manager}
	for _, config := range configs {
		if config.DeviceType == store.DeviceTypeUSBSIMReader {
			continue
		}
		if config.VoWiFiEnabled {
			continue
		}
		entry, err := mapper.Get(config.ID)
		if err != nil || entry.Snapshot == nil || !entry.Snapshot.FlightMode {
			continue
		}
		iccid := strings.TrimSpace(entry.Snapshot.ICCID)
		if iccid == "" {
			continue
		}
		if iccid != "" {
			policy, policyErr := database.CardPolicy(ctx, iccid)
			switch {
			case policyErr == nil && policy.AirplaneEnabled:
				continue
			case errors.Is(policyErr, store.ErrNotFound):
				continue
			case policyErr != nil && !errors.Is(policyErr, store.ErrNotFound):
				logger.Warn("startup cellular recovery: read card policy", "device_id", config.ID, "error", policyErr)
				continue
			}
		}
		restoreContext, cancel := context.WithTimeout(ctx, 10*time.Second)
		_, err = manager.SetFlight(restoreContext, entry.ID, false)
		cancel()
		if err != nil {
			logger.Warn("startup cellular recovery failed", "device_id", config.ID, "error", err)
			continue
		}
		logger.Info("restored cellular radio after disabled VoWiFi", "device_id", config.ID, "iccid", iccid)
	}
}

func restoreConfiguredCellularData(
	ctx context.Context,
	logger *slog.Logger,
	database *store.Store,
	manager *device.Manager,
) {
	configs, err := database.ListDevices(ctx)
	if err != nil {
		logger.Warn("startup cellular data recovery: list devices", "error", err)
		return
	}
	mapper := integration.ATMapper{Store: database, Devices: manager}
	for _, config := range configs {
		if config.DeviceType == store.DeviceTypeUSBSIMReader {
			continue
		}
		if !config.NetworkEnabled || config.VoWiFiEnabled {
			continue
		}
		entry, err := mapper.Get(config.ID)
		if err != nil {
			continue
		}
		networkRequest := device.NetworkRequest{
			Enabled: true, APN: config.APN, IPVersion: "IPV4V6", Backend: config.DeviceBackend,
		}
		if entry.Snapshot != nil {
			iccid := strings.TrimSpace(entry.Snapshot.ICCID)
			if policy, policyErr := database.CardPolicy(ctx, iccid); policyErr == nil {
				networkRequest.APN = policy.APN
				if policy.IPVersion != "" {
					networkRequest.IPVersion = policy.IPVersion
				}
				if profile, profileErr := database.CardAPNProfileByAPN(ctx, iccid, policy.APN, policy.IPVersion); profileErr == nil {
					networkRequest.Username = profile.Username
					networkRequest.Password = profile.Password
					networkRequest.Authentication = profile.AuthType
					if entry.Snapshot.RegistrationStatus == 5 && profile.RoamingIPVersion != "" {
						networkRequest.IPVersion = profile.RoamingIPVersion
					}
				}
			}
		}
		dataContext, cancel := context.WithTimeout(ctx, 60*time.Second)
		_, err = manager.SetNetwork(dataContext, entry.ID, networkRequest)
		cancel()
		if err != nil {
			logger.Warn("startup cellular data recovery failed", "device_id", config.ID)
			continue
		}
		logger.Info("restored protected cellular data route", "device_id", config.ID, "interface", config.Interface)
	}
}

func disableAllDeveloperCellularData(
	ctx context.Context,
	logger *slog.Logger,
	database *store.Store,
	manager *device.Manager,
) {
	configs, err := database.ListDevices(ctx)
	if err != nil {
		logger.Warn("developer cleanup: list devices", "error", err)
		return
	}
	mapper := integration.ATMapper{Store: database, Devices: manager}
	for _, config := range configs {
		if config.DeviceType == store.DeviceTypeUSBSIMReader {
			continue
		}
		entry, err := mapper.Get(config.ID)
		if err != nil {
			continue
		}
		disableContext, cancel := context.WithTimeout(ctx, 30*time.Second)
		_, err = manager.SetNetwork(disableContext, entry.ID, device.NetworkRequest{Enabled: false, Backend: config.DeviceBackend})
		cancel()
		if err != nil && ctx.Err() == nil {
			logger.Warn("developer cleanup: stop cellular data", "device_id", config.ID)
		}
	}
}

func watchDeveloperDisable(
	ctx context.Context,
	logger *slog.Logger,
	database *store.Store,
	manager *device.Manager,
	exportProxy *exportproxy.Manager,
	legacyConfigPath string,
) {
	ticker := time.NewTicker(2 * time.Second)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			if developer.Enabled(ctx, database) {
				continue
			}
			if exportProxy != nil {
				if err := exportProxy.DeleteAllAndDisable(ctx); err != nil && ctx.Err() == nil {
					logger.Warn("developer cleanup: delete export proxies", "error", err)
				}
			}
			if err := exportproxy.RemoveLegacyConfig(legacyConfigPath); err != nil {
				logger.Warn("developer cleanup: remove legacy export proxy configuration", "error", err)
			}
			if err := developer.ResetExperimental(ctx, database); err != nil && ctx.Err() == nil {
				logger.Warn("developer cleanup: reset settings", "error", err)
			}
			disableAllDeveloperCellularData(ctx, logger, database, manager)
			logger.Info("developer mode disabled; roaming data and export proxies were removed")
			return
		}
	}
}

func configureVoWiFiRuntime(
	ctx context.Context,
	logger *slog.Logger,
	database *store.Store,
	deviceManager *device.Manager,
	cardReaders *pcsc.Service,
) (*vowifiruntime.Manager, error) {
	mapper := integration.ATMapper{
		Store:   database,
		Devices: deviceManager,
	}
	ec20Adapter, err := vowifi.NewEC20Adapter(mapper, vowifi.EC20AdapterOptions{
		// The test deployment is deliberately non-cellular. VoWiFi teardown
		// may restore CFUN, but it must never reactivate a PDP context.
		RestoreCellularData: false,
		// VoWiFi is always fail-closed with respect to cellular RF. Its teardown
		// leaves CFUN=4; only the explicit airplane-mode-off endpoint restores
		// CFUN=1.
		PureAirplanePolicy: func(deviceID string) bool {
			deviceConfig, configErr := database.Device(context.Background(), deviceID)
			return configErr == nil && deviceConfig.VoWiFiEnabled
		},
	})
	if err != nil {
		return nil, err
	}
	pcscAdapter, err := vowifi.NewPCSCAdapter(cardReaders, func(ctx context.Context, deviceID string) (pcsc.Selector, string, error) {
		config, resolveErr := database.Device(ctx, strings.TrimSpace(deviceID))
		if resolveErr != nil {
			return pcsc.Selector{}, "", resolveErr
		}
		return pcsc.Selector{USBPath: config.USBPath, ReaderName: config.ControlDevice}, config.SIMPIN, nil
	})
	if err != nil {
		return nil, err
	}
	projector := integration.StateProjector{
		Store:   database,
		Devices: mapper,
	}
	manager := vowifiruntime.New(vowifiruntime.Options{
		Logger:  logger,
		OnState: projector.Save,
		Factory: func(factoryContext context.Context, deviceID string) (*vowifi.Orchestrator, error) {
			deviceConfig, err := database.Device(factoryContext, deviceID)
			if err != nil {
				return nil, fmt.Errorf("load device %q VoWiFi config: %w", deviceID, err)
			}
			adapter := vowifiDeviceAdapter(ec20Adapter)
			if deviceConfig.DeviceType == store.DeviceTypeUSBSIMReader {
				adapter = pcscAdapter
			}
			return newVoWiFiOrchestrator(deviceConfig, database, adapter)
		},
	})

	configured, err := database.ListDevices(ctx)
	if err != nil {
		_ = manager.Close(context.Background())
		return nil, err
	}
	for _, deviceConfig := range configured {
		if err := manager.Ensure(ctx, deviceConfig.ID); err != nil {
			_ = manager.Close(context.Background())
			return nil, fmt.Errorf("register device %q VoWiFi runtime: %w", deviceConfig.ID, err)
		}
		if deviceConfig.VoWiFiEnabled {
			if entry, mapErr := mapper.Get(deviceConfig.ID); mapErr == nil {
				flightErr := protectVoWiFiStartupRadio(ctx, deviceManager, entry.ID)
				if flightErr != nil {
					// A modem can be temporarily unavailable while OpenWrt/procd is
					// restarting the service (notably after loading XFRM modules). Do
					// not take the Web/API service down with it: the orchestrator below
					// remains fail-closed and its runtime manager retries until CFUN=4
					// can be established.
					logger.Warn(
						"VoWiFi startup radio protection deferred to automatic retry",
						"device_id", deviceConfig.ID,
						"error", flightErr,
					)
				}
			}
			if _, err := manager.RequestEnabled(deviceConfig.ID, true); err != nil {
				_ = manager.Close(context.Background())
				return nil, fmt.Errorf("start device %q VoWiFi policy: %w", deviceConfig.ID, err)
			}
		}
	}
	return manager, nil
}

const (
	vowifiStartupRadioAttempts = 3
	vowifiStartupRadioDelay    = time.Second
)

type flightModeSetter interface {
	SetFlight(context.Context, string, bool) (device.FlightResult, error)
}

func protectVoWiFiStartupRadio(ctx context.Context, manager flightModeSetter, physicalID string) error {
	return protectVoWiFiStartupRadioWithRetry(
		ctx,
		manager,
		physicalID,
		vowifiStartupRadioAttempts,
		vowifiStartupRadioDelay,
	)
}

func protectVoWiFiStartupRadioWithRetry(
	ctx context.Context,
	manager flightModeSetter,
	physicalID string,
	attempts int,
	delay time.Duration,
) error {
	var lastErr error
	for attempt := 0; attempt < attempts; attempt++ {
		flightContext, cancel := context.WithTimeout(ctx, 10*time.Second)
		_, lastErr = manager.SetFlight(flightContext, physicalID, true)
		cancel()
		if lastErr == nil {
			return nil
		}
		if attempt+1 == attempts {
			break
		}
		timer := time.NewTimer(delay)
		select {
		case <-ctx.Done():
			if !timer.Stop() {
				<-timer.C
			}
			return errors.Join(lastErr, ctx.Err())
		case <-timer.C:
		}
	}
	return lastErr
}

type vowifiDeviceAdapter interface {
	vowifi.SIMIdentityReader
	vowifi.AKAProvider
	vowifi.RadioController
}

func newVoWiFiOrchestrator(
	deviceConfig store.Device,
	database *store.Store,
	adapter vowifiDeviceAdapter,
) (*vowifi.Orchestrator, error) {
	apn := deviceConfig.APN
	if apn == "" {
		apn = "ims"
	}
	tunnelProvider, err := ike.NewProvider(ike.Config{APN: apn})
	if err != nil {
		return nil, fmt.Errorf("device %q IKE provider: %w", deviceConfig.ID, err)
	}
	imsProvider, err := ims.NewProvider(adapter, ims.Config{
		// The userspace SWu data plane carries protected P-CSCF signalling over
		// TCP by default. UK PLMN 234-10 exposes its P-CSCF over UDP/5060 on SWu.
		Transport: "tcp",
		TransportByPLMN: map[string]string{
			"23410":  "udp",
			"234010": "udp",
		},
		// Some Vodafone UK SIM profiles leave AT+CSCA empty; Vodafone publishes
		// this service-centre number for manual SMS setup.
		SMSCenter: "+447785016005",
		OnSMS: func(ctx context.Context, message ims.ReceivedSMS) error {
			extra, _ := json.Marshal(map[string]any{
				"transport":                "ims",
				"encoding":                 message.Encoding,
				"concat":                   message.Concat,
				"rp_reference":             message.RPReference,
				"call_id":                  message.CallID,
				"received_at":              message.Timestamp,
				"service_center_timestamp": message.ServiceCenterTimestamp,
				"raw_rpdu":                 message.RawRPDU,
				"raw_tpdu":                 message.RawTPDU,
			})
			partsTotal := 1
			if message.Concat != nil && message.Concat.Total > 0 {
				partsTotal = message.Concat.Total
			}
			messageID := message.MessageID
			if message.Concat != nil && message.Concat.Total > 1 {
				// A segment of a carrier-split long SMS over IMS. Address the whole
				// message with a stable id so SaveSMSMessage folds every segment
				// into one progressively merged row instead of one row per segment.
				messageID = store.StableConcatMessageID(
					"ims", deviceConfig.ModemIMEI, message.DeviceID, message.From,
					message.Concat.Reference, message.Concat.Total,
				)
			}
			_, saveErr := database.SaveSMSMessage(ctx, store.SMSMessage{
				MessageID:  messageID,
				DeviceID:   message.DeviceID,
				ModemIMEI:  deviceConfig.ModemIMEI,
				IMSI:       message.IMSI,
				Peer:       message.From,
				Direction:  "inbound",
				Body:       message.Text,
				Timestamp:  message.Timestamp,
				Status:     "received",
				Source:     "ims",
				PartsTotal: partsTotal,
				Read:       false,
				Extra:      extra,
			})
			return saveErr
		},
		OnSMSStatus: func(ctx context.Context, report ims.ReceivedSMSStatus) error {
			deliveryReport := store.SMSDeliveryReport{
				DeviceID:          report.DeviceID,
				ModemIMEI:         deviceConfig.ModemIMEI,
				IMSI:              report.IMSI,
				Peer:              report.To,
				Source:            "ims",
				MessageReference:  report.MessageReference,
				StatusCode:        report.StatusCode,
				DeliveryState:     report.DeliveryStatus,
				ServiceCenterTime: report.ServiceCenterTimestamp,
				DischargeTime:     report.DischargeTimestamp,
				ReceivedAt:        report.Timestamp,
			}
			var applyErr error
			for attempt := 0; attempt < 10; attempt++ {
				_, applyErr = database.ApplySMSDeliveryReport(ctx, deliveryReport)
				if !errors.Is(applyErr, store.ErrNotFound) {
					return applyErr
				}
				// A status report can race the API handler persisting the SIP 202
				// result. Give that write a brief chance to complete.
				select {
				case <-ctx.Done():
					return ctx.Err()
				case <-time.After(100 * time.Millisecond):
				}
			}
			// A late report from before this process started must still be
			// acknowledged, otherwise the SMSC will keep retransmitting it.
			return nil
		},
	})
	if err != nil {
		return nil, fmt.Errorf("device %q IMS provider: %w", deviceConfig.ID, err)
	}
	orchestrator, err := vowifi.New(vowifi.Dependencies{
		SIM:    adapter,
		AKA:    adapter,
		Radio:  adapter,
		Proxy:  integration.ProxyResolver{Store: database},
		Tunnel: tunnelProvider,
		IMS:    imsProvider,
		Phones: integration.PhoneStore{Store: database, DeviceID: deviceConfig.ID},
	}, vowifi.Options{
		DeviceID:           deviceConfig.ID,
		AllowIMSWithoutSMS: true,
	})
	if err != nil {
		return nil, fmt.Errorf("device %q VoWiFi orchestrator: %w", deviceConfig.ID, err)
	}
	return orchestrator, nil
}

func provisionDiscoveredDevices(
	ctx context.Context,
	database *store.Store,
	manager *device.Manager,
) error {
	configured, err := database.ListDevices(ctx)
	if err != nil {
		return err
	}
	if len(configured) != 0 {
		return nil
	}
	for _, discovered := range manager.List() {
		candidate := discovered.Candidate
		deviceType := provisionedDeviceType(candidate)
		backend := "at"
		control := candidate.ATPort.OpenPath()
		esimTransport := backend
		if candidate.QMIControl != "" {
			backend = "qmi"
			control = candidate.QMIControl
			esimTransport = backend
		}
		if candidate.HardwareKind == pcsc.HardwareKind {
			backend = "pcsc"
			control = candidate.ReaderName
			deviceType = store.DeviceTypeUSBSIMReader
			esimTransport = "pcsc"
		}
		name := candidate.Product
		if name == "" || strings.EqualFold(name, "Android") {
			name = "Quectel EC20 / EC25"
		}
		if err := database.UpsertDevice(ctx, store.Device{
			ID:             discovered.ID,
			Name:           name,
			DeviceType:     deviceType,
			Interface:      candidate.NetworkInterface,
			ControlDevice:  control,
			ATPort:         candidate.ATPort.OpenPath(),
			USBPath:        candidate.USBPath,
			ProxyPort:      1080,
			BaudRate:       115200,
			DataBits:       8,
			StopBits:       1,
			Parity:         "none",
			DeviceBackend:  backend,
			ESIMTransport:  esimTransport,
			NetworkEnabled: false,
			SMSEnabled:     true,
			VoWiFiEnabled:  true,
		}); err != nil {
			return err
		}
	}
	return nil
}

func provisionedDeviceType(candidate modem.Candidate) string {
	controlName := filepath.Base(filepath.Clean(candidate.QMIControl))
	if candidate.HardwareKind == "wwan" &&
		strings.HasPrefix(controlName, "wwan") && strings.Contains(controlName, "qmi") {
		return store.DeviceTypeWiFi410
	}
	return store.DeviceTypePCIeEC20EC25
}

// persistLogsToStore subscribes to the live log hub and durably appends every
// entry to the log_events table, so runtime logs survive restarts and can be
// pruned by the configured retention policy.
func persistLogsToStore(
	ctx context.Context,
	logger *slog.Logger,
	logs *loghub.Hub,
	database *store.Store,
) {
	entries, cancel := logs.Subscribe(512)
	defer cancel()
	for {
		select {
		case <-ctx.Done():
			return
		case entry, ok := <-entries:
			if !ok {
				return
			}
			var fields json.RawMessage
			if len(entry.Fields) > 0 {
				if raw, err := json.Marshal(entry.Fields); err == nil {
					fields = raw
				}
			}
			if _, err := database.AppendLogEvent(ctx, store.LogEvent{
				Time:    entry.Time,
				Level:   entry.Level,
				Message: entry.Message,
				Caller:  entry.Caller,
				Fields:  fields,
			}); err != nil && ctx.Err() == nil {
				logger.Warn("persist log event failed", "error", err)
			}
		}
	}
}

func pollDeviceSnapshots(
	ctx context.Context,
	logger *slog.Logger,
	database *store.Store,
	manager *device.Manager,
) {
	refresh := func() {
		discoveryContext, cancelDiscovery := context.WithTimeout(ctx, 10*time.Second)
		_, err := manager.Discover(discoveryContext)
		cancelDiscovery()
		if err != nil {
			logger.Debug("periodic modem discovery failed", "error", err)
			return
		}
		// Hotplug can replace the physical discovery ID. Rebind each configured
		// device's selected QMI/AT control plane before collecting its snapshot.
		configureDeviceBackends(ctx, logger, database, manager)
		entries := manager.List()
		// Each physical modem owns its own operation lock. Refresh them in
		// parallel so a slow or wedged EC20 on one hub port cannot delay signal
		// and identity updates for every other modem by 30 seconds at a time.
		var refreshGroup sync.WaitGroup
		refreshSlots := make(chan struct{}, 4)
		for _, entry := range entries {
			if !entry.Discovered || entry.Candidate.DiscoveryIssue != "" {
				continue
			}
			entry := entry
			refreshGroup.Add(1)
			go func() {
				defer refreshGroup.Done()
				select {
				case refreshSlots <- struct{}{}:
					defer func() { <-refreshSlots }()
				case <-ctx.Done():
					return
				}
				refreshContext, cancelRefresh := context.WithTimeout(ctx, 30*time.Second)
				snapshot, refreshErr := manager.Refresh(refreshContext, entry.ID)
				cancelRefresh()
				if refreshErr != nil && ctx.Err() == nil {
					logger.Warn("modem snapshot refresh failed", "device_id", entry.ID, "error", refreshErr)
				}
				if refreshErr == nil && ctx.Err() == nil {
					enforceCardRegion(ctx, logger, database, manager, entry.ID, &snapshot)
					enforceDefaultSafeCardPolicy(ctx, logger, database, manager, entry.ID, &snapshot)
				}
			}()
		}
		refreshGroup.Wait()
	}
	refresh()
	ticker := time.NewTicker(30 * time.Second)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			refresh()
		}
	}
}

// enforceDefaultSafeCardPolicy handles a newly inserted physical SIM or a
// profile that has never had a policy. RF is turned off before the default is
// persisted; the VoWiFi runtime reconciler then starts service asynchronously.
func enforceDefaultSafeCardPolicy(
	ctx context.Context,
	logger *slog.Logger,
	database *store.Store,
	manager *device.Manager,
	physicalID string,
	snapshot *device.Snapshot,
) {
	if snapshot == nil || !snapshot.SIMReady || strings.TrimSpace(snapshot.ICCID) == "" ||
		device.RegionBlockReason(snapshot.IMSI) != "" {
		return
	}
	iccid := strings.TrimSpace(snapshot.ICCID)
	if _, err := database.CardPolicy(ctx, iccid); err == nil {
		return
	} else if !errors.Is(err, store.ErrNotFound) {
		logger.Warn("default card policy: read policy", "iccid", iccid, "error", err)
		return
	}
	flightContext, cancel := context.WithTimeout(ctx, 10*time.Second)
	_, err := manager.SetFlight(flightContext, physicalID, true)
	cancel()
	if err != nil {
		logger.Warn("default card policy: failed to establish airplane mode", "device_id", physicalID, "iccid", iccid, "error", err)
		return
	}
	if err := database.UpsertCardPolicy(ctx, store.CardPolicy{
		ICCID: iccid, VoWiFiEnabled: true, AirplaneEnabled: true,
		IPVersion: "IPV4V6", Source: "default",
	}); err != nil {
		logger.Warn("default card policy: persist policy", "iccid", iccid, "error", err)
		return
	}
	mapper := integration.ATMapper{Store: database, Devices: manager}
	configs, err := database.ListDevices(ctx)
	if err != nil {
		return
	}
	for _, config := range configs {
		entry, mapErr := mapper.Get(config.ID)
		if mapErr != nil || entry.ID != physicalID {
			continue
		}
		config.NetworkEnabled = false
		config.VoWiFiEnabled = true
		if err := database.UpsertDevice(ctx, config); err != nil {
			logger.Warn("default card policy: update device policy", "device_id", config.ID, "error", err)
		}
		break
	}
	logger.Info("new SIM protected by default VoWiFi/airplane policy", "device_id", physicalID, "iccid", iccid)
}

func reconcileCardPolicies(
	ctx context.Context,
	logger *slog.Logger,
	database *store.Store,
	manager *device.Manager,
	vowifiManager *vowifiruntime.Manager,
) {
	observedCards := make(map[string]string)
	reconcile := func() {
		policies, policyListErr := database.ListCardPolicies(ctx)
		if policyListErr == nil {
			for _, policy := range policies {
				if !policy.VoWiFiEnabled || (policy.AirplaneEnabled && !policy.NetworkEnabled) {
					continue
				}
				policy.AirplaneEnabled = true
				policy.NetworkEnabled = false
				if err := database.UpsertCardPolicy(ctx, policy); err != nil {
					logger.Warn("reconcile card policy: normalize stored RF-safe VoWiFi policy", "iccid", policy.ICCID, "error", err)
				}
			}
		}
		configs, err := database.ListDevices(ctx)
		if err != nil {
			return
		}
		mapper := integration.ATMapper{Store: database, Devices: manager}
		for _, config := range configs {
			entry, mapErr := mapper.Get(config.ID)
			if mapErr != nil || entry.Snapshot == nil {
				if config.DeviceType == store.DeviceTypeUSBSIMReader && observedCards[config.ID] != "missing" {
					if state, stateErr := vowifiManager.State(config.ID); stateErr == nil && state.ICCID != "" {
						_, _ = vowifiManager.RequestReconnect(config.ID)
					}
					observedCards[config.ID] = "missing"
				}
				continue
			}
			iccid := strings.TrimSpace(entry.Snapshot.ICCID)
			if iccid == "" {
				if config.DeviceType == store.DeviceTypeUSBSIMReader && observedCards[config.ID] != "missing" {
					if state, stateErr := vowifiManager.State(config.ID); stateErr == nil && state.ICCID != "" {
						_, _ = vowifiManager.RequestReconnect(config.ID)
					}
					observedCards[config.ID] = "missing"
				}
				continue
			}
			previousObserved := observedCards[config.ID]
			observedCards[config.ID] = iccid
			policy, policyErr := database.CardPolicy(ctx, iccid)
			if policyErr != nil {
				continue
			}
			if policy.VoWiFiEnabled && (!policy.AirplaneEnabled || policy.NetworkEnabled) {
				policy.AirplaneEnabled = true
				policy.NetworkEnabled = false
				if err := database.UpsertCardPolicy(ctx, policy); err != nil {
					logger.Warn("reconcile card policy: normalize RF-safe VoWiFi policy", "device_id", config.ID, "iccid", iccid, "error", err)
					continue
				}
			}
			deviceChanged := false
			if config.VoWiFiEnabled != policy.VoWiFiEnabled || (policy.VoWiFiEnabled && config.NetworkEnabled) {
				config.VoWiFiEnabled = policy.VoWiFiEnabled
				if policy.VoWiFiEnabled {
					config.NetworkEnabled = false
				}
				deviceChanged = true
			}
			if config.APN != strings.TrimSpace(policy.APN) {
				config.APN = strings.TrimSpace(policy.APN)
				deviceChanged = true
			}
			if deviceChanged {
				if err := database.UpsertDevice(ctx, config); err != nil {
					logger.Warn("reconcile card policy: update device", "device_id", config.ID, "error", err)
					continue
				}
			}
			state, stateErr := vowifiManager.State(config.ID)
			if policy.VoWiFiEnabled {
				if !entry.Snapshot.FlightMode {
					flightContext, cancel := context.WithTimeout(ctx, 10*time.Second)
					_, _ = manager.SetFlight(flightContext, entry.ID, true)
					cancel()
				}
				switch {
				case stateErr != nil || !state.Enabled:
					_, _ = vowifiManager.RequestEnabled(config.ID, true)
				case state.ICCID != "" && !strings.EqualFold(strings.TrimSpace(state.ICCID), iccid):
					_, _ = vowifiManager.RequestReconnect(config.ID)
				case config.DeviceType == store.DeviceTypeUSBSIMReader && previousObserved == "missing":
					_, _ = vowifiManager.RequestReconnect(config.ID)
				}
				continue
			}
			if stateErr == nil && state.Enabled {
				_, _ = vowifiManager.RequestEnabled(config.ID, false)
				continue
			}
			if policy.AirplaneEnabled != entry.Snapshot.FlightMode {
				flightContext, cancel := context.WithTimeout(ctx, 10*time.Second)
				_, _ = manager.SetFlight(flightContext, entry.ID, policy.AirplaneEnabled)
				cancel()
			}
		}
	}
	reconcile()
	ticker := time.NewTicker(5 * time.Second)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			reconcile()
		}
	}
}

// cardPolicySourceRegionBlock marks a card policy that was written automatically
// because the inserted SIM belongs to a region the product does not serve. It
// doubles as the persistent record that the radio was forced off by us, so the
// block survives restarts and can be lifted when an allowed card is detected.
const cardPolicySourceRegionBlock = "auto_region_block"

// enforceCardRegion applies the regional service policy for one refreshed
// device. A SIM whose IMSI home MCC is blocked (mainland China, 460/461) is
// denied service: the radio is forced into airplane mode and a blocking card
// policy is persisted. The check is fail-open — it only acts on a positively
// read blocked IMSI — and the lift path only runs once the current card is
// positively confirmed to be allowed, so an unreadable IMSI never causes a
// block or a spurious restore.
func enforceCardRegion(
	ctx context.Context,
	logger *slog.Logger,
	database *store.Store,
	manager *device.Manager,
	id string,
	snapshot *device.Snapshot,
) {
	if snapshot == nil || !snapshot.SIMReady {
		return
	}
	imsi := strings.TrimSpace(snapshot.IMSI)
	if imsi == "" {
		// Region unknown: hold the current state rather than block or restore.
		return
	}
	if reason := device.RegionBlockReason(imsi); reason != "" {
		if !snapshot.FlightMode {
			flightContext, cancelFlight := context.WithTimeout(ctx, 30*time.Second)
			_, err := manager.SetFlight(flightContext, id, true)
			cancelFlight()
			if err != nil && ctx.Err() == nil {
				logger.Warn(
					"region block: failed to force airplane mode",
					"device_id", id, "error", err,
				)
			}
		}
		if snapshot.ICCID != "" {
			policy, policyErr := database.CardPolicy(ctx, snapshot.ICCID)
			if errors.Is(policyErr, store.ErrNotFound) {
				policy = store.CardPolicy{ICCID: snapshot.ICCID, IPVersion: "IPV4V6"}
				policyErr = nil
			}
			policy.NetworkEnabled = false
			policy.VoWiFiEnabled = false
			policy.AirplaneEnabled = true
			policy.Source = cardPolicySourceRegionBlock
			if policyErr != nil && ctx.Err() == nil {
				logger.Warn("region block: failed to read card policy", "device_id", id, "iccid", snapshot.ICCID, "error", policyErr)
			} else if err := database.UpsertCardPolicy(ctx, policy); err != nil && ctx.Err() == nil {
				logger.Warn(
					"region block: failed to persist card policy",
					"device_id", id, "iccid", snapshot.ICCID, "error", err,
				)
			}
		}
		logger.Warn(
			"blocked SIM detected; service disabled and radio forced off",
			"device_id", id, "iccid", snapshot.ICCID, "imsi", imsi, "reason", reason,
		)
		return
	}
	liftCardRegionBlock(ctx, logger, database, manager, id, snapshot)
}

// liftCardRegionBlock removes the regional marker once an allowed SIM is
// confirmed. It deliberately does not restore RF: the replacement SIM is
// picked up by enforceDefaultSafeCardPolicy and remains in airplane/VoWiFi
// mode until an explicit user action.
func liftCardRegionBlock(
	ctx context.Context,
	logger *slog.Logger,
	database *store.Store,
	manager *device.Manager,
	id string,
	snapshot *device.Snapshot,
) {
	policies, err := database.ListCardPolicies(ctx)
	if err != nil {
		if ctx.Err() == nil {
			logger.Warn("region block: failed to list card policies", "error", err)
		}
		return
	}
	outstanding := make([]store.CardPolicy, 0, 1)
	for _, policy := range policies {
		if policy.Source == cardPolicySourceRegionBlock {
			outstanding = append(outstanding, policy)
		}
	}
	if len(outstanding) == 0 {
		return
	}
	for _, policy := range outstanding {
		if err := database.DeleteCardPolicy(ctx, policy.ICCID); err != nil && ctx.Err() == nil {
			logger.Warn(
				"region block: failed to clear auto policy",
				"iccid", policy.ICCID, "error", err,
			)
		}
	}
	logger.Info(
		"region marker removed; allowed SIM remains RF protected",
		"device_id", id, "iccid", snapshot.ICCID, "imsi", snapshot.IMSI,
	)
}
