import AppKit
import Foundation
import Observation
import SwiftUI

@MainActor
final class MenuSessionsInjector: NSObject, NSMenuDelegate {
    static let shared = MenuSessionsInjector()

    private let tag = 9_415_557
    private let nodesTag = 9_415_558
    private let fallbackWidth: CGFloat = 320
    private let activeWindowSeconds: TimeInterval = 24 * 60 * 60

    private weak var originalDelegate: NSMenuDelegate?
    private weak var statusItem: NSStatusItem?
    private var loadTask: Task<Void, Never>?
    private var nodesLoadTask: Task<Void, Never>?
    private var previewTasks: [Task<Void, Never>] = []
    private var isMenuOpen = false
    private var lastKnownMenuWidth: CGFloat?
    private var menuOpenWidth: CGFloat?
    private var isObservingControlChannel = false

    private var cachedSnapshot: SessionStoreSnapshot?
    private var cachedErrorText: String?
    private var cacheUpdatedAt: Date?
    private let refreshIntervalSeconds: TimeInterval = 12
    private var cachedUsageSummary: GatewayUsageSummary?
    private var cachedUsageErrorText: String?
    private var usageCacheUpdatedAt: Date?
    private let usageRefreshIntervalSeconds: TimeInterval = 30
    private var cachedCostSummary: GatewayCostUsageSummary?
    private var cachedCostErrorText: String?
    private var costCacheUpdatedAt: Date?
    private let costRefreshIntervalSeconds: TimeInterval = 45
    private let nodesStore = NodesStore.shared
    #if DEBUG
    private var testControlChannelConnected: Bool?
    #endif

    func install(into statusItem: NSStatusItem) {
        self.statusItem = statusItem
        guard let menu = statusItem.menu else { return }

        // Preserve SwiftUI's internal NSMenuDelegate, otherwise it may stop populating menu items.
        if menu.delegate !== self {
            self.originalDelegate = menu.delegate
            menu.delegate = self
        }

        if self.loadTask == nil {
            self.loadTask = Task { await self.refreshCache(force: true) }
        }

        self.startControlChannelObservation()
        self.nodesStore.start()
    }

    func menuWillOpen(_ menu: NSMenu) {
        self.originalDelegate?.menuWillOpen?(menu)
        self.isMenuOpen = true
        self.menuOpenWidth = self.currentMenuWidth(for: menu)

        self.inject(into: menu)
        self.injectNodes(into: menu)

        // Refresh in background for the next open; keep width stable while open.
        self.loadTask?.cancel()
        let forceRefresh = self.cachedSnapshot == nil || self.cachedErrorText != nil
        self.loadTask = Task { [weak self] in
            guard let self else { return }
            await self.refreshCache(force: forceRefresh)
            await self.refreshUsageCache(force: forceRefresh)
            await self.refreshCostUsageCache(force: forceRefresh)
            await MainActor.run {
                guard self.isMenuOpen else { return }
                self.inject(into: menu)
                self.injectNodes(into: menu)
            }
        }

        self.nodesLoadTask?.cancel()
        self.nodesLoadTask = Task { [weak self] in
            guard let self else { return }
            await self.nodesStore.refresh()
            await MainActor.run {
                guard self.isMenuOpen else { return }
                self.injectNodes(into: menu)
            }
        }
    }

    func menuDidClose(_ menu: NSMenu) {
        self.originalDelegate?.menuDidClose?(menu)
        self.isMenuOpen = false
        self.menuOpenWidth = nil
        self.loadTask?.cancel()
        self.nodesLoadTask?.cancel()
        self.cancelPreviewTasks()
    }

    private func startControlChannelObservation() {
        guard !self.isObservingControlChannel else { return }
        self.isObservingControlChannel = true
        self.observeControlChannelState()
    }

    private func observeControlChannelState() {
        withObservationTracking {
            _ = ControlChannel.shared.state
        } onChange: { [weak self] in
            Task { @MainActor [weak self] in
                guard let self else { return }
                self.handleControlChannelStateChange()
                self.observeControlChannelState()
            }
        }
    }

    private func handleControlChannelStateChange() {
        guard self.isMenuOpen, let menu = self.statusItem?.menu else { return }
        self.loadTask?.cancel()
        self.loadTask = Task { [weak self, weak menu] in
            guard let self, let menu else { return }
            await self.refreshCache(force: true)
            await self.refreshUsageCache(force: true)
            await self.refreshCostUsageCache(force: true)
            await MainActor.run {
                guard self.isMenuOpen else { return }
                self.inject(into: menu)
                self.injectNodes(into: menu)
            }
        }

        self.nodesLoadTask?.cancel()
        self.nodesLoadTask = Task { [weak self, weak menu] in
            guard let self, let menu else { return }
            await self.nodesStore.refresh()
            await MainActor.run {
                guard self.isMenuOpen else { return }
                self.injectNodes(into: menu)
            }
        }
    }

    func menuNeedsUpdate(_ menu: NSMenu) {
        self.originalDelegate?.menuNeedsUpdate?(menu)
    }

