import Foundation
import OSLog

private let logger = Logger(subsystem: AppIdentifiers.subsystem, category: "WorkspaceStore")

/// Observable store for workspaces and the available skill pool.
///
/// Canonical source of truth is per-server storage (`workspacesByServer`,
/// `skillsByServer`). The `workspaces` / `skills` computed properties are
/// active-server convenience views over that same storage, so single- and
/// multi-server flows share one data path.
@MainActor @Observable
final class WorkspaceStore {
    // MARK: - Active server context

    /// Which server's catalog is exposed through `workspaces` / `skills`.
    private(set) var activeServerId: String?

    #if DEBUG
    /// Set the active server ID for preview/testing purposes.
    func setActiveServer(_ id: String) {
        activeServerId = id
    }
    #endif

    /// Tracks whether each server catalog has been loaded at least once.
    private var serverLoaded: [String: Bool] = [:]

    private var activeKey: String { activeServerId ?? "" }

    // MARK: - Canonical per-server storage

    /// Workspaces keyed by server ID (fingerprint).
    var workspacesByServer: [String: [Workspace]] = [:]

    /// Skills keyed by server ID (fingerprint).
    var skillsByServer: [String: [SkillInfo]] = [:]

    /// Visible workspace home summaries keyed by server ID then workspace ID.
    ///
    /// This map is what the UI reads after local live overlays are merged in.
    var workspaceSummariesByServer: [String: [String: WorkspaceListSummary]] = [:]

    /// Last server-provided workspace home summaries keyed by server ID.
    ///
    /// This stays as the authoritative server snapshot so local attention/error
    /// overlays can be recomputed without permanently mutating the base counts.
    private var storedWorkspaceSummariesByServer: [String: [String: WorkspaceListSummary]] = [:]

    /// Per-server sync freshness tracking.
    var serverFreshness: [String: ServerSyncState] = [:]

    /// Ordered server IDs reflecting display order.
    var serverOrder: [String] = []

    /// Test seam: inject isolated cache instance.
    var _cacheForTesting: TimelineCache?

    // MARK: - Active-server convenience API

    /// Active server workspaces.
    var workspaces: [Workspace] {
        get { workspacesByServer[activeKey] ?? [] }
        set { workspacesByServer[activeKey] = newValue }
    }

    /// Active server skills.
    var skills: [SkillInfo] {
        get { skillsByServer[activeKey] ?? [] }
        set { skillsByServer[activeKey] = newValue }
    }

    /// Active server workspace home summaries keyed by workspace ID.
    var workspaceSummaries: [String: WorkspaceListSummary] {
        get { workspaceSummariesByServer[activeKey] ?? [:] }
        set { workspaceSummariesByServer[activeKey] = newValue }
    }

    /// Active server's last server-provided workspace summaries.
    var storedWorkspaceSummaries: [String: WorkspaceListSummary] {
        storedWorkspaceSummariesByServer[activeKey] ?? [:]
    }

    /// Active server loaded state.
    var isLoaded: Bool {
        get { serverLoaded[activeKey] ?? false }
        set { serverLoaded[activeKey] = newValue }
    }

    /// Active server freshness.
    var lastSuccessfulSyncAt: Date? {
        get { serverFreshness[activeKey]?.lastSuccessfulSyncAt }
        set {
            mutateFreshness(for: activeKey) { state in
                state.lastSuccessfulSyncAt = newValue
            }
        }
    }

    // periphery:ignore - store API surface; active-server convenience accessor
    /// Active server syncing flag.
    var isSyncing: Bool {
        get { serverFreshness[activeKey]?.isSyncing ?? false }
        set {
            mutateFreshness(for: activeKey) { state in
                state.isSyncing = newValue
            }
        }
    }

    /// Active server last-failure flag.
    var lastSyncFailed: Bool {
        get { serverFreshness[activeKey]?.lastSyncFailed ?? false }
        set {
            mutateFreshness(for: activeKey) { state in
                state.lastSyncFailed = newValue
            }
        }
    }

    // MARK: - Cross-server queries

    // periphery:ignore - used by MultiServerStoreTests via @testable import
    /// All workspaces flattened from all servers, ordered by server sort order.
    var allWorkspaces: [Workspace] {
        serverOrder.flatMap { workspacesByServer[$0] ?? [] }
    }

