import Combine
import Foundation
import Network

final class ConnectionMiddleware {
    private var callback: StoreCallback?
    private let networkLayer: NetworkLayer

    init(jid: JID) {
        networkLayer = NetworkLayer(forJid: jid)
        networkLayer.received = { [weak self] data in
            self?.callback?(.connectionAction(.connectionReceived(data)))
        }
        networkLayer.connected = { [weak self] result, isTls in
            let status = result ? ConnectionStatus.connected : ConnectionStatus.disconnected
            if isTls {
                self?.callback?(.connectionAction(.tlsStarted))
            } else {
                self?.callback?(.connectionAction(.connectionUpdated(status)))
            }
        }
        networkLayer.errorHappens = { [weak self] error in
            self?.callback?(.clientError(error))
        }
    }

    func setCallback(_ callback: @escaping StoreCallback) {
        self.callback = callback
    }

    func middleware(state: ClientState, action: ClientAction) -> AnyPublisher<ClientAction, Never> {
        switch action {
        case .connectionAction(.srvRecordsUpdated):
            return Just(ClientAction.connectionAction(.startConnection(recordIndex: 0)))
                .eraseToAnyPublisher()

        case .connectionAction(.startConnection(let recordIndex)):
            return Future<ClientAction, Never> { [weak self] promise in
                // return error if all records is unavailable
                if recordIndex > state.connectionState.srvRecords.count - 1 {
                    promise(.success(.clientError(.conNoAliveRecords)))
                    return
                }

                // try to connect
                // TODO: add test for case when some records incorrect
                let record = state.connectionState.srvRecords[recordIndex]
                self?.networkLayer.connectTo(host: record.target, port: record.port)
                promise(.success(.empty))
            }
            .eraseToAnyPublisher()

        case .connectionAction(.connectionUpdated(let status)):
            switch status {
            case .connected:
                return Just(ClientAction.connectionAction(.connected(networkLayer.host, networkLayer.port)))
                    .eraseToAnyPublisher()

            default:
                return Empty().eraseToAnyPublisher()
            }

        case .connectionAction(.connectionSend(let data)):
            return Future<ClientAction, Never> { [weak self] promise in
                self?.networkLayer.write(data: data)
                promise(.success(.empty))
            }
            .eraseToAnyPublisher()

        case .connectionAction(.startTls):
            return Future<ClientAction, Never> { [weak self] promise in
                self?.networkLayer.startTls()
                promise(.success(.empty))
            }
            .eraseToAnyPublisher()

        default:
            return Empty().eraseToAnyPublisher()
        }
    }
}