    func confinementRect(for menu: NSMenu, on screen: NSScreen?) -> NSRect {
        if let rect = self.originalDelegate?.confinementRect?(for: menu, on: screen) {
            return rect
        }
        return NSRect.zero
    }
}

extension MenuSessionsInjector {
    // MARK: - Injection

    private var mainSessionKey: String { WorkActivityStore.shared.mainSessionKey }

    private func inject(into menu: NSMenu) {
        self.cancelPreviewTasks()
        // Remove any previous injected items.
        for item in menu.items where item.tag == self.tag {
            menu.removeItem(item)
        }

        guard let insertIndex = self.findInsertIndex(in: menu) else { return }
        let width = self.initialWidth(for: menu)
        let isConnected = self.isControlChannelConnected
        let channelState = ControlChannel.shared.state

        var cursor = insertIndex
        var headerView: NSView?

        if let snapshot = self.cachedSnapshot {
            let now = Date()
            let mainKey = self.mainSessionKey
            let rows = snapshot.rows.filter { row in
                if row.key == "main", mainKey != "main" { return false }
                if row.key == mainKey { return true }
                guard let updatedAt = row.updatedAt else { return false }
                return now.timeIntervalSince(updatedAt) <= self.activeWindowSeconds
            }.sorted { lhs, rhs in
                if lhs.key == mainKey { return true }
                if rhs.key == mainKey { return false }
                return (lhs.updatedAt ?? .distantPast) > (rhs.updatedAt ?? .distantPast)
            }
            if !rows.isEmpty {
                let previewKeys = rows.prefix(20).map(\.key)
                let task = Task {
                    await SessionMenuPreviewLoader.prewarm(sessionKeys: previewKeys, maxItems: 10)
                }
                self.previewTasks.append(task)
            }

            let headerItem = NSMenuItem()
            headerItem.tag = self.tag
            headerItem.isEnabled = false
            let statusText = self
                .cachedErrorText ?? (isConnected ? nil : self.controlChannelStatusText(for: channelState))
            let hosted = self.makeHostedView(
                rootView: AnyView(MenuSessionsHeaderView(
                    count: rows.count,
                    statusText: statusText)),
                width: width,
                highlighted: false)
            headerItem.view = hosted
            headerView = hosted
            menu.insertItem(headerItem, at: cursor)
            cursor += 1

            if rows.isEmpty {
                menu.insertItem(
                    self.makeMessageItem(text: "No active sessions", symbolName: "minus", width: width),
                    at: cursor)
                cursor += 1
            } else {
                for row in rows {
                    let item = NSMenuItem()
                    item.tag = self.tag
                    item.isEnabled = true
                    item.submenu = self.buildSubmenu(for: row, storePath: snapshot.storePath)
                    item.view = self.makeHostedView(
                        rootView: AnyView(SessionMenuLabelView(row: row, width: width)),
                        width: width,
                        highlighted: true)
                    menu.insertItem(item, at: cursor)
                    cursor += 1
                }
            }
        } else {
            let headerItem = NSMenuItem()
            headerItem.tag = self.tag
            headerItem.isEnabled = false
            let statusText = isConnected
                ? (self.cachedErrorText ?? "Loading sessions…")
                : self.controlChannelStatusText(for: channelState)
            let hosted = self.makeHostedView(
                rootView: AnyView(MenuSessionsHeaderView(
                    count: 0,
                    statusText: statusText)),
                width: width,
                highlighted: false)
            headerItem.view = hosted
            headerView = hosted
            menu.insertItem(headerItem, at: cursor)
            cursor += 1

            if !isConnected {
                menu.insertItem(
                    self.makeMessageItem(
                        text: "Connect the gateway to see sessions",
                        symbolName: "bolt.slash",
                        width: width),
                    at: cursor)
                cursor += 1
            }
        }

        cursor = self.insertUsageSection(into: menu, at: cursor, width: width)
        cursor = self.insertCostUsageSection(into: menu, at: cursor, width: width)

        DispatchQueue.main.async { [weak self, weak headerView] in
            guard let self, let headerView else { return }
            self.captureMenuWidthIfAvailable(from: headerView)
        }
    }

