package handlers

import (
	"bytes"
	"context"
	"fmt"
	"log"
	"net"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"time"

	"github.com/gesellix/bose-soundtouch/pkg/discovery"
	"github.com/gesellix/bose-soundtouch/pkg/models"
	"github.com/gesellix/bose-soundtouch/pkg/service/amazon"
	"github.com/gesellix/bose-soundtouch/pkg/service/datastore"
	"github.com/gesellix/bose-soundtouch/pkg/service/proxy"
	"github.com/gesellix/bose-soundtouch/pkg/service/setup"
	"github.com/gesellix/bose-soundtouch/pkg/service/spotify"
	"github.com/miekg/dns"
)

// Server handles HTTP requests for the SoundTouch service.
type Server struct {
	ds                  *datastore.DataStore
	sm                  *setup.Manager
	mu                  sync.RWMutex
	serverURL           string
	httpsServerURL      string
	discovering         bool
	proxyRedact         bool
	proxyLogBody        bool
	recordEnabled       bool
	discoveryInterval   time.Duration
	discoveryEnabled    bool
	dnsEnabled          bool
	dnsUpstream         []string
	dnsBindAddr         string
	mirrorEnabled       bool
	mirrorEndpoints     []string
	skipMirrorEndpoints []string
	preferredSource     string
	internalPaths       []string
	shortcuts           map[string]int
	recorder            *proxy.Recorder
	dnsDiscovery        *discovery.DNSDiscovery
	UpstreamProxy       http.Handler
	Version             string
	Commit              string
	Date                string
	RepoURL             string
	mgmtUsername        string
	mgmtPassword        string
	spotifyClientID     string
	spotifyClientSecret string
	spotifyRedirectURI  string
	spotifyService      *spotify.Service
	amazonClientID      string
	amazonClientSecret  string
	amazonRedirectURI   string
	amazonService       *amazon.Service
}

// RequestSnapshot represents an immutable snapshot of an HTTP request.
type RequestSnapshot struct {
	Method    string
	URL       *url.URL
	Headers   http.Header
	Body      []byte
	Host      string
	Timestamp time.Time
}

type ctxKey struct{ name string }

// SnapshotKey is the context key for the RequestSnapshot.
var SnapshotKey = &ctxKey{"request_snapshot"}

var bufferPool = sync.Pool{
	New: func() interface{} {
		return new(bytes.Buffer)
	},
}

// NewServer creates a new SoundTouch service server.
func NewServer(ds *datastore.DataStore, sm *setup.Manager, serverURL string, proxyRedact, proxyLogBody, recordEnabled bool) *Server {
	s := &Server{
		ds:                ds,
		sm:                sm,
		serverURL:         serverURL,
		proxyRedact:       proxyRedact,
		proxyLogBody:      proxyLogBody,
		recordEnabled:     recordEnabled,
		discoveryInterval: 5 * time.Minute,
		discoveryEnabled:  true,
	}

	return s
}

// SetVersionInfo sets the version information for the server.
func (s *Server) SetVersionInfo(version, commit, date, repoURL string) {
	s.mu.Lock()
	defer s.mu.Unlock()

	s.Version = version
	s.Commit = commit
	s.Date = date
	s.RepoURL = repoURL
}

// SetDiscoverySettings sets the discovery settings for the server.
func (s *Server) SetDiscoverySettings(interval time.Duration, enabled bool) {
	s.mu.Lock()
	defer s.mu.Unlock()

	s.discoveryInterval = interval
	s.discoveryEnabled = enabled
}

// parseUpstreamDNS splits a comma-separated string of DNS servers.
func parseUpstreamDNS(upstream string) []string {
	var upstreamList []string

	if upstream != "" {
		for _, u := range strings.Split(upstream, ",") {
			u = strings.TrimSpace(u)
			if u != "" {
				upstreamList = append(upstreamList, u)
			}
		}
	}

	return upstreamList
}

