//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation
public import LibSignalClient

public protocol ChatConnectionManager {
    func updateCanOpenWebSocket()
    func waitForIdentifiedConnectionToOpen() async throws(CancellationError)
    func waitForUnidentifiedConnectionToOpen() async throws(CancellationError)
    /// Waits until we're no longer trying to open a web socket.
    ///
    /// - Note: If an existing socket gets interrupted but we'll try to
    /// re-connect, this will keep waiting. In other words, this waits until we
    /// are no longer capable of opening a socket (e.g., we are deregistered,
    /// all connection tokens are released).
    func waitUntilIdentifiedConnectionShouldBeClosed() async throws(CancellationError)
    @MainActor
    var unidentifiedConnectionState: OWSChatConnectionState { get }
    var hasEmptiedInitialQueue: Bool { get async }

    func requestIdentifiedConnection() -> OWSChatConnection.ConnectionToken
    func requestUnidentifiedConnection() -> OWSChatConnection.ConnectionToken
    func waitForDisconnectIfClosed() async
    func makeRequest(_ request: TSRequest) async throws -> HTTPResponse

    func setRegistrationOverride(_ chatServiceAuth: ChatServiceAuth) async
    func clearRegistrationOverride() async

    func keyTransparencyClient() async throws -> KeyTransparency.Client

    /// Access a libsignal "service" on the active unauthenticated connection.
    ///
    /// Intended to be used with code completion; ``UnauthServiceSelector``
    /// has static members for each valid service. See the docs for that protocol
    /// for under-the-hood information.
    ///
    /// This will attempt to hold the connection open until the operation
    /// completes, so make sure to do any complex processing of the result
    /// *outside* the callback.
    ///
    /// This method can be called from any thread.
    func withUnauthServiceImpl<Service, Output>(
        _ service: Service,
        timeout: TimeInterval,
        do callback: @escaping (Service.Api) async throws -> Output,
    ) async throws -> Output where Service: UnauthServiceSelector

    /// Access a libsignal "service" on the active authenticated connection.
    ///
    /// Intended to be used with code completion; ``AuthServiceSelector``
    /// has static members for each valid service. See the docs for that protocol
    /// for under-the-hood information.
    ///
    /// This will attempt to hold the connection open until the operation
    /// completes, so make sure to do any complex processing of the result
    /// *outside* the callback.
    ///
    /// This method can be called from any thread.
    func withAuthServiceImpl<Service, Output>(
        _ service: Service,
        timeout: TimeInterval,
        do callback: @escaping (Service.Api) async throws -> Output,
    ) async throws -> Output where Service: AuthServiceSelector
}

extension ChatConnectionManager {
    public func requestConnections() -> [OWSChatConnection.ConnectionToken] {
        return [
            requestIdentifiedConnection(),
            requestUnidentifiedConnection(),
        ]
    }

    public func withUnauthService<Service, Output>(
        _ service: Service,
        timeout: TimeInterval = .infinity,
        do callback: @escaping (Service.Api) async throws -> Output,
    ) async throws -> Output where Service: UnauthServiceSelector {
        return try await withUnauthServiceImpl(service, timeout: timeout, do: callback)
    }

    public func withAuthService<Service, Output>(
        _ service: Service,
        timeout: TimeInterval = .infinity,
        do callback: @escaping (Service.Api) async throws -> Output,
    ) async throws -> Output where Service: AuthServiceSelector {
        return try await withAuthServiceImpl(service, timeout: timeout, do: callback)
    }
}

public class ChatConnectionManagerImpl: ChatConnectionManager {
    private let connectionIdentified: OWSAuthConnectionUsingLibSignal
    private let connectionUnidentified: OWSUnauthConnectionUsingLibSignal
    private var connections: [OWSChatConnection] { [connectionIdentified, connectionUnidentified] }

    // TODO: This is a workaround to solve the circular dependency between
    // ChatConnectionManager and RegistrationStateChangeManager. In the event that
    // someday RegistrationStateChangeManager is split up into smaller pieces, this
    // can be revisited.
    public var onRegistrationStateChange: ((_ isDelinkedOrDeregisterd: Bool, _ tx: DBWriteTransaction) -> Void)? {
        get {
            connectionIdentified.onRegistrationStateChange
        }
        set {
            connectionIdentified.onRegistrationStateChange = newValue
        }
    }

    @MainActor
    public init(
        accountManager: TSAccountManager,
        appContext: any AppContext,
        appExpiry: AppExpiry,
        appReadiness: AppReadiness,
        db: any DB,
        inactivePrimaryDeviceStore: InactivePrimaryDeviceStore,
        libsignalNet: Net,
    ) {
        self.connectionIdentified = OWSAuthConnectionUsingLibSignal(
            libsignalNet: libsignalNet,
            accountManager: accountManager,
            appContext: appContext,
            appExpiry: appExpiry,
            appReadiness: appReadiness,
            db: db,
            inactivePrimaryDeviceStore: inactivePrimaryDeviceStore,
        )
        self.connectionUnidentified = OWSUnauthConnectionUsingLibSignal(
            libsignalNet: libsignalNet,
            appExpiry: appExpiry,
            appReadiness: appReadiness,
            db: db,
        )

        SwiftSingletons.register(self)
    }

    private func connection(ofType type: OWSChatConnectionType) -> OWSChatConnection {
        switch type {
        case .identified:
            return connectionIdentified
        case .unidentified:
            return connectionUnidentified
        }
    }