    private func injectNodes(into menu: NSMenu) {
        for item in menu.items where item.tag == self.nodesTag {
            menu.removeItem(item)
        }

        guard let insertIndex = self.findNodesInsertIndex(in: menu) else { return }
        let width = self.initialWidth(for: menu)
        var cursor = insertIndex

        let entries = self.sortedNodeEntries()
        let topSeparator = NSMenuItem.separator()
        topSeparator.tag = self.nodesTag
        menu.insertItem(topSeparator, at: cursor)
        cursor += 1

        if let gatewayEntry = self.gatewayEntry() {
            let gatewayItem = self.makeNodeItem(entry: gatewayEntry, width: width)
            menu.insertItem(gatewayItem, at: cursor)
            cursor += 1
        }

        if case .connecting = ControlChannel.shared.state {
            menu.insertItem(
                self.makeMessageItem(text: "Connecting…", symbolName: "circle.dashed", width: width),
                at: cursor)
            cursor += 1
            return
        }

        guard self.isControlChannelConnected else { return }

        if let error = self.nodesStore.lastError?.nonEmpty {
            menu.insertItem(
                self.makeMessageItem(
                    text: "Error: \(error)",
                    symbolName: "exclamationmark.triangle",
                    width: width),
                at: cursor)
            cursor += 1
        } else if let status = self.nodesStore.statusMessage?.nonEmpty {
            menu.insertItem(
                self.makeMessageItem(text: status, symbolName: "info.circle", width: width),
                at: cursor)
            cursor += 1
        }

        if entries.isEmpty {
            let title = self.nodesStore.isLoading ? "Loading devices..." : "No devices yet"
            menu.insertItem(
                self.makeMessageItem(text: title, symbolName: "circle.dashed", width: width),
                at: cursor)
            cursor += 1
        } else {
            for entry in entries.prefix(8) {
                let item = self.makeNodeItem(entry: entry, width: width)
                menu.insertItem(item, at: cursor)
                cursor += 1
            }

            if entries.count > 8 {
                let moreItem = NSMenuItem()
                moreItem.tag = self.nodesTag
                moreItem.title = "More Devices..."
                moreItem.image = NSImage(systemSymbolName: "ellipsis.circle", accessibilityDescription: nil)
                let overflow = Array(entries.dropFirst(8))
                moreItem.submenu = self.buildNodesOverflowMenu(entries: overflow, width: width)
                menu.insertItem(moreItem, at: cursor)
                cursor += 1
            }
        }

        _ = cursor
    }

    private func insertUsageSection(into menu: NSMenu, at cursor: Int, width: CGFloat) -> Int {
        let rows = self.usageRows
        if rows.isEmpty {
            return cursor
        }

        var cursor = cursor

        if cursor > 0, !menu.items[cursor - 1].isSeparatorItem {
            let separator = NSMenuItem.separator()
            separator.tag = self.tag
            menu.insertItem(separator, at: cursor)
            cursor += 1
        }

        let headerItem = NSMenuItem()
        headerItem.tag = self.tag
        headerItem.isEnabled = false
        headerItem.view = self.makeHostedView(
            rootView: AnyView(MenuUsageHeaderView(
                count: rows.count)),
            width: width,
            highlighted: false)
        menu.insertItem(headerItem, at: cursor)
        cursor += 1

        if let selectedProvider = self.selectedUsageProviderId,
           let primary = rows.first(where: { $0.providerId.lowercased() == selectedProvider }),
           rows.count > 1
        {
            let others = rows.filter { $0.providerId.lowercased() != selectedProvider }

            let item = NSMenuItem()
            item.tag = self.tag
            item.isEnabled = true
            if !others.isEmpty {
                item.submenu = self.buildUsageOverflowMenu(rows: others, width: width)
            }
            item.view = self.makeHostedView(
                rootView: AnyView(UsageMenuLabelView(row: primary, width: width, showsChevron: !others.isEmpty)),
                width: width,
                highlighted: true)
            menu.insertItem(item, at: cursor)
            cursor += 1

            return cursor
        }

        for row in rows {
            let item = NSMenuItem()
            item.tag = self.tag
            item.isEnabled = false
            item.view = self.makeHostedView(
                rootView: AnyView(UsageMenuLabelView(row: row, width: width)),
                width: width,
                highlighted: false)
            menu.insertItem(item, at: cursor)
            cursor += 1
        }

        return cursor
    }

    private func insertCostUsageSection(into menu: NSMenu, at cursor: Int, width: CGFloat) -> Int {
        guard self.isControlChannelConnected else { return cursor }
        guard let submenu = self.buildCostUsageSubmenu(width: width) else { return cursor }
        var cursor = cursor

        if cursor > 0, !menu.items[cursor - 1].isSeparatorItem {
            let separator = NSMenuItem.separator()
            separator.tag = self.tag
            menu.insertItem(separator, at: cursor)
            cursor += 1
        }

        let item = NSMenuItem(title: "Usage cost (30 days)", action: nil, keyEquivalent: "")
        item.tag = self.tag
        item.isEnabled = true
        item.image = NSImage(systemSymbolName: "chart.bar.xaxis", accessibilityDescription: nil)
        item.submenu = submenu
        menu.insertItem(item, at: cursor)
        cursor += 1
        return cursor
    }

    private var selectedUsageProviderId: String? {
        guard let model = self.cachedSnapshot?.defaults.model.nonEmpty else { return nil }
        let trimmed = model.trimmingCharacters(in: .whitespacesAndNewlines)
        guard let slash = trimmed.firstIndex(of: "/") else { return nil }
        let provider = trimmed[..<slash].trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
        return provider.nonEmpty
    }

    private var usageRows: [UsageRow] {
        guard let summary = self.cachedUsageSummary else { return [] }
        return summary.primaryRows()
    }