// getSystemDNS returns the DNS servers from /etc/resolv.conf.
func getSystemDNS() []string {
	config, _ := dns.ClientConfigFromFile("/etc/resolv.conf")
	if config != nil && len(config.Servers) > 0 {
		return config.Servers
	}

	return nil
}

// areUpstreamsEqual compares two slices of DNS server addresses.
func areUpstreamsEqual(a, b []string) bool {
	if len(a) != len(b) {
		return false
	}

	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}

	return true
}

// SetDNSSettings sets the DNS discovery settings for the server.
func (s *Server) SetDNSSettings(enabled bool, upstream, bind string) {
	s.mu.Lock()
	defer s.mu.Unlock()

	oldBind := s.dnsBindAddr
	oldUpstream := s.dnsUpstream

	s.dnsEnabled = enabled
	s.dnsBindAddr = bind

	upstreamList := parseUpstreamDNS(upstream)

	// Try to get system DNS if none provided
	if enabled && len(upstreamList) == 0 {
		upstreamList = getSystemDNS()
		if len(upstreamList) > 0 {
			log.Printf("[DNS] Using system DNS servers from /etc/resolv.conf: %v", upstreamList)
		}
	}

	s.dnsUpstream = upstreamList
	upstreamChanged := !areUpstreamsEqual(upstreamList, oldUpstream)

	if s.dnsDiscovery != nil {
		if !enabled || bind != oldBind || upstreamChanged {
			log.Printf("[DNS] Settings changed, stopping DNS discovery server")

			_ = s.dnsDiscovery.Shutdown()
			s.dnsDiscovery = nil
		}
	}

	if enabled && len(upstreamList) == 0 {
		log.Printf("[DNS] Cannot start DNS discovery server: upstream DNS is empty and no system DNS found")

		s.dnsEnabled = false

		return
	}

	if enabled && s.dnsDiscovery == nil {
		s.startDNSDiscovery(bind, upstreamList)
	}
}

func (s *Server) startDNSDiscovery(bind string, upstreamList []string) {
	log.Printf("[DNS] Starting DNS discovery server on %s", bind)

	u, _ := url.Parse(s.serverURL)

	serviceIP := u.Hostname()
	if serviceIP == "localhost" || serviceIP == "" {
		serviceIP = "127.0.0.1"
	}

	if s.sm != nil {
		serviceIP = s.sm.GetResolvedIP(serviceIP)
	}

	s.dnsDiscovery = discovery.NewDNSDiscovery(upstreamList, serviceIP)
	go func(d *discovery.DNSDiscovery, addr string) {
		if err := d.Start(addr); err != nil {
			log.Printf("Warning: DNS discovery server error: %v", err)
		}
	}(s.dnsDiscovery, bind)
}

// GetDNSRunning returns whether DNS discovery is active and its bind address.
func (s *Server) GetDNSRunning() (bool, string) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	if s.dnsDiscovery == nil {
		return false, ""
	}

	return s.dnsDiscovery.IsRunning(s.dnsBindAddr), s.dnsBindAddr
}

// SetDNSDiscoveries sets the initial DNS discoveries for the server.
func (s *Server) SetDNSDiscoveries(discoveries map[string]*discovery.DiscoveredHost) {
	s.mu.Lock()
	defer s.mu.Unlock()

	if s.dnsDiscovery != nil {
		s.dnsDiscovery.SetDiscovered(discoveries)
	}
}

// GetDNSDiscovery returns the current DNS discoveries.
func (s *Server) GetDNSDiscovery() map[string]*discovery.DiscoveredHost {
	s.mu.RLock()
	defer s.mu.RUnlock()

	if s.dnsDiscovery == nil {
		return nil
	}

	return s.dnsDiscovery.GetDiscovered()
}

// SetShortcuts sets the request shortcuts for the server.
func (s *Server) SetShortcuts(shortcuts map[string]int) {
	s.mu.Lock()
	defer s.mu.Unlock()

	s.shortcuts = shortcuts
}