    func workspaceSummaries(forServer serverId: String) -> [String: WorkspaceListSummary] {
        workspaceSummariesByServer[serverId] ?? [:]
    }

    // periphery:ignore - used by MultiServerStoreTests via @testable import
    func storedWorkspaceSummaries(forServer serverId: String) -> [String: WorkspaceListSummary] {
        storedWorkspaceSummariesByServer[serverId] ?? [:]
    }

    #if DEBUG
    // periphery:ignore - used by MultiServerStoreTests + ServerConnectionTests via @testable import
    func setStoredWorkspaceSummariesForTesting(
        _ summaries: [String: WorkspaceListSummary],
        serverId: String? = nil
    ) {
        let key = serverId ?? activeKey
        storedWorkspaceSummariesByServer[key] = summaries
    }
    #endif

    // periphery:ignore - used by MultiServerStoreTests via @testable import
    /// All skills flattened from all servers (deduplicated by name).
    var allSkills: [SkillInfo] {
        var seen = Set<String>()
        return serverOrder.flatMap { skillsByServer[$0] ?? [] }
            .filter { seen.insert($0.name).inserted }
    }

    // periphery:ignore - used by MultiServerStoreTests via @testable import
    /// Whether ALL servers have successfully synced at least once.
    var isAllLoaded: Bool {
        guard !serverOrder.isEmpty else { return false }
        return serverOrder.allSatisfy { serverFreshness[$0]?.lastSuccessfulSyncAt != nil }
    }

    // periphery:ignore - used by MultiServerStoreTests via @testable import
    /// Whether ANY server is currently syncing.
    var isAnySyncing: Bool {
        serverFreshness.values.contains { $0.isSyncing }
    }

    // MARK: - Server context

    /// Switch active-server compatibility view to a different server.
    func switchServer(to serverId: String) {
        guard serverId != activeServerId else { return }
        activeServerId = serverId

        if workspacesByServer[serverId] == nil {
            workspacesByServer[serverId] = []
        }
        if skillsByServer[serverId] == nil {
            skillsByServer[serverId] = []
        }
        if workspaceSummariesByServer[serverId] == nil {
            workspaceSummariesByServer[serverId] = [:]
        }
        if storedWorkspaceSummariesByServer[serverId] == nil {
            storedWorkspaceSummariesByServer[serverId] = [:]
        }
        if serverFreshness[serverId] == nil {
            serverFreshness[serverId] = ServerSyncState()
        }
        if serverLoaded[serverId] == nil {
            serverLoaded[serverId] = false
        }
    }

    // MARK: - Freshness (active server compatibility)

    // periphery:ignore - used by ServerConnectionTests via @testable import
    func markSyncStarted() {
        markSyncStarted(forServer: activeKey)
    }

    // periphery:ignore - used by ServerConnectionTests via @testable import
    func markSyncSucceeded(at date: Date = Date()) {
        markSyncSucceeded(forServer: activeKey, at: date)
    }

    // periphery:ignore - used by ServerConnectionTests via @testable import
    func markSyncFailed() {
        markSyncFailed(forServer: activeKey)
    }

    // periphery:ignore - store freshness API surface; active-server convenience accessor
    func freshnessState(now: Date = Date(), staleAfter: TimeInterval = 300) -> FreshnessState {
        freshnessState(forServer: activeKey, now: now, staleAfter: staleAfter)
    }

    // periphery:ignore - used by MultiServerStoreTests via @testable import
    func freshnessLabel(now: Date = Date()) -> String {
        freshnessLabel(forServer: activeKey, now: now)
    }

    // MARK: - Freshness (per-server)

    /// Get freshness state for a specific server.
    func freshnessState(forServer serverId: String, now: Date = Date(), staleAfter: TimeInterval = 300) -> FreshnessState {
        guard let state = serverFreshness[serverId] else { return .offline }
        return state.freshnessState(now: now, staleAfter: staleAfter)
    }

    /// Get freshness label for a specific server.
    func freshnessLabel(forServer serverId: String, now: Date = Date()) -> String {
        guard let state = serverFreshness[serverId] else { return "Updated never" }
        return state.freshnessLabel(now: now)
    }

    // MARK: - Mutations

    // periphery:ignore - used by MultiServerStoreTests via @testable import
    /// Insert or update a workspace in the active-server partition.
    func upsert(_ workspace: Workspace) {
        upsert(workspace, serverId: activeKey)
    }

