import CodexBarCore
import Foundation
import Observation

struct AgentSessionRemoteRefreshGate {
    private(set) var generation = 0
    private(set) var isInFlight = false
    private(set) var isPending = false

    mutating func settingsDidChange() {
        self.generation += 1
        self.isPending = self.isInFlight
    }

    mutating func begin() -> Int? {
        guard !self.isInFlight else {
            return nil
        }
        self.isInFlight = true
        self.isPending = false
        return self.generation
    }

    mutating func finish(generation: Int) -> (shouldPublish: Bool, shouldRetry: Bool) {
        self.isInFlight = false
        let outcome = (generation == self.generation, self.isPending)
        self.isPending = false
        return outcome
    }
}

@MainActor
@Observable
final class AgentSessionsStore {
    typealias LocalScan = @Sendable (_ includeFileOnlySessions: Bool) async -> [AgentSession]

    private let settings: SettingsStore
    private let localScan: LocalScan
    private let remoteFetcher: RemoteSessionFetcher
    @ObservationIgnored private var localRefreshTask: Task<Void, Never>?
    @ObservationIgnored private var remoteRefreshTask: Task<Void, Never>?
    @ObservationIgnored private var localRefreshInFlight = false
    @ObservationIgnored private var remoteRefreshGate = AgentSessionRemoteRefreshGate()
    @ObservationIgnored var onUpdate: (@MainActor () -> Void)?

    private(set) var localSessions: [AgentSession] = []
    private(set) var remoteHosts: [RemoteSessionHostResult] = []
    private(set) var lastUpdatedAt: Date?
    private(set) var latestLocalActivityAt: Date?

    init(
        settings: SettingsStore,
        localScanner: LocalAgentSessionScanner = LocalAgentSessionScanner(),
        remoteFetcher: RemoteSessionFetcher = RemoteSessionFetcher())
    {
        self.settings = settings
        self.localScan = { includeFileOnlySessions in
            await localScanner.scan(includeFileOnlySessions: includeFileOnlySessions)
        }
        self.remoteFetcher = remoteFetcher
    }

    init(
        settings: SettingsStore,
        localScan: @escaping LocalScan,
        remoteFetcher: RemoteSessionFetcher = RemoteSessionFetcher())
    {
        self.settings = settings
        self.localScan = localScan
        self.remoteFetcher = remoteFetcher
    }

    var totalCount: Int {
        self.localSessions.count + self.remoteHosts.reduce(0) { $0 + $1.sessions.count }
    }

    /// Adaptive refresh uses local metadata only after explicit consent. Remote sessions remain
    /// behind the Agent Sessions setting because they can involve Tailscale discovery and SSH.
    var localMonitoringEnabled: Bool {
        self.settings.agentSessionsEnabled || self.settings.adaptiveActivityScanningEnabled
    }

    nonisolated static func latestActivityAt(in sessions: [AgentSession]) -> Date? {
        sessions.compactMap(\.lastActivityAt).max()
    }

    nonisolated static func shouldScanLocally(
        agentSessionsEnabled: Bool,
        adaptiveActivityScanningEnabled: Bool,
        lowPowerModeEnabled: Bool,
        thermalState: ProcessInfo.ThermalState) -> Bool
    {
        if agentSessionsEnabled {
            return true
        }
        guard adaptiveActivityScanningEnabled, !lowPowerModeEnabled else { return false }
        return thermalState != .serious && thermalState != .critical
    }

    func start() {
        guard self.localRefreshTask == nil, self.remoteRefreshTask == nil else { return }
        self.localRefreshTask = Task { [weak self] in
            while !Task.isCancelled {
                await self?.refreshLocal()
                try? await Task.sleep(for: .seconds(30))
            }
        }
        self.remoteRefreshTask = Task { [weak self] in
            while !Task.isCancelled {
                await self?.refreshRemote()
                try? await Task.sleep(for: .seconds(60))
            }
        }
    }

    func stop() {
        self.localRefreshTask?.cancel()
        self.remoteRefreshTask?.cancel()
        self.localRefreshTask = nil
        self.remoteRefreshTask = nil
    }