// GetShortcuts returns the current request shortcuts.
func (s *Server) GetShortcuts() map[string]int {
	s.mu.RLock()
	defer s.mu.RUnlock()

	return s.shortcuts
}

// GetDiscoverySettings returns the current discovery settings.
func (s *Server) GetDiscoverySettings() (time.Duration, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	return s.discoveryInterval, s.discoveryEnabled
}

// SetHTTPServerURL sets the external HTTPS URL of the service.
func (s *Server) SetHTTPServerURL(url string) {
	s.mu.Lock()
	defer s.mu.Unlock()

	s.httpsServerURL = url
}

// SetRecorder sets the recorder for the server.
func (s *Server) SetRecorder(r *proxy.Recorder) {
	s.mu.Lock()
	defer s.mu.Unlock()

	s.recorder = r
	if r != nil {
		r.Redact = s.proxyRedact
	}
}

// SetSpotifyConfig sets the Spotify OAuth configuration.
func (s *Server) SetSpotifyConfig(clientID, clientSecret, redirectURI string) {
	s.mu.Lock()
	defer s.mu.Unlock()

	s.spotifyClientID = clientID
	s.spotifyClientSecret = clientSecret
	s.spotifyRedirectURI = redirectURI
}

// SetAmazonConfig sets the Amazon LWA OAuth configuration.
func (s *Server) SetAmazonConfig(clientID, clientSecret, redirectURI string) {
	s.mu.Lock()
	defer s.mu.Unlock()

	s.amazonClientID = clientID
	s.amazonClientSecret = clientSecret
	s.amazonRedirectURI = redirectURI
}

// GetSpotifyConfig returns the current Spotify OAuth configuration.
func (s *Server) GetSpotifyConfig() (clientID, clientSecret, redirectURI string) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	return s.spotifyClientID, s.spotifyClientSecret, s.spotifyRedirectURI
}

// GetAmazonConfig returns the current Amazon LWA OAuth configuration.
func (s *Server) GetAmazonConfig() (clientID, clientSecret, redirectURI string) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	return s.amazonClientID, s.amazonClientSecret, s.amazonRedirectURI
}

// applyMusicServiceCredentials updates music service credential fields on the server.
// Must be called with s.mu held. Empty string or "***" (the masked GET value) means "unchanged".
func (s *Server) applyMusicServiceCredentials(spotifyID, spotifySecret, spotifyURI, amazonID, amazonSecret, amazonURI string) {
	if spotifyID != "" {
		s.spotifyClientID = spotifyID
	}

	if spotifySecret != "" && spotifySecret != "***" {
		s.spotifyClientSecret = spotifySecret
	}

	if spotifyURI != "" {
		s.spotifyRedirectURI = spotifyURI
	}

	if amazonID != "" {
		s.amazonClientID = amazonID
	}

	if amazonSecret != "" && amazonSecret != "***" {
		s.amazonClientSecret = amazonSecret
	}

	if amazonURI != "" {
		s.amazonRedirectURI = amazonURI
	}
}

// ReinitSpotifyService creates a new Spotify service from current config and replaces the running one.
func (s *Server) ReinitSpotifyService() {
	clientID, clientSecret, redirectURI := s.GetSpotifyConfig()
	if clientID == "" {
		return
	}

	if redirectURI == "" {
		redirectURI = s.serverURL + "/mgmt/spotify/callback"
	}

	svc := spotify.NewSpotifyService(clientID, clientSecret, redirectURI, s.ds.DataDir)
	if err := svc.Load(); err != nil {
		log.Printf("[Spotify] Failed to load accounts during reinit: %v", err)
	}

	s.SetSpotifyService(svc)
	log.Printf("[Spotify] Service reinitialized")
}