    /// Insert or update a workspace for a specific server.
    func upsert(_ workspace: Workspace, serverId: String) {
        var list = workspacesByServer[serverId] ?? []
        if let idx = list.firstIndex(where: { $0.id == workspace.id }) {
            list[idx] = workspace
        } else {
            list.append(workspace)
        }
        workspacesByServer[serverId] = list
    }

    /// Replace the compact Git state without disturbing live session overlays.
    /// A failed refresh passes nil only when the server authoritatively reports
    /// that the workspace is not a repository; transport failures keep stale data.
    func updateGitSummary(
        _ gitSummary: WorkspaceGitSummary?,
        workspaceId: String,
        serverId: String? = nil
    ) {
        let key = serverId ?? activeKey
        if var stored = storedWorkspaceSummariesByServer[key]?[workspaceId] {
            stored.gitSummary = gitSummary
            storedWorkspaceSummariesByServer[key]?[workspaceId] = stored
        }
        if var visible = workspaceSummariesByServer[key]?[workspaceId] {
            visible.gitSummary = gitSummary
            workspaceSummariesByServer[key]?[workspaceId] = visible
        }
    }

    // periphery:ignore - used by MultiServerStoreTests via @testable import
    /// Remove a workspace by ID from active-server partition.
    func remove(id: String) {
        remove(id: id, serverId: activeKey)
    }

    /// Remove a workspace by ID from a specific server.
    func remove(id: String, serverId: String) {
        workspacesByServer[serverId]?.removeAll { $0.id == id }
        workspaceSummariesByServer[serverId]?.removeValue(forKey: id)
        storedWorkspaceSummariesByServer[serverId]?.removeValue(forKey: id)
    }

    // periphery:ignore - used by MultiServerStoreTests via @testable import
    /// Remove all data for a server (on unpair).
    func removeServer(_ serverId: String) {
        workspacesByServer.removeValue(forKey: serverId)
        skillsByServer.removeValue(forKey: serverId)
        workspaceSummariesByServer.removeValue(forKey: serverId)
        storedWorkspaceSummariesByServer.removeValue(forKey: serverId)
        serverFreshness.removeValue(forKey: serverId)
        serverLoaded.removeValue(forKey: serverId)
        serverOrder.removeAll { $0 == serverId }
        if activeServerId == serverId {
            activeServerId = nil
        }
    }

    // MARK: - Loading

    /// Load only cached workspaces + skills for a server (no network fetch).
    ///
    /// Used during app launch to populate the workspace list before revealing
    /// the UI. Returns `true` if any cached data was applied.
    @discardableResult
    func loadCachedCatalog(serverId: String) async -> Bool {
        let cache = _cacheForTesting ?? TimelineCache.shared

        if !serverId.isEmpty && !serverOrder.contains(serverId) {
            serverOrder.append(serverId)
        }
        ensureFreshness(for: serverId)

        let cached = await loadCachedCatalog(serverId: serverId, cache: cache)

        if let cws = cached.workspaces {
            workspacesByServer[serverId] = cws
        }
        if let csk = cached.skills {
            skillsByServer[serverId] = csk
        }

        let hasCachedData = (cached.workspaces?.isEmpty == false) || (cached.skills?.isEmpty == false)
        if hasCachedData {
            serverLoaded[serverId] = true
        }
        return hasCachedData
    }

    /// Load workspaces + skills for the active server.
    ///
    /// Uses cache immediately when first loading this server, then refreshes
    /// from network. Single- and multi-server paths share `loadServer`.
    func load(api: APIClient) async {
        await loadServer(serverId: activeKey, api: api)
    }