    public func updateCanOpenWebSocket() {
        for connection in connections {
            connection.updateCanOpenWebSocket()
        }
    }

    public func waitForIdentifiedConnectionToOpen() async throws(CancellationError) {
        try await self.connectionIdentified.waitForOpen()
    }

    public func waitForUnidentifiedConnectionToOpen() async throws(CancellationError) {
        try await self.connectionUnidentified.waitForOpen()
    }

    public func waitUntilIdentifiedConnectionShouldBeClosed() async throws(CancellationError) {
        try await self.connectionIdentified.waitUntilSocketShouldBeClosed()
    }

    public func requestIdentifiedConnection() -> OWSChatConnection.ConnectionToken {
        return connectionIdentified.requestConnection()
    }

    public func requestUnidentifiedConnection() -> OWSChatConnection.ConnectionToken {
        return connectionUnidentified.requestConnection()
    }

    public func waitForDisconnectIfClosed() async {
        await withTaskGroup { taskGroup in
            for connection in connections {
                taskGroup.addTask { await connection.waitForDisconnectIfClosed() }
            }
            await taskGroup.waitForAll()
        }
    }

    // This method can be called from any thread.
    public func makeRequest(_ request: TSRequest) async throws -> HTTPResponse {
        let connectionType = try request.auth.connectionType

        return try await connection(ofType: connectionType).makeRequest(request)
    }

    @MainActor
    public var unidentifiedConnectionState: OWSChatConnectionState {
        return connectionUnidentified.currentState
    }

    // MARK: -

    public func keyTransparencyClient() async throws -> KeyTransparency.Client {
        return try await connectionUnidentified.withLibsignalConnection { connection in
            connection.keyTransparencyClient
        }
    }

    // This method can be called from any thread.
    public func withUnauthServiceImpl<Service, Output>(
        _ service: Service,
        timeout: TimeInterval,
        do callback: @escaping (Service.Api) async throws -> Output,
    ) async throws -> Output where Service: UnauthServiceSelector {
        try await connectionUnidentified.withLibsignalConnection(timeout: timeout) { connection in
            // This force-cast is guaranteed by UnauthServiceSelector only being provided for valid service protocols.
            try await callback(connection as! Service.Api)
        }
    }

    // This method can be called from any thread.
    public func withAuthServiceImpl<Service, Output>(
        _ service: Service,
        timeout: TimeInterval,
        do callback: @escaping (Service.Api) async throws -> Output,
    ) async throws -> Output where Service: AuthServiceSelector {
        try await connectionIdentified.withLibsignalConnection(timeout: timeout) { connection in
            // This force-cast is guaranteed by AuthServiceSelector only being provided for valid service protocols.
            try await callback(connection as! Service.Api)
        }
    }

    // MARK: -

    public var hasEmptiedInitialQueue: Bool {
        get async {
            return await connectionIdentified.hasEmptiedInitialQueue
        }
    }

    public func setRegistrationOverride(_ chatServiceAuth: ChatServiceAuth) async {
        await connectionIdentified.setRegistrationOverride(chatServiceAuth)
    }

    public func clearRegistrationOverride() async {
        await connectionIdentified.clearRegistrationOverride()
    }
}

#if TESTABLE_BUILD

public class ChatConnectionManagerMock: ChatConnectionManager {

    public init() {}

    public func updateCanOpenWebSocket() {
    }

    public var hasEmptiedInitialQueue: Bool = false

    public func waitForIdentifiedConnectionToOpen() async throws(CancellationError) {
    }

    public func waitForUnidentifiedConnectionToOpen() async throws(CancellationError) {
    }

    public func waitUntilIdentifiedConnectionShouldBeClosed() async throws(CancellationError) {
    }

    public var unidentifiedConnectionState: OWSChatConnectionState = .closed

    public var shouldWaitForSocketToMakeRequestPerType = [OWSChatConnectionType: Bool]()

    public func requestIdentifiedConnection() -> OWSChatConnection.ConnectionToken {
        fatalError()
    }

    public func requestUnidentifiedConnection() -> OWSChatConnection.ConnectionToken {
        fatalError()
    }

    public func waitForDisconnectIfClosed() async {
    }

    public var requestHandler: (_ request: TSRequest) async throws -> HTTPResponse = { _ in
        fatalError("must override for tests")
    }

    public func makeRequest(_ request: TSRequest) async throws -> HTTPResponse {
        return try await requestHandler(request)
    }

    public func setRegistrationOverride(_ chatServiceAuth: ChatServiceAuth) async {
    }

    public func clearRegistrationOverride() async {
    }

    public func keyTransparencyClient() async throws -> KeyTransparency.Client {
        fatalError("must override for tests")
    }

    public func withUnauthServiceImpl<Service, Output>(
        _ service: Service,
        timeout: TimeInterval,
        do callback: @escaping (Service.Api) async throws -> Output,
    ) async throws -> Output where Service: UnauthServiceSelector {
        fatalError("must override for tests")
    }

    public func withAuthServiceImpl<Service, Output>(
        _ service: Service,
        timeout: TimeInterval,
        do callback: @escaping (Service.Api) async throws -> Output,
    ) async throws -> Output where Service: AuthServiceSelector {
        fatalError("must override for tests")
    }
}

#endif