// ReinitAmazonService creates a new Amazon service from current config and replaces the running one.
func (s *Server) ReinitAmazonService() {
	clientID, clientSecret, redirectURI := s.GetAmazonConfig()
	if clientID == "" {
		return
	}

	if redirectURI == "" {
		redirectURI = s.serverURL + "/mgmt/amazon/callback"
	}

	svc := amazon.NewAmazonService(clientID, clientSecret, redirectURI, s.ds.DataDir)
	if err := svc.Load(); err != nil {
		log.Printf("[Amazon] Failed to load accounts during reinit: %v", err)
	}

	s.SetAmazonService(svc)
	log.Printf("[Amazon] Service reinitialized")
}

// SetMgmtConfig sets the management API authentication credentials.
func (s *Server) SetMgmtConfig(username, password string) {
	s.mu.Lock()
	defer s.mu.Unlock()

	s.mgmtUsername = username
	s.mgmtPassword = password
}

// SetMirrorSettings sets the mirroring settings for the server.
func (s *Server) SetMirrorSettings(enabled bool, endpoints, skipEndpoints []string, preferredSource string) {
	s.mu.Lock()
	defer s.mu.Unlock()

	s.mirrorEnabled = enabled
	s.mirrorEndpoints = endpoints
	s.skipMirrorEndpoints = skipEndpoints
	s.preferredSource = preferredSource
}

// SetInternalPaths sets the internal paths for the server.
func (s *Server) SetInternalPaths(paths []string) {
	s.mu.Lock()
	defer s.mu.Unlock()

	s.internalPaths = paths
}

// SetAmazonService sets the Amazon OAuth service.
func (s *Server) SetAmazonService(as *amazon.Service) {
	s.mu.Lock()
	defer s.mu.Unlock()

	s.amazonService = as
}

// IsAmazonConfigured returns whether Amazon Music integration is configured.
func (s *Server) IsAmazonConfigured() bool {
	s.mu.RLock()
	defer s.mu.RUnlock()

	return s.amazonService != nil
}

// SetSpotifyService sets the Spotify OAuth service.
func (s *Server) SetSpotifyService(ss *spotify.Service) {
	s.mu.Lock()
	defer s.mu.Unlock()

	s.spotifyService = ss
}

// GetRecordEnabled returns whether recording is enabled.
func (s *Server) GetRecordEnabled() bool {
	s.mu.RLock()
	defer s.mu.RUnlock()

	return s.recordEnabled
}

// GetSettings returns the current server settings.
func (s *Server) GetSettings() (string, string) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	return s.serverURL, s.httpsServerURL
}

// IsSpotifyConfigured returns whether Spotify integration is configured.
func (s *Server) IsSpotifyConfigured() bool {
	s.mu.RLock()
	defer s.mu.RUnlock()

	return s.spotifyService != nil
}

// GetProxySettings returns the current proxy settings.
func (s *Server) GetProxySettings() (bool, bool, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	return s.proxyRedact, s.proxyLogBody, s.recordEnabled
}

// DiscoverDevices starts a background device discovery process.
//
//nolint:contextcheck
func (s *Server) DiscoverDevices(ctx context.Context) {
	s.discovering = true

	defer func() { s.discovering = false }()

	log.Println("Scanning for Bose devices...")

	// Use background context if none provided or if it's likely a request context
	if ctx == nil {
		ctx = context.Background()
	}

	// Always wrap in a timeout to prevent hanging forever
	discoveryCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
	defer cancel()

	svc := discovery.NewService(10 * time.Second)

	devices, err := svc.DiscoverDevices(discoveryCtx)
	if err != nil {
		log.Printf("Discovery error: %v", err)
		return
	}

	for _, d := range devices {
		s.handleDiscoveredDevice(*d)
	}

	// Post-discovery cleanup: merge overlapping IP/Serial entries
	s.mergeOverlappingDevices()
}

