import Foundation
import OSLog

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

/// Per-server sync state for freshness tracking.
struct ServerSyncState: Sendable {
    var lastSuccessfulSyncAt: Date?
    var isSyncing: Bool = false
    var lastSyncFailed: Bool = false

    // periphery:ignore - used by MultiServerStoreTests via @testable import
    var freshnessState: FreshnessState {
        freshnessState()
    }

    func freshnessState(now: Date = Date(), staleAfter: TimeInterval = 300) -> FreshnessState {
        FreshnessState.derive(
            lastSuccessfulSyncAt: lastSuccessfulSyncAt,
            isSyncing: isSyncing,
            lastSyncFailed: lastSyncFailed,
            staleAfter: staleAfter,
            now: now
        )
    }

    func freshnessLabel(now: Date = Date()) -> String {
        FreshnessState.updatedLabel(lastSuccessfulSyncAt: lastSuccessfulSyncAt, now: now)
    }

    mutating func markSyncStarted() {
        isSyncing = true
    }

    mutating func markSyncSucceeded(at date: Date = Date()) {
        isSyncing = false
        lastSyncFailed = false
        lastSuccessfulSyncAt = date
    }

    mutating func markSyncFailed() {
        isSyncing = false
        lastSyncFailed = true
    }
}

/// 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]] = [:]

    /// 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 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] ?? [] }
    }

    // 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 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
    }

    // 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 }
    }

    // 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)
        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 fetchWorkspaces = api.listWorkspaces()
        async let fetchSkills = api.listSkills()

        do {
            let (ws, sk) = try await (fetchWorkspaces, fetchSkills)
            workspacesByServer[serverId] = ws
            skillsByServer[serverId] = sk
            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
                }
            }
        }
    }

    // periphery:ignore - used by MultiServerStoreTests via @testable import
    /// Load workspaces and skills from ALL paired servers.
    ///
    /// Uses the same per-server path as single-server loads (`loadServer`).
    func loadAll(servers: [PairedServer]) async {
        serverOrder = servers.map(\.id)

        for server in servers {
            ensureFreshness(for: server.id)
            if serverLoaded[server.id] == nil {
                serverLoaded[server.id] = false
            }
        }

        for server in servers {
            guard let baseURL = server.baseURL else { continue }
            let api = APIClient(
                baseURL: baseURL,
                token: server.token,
                tlsCertFingerprint: server.tlsCertFingerprint
            )
            await loadServer(serverId: server.id, api: api)
        }
    }

    // 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)
    }
}
