//go:build !android && !ios && !freebsd && !js

package main

import (
	"strings"

	"github.com/wailsapp/wails/v3/pkg/application"

	"github.com/netbirdio/netbird/client/ui/services"
)

func (t *Tray) onStatusEvent(ev *application.CustomEvent) {
	st, ok := ev.Data.(services.Status)
	if !ok {
		return
	}
	t.applyStatus(st)
}

// applyStatus updates the tray icon, status label, exit-node submenu, and
// connect/disconnect enablement based on the latest daemon snapshot.
// Skips the icon refresh when none of the icon-relevant inputs
// (connected, hasUpdate, status label) changed — the daemon emits
// rapid SubscribeStatus bursts during health probes that would
// otherwise spam Shell_NotifyIcon and the log.
//
// Profile-switch suppression lives one layer up in services/daemon_feed.go
// (DaemonFeed.BeginProfileSwitch / consumeForSwitch) so the optimistic
// Connecting paint and the suppressed Idle/Connected events are shared
// with the React Status page rather than being a tray-only behaviour.
func (t *Tray) applyStatus(st services.Status) {
	t.statusMu.Lock()
	connected := strings.EqualFold(st.Status, services.StatusConnected)
	iconChanged := connected != t.connected || st.Status != t.lastStatus
	// Detect the transition into SessionExpired: the daemon emits the
	// state on every Status snapshot for as long as the session stays
	// expired, so without this guard we would re-fire the notification
	// on every push. Mirrors the legacy Fyne client's sendNotification
	// flag in onSessionExpire.
	sessionExpiredEnter := strings.EqualFold(st.Status, services.StatusSessionExpired) &&
		!strings.EqualFold(t.lastStatus, services.StatusSessionExpired)

	triggerLogin := t.consumePendingConnectLogin(st.Status)

	daemonVersionChanged := st.DaemonVersion != "" && st.DaemonVersion != t.lastDaemonVersion
	t.connected = connected
	t.lastStatus = st.Status
	if daemonVersionChanged {
		t.lastDaemonVersion = st.DaemonVersion
	}

	revisionChanged := st.NetworksRevision != t.lastNetworksRevision
	t.lastNetworksRevision = st.NetworksRevision
	t.statusMu.Unlock()

	if triggerLogin {
		t.app.Event.Emit(services.EventTriggerLogin)
	}

	if iconChanged {
		t.applyIcon()
		t.refreshMenuItemsForStatus(st, connected)
	}
	// Re-fetch the selectable exit-node list whenever the daemon's routed-
	// networks revision bumps (a route candidate added/removed, or a selection
	// applied from any surface) or the tunnel flips state (iconChanged). The
	// revision is the only reliable signal: candidate routes never appear in
	// the peer-status snapshot, so a removed exit node would otherwise go
	// unnoticed. The refresh owns the parent item's enablement and the rebuild.
	if iconChanged || revisionChanged {
		go t.refreshExitNodes()
	}
	if daemonVersionChanged && t.daemonVersionItem != nil {
		// The version row lives in the About submenu, which KDE/Plasma caches on
		// first open and never re-fetches on a plain SetLabel (see relayoutMenu's
		// doc comment). Drive a full relayout so the new version actually paints.
		// relayoutMenu repaints the label from the cached lastDaemonVersion.
		t.relayoutMenu()
	}
	if sessionExpiredEnter {
		t.handleSessionExpired()
	}

	t.applySessionExpiry(st.SessionExpiresAt, connected)
}

// consumePendingConnectLogin acts on the SSO auto-handoff flag armed by
// handleConnect. It returns true (and clears the flag) when the daemon
// reached NeedsLogin, signalling the browser-login flow should start so the
// user doesn't need to click Connect a second time. The flag is also cleared
// on any other terminal state — including Connecting bursts that resolve to
// Connected / Idle / LoginFailed / DaemonUnavailable — so a stale flag can't
// fire weeks later when the daemon happens to flip. Must be called with
// statusMu held.
func (t *Tray) consumePendingConnectLogin(status string) bool {
	if !t.pendingConnectLogin {
		return false
	}
	switch {
	case strings.EqualFold(status, services.StatusNeedsLogin):
		t.pendingConnectLogin = false
		return true
	case strings.EqualFold(status, services.StatusConnected),
		strings.EqualFold(status, services.StatusIdle),
		strings.EqualFold(status, services.StatusLoginFailed),
		strings.EqualFold(status, services.StatusSessionExpired),
		strings.EqualFold(status, services.StatusDaemonUnavailable):
		t.pendingConnectLogin = false
	}
	return false
}