// findExistingDeviceInfoByDeviceID looks for existing device info by deviceID
func (s *Server) findExistingDeviceInfoByDeviceID(deviceID string) *models.ServiceDeviceInfo {
	allDevices, err := s.ds.ListAllDevices()
	if err != nil {
		return nil
	}

	for i := range allDevices {
		device := &allDevices[i]
		if device.DeviceID == deviceID {
			return device
		}
	}

	return nil
}

// PrimeDeviceWithSpotify triggers a Spotify priming of the speaker if a Spotify account is linked.
func (s *Server) PrimeDeviceWithSpotify(deviceIP string) {
	s.mu.RLock()
	svc := s.spotifyService
	s.mu.RUnlock()

	if svc == nil {
		return
	}

	accounts := svc.GetAccounts()
	if len(accounts) == 0 {
		return
	}

	// We'll use the first linked account. In the future, we might want to let the user
	// pick or map accounts to speakers, but for now, we follow the "One linked account" model.
	accessToken, username, err := svc.GetFreshToken()
	if err != nil {
		log.Printf("[Spotify Watchdog] Failed to get fresh token for %s: %v", deviceIP, err)
		return
	}

	log.Printf("[Spotify Watchdog] Proactively priming %s with Spotify user %s", deviceIP, username)

	if err := s.pushSpotifyTokenToDevice(deviceIP, username, accessToken); err != nil {
		log.Printf("[Spotify Watchdog] Failed to prime %s: %v", deviceIP, err)
	} else {
		log.Printf("[Spotify Watchdog] Successfully primed %s", deviceIP)
	}
}

func (s *Server) pushSpotifyTokenToDevice(deviceIP, username, accessToken string) error {
	var zcURL string
	if _, _, err := net.SplitHostPort(deviceIP); err == nil {
		zcURL = fmt.Sprintf("http://%s/zc", deviceIP)
	} else {
		zcURL = fmt.Sprintf("http://%s:8200/zc", deviceIP)
	}

	return spotify.PushSpotifyCredentials(zcURL, username, accessToken)
}

// PrimeDeviceWithAmazon triggers an Amazon Music priming of the speaker if an Amazon account is linked.
func (s *Server) PrimeDeviceWithAmazon(deviceIP string) {
	s.mu.RLock()
	svc := s.amazonService
	s.mu.RUnlock()

	if svc == nil {
		return
	}

	accounts := svc.GetAccounts()
	if len(accounts) == 0 {
		return
	}

	accessToken, username, err := svc.GetFreshToken()
	if err != nil {
		log.Printf("[Amazon Watchdog] Failed to get fresh token for %s: %v", deviceIP, err)
		return
	}

	log.Printf("[Amazon Watchdog] Proactively priming %s with Amazon user %s", deviceIP, username)

	if err := s.pushAmazonTokenToDevice(deviceIP, username, accessToken); err != nil {
		log.Printf("[Amazon Watchdog] Failed to prime %s: %v", deviceIP, err)
	} else {
		log.Printf("[Amazon Watchdog] Successfully primed %s", deviceIP)
	}
}

func (s *Server) pushAmazonTokenToDevice(deviceIP, username, accessToken string) error {
	var zcURL string
	if _, _, err := net.SplitHostPort(deviceIP); err == nil {
		zcURL = fmt.Sprintf("http://%s/zc", deviceIP)
	} else {
		zcURL = fmt.Sprintf("http://%s:8200/zc", deviceIP)
	}

	return amazon.PushAmazonCredentials(zcURL, username, accessToken)
}