    private func buildUsageOverflowMenu(rows: [UsageRow], width: CGFloat) -> NSMenu {
        let menu = NSMenu()
        for row in rows {
            let item = NSMenuItem()
            item.tag = self.tag
            item.isEnabled = false
            item.view = self.makeHostedView(
                rootView: AnyView(UsageMenuLabelView(row: row, width: width)),
                width: width,
                highlighted: false)
            menu.addItem(item)
        }
        return menu
    }

    private var isControlChannelConnected: Bool {
        #if DEBUG
        if let override = self.testControlChannelConnected { return override }
        #endif
        if case .connected = ControlChannel.shared.state { return true }
        return false
    }

    private func controlChannelStatusText(for state: ControlChannel.ConnectionState) -> String {
        switch state {
        case .connected:
            "Loading sessions…"
        case .connecting:
            "Connecting…"
        case let .degraded(message):
            message.nonEmpty ?? "Gateway disconnected"
        case .disconnected:
            "Gateway disconnected"
        }
    }

    private func buildCostUsageSubmenu(width: CGFloat) -> NSMenu? {
        if let error = self.cachedCostErrorText, !error.isEmpty, self.cachedCostSummary == nil {
            let menu = NSMenu()
            let item = NSMenuItem(title: error, action: nil, keyEquivalent: "")
            item.isEnabled = false
            menu.addItem(item)
            return menu
        }

        guard let summary = self.cachedCostSummary else { return nil }
        guard !summary.daily.isEmpty else { return nil }

        let menu = NSMenu()
        menu.delegate = self

        let chartView = CostUsageHistoryMenuView(summary: summary, width: width)
        let hosting = NSHostingView(rootView: AnyView(chartView))
        let controller = NSHostingController(rootView: AnyView(chartView))
        let size = controller.sizeThatFits(in: CGSize(width: width, height: .greatestFiniteMagnitude))
        hosting.frame = NSRect(origin: .zero, size: NSSize(width: width, height: size.height))

        let chartItem = NSMenuItem()
        chartItem.view = hosting
        chartItem.isEnabled = false
        chartItem.representedObject = "costUsageChart"
        menu.addItem(chartItem)

        return menu
    }

    private func gatewayEntry() -> NodeInfo? {
        let mode = AppStateStore.shared.connectionMode
        let isConnected = self.isControlChannelConnected
        let port = GatewayEnvironment.gatewayPort()
        var host: String?
        var platform: String?

        switch mode {
        case .remote:
            platform = "remote"
            if AppStateStore.shared.remoteTransport == .direct {
                let trimmedUrl = AppStateStore.shared.remoteUrl
                    .trimmingCharacters(in: .whitespacesAndNewlines)
                if let url = URL(string: trimmedUrl), let urlHost = url.host, !urlHost.isEmpty {
                    if let port = url.port {
                        host = "\(urlHost):\(port)"
                    } else {
                        host = urlHost
                    }
                } else {
                    host = trimmedUrl.nonEmpty
                }
            } else {
                let target = AppStateStore.shared.remoteTarget
                if let parsed = CommandResolver.parseSSHTarget(target) {
                    host = parsed.port == 22 ? parsed.host : "\(parsed.host):\(parsed.port)"
                } else {
                    host = target.nonEmpty
                }
            }
        case .local:
            platform = "local"
            host = GatewayConnectivityCoordinator.shared.localEndpointHostLabel ?? "127.0.0.1:\(port)"
        case .unconfigured:
            platform = nil
            host = nil
        }

        return NodeInfo(
            nodeId: "gateway",
            displayName: "Gateway",
            platform: platform,
            version: nil,
            coreVersion: nil,
            uiVersion: nil,
            deviceFamily: nil,
            modelIdentifier: nil,
            remoteIp: host,
            caps: nil,
            commands: nil,
            permissions: nil,
            paired: nil,
            connected: isConnected)
    }

    private func makeNodeItem(entry: NodeInfo, width: CGFloat) -> NSMenuItem {
        let item = NSMenuItem()
        item.tag = self.nodesTag
        item.target = self
        item.action = #selector(self.copyNodeSummary(_:))
        item.representedObject = NodeMenuEntryFormatter.summaryText(entry)
        item.view = HighlightedMenuItemHostView(
            rootView: AnyView(NodeMenuRowView(entry: entry, width: width)),
            width: width)
        item.submenu = self.buildNodeSubmenu(entry: entry, width: width)
        return item
    }

    private func makeSessionPreviewItem(
        sessionKey: String,
        title: String,
        width: CGFloat,
        maxLines: Int) -> NSMenuItem
    {
        let item = NSMenuItem()
        item.tag = self.tag
        item.isEnabled = false
        let view = AnyView(SessionMenuPreviewView(
            width: width,
            maxLines: maxLines,
            title: title,
            items: [],
            status: .loading))
        let hosting = NSHostingView(rootView: view)
        hosting.frame.size.width = max(1, width)
        let size = hosting.fittingSize
        hosting.frame = NSRect(origin: .zero, size: NSSize(width: width, height: size.height))
        item.view = hosting

        let task = Task { [weak hosting] in
            let snapshot = await SessionMenuPreviewLoader.load(sessionKey: sessionKey, maxItems: 10)
            guard !Task.isCancelled else { return }
            await MainActor.run {
                guard let hosting else { return }
                let nextView = AnyView(SessionMenuPreviewView(
                    width: width,
                    maxLines: maxLines,
                    title: title,
                    items: snapshot.items,
                    status: snapshot.status))
                hosting.rootView = nextView
                hosting.invalidateIntrinsicContentSize()
                hosting.frame.size.width = max(1, width)
                let size = hosting.fittingSize
                hosting.frame.size.height = size.height
            }
        }
        self.previewTasks.append(task)
        return item
    }

