import Foundation
import Combine
import Network

final class StreamMiddleware {
    func middleware(state: ClientState, action: ClientAction) -> AnyPublisher<ClientAction, Never> {
        switch action {
        case .connectionAction(.connected):
            return Just(ClientAction.streamAction(.startStream))
                .eraseToAnyPublisher()

        case .streamAction(.startStream):
            let req = XMLElement(
                name: "stream:stream",
                xmlns: nil,
                attributes: [
                    "from": state.jid.description,
                    "to": state.jid.domainPart,
                    "xml:lang": "en",
                    "version": "1.0",
                    "xmlns": "jabber:client",
                    "xmlns:stream": "http://etherx.jabber.org/streams"
                ],
                content: nil,
                nodes: [],
                woClose: true
            )
            return Just(ClientAction.streamAction(.streamSend(req)))
                .eraseToAnyPublisher()

        case .connectionAction(.tlsStarted):
            var feature = StreamFeature("starttls", true)
            feature.isProcessed = true
            return Just(ClientAction.streamAction(.featureProcessed(feature)))
                .eraseToAnyPublisher()

        case .streamAction(.featureProcessed(let feature)):
            switch feature.featureName {
            case "starttls":
                return Just(ClientAction.parserAction(.restartParser))
                    .eraseToAnyPublisher()

            default:
                return Empty().eraseToAnyPublisher()
            }

        case .parserAction(.restartParser):
            return Just(ClientAction.streamAction(.startStream))
                .eraseToAnyPublisher()

        case .streamAction(.streamSend(let element)):
            let data = element.stringRepresentation.data(using: .utf8) ?? Data()
            return Just(ClientAction.connectionAction(.connectionSend(data)))
                .eraseToAnyPublisher()

        case .parserAction(.elementParsed(let element)):
            switch element.name {
            case "stream:features":
                let features = parseFeatures(element)
                if features.isEmpty {
                    return Empty().eraseToAnyPublisher()
                } else {
                    return Just(ClientAction.streamAction(.featuresListUpdated(features)))
                        .eraseToAnyPublisher()
                }

            case "stream:error":
                var errType: StreamErrorType = .unknown
                for nodeName in element.nodes.map({ $0.name }) {
                    for err in StreamErrorType.allCases where err.rawValue == nodeName {
                        errType = StreamErrorType(rawValue: nodeName) ?? .unknown
                        break
                    }
                }
                let text = element.nodes.first(where: { $0.name == "text"})?.content
                let error = StreamError(type: errType, text: text)
                return Just(ClientAction.streamAction(.streamError(error)))
                    .eraseToAnyPublisher()

            case "proceed":
                return Just(ClientAction.connectionAction(.startTls))
                    .eraseToAnyPublisher()

            default:
                return Empty().eraseToAnyPublisher()
            }

        case .streamAction(.featuresListUpdated):
            if let required = state.streamState.streamFeatures.first(where: { $0.required && !$0.isProcessed }) {
                return Just(ClientAction.streamAction(.processFeature(required)))
                    .eraseToAnyPublisher()
            } else {
                return Empty().eraseToAnyPublisher()
            }

        case .streamAction(.processFeature(let feature)):
            switch feature.featureName {
            case "starttls":
                let req = XMLElement(
                    name: "starttls",
                    xmlns: "urn:ietf:params:xml:ns:xmpp-tls",
                    attributes: [:],
                    content: nil,
                    nodes: []
                )
                return Just(ClientAction.streamAction(.streamSend(req)))
                    .eraseToAnyPublisher()

            case "bind":
                return Just(ClientAction.bindResourceAction(.bindResource))
                    .eraseToAnyPublisher()

            default:
                return Empty().eraseToAnyPublisher()
            }

        case .authAction(.gotAuthResult(let result)):
            if result == .authorized {
                return Just(ClientAction.parserAction(.restartParser))
                    .eraseToAnyPublisher()
            } else {
                return Empty().eraseToAnyPublisher()
            }

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

private extension StreamMiddleware {
    func parseFeatures(_ element: XMLElement) -> [StreamFeature] {
        var features: [StreamFeature] = []
        for node in element.nodes {
            switch node.name {
            case "mechanisms":
                let mechanisms = node.nodes.map({
                    StreamFeature(
                        $0.name,
                        node.nodes.map({ $0.name }).contains("required"),
                        _additionalInfo: $0.content
                    )
                })
                features.append(contentsOf: mechanisms)

            case "register", "auth":
                features.append(.init(node.name, false, _additionalInfo: node.xmlns))

            default:
                features.append(StreamFeature(node.name, node.nodes.map({ $0.name }).contains("required")))
            }
        }
        return features
    }
}