func (s *Server) handleDiscoveredDevice(d models.DiscoveredDevice) {
	log.Printf("Discovered Bose device: %s at %s (Serial: %s)", d.Name, d.Host, d.SerialNo)

	// 1. Always fetch live device info from /info endpoint as the authoritative source
	liveInfo, err := s.sm.GetLiveDeviceInfo(d.Host)
	if err != nil {
		log.Printf("Failed to fetch live device info for %s at %s: %v", d.Name, d.Host, err)
		// Fallback to discovery info if /info is not available
		s.handleDiscoveredDeviceFallback(d)

		return
	}

	// 2. Use deviceID from /info as the canonical device identifier
	deviceID := liveInfo.DeviceID
	if deviceID == "" {
		log.Printf("No deviceID found in /info response for %s at %s, using fallback", d.Name, d.Host)
		s.handleDiscoveredDeviceFallback(d)

		return
	}

	log.Printf("Using deviceID '%s' from /info for device %s at %s", deviceID, d.Name, d.Host)

	// 3. Get account ID from live info or fallback to existing/default
	accountID := liveInfo.MargeAccountUUID
	if accountID == "" {
		// Try to find account ID from existing device entries
		if existing := s.findExistingDeviceInfoByDeviceID(deviceID); existing != nil {
			accountID = existing.AccountID
		}
	}

	if accountID == "" {
		accountID = "default"
	}

	// 4. Get primary MAC address from networkInfo
	macAddress := liveInfo.GetPrimaryMacAddress()

	// 5. Build complete device info from live data
	info := &models.ServiceDeviceInfo{
		DeviceID:            deviceID, // Use deviceID from /info (MAC address)
		AccountID:           accountID,
		Name:                liveInfo.Name,                             // Use name from /info
		IPAddress:           d.Host,                                    // IP from discovery
		MacAddress:          macAddress,                                // MAC from /info networkInfo
		DeviceSerialNumber:  liveInfo.SerialNumber,                     // Serial from components
		ProductCode:         liveInfo.Type + " " + liveInfo.ModuleType, // Type + ModuleType
		FirmwareVersion:     liveInfo.SoftwareVer,
		ProductSerialNumber: "", // Will be populated from components if available
		DiscoveryMethod:     d.DiscoveryMethod,
	}

	// 6. Extract product serial number from PackagedProduct component
	for _, comp := range liveInfo.Components {
		if comp.Category == "PackagedProduct" && comp.SerialNumber != "" {
			info.ProductSerialNumber = comp.SerialNumber
			break
		}
	}

	// 7. Save the updated device info
	if err := s.ds.SaveDeviceInfo(accountID, deviceID, info); err != nil {
		log.Printf("Failed to save device info for %s: %v", deviceID, err)
		return
	}

	// 8. Create default Sources.xml only when no sources file exists yet
	if !s.ds.HasConfiguredSources(accountID, deviceID) {
		if sources, err := s.ds.GetConfiguredSources(accountID, deviceID); err == nil {
			log.Printf("Creating default Sources.xml for device %s", deviceID)

			if err := s.ds.SaveConfiguredSources(accountID, deviceID, sources); err != nil {
				log.Printf("Failed to save default sources for %s: %v", deviceID, err)
			}
		}
	}

	log.Printf("Successfully saved device %s (%s) with MAC-based deviceID: %s", info.Name, d.Host, deviceID)
}