    private func cancelPreviewTasks() {
        for task in self.previewTasks {
            task.cancel()
        }
        self.previewTasks.removeAll()
    }

    private func makeMessageItem(text: String, symbolName: String, width: CGFloat, maxLines: Int? = 2) -> NSMenuItem {
        let view = AnyView(
            HStack(alignment: .top, spacing: 8) {
                Image(systemName: symbolName)
                    .font(.caption)
                    .foregroundStyle(.secondary)
                    .frame(width: 14, alignment: .leading)
                    .padding(.top, 1)

                Text(text)
                    .font(.caption)
                    .foregroundStyle(.secondary)
                    .multilineTextAlignment(.leading)
                    .lineLimit(maxLines)
                    .truncationMode(.tail)
                    .fixedSize(horizontal: false, vertical: true)
                    .layoutPriority(1)
                    .frame(maxWidth: .infinity, alignment: .leading)

                Spacer(minLength: 0)
            }
            .padding(.leading, 18)
            .padding(.trailing, 12)
            .padding(.vertical, 6)
            .frame(width: max(1, width), alignment: .leading))

        let item = NSMenuItem()
        item.tag = self.tag
        item.isEnabled = false
        item.view = self.makeHostedView(rootView: view, width: width, highlighted: false)
        return item
    }
}

extension MenuSessionsInjector {
    // MARK: - Cache

    private func refreshCache(force: Bool) async {
        if !force, let updated = self.cacheUpdatedAt, Date().timeIntervalSince(updated) < self.refreshIntervalSeconds {
            return
        }

        guard self.isControlChannelConnected else {
            if self.cachedSnapshot != nil {
                self.cachedErrorText = "Gateway disconnected (showing cached)"
            } else {
                self.cachedErrorText = nil
            }
            self.cacheUpdatedAt = Date()
            return
        }

        do {
            self.cachedSnapshot = try await SessionLoader.loadSnapshot(limit: 32)
            self.cachedErrorText = nil
            self.cacheUpdatedAt = Date()
        } catch {
            self.cachedSnapshot = nil
            self.cachedErrorText = self.compactError(error)
            self.cacheUpdatedAt = Date()
        }
    }

    private func refreshUsageCache(force: Bool) async {
        if !force,
           let updated = self.usageCacheUpdatedAt,
           Date().timeIntervalSince(updated) < self.usageRefreshIntervalSeconds
        {
            return
        }

        guard self.isControlChannelConnected else {
            self.usageCacheUpdatedAt = Date()
            return
        }

        do {
            self.cachedUsageSummary = try await UsageLoader.loadSummary()
        } catch {
            self.cachedUsageSummary = nil
            self.cachedUsageErrorText = nil
        }
        self.usageCacheUpdatedAt = Date()
    }

    private func refreshCostUsageCache(force: Bool) async {
        if !force,
           let updated = self.costCacheUpdatedAt,
           Date().timeIntervalSince(updated) < self.costRefreshIntervalSeconds
        {
            return
        }

        guard self.isControlChannelConnected else {
            self.costCacheUpdatedAt = Date()
            return
        }

        do {
            self.cachedCostSummary = try await CostUsageLoader.loadSummary()
            self.cachedCostErrorText = nil
        } catch {
            self.cachedCostSummary = nil
            self.cachedCostErrorText = self.compactUsageError(error)
        }
        self.costCacheUpdatedAt = Date()
    }

    private func compactUsageError(_ error: Error) -> String {
        let message = error.localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines)
        if message.isEmpty { return "Usage unavailable" }
        if message.count > 90 { return "\(message.prefix(87))…" }
        return message
    }

    private func compactError(_ error: Error) -> String {
        if let loadError = error as? SessionLoadError {
            switch loadError {
            case .gatewayUnavailable:
                return "No connection to gateway"
            case .decodeFailed:
                return "Sessions unavailable"
            }
        }
        return "Sessions unavailable"
    }
}

extension MenuSessionsInjector {
    // MARK: - Submenus