    func settingsDidChange(remoteConfigurationChanged: Bool = true) {
        if remoteConfigurationChanged {
            self.remoteRefreshGate.settingsDidChange()
        }
        if !self.settings.agentSessionsEnabled {
            // Adaptive keeps only the timestamp signal. Retained session paths and identities
            // remain scoped to the explicitly enabled Agent Sessions UI.
            self.localSessions = []
            self.remoteHosts = []
        }
        guard self.localMonitoringEnabled else {
            self.latestLocalActivityAt = nil
            self.onUpdate?()
            return
        }
        guard !SettingsStore.isRunningTests else { return }
        Task { [weak self] in
            await self?.refreshLocal()
            if remoteConfigurationChanged, self?.settings.agentSessionsEnabled == true {
                await self?.refreshRemote()
            }
        }
    }

    func refreshOnMenuOpen() {
        guard self.localMonitoringEnabled, !SettingsStore.isRunningTests else { return }
        Task { [weak self] in
            await self?.refreshLocal()
            if self?.settings.agentSessionsEnabled == true {
                await self?.refreshRemote()
            }
        }
    }

    func focus(_ session: AgentSession, remoteHost: String?) {
        if let remoteHost {
            Task {
                await self.remoteFetcher.focus(sessionID: session.id, host: remoteHost)
            }
        } else {
            _ = SessionWindowFocuser.focus(session)
        }
    }

    func refreshLocal() async {
        guard self.localMonitoringEnabled, !self.localRefreshInFlight else { return }
        let processInfo = ProcessInfo.processInfo
        guard Self.shouldScanLocally(
            agentSessionsEnabled: self.settings.agentSessionsEnabled,
            adaptiveActivityScanningEnabled: self.settings.adaptiveActivityScanningEnabled,
            lowPowerModeEnabled: processInfo.isLowPowerModeEnabled,
            thermalState: processInfo.thermalState)
        else { return }
        self.localRefreshInFlight = true
        let sessions = await self.localScan(self.settings.agentSessionsEnabled)
        self.localRefreshInFlight = false
        guard !Task.isCancelled, self.localMonitoringEnabled else { return }
        self.applyLocalScanResult(sessions)
    }

    func applyLocalScanResult(_ sessions: [AgentSession], updatedAt: Date = Date()) {
        let latestActivityAt = Self.latestActivityAt(in: sessions)
        let effectiveSessions = self.settings.agentSessionsEnabled ? sessions : []
        // Rescans that reproduce the current content must not publish: `onUpdate` invalidates
        // menus, and a redundant invalidation landing while the user hovers an Overview row's
        // chart submenu fed the open/close rebuild flicker loop in #2652.
        if latestActivityAt == self.latestLocalActivityAt, effectiveSessions == self.localSessions {
            self.lastUpdatedAt = updatedAt
            return
        }
        self.latestLocalActivityAt = latestActivityAt
        self.localSessions = effectiveSessions
        self.lastUpdatedAt = updatedAt
        self.onUpdate?()
    }

    private func refreshRemote() async {
        guard self.settings.agentSessionsEnabled else { return }
        guard var generation = self.remoteRefreshGate.begin() else { return }
        while self.settings.agentSessionsEnabled {
            var hosts = self.manualHosts
            await hosts.append(contentsOf: self.remoteFetcher.discoveredHosts())
            let results = await self.remoteFetcher.fetch(hosts: hosts)
            let outcome = self.remoteRefreshGate.finish(generation: generation)
            guard !Task.isCancelled, self.settings.agentSessionsEnabled else { return }
            if outcome.shouldPublish, results != self.remoteHosts {
                self.remoteHosts = results
                self.lastUpdatedAt = Date()
                self.onUpdate?()
            }
            guard outcome.shouldRetry, let nextGeneration = self.remoteRefreshGate.begin() else { return }
            generation = nextGeneration
        }
    }

    private var manualHosts: [String] {
        self.settings.agentSessionsManualHosts
            .split(separator: ",")
            .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
            .filter { !$0.isEmpty }
    }
}
