import ForgejoKit
import Foundation
import SwiftData

@MainActor
@Observable
final class MultiInstanceManager {
    private(set) var connections: [(instance: ForgejoInstance, authService: AuthenticationService)] = []
    private(set) var failedInstances: [(instance: ForgejoInstance, error: String)] = []
    private(set) var isConnecting = false

    var isConnected: Bool {
        !connections.isEmpty
    }

    /// Fast path: restore clients from keychain so UI can render immediately.
    /// Full validation should run afterwards via `connect(instances:)`.
    func bootstrap(instances: [ForgejoInstance]) {
        connections = []
        failedInstances = []

        for instance in instances {
            let authService = AuthenticationService()
            if authService.bootstrapSession(instance: instance) {
                connections.append((instance: instance, authService: authService))
            }
        }
    }

    func connect(instances: [ForgejoInstance]) async {
        isConnecting = true
        let snapshots = instances.map { instance in
            InstanceSnapshot(
                serverURL: instance.serverURL, username: instance.username,
                allowSelfSigned: instance.allowSelfSignedCertificates, useTokenAuth: instance.useTokenAuth,
            )
        }
        let authServices = snapshots.map { _ in AuthenticationService() }

        let results = await withTaskGroup(of: (Int, Bool, String?).self) { group in
            for (index, snapshot) in snapshots.enumerated() {
                let auth = authServices[index]
                group.addTask {
                    do {
                        try await auth.restoreFromSnapshot(snapshot)
                        return (index, true, nil)
                    } catch {
                        return (index, false, error.localizedDescription)
                    }
                }
            }
            return await group.reduce(into: [(Int, Bool, String?)]()) { $0.append($1) }
        }

        let bootstrapped = bootstrappedConnectionsByAccountKey()
        var newConnections: [(instance: ForgejoInstance, authService: AuthenticationService)] = []
        var newFailed: [(instance: ForgejoInstance, error: String)] = []

        for (index, success, error) in results.sorted(by: { $0.0 < $1.0 }) {
            let instance = instances[index]
            if success {
                newConnections.append((instance: instance, authService: authServices[index]))
            } else if let existing = bootstrapped[accountKey(for: instance)] {
                newConnections.append(existing)
            } else {
                newFailed.append((instance: instance, error: error ?? "Unknown error"))
            }
        }

        connections = newConnections
        failedInstances = newFailed
        isConnecting = false
    }

    func disconnect() {
        for connection in connections {
            connection.authService.disconnect()
        }
        connections = []
        failedInstances = []
    }

    var totalUnreadCount: Int {
        get async {
            let clients = connections.compactMap(\.authService.client)
            return await withTaskGroup(of: Int.self) { group in
                for client in clients {
                    group.addTask {
                        let service = NotificationService(client: client)
                        return await (try? service.fetchUnreadCount()) ?? 0
                    }
                }
                var total = 0
                for await count in group {
                    total += count
                }
                return total
            }
        }
    }

    func connectionSources() -> [ConnectionSource] {
        connections.compactMap { connection in
            guard let client = connection.authService.client else { return nil }
            let instance = connection.instance
            return ConnectionSource(
                sourceKey: "\(instance.serverURL):\(instance.username)",
                name: displayName(for: instance),
                client: client,
                authService: connection.authService,
            )
        }
    }

    func displayName(for instance: ForgejoInstance) -> String {
        instance.name.isEmpty ? instance.serverURL : instance.name
    }

    func accountKey(for instance: ForgejoInstance) -> String {
        "\(ForgejoClient.normalizeServerURL(instance.serverURL)):\(instance.username)"
    }

    func bootstrappedConnectionsByAccountKey()
        -> [String: (instance: ForgejoInstance, authService: AuthenticationService)]
    {
        var bootstrapped: [String: (instance: ForgejoInstance, authService: AuthenticationService)] = [:]
        for connection in connections {
            bootstrapped[accountKey(for: connection.instance)] = connection
        }
        return bootstrapped
    }
}

/// Sendable snapshot of ForgejoInstance data needed for session restore.
struct InstanceSnapshot {
    let serverURL: String
    let username: String
    let allowSelfSigned: Bool
    let useTokenAuth: Bool
}

struct ConnectionSource {
    let sourceKey: String
    let name: String
    let client: ForgejoClient
    let authService: AuthenticationService
}

/// Runs a fetch, returning empty on timeout or cancellation.
/// All other errors propagate so callers can surface them.
func resilientFetch<T>(_ fetch: () async throws -> [T]) async throws -> [T] {
    do {
        return try await fetch()
    } catch is CancellationError {
        return []
    } catch let urlError as URLError where urlError.code == .timedOut || urlError.code == .cancelled {
        return []
    }
}