// refreshMenuItemsForStatus updates the status row, Connect/Disconnect
// enablement, Settings/Profiles gating, and Profiles submenu on a status-text
// transition (called from applyStatus only when iconChanged).
func (t *Tray) refreshMenuItemsForStatus(st services.Status, connected bool) {
	daemonUnavailable := strings.EqualFold(st.Status, services.StatusDaemonUnavailable)
	connecting := strings.EqualFold(st.Status, services.StatusConnecting)
	if t.statusItem != nil {
		// Label-only: row is informational (no OnClick). Enablement
		// is platform-dependent via statusRowEnabled — Windows
		// keeps it enabled so the Win32 disabled-state mask does
		// not desaturate the coloured dot; macOS/Linux disable it.
		// Swap the displayed text so the user sees a familiar
		// phrase instead of the raw daemon enum.
		t.statusItem.SetLabel(t.loc.StatusLabel(st.Status))
		t.statusItem.SetEnabled(statusRowEnabled())
		t.applyStatusIndicator(st.Status)
	}
	if t.upItem != nil {
		// Connect stays visible/clickable in NeedsLogin/SessionExpired/
		// LoginFailed too — the daemon's Up RPC kicks off the SSO flow
		// when re-auth is required, mirroring the legacy Fyne client
		// where the same button drove the initial and the re-login
		// paths. Hidden only when the action would be a no-op (tunnel
		// up, daemon mid-connect — Disconnect takes the slot) or
		// would fail with no useful side effect (daemon unreachable).
		t.upItem.SetHidden(connected || connecting || daemonUnavailable)
		t.upItem.SetEnabled(!connected && !connecting && !daemonUnavailable)
	}
	if t.downItem != nil {
		// Disconnect is the abort path while the daemon is still
		// retrying the management dial — without it the user has no
		// way to stop the loop short of killing the daemon.
		t.downItem.SetHidden(!connected && !connecting)
		t.downItem.SetEnabled(connected || connecting)
	}
	// Exit Node parent-item enablement (greyed unless the tunnel is up
	// AND at least one candidate exists) is owned by refreshExitNodes,
	// triggered by applyStatus on this same transition. Settings just needs
	// the daemon socket reachable.
	if t.settingsItem != nil {
		t.settingsItem.SetEnabled(!daemonUnavailable)
	}
	if t.profileSubmenuItem != nil {
		t.profileSubmenuItem.SetEnabled(!daemonUnavailable)
	}
	// Refresh the Profiles submenu on every status-text transition: the
	// daemon does not emit an active-profile event, so the startup race
	// (UI loads profiles before autoconnect picks the persisted profile)
	// and a CLI "profile select && up" both surface here. loadProfiles
	// fetches the rows and drives a full relayoutMenu (serialised by menuMu),
	// so it cannot race the SetHidden/SetEnabled writes on the static items
	// above — the Wails 3 alpha menu API is not goroutine-safe and reads
	// item.disabled/item.hidden at NSMenuItem construction time.
	go t.loadProfiles()
}

// applyStatusIndicator sets the small coloured dot shown on the status
// menu entry. The dot mirrors the tray icon's state through a fixed
// palette: green for Connected, yellow for Connecting, blue for the
// login states, red for hard errors, grey for the idle/disconnected
// pair and a darker grey when the daemon socket is unreachable.
//
// Wails v3 alpha's setMenuItemBitmap calls NSMenuItem.setImage from
// whichever thread invoked SetBitmap — unlike setMenuItemLabel/Disabled/
// Hidden/Checked which dispatch_sync onto the main queue. The off-thread
// AppKit call leaves the visible dot stale until the next time the menu
// is reopened (close+reopen workaround). Rebuilding via tray.SetMenu
// reruns processMenu inside InvokeSync, so the bitmap is applied to a
// fresh NSMenuItem on the main thread and macOS picks it up.
func (t *Tray) applyStatusIndicator(status string) {
	if t.statusItem == nil {
		return
	}
	t.statusItem.SetBitmap(statusIndicatorBitmap(status))
	if t.menu != nil {
		t.tray.SetMenu(t.menu)
	}
}

func statusIndicatorBitmap(status string) []byte {
	switch {
	case strings.EqualFold(status, services.StatusConnected):
		return iconMenuDotConnected
	case strings.EqualFold(status, services.StatusConnecting):
		return iconMenuDotConnecting
	case strings.EqualFold(status, services.StatusNeedsLogin),
		strings.EqualFold(status, services.StatusSessionExpired):
		return iconMenuDotConnecting
	case strings.EqualFold(status, services.StatusLoginFailed),
		strings.EqualFold(status, statusError):
		return iconMenuDotError
	case strings.EqualFold(status, services.StatusDaemonUnavailable):
		return iconMenuDotOffline
	default:
		return iconMenuDotIdle
	}
}