    private func buildSubmenu(for row: SessionRow, storePath: String) -> NSMenu {
        let menu = NSMenu()
        let width = self.submenuWidth()

        menu.addItem(self.makeSessionPreviewItem(
            sessionKey: row.key,
            title: "Recent messages (last 10)",
            width: width,
            maxLines: 3))

        let morePreview = NSMenuItem(title: "More preview…", action: nil, keyEquivalent: "")
        morePreview.submenu = self.buildPreviewSubmenu(sessionKey: row.key, width: width)
        menu.addItem(morePreview)

        menu.addItem(NSMenuItem.separator())

        let thinking = NSMenuItem(title: "Thinking", action: nil, keyEquivalent: "")
        thinking.submenu = self.buildThinkingMenu(for: row)
        menu.addItem(thinking)

        let verbose = NSMenuItem(title: "Verbose", action: nil, keyEquivalent: "")
        verbose.submenu = self.buildVerboseMenu(for: row)
        menu.addItem(verbose)

        if AppStateStore.shared.debugPaneEnabled,
           AppStateStore.shared.connectionMode == .local,
           let sessionId = row.sessionId,
           !sessionId.isEmpty
        {
            menu.addItem(NSMenuItem.separator())
            let openLog = NSMenuItem(
                title: "Open Session Log",
                action: #selector(self.openSessionLog(_:)),
                keyEquivalent: "")
            openLog.target = self
            openLog.representedObject = [
                "sessionId": sessionId,
                "storePath": storePath,
            ]
            menu.addItem(openLog)
        }

        menu.addItem(NSMenuItem.separator())

        let reset = NSMenuItem(title: "Reset Session", action: #selector(self.resetSession(_:)), keyEquivalent: "")
        reset.target = self
        reset.representedObject = row.key
        menu.addItem(reset)

        let compact = NSMenuItem(
            title: "Compact Session Log",
            action: #selector(self.compactSession(_:)),
            keyEquivalent: "")
        compact.target = self
        compact.representedObject = row.key
        menu.addItem(compact)

        if row.key != self.mainSessionKey, row.key != "global" {
            let del = NSMenuItem(title: "Delete Session", action: #selector(self.deleteSession(_:)), keyEquivalent: "")
            del.target = self
            del.representedObject = row.key
            del.isAlternate = false
            del.keyEquivalentModifierMask = []
            menu.addItem(del)
        }

        return menu
    }

    private func buildThinkingMenu(for row: SessionRow) -> NSMenu {
        let menu = NSMenu()
        menu.autoenablesItems = false
        menu.showsStateColumn = true
        let levels: [String] = ["off", "minimal", "low", "medium", "high"]
        let current = levels.contains(row.thinkingLevel ?? "") ? row.thinkingLevel ?? "off" : "off"
        for level in levels {
            let title = level.capitalized
            let item = NSMenuItem(title: title, action: #selector(self.patchThinking(_:)), keyEquivalent: "")
            item.target = self
            item.representedObject = [
                "key": row.key,
                "value": level as Any,
            ]
            item.state = (current == level) ? .on : .off
            menu.addItem(item)
        }
        return menu
    }

    private func buildVerboseMenu(for row: SessionRow) -> NSMenu {
        let menu = NSMenu()
        menu.autoenablesItems = false
        menu.showsStateColumn = true
        let levels: [String] = ["on", "off"]
        let current = levels.contains(row.verboseLevel ?? "") ? row.verboseLevel ?? "off" : "off"
        for level in levels {
            let title = level.capitalized
            let item = NSMenuItem(title: title, action: #selector(self.patchVerbose(_:)), keyEquivalent: "")
            item.target = self
            item.representedObject = [
                "key": row.key,
                "value": level as Any,
            ]
            item.state = (current == level) ? .on : .off
            menu.addItem(item)
        }
        return menu
    }

    private func buildPreviewSubmenu(sessionKey: String, width: CGFloat) -> NSMenu {
        let menu = NSMenu()
        menu.addItem(self.makeSessionPreviewItem(
            sessionKey: sessionKey,
            title: "Recent messages (expanded)",
            width: width,
            maxLines: 8))
        return menu
    }

    private func buildNodesOverflowMenu(entries: [NodeInfo], width: CGFloat) -> NSMenu {
        let menu = NSMenu()
        for entry in entries {
            let item = NSMenuItem()
            item.target = self
            item.action = #selector(self.copyNodeSummary(_:))
            item.representedObject = NodeMenuEntryFormatter.summaryText(entry)
            item.view = HighlightedMenuItemHostView(
                rootView: AnyView(NodeMenuRowView(entry: entry, width: width)),
                width: width)
            item.submenu = self.buildNodeSubmenu(entry: entry, width: width)
            menu.addItem(item)
        }
        return menu
    }

    private func buildNodeSubmenu(entry: NodeInfo, width: CGFloat) -> NSMenu {
        let menu = NSMenu()
        menu.autoenablesItems = false

        menu.addItem(self.makeNodeCopyItem(label: "Node ID", value: entry.nodeId))

        if let name = entry.displayName?.nonEmpty {
            menu.addItem(self.makeNodeCopyItem(label: "Name", value: name))
        }

        if let ip = entry.remoteIp?.nonEmpty {
            menu.addItem(self.makeNodeCopyItem(label: "IP", value: ip))
        }

        menu.addItem(self.makeNodeCopyItem(label: "Status", value: NodeMenuEntryFormatter.roleText(entry)))

        if let platform = NodeMenuEntryFormatter.platformText(entry) {
            menu.addItem(self.makeNodeCopyItem(label: "Platform", value: platform))
        }

        if let version = NodeMenuEntryFormatter.detailRightVersion(entry)?.nonEmpty {
            menu.addItem(self.makeNodeCopyItem(label: "Version", value: version))
        }

        menu.addItem(self.makeNodeDetailItem(label: "Connected", value: entry.isConnected ? "Yes" : "No"))
        menu.addItem(self.makeNodeDetailItem(label: "Paired", value: entry.isPaired ? "Yes" : "No"))

        if let caps = entry.caps?.filter({ !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }),
           !caps.isEmpty
        {
            menu.addItem(self.makeNodeCopyItem(label: "Caps", value: caps.joined(separator: ", ")))
        }

        if let commands = entry.commands?.filter({ !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }),
           !commands.isEmpty
        {
            menu.addItem(self.makeNodeMultilineItem(
                label: "Commands",
                value: commands.joined(separator: ", "),
                width: width))
        }

        return menu
    }

    private func makeNodeDetailItem(label: String, value: String) -> NSMenuItem {
        let item = NSMenuItem(title: "\(label): \(value)", action: nil, keyEquivalent: "")
        item.isEnabled = false
        return item
    }

    private func makeNodeCopyItem(label: String, value: String) -> NSMenuItem {
        let item = NSMenuItem(title: "\(label): \(value)", action: #selector(self.copyNodeValue(_:)), keyEquivalent: "")
        item.target = self
        item.representedObject = value
        return item
    }

    private func makeNodeMultilineItem(label: String, value: String, width: CGFloat) -> NSMenuItem {
        let item = NSMenuItem()
        item.target = self
        item.action = #selector(self.copyNodeValue(_:))
        item.representedObject = value
        item.view = HighlightedMenuItemHostView(
            rootView: AnyView(NodeMenuMultilineView(label: label, value: value, width: width)),
            width: width)
        return item
    }

    private func formatVersionLabel(_ version: String) -> String {
        let trimmed = version.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return version }
        if trimmed.hasPrefix("v") { return trimmed }
        if let first = trimmed.unicodeScalars.first, CharacterSet.decimalDigits.contains(first) {
            return "v\(trimmed)"
        }
        return trimmed
    }

    @objc
    private func patchThinking(_ sender: NSMenuItem) {
        guard let dict = sender.representedObject as? [String: Any],
              let key = dict["key"] as? String
        else { return }
        let value = dict["value"] as? String
        Task {
            do {
                try await SessionActions.patchSession(key: key, thinking: .some(value))
                await self.refreshCache(force: true)
            } catch {
                await MainActor.run {
                    SessionActions.presentError(title: "Update thinking failed", error: error)
                }
            }
        }
    }

    @objc
    private func patchVerbose(_ sender: NSMenuItem) {
        guard let dict = sender.representedObject as? [String: Any],
              let key = dict["key"] as? String
        else { return }
        let value = dict["value"] as? String
        Task {
            do {
                try await SessionActions.patchSession(key: key, verbose: .some(value))
                await self.refreshCache(force: true)
            } catch {
                await MainActor.run {
                    SessionActions.presentError(title: "Update verbose failed", error: error)
                }
            }
        }
    }

    @objc
    private func openSessionLog(_ sender: NSMenuItem) {
        guard let dict = sender.representedObject as? [String: String],
              let sessionId = dict["sessionId"],
              let storePath = dict["storePath"]
        else { return }
        SessionActions.openSessionLogInCode(sessionId: sessionId, storePath: storePath)
    }

    @objc
    private func resetSession(_ sender: NSMenuItem) {
        guard let key = sender.representedObject as? String else { return }
        Task { @MainActor in
            guard SessionActions.confirmDestructiveAction(
                title: "Reset session?",
                message: "Starts a new session id for “\(key)”.",
                action: "Reset")
            else { return }

            do {
                try await SessionActions.resetSession(key: key)
                await self.refreshCache(force: true)
            } catch {
                SessionActions.presentError(title: "Reset failed", error: error)
            }
        }
    }

    @objc
    private func compactSession(_ sender: NSMenuItem) {
        guard let key = sender.representedObject as? String else { return }
        Task { @MainActor in
            guard SessionActions.confirmDestructiveAction(
                title: "Compact session log?",
                message: "Keeps the last 400 lines; archives the old file.",
                action: "Compact")
            else { return }

            do {
                try await SessionActions.compactSession(key: key, maxLines: 400)
                await self.refreshCache(force: true)
            } catch {
                SessionActions.presentError(title: "Compact failed", error: error)
            }
        }
    }

    @objc
    private func deleteSession(_ sender: NSMenuItem) {
        guard let key = sender.representedObject as? String else { return }
        Task { @MainActor in
            guard SessionActions.confirmDestructiveAction(
                title: "Delete session?",
                message: "Deletes the “\(key)” entry and archives its transcript.",
                action: "Delete")
            else { return }

            do {
                try await SessionActions.deleteSession(key: key)
                await self.refreshCache(force: true)
            } catch {
                SessionActions.presentError(title: "Delete failed", error: error)
            }
        }
    }

    @objc
    private func copyNodeSummary(_ sender: NSMenuItem) {
        guard let summary = sender.representedObject as? String else { return }
        NSPasteboard.general.clearContents()
        NSPasteboard.general.setString(summary, forType: .string)
    }

    @objc
    private func copyNodeValue(_ sender: NSMenuItem) {
        guard let value = sender.representedObject as? String else { return }
        NSPasteboard.general.clearContents()
        NSPasteboard.general.setString(value, forType: .string)
    }
}

extension MenuSessionsInjector {
    // MARK: - Width + placement

    private func findInsertIndex(in menu: NSMenu) -> Int? {
        // Insert right before the separator above "Send Heartbeats".
        if let idx = menu.items.firstIndex(where: { $0.title == "Send Heartbeats" }) {
            if let sepIdx = menu.items[..<idx].lastIndex(where: { $0.isSeparatorItem }) {
                return sepIdx
            }
            return idx
        }

        if let sepIdx = menu.items.firstIndex(where: { $0.isSeparatorItem }) {
            return sepIdx
        }

        if menu.items.count >= 1 { return 1 }
        return menu.items.count
    }

    private func findNodesInsertIndex(in menu: NSMenu) -> Int? {
        if let idx = menu.items.firstIndex(where: { $0.title == "Send Heartbeats" }) {
            if let sepIdx = menu.items[..<idx].lastIndex(where: { $0.isSeparatorItem }) {
                return sepIdx
            }
            return idx
        }

        if let sepIdx = menu.items.firstIndex(where: { $0.isSeparatorItem }) {
            return sepIdx
        }

        if menu.items.count >= 1 { return 1 }
        return menu.items.count
    }

    private func initialWidth(for menu: NSMenu) -> CGFloat {
        if let openWidth = self.menuOpenWidth {
            return max(300, openWidth)
        }
        return self.currentMenuWidth(for: menu)
    }

    private func submenuWidth() -> CGFloat {
        if let openWidth = self.menuOpenWidth {
            return max(300, openWidth)
        }
        if let cached = self.lastKnownMenuWidth {
            return max(300, cached)
        }
        return self.fallbackWidth
    }

    private func menuWindowWidth(for menu: NSMenu) -> CGFloat? {
        var menuWindow: NSWindow?
        for item in menu.items {
            if let window = item.view?.window {
                menuWindow = window
                break
            }
        }
        guard let width = menuWindow?.contentView?.bounds.width, width > 0 else { return nil }
        return width
    }

    private func sortedNodeEntries() -> [NodeInfo] {
        let entries = self.nodesStore.nodes.filter(\.isConnected)
        return entries.sorted { lhs, rhs in
            if lhs.isConnected != rhs.isConnected { return lhs.isConnected }
            if lhs.isPaired != rhs.isPaired { return lhs.isPaired }
            let lhsName = NodeMenuEntryFormatter.primaryName(lhs).lowercased()
            let rhsName = NodeMenuEntryFormatter.primaryName(rhs).lowercased()
            if lhsName == rhsName { return lhs.nodeId < rhs.nodeId }
            return lhsName < rhsName
        }
    }
}

extension MenuSessionsInjector {
    // MARK: - Views

    private func makeHostedView(rootView: AnyView, width: CGFloat, highlighted: Bool) -> NSView {
        if highlighted {
            let container = HighlightedMenuItemHostView(rootView: rootView, width: width)
            return container
        }

        let hosting = NSHostingView(rootView: rootView)
        hosting.frame.size.width = max(1, width)
        let size = hosting.fittingSize
        hosting.frame = NSRect(origin: .zero, size: NSSize(width: width, height: size.height))
        return hosting
    }

    private func captureMenuWidthIfAvailable(from view: NSView) {
        guard !self.isMenuOpen else { return }
        guard let width = view.window?.contentView?.bounds.width, width > 0 else { return }
        self.lastKnownMenuWidth = max(300, width)
    }

    private func currentMenuWidth(for menu: NSMenu) -> CGFloat {
        if let width = self.menuWindowWidth(for: menu) {
            return max(300, width)
        }
        let candidates: [CGFloat] = [
            menu.size.width,
            menu.minimumWidth,
            self.lastKnownMenuWidth ?? 0,
            self.fallbackWidth,
        ]
        let resolved = candidates.max() ?? self.fallbackWidth
        return max(300, resolved)
    }
}

#if DEBUG
extension MenuSessionsInjector {
    func setTestingControlChannelConnected(_ connected: Bool?) {
        self.testControlChannelConnected = connected
    }

    func setTestingSnapshot(_ snapshot: SessionStoreSnapshot?, errorText: String? = nil) {
        self.cachedSnapshot = snapshot
        self.cachedErrorText = errorText
        self.cacheUpdatedAt = Date()
    }

    func setTestingUsageSummary(_ summary: GatewayUsageSummary?, errorText: String? = nil) {
        self.cachedUsageSummary = summary
        self.cachedUsageErrorText = errorText
        self.usageCacheUpdatedAt = Date()
    }

    func injectForTesting(into menu: NSMenu) {
        self.inject(into: menu)
    }
}
#endif