// handleDiscoveredDeviceFallback handles device discovery when /info endpoint is not available
func (s *Server) handleDiscoveredDeviceFallback(d models.DiscoveredDevice) {
	log.Printf("Using fallback discovery method for device: %s at %s", d.Name, d.Host)

	// Use discovery data as-is with the old logic
	existingID := s.findExistingDeviceID(d)

	deviceID := d.SerialNo
	if deviceID == "" {
		deviceID = d.Host
	}

	accountID := "default"
	if existing := s.findExistingDeviceInfo(d); existing != nil {
		accountID = existing.AccountID
	}

	info := &models.ServiceDeviceInfo{
		DeviceID:           deviceID,
		AccountID:          accountID,
		Name:               d.Name,
		IPAddress:          d.Host,
		DeviceSerialNumber: d.SerialNo,
		ProductCode:        d.ModelID,
		FirmwareVersion:    "0.0.0", // Unknown from discovery
		DiscoveryMethod:    d.DiscoveryMethod,
	}

	// If we had an IP-based entry and now have a Serial, clean up the IP-based entry
	if d.SerialNo != "" && existingID != "" && existingID != d.SerialNo {
		log.Printf("Device %s previously known as %s, migrating to serial-based ID %s", d.Name, existingID, d.SerialNo)
		_ = s.ds.RemoveDevice(accountID, existingID)
	}

	if err := s.ds.SaveDeviceInfo(accountID, deviceID, info); err != nil {
		log.Printf("Failed to save device info for %s: %v", deviceID, err)
		return
	}

	// Create default Sources.xml only when no sources file exists yet
	if !s.ds.HasConfiguredSources(accountID, deviceID) {
		if sources, err := s.ds.GetConfiguredSources(accountID, deviceID); err == nil {
			log.Printf("Creating default Sources.xml for device %s (fallback)", deviceID)

			if err := s.ds.SaveConfiguredSources(accountID, deviceID, sources); err != nil {
				log.Printf("Failed to save default sources for %s: %v", deviceID, err)
			}
		}
	}

	log.Printf("Successfully saved device %s (%s) with fallback deviceID: %s", info.Name, d.Host, deviceID)
}

func (s *Server) mergeOverlappingDevices() {
	allDevices, err := s.ds.ListAllDevices()
	if err != nil {
		return
	}

	// Group devices by IP
	byIP := make(map[string][]models.ServiceDeviceInfo)

	for i := range allDevices {
		dev := allDevices[i]
		if dev.IPAddress != "" {
			byIP[dev.IPAddress] = append(byIP[dev.IPAddress], dev)
		}
	}

	for ip, devices := range byIP {
		if len(devices) <= 1 {
			continue
		}

		// We have multiple entries for the same IP.
		// Try to find one with a Serial Number to be the master.
		var master *models.ServiceDeviceInfo

		for i := range devices {
			if devices[i].DeviceSerialNumber != "" {
				master = &devices[i]
				break
			}
		}

		if master == nil {
			// Fallback: look for one with DeviceID that isn't the IP
			for i := range devices {
				if devices[i].DeviceID != "" && devices[i].DeviceID != devices[i].IPAddress {
					master = &devices[i]
					break
				}
			}
		}

		if master == nil {
			// None have serials, just keep the first one
			continue
		}

		masterID := master.DeviceID
		if masterID == "" {
			masterID = master.DeviceSerialNumber
		}

		for i := range devices {
			dev := devices[i]
			devID := dev.DeviceID

			if devID == "" {
				devID = dev.IPAddress
			}

			if devID != masterID && dev.IPAddress == ip {
				log.Printf("Merging overlapping device entry %s into %s (IP: %s)", devID, masterID, ip)
				_ = s.ds.RemoveDevice(dev.AccountID, devID)
			}
		}
	}
}

func (s *Server) findExistingDeviceID(d models.DiscoveredDevice) string {
	info := s.findExistingDeviceInfo(d)
	if info != nil {
		return info.DeviceID
	}

	return ""
}

func (s *Server) findExistingDeviceInfo(d models.DiscoveredDevice) *models.ServiceDeviceInfo {
	allDevices, _ := s.ds.ListAllDevices()
	for i := range allDevices {
		known := allDevices[i]
		// Match by Serial
		if d.SerialNo != "" && (known.DeviceID == d.SerialNo || known.DeviceSerialNumber == d.SerialNo) {
			return &known
		}
		// Match by IP
		if d.Host != "" && known.IPAddress == d.Host {
			return &known
		}
	}

	return nil
}

func (s *Server) resolveDeviceIDToIP(deviceID string) (string, error) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	// 1. Try to find in Datastore
	devices, err := s.ds.ListAllDevices()
	if err == nil {
		for i := range devices {
			if devices[i].DeviceID == deviceID {
				return devices[i].IPAddress, nil
			}
		}
	}

	return "", fmt.Errorf("device not found: %s", deviceID)
}