    /// Load workspaces + skills for a specific server.
    func loadServer(serverId: String, api: APIClient) async {
        let cache = _cacheForTesting ?? TimelineCache.shared

        if !serverId.isEmpty && !serverOrder.contains(serverId) {
            serverOrder.append(serverId)
        }
        if serverLoaded[serverId] == nil {
            serverLoaded[serverId] = false
        }
        ensureFreshness(for: serverId)

        // Show cached data immediately on first load for this server.
        if serverLoaded[serverId] != true {
            let cached = await loadCachedCatalog(serverId: serverId, cache: cache)

            if let cws = cached.workspaces {
                workspacesByServer[serverId] = cws
            }
            if let csk = cached.skills {
                skillsByServer[serverId] = csk
            }

            let hasCachedData = (cached.workspaces?.isEmpty == false) || (cached.skills?.isEmpty == false)
            if hasCachedData {
                serverLoaded[serverId] = true
            }
        }

        markSyncStarted(forServer: serverId)

        async let fetchCatalog = api.listWorkspaceCatalog()
        async let fetchSkills = api.listSkills()

        do {
            let (catalog, sk) = try await (fetchCatalog, fetchSkills)
            let ws = catalog.workspaces
            workspacesByServer[serverId] = ws
            skillsByServer[serverId] = sk
            if let responseSummaries = catalog.summaries {
                let mergedResponseSummaries = responseSummaries.map { response in
                    guard !response.hasGitSummarySnapshot,
                          let previousGitSummary = storedWorkspaceSummariesByServer[serverId]?[response.workspaceId]?.gitSummary else {
                        return response
                    }
                    var merged = response
                    merged.gitSummary = previousGitSummary
                    return merged
                }
                let summaries = Dictionary(
                    uniqueKeysWithValues: mergedResponseSummaries.map { ($0.workspaceId, $0) }
                )
                storedWorkspaceSummariesByServer[serverId] = summaries
                workspaceSummariesByServer[serverId] = summaries
            }
            serverLoaded[serverId] = true
            markSyncSucceeded(forServer: serverId)

            if serverId.isEmpty {
                logger.info("Loaded \(ws.count) workspaces, \(sk.count) skills")
            } else {
                logger.info("Loaded \(ws.count) workspaces, \(sk.count) skills from server \(serverId.prefix(16), privacy: .public)")
            }

            let capturedServerId = serverId
            Task.detached {
                if capturedServerId.isEmpty {
                    await cache.saveWorkspaces(ws)
                    await cache.saveSkills(sk)
                } else {
                    await cache.saveWorkspaces(ws, serverId: capturedServerId)
                    await cache.saveSkills(sk, serverId: capturedServerId)
                }
            }
        } catch {
            markSyncFailed(forServer: serverId)
            if serverId.isEmpty {
                logger.error("Failed to load workspaces/skills: \(error.localizedDescription, privacy: .public)")
            } else {
                logger.error("Failed to load from server \(serverId.prefix(16), privacy: .public): \(error.localizedDescription, privacy: .public)")
            }

            // Keep stale/cached data on error.
            if serverLoaded[serverId] != true {
                let hasCachedData = !(workspacesByServer[serverId] ?? []).isEmpty
                    || !(skillsByServer[serverId] ?? []).isEmpty
                if hasCachedData {
                    serverLoaded[serverId] = true
                }
            }
        }
    }

    // MARK: - Private helpers

    private struct CachedCatalog {
        var workspaces: [Workspace]?
        var skills: [SkillInfo]?
    }

    private func ensureFreshness(for serverId: String) {
        if serverFreshness[serverId] == nil {
            serverFreshness[serverId] = ServerSyncState()
        }
    }

    private func mutateFreshness(for serverId: String, _ mutate: (inout ServerSyncState) -> Void) {
        var state = serverFreshness[serverId] ?? ServerSyncState()
        mutate(&state)
        serverFreshness[serverId] = state
    }

    private func markSyncStarted(forServer serverId: String) {
        mutateFreshness(for: serverId) { $0.markSyncStarted() }
    }

    private func markSyncSucceeded(forServer serverId: String, at date: Date = Date()) {
        mutateFreshness(for: serverId) { $0.markSyncSucceeded(at: date) }
    }

    private func markSyncFailed(forServer serverId: String) {
        mutateFreshness(for: serverId) { $0.markSyncFailed() }
    }

    private func loadCachedCatalog(
        serverId: String,
        cache: TimelineCache
    ) async -> CachedCatalog {
        if serverId.isEmpty {
            async let ws = cache.loadWorkspaces()
            async let sk = cache.loadSkills()
            return await CachedCatalog(workspaces: ws, skills: sk)
        }

        async let ws = cache.loadWorkspaces(serverId: serverId)
        async let sk = cache.loadSkills(serverId: serverId)
        return await CachedCatalog(workspaces: ws, skills: sk)
    }
}
