/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * license agreements; and to You under the Apache License, version 2.0:
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * This file is part of the Apache Pekko project, which was derived from Akka.
 */

/*
 * Copyright (C) since 2016 Lightbend Inc. <https://www.lightbend.com>
 */

package org.apache.pekko.stream.connectors.mqtt.streaming
package scaladsl

import java.util.concurrent.atomic.AtomicLong

import org.apache.pekko
import pekko.actor.{ ClassicActorSystemProvider, ExtendedActorSystem }
import pekko.actor.typed.scaladsl.adapter._
import pekko.event.{ Logging, LoggingAdapter }
import pekko.stream._
import pekko.stream.connectors.mqtt.streaming.impl._
import pekko.stream.scaladsl.{ BroadcastHub, Flow, Keep, Source }
import pekko.util.ByteString
import pekko.{ Done, NotUsed }

import scala.concurrent.{ Future, Promise }
import scala.util.control.NoStackTrace
import scala.util.{ Failure, Success }

object MqttSession {

  private[streaming] type CommandFlow[A] =
    Flow[Command[A], ByteString, NotUsed]
  private[streaming] type EventFlow[A] =
    Flow[ByteString, Either[MqttCodec.DecodeError, Event[A]], NotUsed]
}

/**
 * Represents MQTT session state for both clients or servers. Session
 * state can survive across connections i.e. their lifetime is
 * generally longer.
 */
abstract class MqttSession {

  /**
   * Tell the session to perform a command regardless of the state it is
   * in. This is important for sending Publish messages in particular,
   * as a connection may not have been established with a session.
   * @param cp The command to perform
   * @tparam A The type of any carry for the command.
   */
  final def tell[A](cp: Command[A]): Unit =
    this ! cp

  /**
   * Tell the session to perform a command regardless of the state it is
   * in. This is important for sending Publish messages in particular,
   * as a connection may not have been established with a session.
   * @param cp The command to perform
   * @tparam A The type of any carry for the command.
   */
  def ![A](cp: Command[A]): Unit

  /**
   * Shutdown the session gracefully
   */
  def shutdown(): Unit
}

/**
 * Represents client-only sessions
 */
abstract class MqttClientSession extends MqttSession {
  import MqttSession._

  /**
   * @return a flow for commands to be sent to the session
   */
  private[streaming] def commandFlow[A](connectionId: ByteString): CommandFlow[A]

  /**
   * @return a flow for events to be emitted by the session
   */
  private[streaming] def eventFlow[A](connectionId: ByteString): EventFlow[A]
}

object ActorMqttClientSession {
  def apply(settings: MqttSessionSettings)(implicit system: ClassicActorSystemProvider): ActorMqttClientSession =
    new ActorMqttClientSession(settings)

  /**
   * No ACK received - the CONNECT failed
   */
  case object ConnectFailed extends Exception with NoStackTrace

  /**
   * No ACK received - the SUBSCRIBE failed
   */
  case object SubscribeFailed extends Exception with NoStackTrace

  /**
   * A PINGREQ failed to receive a PINGRESP - the connection must close
   *
   * http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html
   * 3.1.2.10 Keep Alive
   */
  case object PingFailed extends Exception with NoStackTrace

  private[scaladsl] val clientSessionCounter = new AtomicLong
}

/**
 * Provides an actor implementation of a client session
 * @param settings session settings
 */
final class ActorMqttClientSession(settings: MqttSessionSettings)(implicit systemProvider: ClassicActorSystemProvider)
    extends MqttClientSession {

  import ActorMqttClientSession._

  private val system = systemProvider.classicSystem

  private val clientSessionId = clientSessionCounter.getAndIncrement()
  private val consumerPacketRouter =
    system
      .asInstanceOf[ExtendedActorSystem]
      .systemActorOf(PropsAdapter(RemotePacketRouter[Consumer.Event]),
        "client-consumer-packet-id-allocator-" + clientSessionId)
      .toTyped

  private val producerPacketRouter =
    system
      .asInstanceOf[ExtendedActorSystem]
      .systemActorOf(PropsAdapter(LocalPacketRouter[Producer.Event]),
        "client-producer-packet-id-allocator-" + clientSessionId)
      .toTyped

  private val subscriberPacketRouter =
    system
      .asInstanceOf[ExtendedActorSystem]
      .systemActorOf(PropsAdapter(LocalPacketRouter[Subscriber.Event]),
        "client-subscriber-packet-id-allocator-" + clientSessionId)
      .toTyped

  private val unsubscriberPacketRouter =
    system
      .asInstanceOf[ExtendedActorSystem]
      .systemActorOf(PropsAdapter(LocalPacketRouter[Unsubscriber.Event]),
        "client-unsubscriber-packet-id-allocator-" + clientSessionId)
      .toTyped

  private val clientConnector =
    system
      .asInstanceOf[ExtendedActorSystem]
      .systemActorOf(
        PropsAdapter(
          ClientConnector(consumerPacketRouter,
            producerPacketRouter,
            subscriberPacketRouter,
            unsubscriberPacketRouter,
            settings)),
        "client-connector-" + clientSessionId)
      .toTyped

  import MqttCodec._
  import MqttSession._
  import system.dispatcher

  private implicit val loggingAdapter: LoggingAdapter = Logging(system, classOf[ActorMqttClientSession])

  override def ![A](cp: Command[A]): Unit = cp match {
    case Command(cp: Publish, _, carry) =>
      clientConnector ! ClientConnector.PublishReceivedLocally(cp, carry)
    case c: Command[A] => throw new IllegalStateException(s"$c is not a client command that can be sent directly")
  }

  override def shutdown(): Unit = {
    system.stop(clientConnector.toClassic)
    system.stop(consumerPacketRouter.toClassic)
    system.stop(producerPacketRouter.toClassic)
    system.stop(subscriberPacketRouter.toClassic)
    system.stop(unsubscriberPacketRouter.toClassic)
  }

  private val pingReqBytes = PingReq.encode(ByteString.newBuilder).result()

  private[streaming] override def commandFlow[A](connectionId: ByteString): CommandFlow[A] =
    Flow
      .lazyFutureFlow { () =>
        val killSwitch = KillSwitches.shared("command-kill-switch-" + clientSessionId)

        Future.successful(
          Flow[Command[A]]
            .watch(clientConnector.toClassic)
            .watchTermination {
              case (_, terminated) =>
                terminated.onComplete {
                  case Failure(_: WatchedActorTerminatedException) =>
                  case _                                           =>
                    clientConnector ! ClientConnector.ConnectionLost(connectionId)
                }
                NotUsed
            }
            .via(killSwitch.flow)
            .flatMapMerge(
              settings.commandParallelism,
              {
                case Command(cp: Connect, _, carry) =>
                  val reply = Promise[Source[ClientConnector.ForwardConnectCommand, NotUsed]]()
                  clientConnector ! ClientConnector.ConnectReceivedLocally(connectionId, cp, carry, reply)
                  Source.futureSource(
                    reply.future.map(_.map {
                      case ClientConnector.ForwardConnect                    => cp.encode(ByteString.newBuilder).result()
                      case ClientConnector.ForwardPingReq                    => pingReqBytes
                      case ClientConnector.ForwardPublish(publish, packetId) =>
                        publish.encode(ByteString.newBuilder, packetId).result()
                      case ClientConnector.ForwardPubRel(packetId) =>
                        PubRel(packetId).encode(ByteString.newBuilder).result()
                    }.mapError {
                      case ClientConnector.ConnectFailed => ActorMqttClientSession.ConnectFailed
                      case Subscriber.SubscribeFailed    => ActorMqttClientSession.SubscribeFailed
                      case ClientConnector.PingFailed    => ActorMqttClientSession.PingFailed
                    }
                      .watchTermination { (_, done) =>
                        done.onComplete {
                          case Success(_) => killSwitch.shutdown()
                          case Failure(t) => killSwitch.abort(t)
                        }
                      }))
                case Command(cp: PubAck, completed, _) =>
                  val reply = Promise[Consumer.ForwardPubAck.type]()
                  consumerPacketRouter ! RemotePacketRouter.Route(None,
                    cp.packetId,
                    Consumer.PubAckReceivedLocally(reply),
                    reply)

                  reply.future.onComplete { result =>
                    completed
                      .foreach(_.complete(result.map(_ => Done)))
                  }

                  Source.future(reply.future.map(_ => cp.encode(ByteString.newBuilder).result())).recover {
                    case _: RemotePacketRouter.CannotRoute => ByteString.empty
                  }
                case Command(cp: PubRec, completed, _) =>
                  val reply = Promise[Consumer.ForwardPubRec.type]()
                  consumerPacketRouter ! RemotePacketRouter.Route(None,
                    cp.packetId,
                    Consumer.PubRecReceivedLocally(reply),
                    reply)

                  reply.future.onComplete { result =>
                    completed
                      .foreach(_.complete(result.map(_ => Done)))
                  }

                  Source.future(reply.future.map(_ => cp.encode(ByteString.newBuilder).result())).recover {
                    case _: RemotePacketRouter.CannotRoute => ByteString.empty
                  }
                case Command(cp: PubComp, completed, _) =>
                  val reply = Promise[Consumer.ForwardPubComp.type]()
                  consumerPacketRouter ! RemotePacketRouter.Route(None,
                    cp.packetId,
                    Consumer.PubCompReceivedLocally(reply),
                    reply)

                  reply.future.onComplete { result =>
                    completed
                      .foreach(_.complete(result.map(_ => Done)))
                  }

                  Source.future(reply.future.map(_ => cp.encode(ByteString.newBuilder).result())).recover {
                    case _: RemotePacketRouter.CannotRoute => ByteString.empty
                  }
                case Command(cp: Subscribe, _, carry) =>
                  val reply = Promise[Subscriber.ForwardSubscribe]()
                  clientConnector ! ClientConnector.SubscribeReceivedLocally(connectionId, cp, carry, reply)
                  Source.future(
                    reply.future.map(command => cp.encode(ByteString.newBuilder, command.packetId).result()))
                case Command(cp: Unsubscribe, _, carry) =>
                  val reply = Promise[Unsubscriber.ForwardUnsubscribe]()
                  clientConnector ! ClientConnector.UnsubscribeReceivedLocally(connectionId, cp, carry, reply)
                  Source.future(
                    reply.future.map(command => cp.encode(ByteString.newBuilder, command.packetId).result()))
                case Command(cp: Disconnect.type, _, _) =>
                  val reply = Promise[ClientConnector.ForwardDisconnect.type]()
                  clientConnector ! ClientConnector.DisconnectReceivedLocally(connectionId, reply)
                  Source.future(reply.future.map(_ => cp.encode(ByteString.newBuilder).result()))
                case c: Command[A] => throw new IllegalStateException(s"$c is not a client command")
              })
            .recover {
              case _: WatchedActorTerminatedException => ByteString.empty
            }
            .filter(_.nonEmpty)
            .log("client-commandFlow", _.iterator.decodeControlPacket(settings.maxPacketSize)) // we decode here so we can see the generated packet id
            .withAttributes(ActorAttributes.logLevels(onFailure = Logging.DebugLevel)))
      }
      .mapMaterializedValue(_ => NotUsed)

  private[streaming] override def eventFlow[A](connectionId: ByteString): EventFlow[A] =
    Flow[ByteString]
      .watch(clientConnector.toClassic)
      .watchTermination {
        case (_, terminated) =>
          terminated.onComplete {
            case Failure(_: WatchedActorTerminatedException) =>
            case _                                           =>
              clientConnector ! ClientConnector.ConnectionLost(connectionId)
          }
          NotUsed
      }
      .via(new MqttFrameStage(settings.maxPacketSize))
      .map(_.iterator.decodeControlPacket(settings.maxPacketSize))
      .log("client-events")
      .mapAsync[Either[MqttCodec.DecodeError, Event[A]]](settings.eventParallelism) {
        case Right(cp: ConnAck) =>
          val reply = Promise[ClientConnector.ForwardConnAck]()
          clientConnector ! ClientConnector.ConnAckReceivedFromRemote(connectionId, cp, reply)
          reply.future.map {
            case ClientConnector.ForwardConnAck(carry: Option[A] @unchecked) => Right(Event(cp, carry))
          }
        case Right(cp: SubAck) =>
          val reply = Promise[Subscriber.ForwardSubAck]()
          subscriberPacketRouter ! LocalPacketRouter.Route(cp.packetId,
            Subscriber.SubAckReceivedFromRemote(reply),
            reply)
          reply.future.map {
            case Subscriber.ForwardSubAck(carry: Option[A] @unchecked) => Right(Event(cp, carry))
          }
        case Right(cp: UnsubAck) =>
          val reply = Promise[Unsubscriber.ForwardUnsubAck]()
          unsubscriberPacketRouter ! LocalPacketRouter.Route(cp.packetId,
            Unsubscriber.UnsubAckReceivedFromRemote(reply),
            reply)
          reply.future.map {
            case Unsubscriber.ForwardUnsubAck(carry: Option[A] @unchecked) => Right(Event(cp, carry))
          }
        case Right(cp: Publish) =>
          val reply = Promise[Consumer.ForwardPublish.type]()
          clientConnector ! ClientConnector.PublishReceivedFromRemote(connectionId, cp, reply)
          reply.future.map(_ => Right(Event(cp)))
        case Right(cp: PubAck) =>
          val reply = Promise[Producer.ForwardPubAck]()
          producerPacketRouter ! LocalPacketRouter.Route(cp.packetId, Producer.PubAckReceivedFromRemote(reply), reply)
          reply.future.map {
            case Producer.ForwardPubAck(carry: Option[A] @unchecked) => Right(Event(cp, carry))
          }
        case Right(cp: PubRec) =>
          val reply = Promise[Producer.ForwardPubRec]()
          producerPacketRouter ! LocalPacketRouter.Route(cp.packetId, Producer.PubRecReceivedFromRemote(reply), reply)
          reply.future.map {
            case Producer.ForwardPubRec(carry: Option[A] @unchecked) => Right(Event(cp, carry))
          }
        case Right(cp: PubRel) =>
          val reply = Promise[Consumer.ForwardPubRel.type]()
          consumerPacketRouter ! RemotePacketRouter.Route(None,
            cp.packetId,
            Consumer.PubRelReceivedFromRemote(reply),
            reply)
          reply.future.map(_ => Right(Event(cp)))
        case Right(cp: PubComp) =>
          val reply = Promise[Producer.ForwardPubComp]()
          producerPacketRouter ! LocalPacketRouter.Route(cp.packetId, Producer.PubCompReceivedFromRemote(reply), reply)
          reply.future.map {
            case Producer.ForwardPubComp(carry: Option[A] @unchecked) => Right(Event(cp, carry))
          }
        case Right(PingResp) =>
          val reply = Promise[ClientConnector.ForwardPingResp.type]()
          clientConnector ! ClientConnector.PingRespReceivedFromRemote(connectionId, reply)
          reply.future.map(_ => Right(Event(PingResp)))
        case Right(cp) => Future.failed(new IllegalStateException(s"$cp is not a client event"))
        case Left(de)  => Future.successful(Left(de))
      }
      .withAttributes(ActorAttributes.supervisionStrategy {
        // Benign exceptions
        case _: LocalPacketRouter.CannotRoute | _: RemotePacketRouter.CannotRoute =>
          Supervision.Resume
        case _ =>
          Supervision.Stop
      })
      .recoverWithRetries(-1,
        {
          case _: WatchedActorTerminatedException => Source.empty
        })
      .withAttributes(ActorAttributes.logLevels(onFailure = Logging.DebugLevel))
}

object MqttServerSession {

  /**
   * Used to signal that a client session has ended
   */
  final case class ClientSessionTerminated(clientId: String)
}

/**
 * Represents server-only sessions
 */
abstract class MqttServerSession extends MqttSession {
  import MqttServerSession._
  import MqttSession._

  /**
   * Used to observe client connections being terminated
   */
  def watchClientSessions: Source[ClientSessionTerminated, NotUsed]

  /**
   * @return a flow for commands to be sent to the session in relation to a connection id
   */
  private[streaming] def commandFlow[A](connectionId: ByteString): CommandFlow[A]

  /**
   * @return a flow for events to be emitted by the session in relation t a connection id
   */
  private[streaming] def eventFlow[A](connectionId: ByteString): EventFlow[A]
}

object ActorMqttServerSession {
  def apply(settings: MqttSessionSettings)(implicit system: ClassicActorSystemProvider): ActorMqttServerSession =
    new ActorMqttServerSession(settings)

  /**
   * A PINGREQ was not received within the required keep alive period - the connection must close
   *
   * http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html
   * 3.1.2.10 Keep Alive
   */
  case object PingFailed extends Exception with NoStackTrace

  private[scaladsl] val serverSessionCounter = new AtomicLong
}

/**
 * Provides an actor implementation of a server session
 * @param settings session settings
 */
final class ActorMqttServerSession(settings: MqttSessionSettings)(implicit systemProvider: ClassicActorSystemProvider)
    extends MqttServerSession {

  import ActorMqttServerSession._
  import MqttServerSession._

  private val system = systemProvider.classicSystem
  private val serverSessionId = serverSessionCounter.getAndIncrement()

  private val (terminations, terminationsSource) = Source
    .queue[ServerConnector.ClientSessionTerminated](settings.clientTerminationWatcherBufferSize,
      OverflowStrategy.backpressure)
    .toMat(BroadcastHub.sink)(Keep.both)
    .run()

  def watchClientSessions: Source[ClientSessionTerminated, NotUsed] =
    terminationsSource.map {
      case ServerConnector.ClientSessionTerminated(clientId) => ClientSessionTerminated(clientId)
    }

  private val consumerPacketRouter =
    system
      .asInstanceOf[ExtendedActorSystem]
      .systemActorOf(PropsAdapter(RemotePacketRouter[Consumer.Event]),
        "server-consumer-packet-id-allocator-" + serverSessionId)
      .toTyped

  private val producerPacketRouter =
    system
      .asInstanceOf[ExtendedActorSystem]
      .systemActorOf(PropsAdapter(LocalPacketRouter[Producer.Event]),
        "server-producer-packet-id-allocator-" + serverSessionId)
      .toTyped

  private val publisherPacketRouter =
    system
      .asInstanceOf[ExtendedActorSystem]
      .systemActorOf(PropsAdapter(RemotePacketRouter[Publisher.Event]),
        "server-publisher-packet-id-allocator-" + serverSessionId)
      .toTyped

  private val unpublisherPacketRouter =
    system
      .asInstanceOf[ExtendedActorSystem]
      .systemActorOf(PropsAdapter(RemotePacketRouter[Unpublisher.Event]),
        "server-unpublisher-packet-id-allocator-" + serverSessionId)
      .toTyped

  private val serverConnector =
    system
      .asInstanceOf[ExtendedActorSystem]
      .systemActorOf(
        PropsAdapter(
          ServerConnector(terminations,
            consumerPacketRouter,
            producerPacketRouter,
            publisherPacketRouter,
            unpublisherPacketRouter,
            settings)),
        "server-connector-" + serverSessionId)
      .toTyped

  import MqttCodec._
  import MqttSession._
  import system.dispatcher

  private implicit val loggingAdapter: LoggingAdapter = Logging(system, classOf[ActorMqttClientSession])

  override def ![A](cp: Command[A]): Unit = cp match {
    case Command(cp: Publish, _, carry) =>
      serverConnector ! ServerConnector.PublishReceivedLocally(cp, carry)
    case c: Command[A] => throw new IllegalStateException(s"$c is not a server command that can be sent directly")
  }

  override def shutdown(): Unit = {
    system.stop(serverConnector.toClassic)
    system.stop(consumerPacketRouter.toClassic)
    system.stop(producerPacketRouter.toClassic)
    system.stop(publisherPacketRouter.toClassic)
    system.stop(unpublisherPacketRouter.toClassic)
    terminations.complete()
  }

  private val pingRespBytes = PingResp.encode(ByteString.newBuilder).result()

  override def commandFlow[A](connectionId: ByteString): CommandFlow[A] =
    Flow
      .lazyFutureFlow { () =>
        val killSwitch = KillSwitches.shared("command-kill-switch-" + serverSessionId)

        Future.successful(
          Flow[Command[A]]
            .watch(serverConnector.toClassic)
            .watchTermination {
              case (_, terminated) =>
                terminated.onComplete {
                  case Failure(_: WatchedActorTerminatedException) =>
                  case _                                           =>
                    serverConnector ! ServerConnector.ConnectionLost(connectionId)
                }
                NotUsed
            }
            .via(killSwitch.flow)
            .flatMapMerge(
              settings.commandParallelism,
              {
                case Command(cp: ConnAck, _, _) =>
                  val reply = Promise[Source[ClientConnection.ForwardConnAckCommand, NotUsed]]()
                  serverConnector ! ServerConnector.ConnAckReceivedLocally(connectionId, cp, reply)
                  Source.futureSource(
                    reply.future.map(_.map {
                      case ClientConnection.ForwardConnAck =>
                        cp.encode(ByteString.newBuilder).result()
                      case ClientConnection.ForwardPingResp =>
                        pingRespBytes
                      case ClientConnection.ForwardPublish(publish, packetId) =>
                        publish.encode(ByteString.newBuilder, packetId).result()
                      case ClientConnection.ForwardPubRel(packetId) =>
                        PubRel(packetId).encode(ByteString.newBuilder).result()
                    }.mapError {
                      case ServerConnector.PingFailed => ActorMqttServerSession.PingFailed
                    }
                      .watchTermination { (_, done) =>
                        done.onComplete {
                          case Success(_) => killSwitch.shutdown()
                          case Failure(t) => killSwitch.abort(t)
                        }
                      }))
                case Command(cp: SubAck, completed, _) =>
                  val reply = Promise[Publisher.ForwardSubAck.type]()
                  publisherPacketRouter ! RemotePacketRouter.RouteViaConnection(connectionId,
                    cp.packetId,
                    Publisher.SubAckReceivedLocally(reply),
                    reply)

                  reply.future.onComplete { result =>
                    completed
                      .foreach(_.complete(result.map(_ => Done)))
                  }

                  Source
                    .future(reply.future.map(_ => cp.encode(ByteString.newBuilder).result()))
                    .recover {
                      case _: RemotePacketRouter.CannotRoute => ByteString.empty
                    }

                case Command(cp: UnsubAck, completed, _) =>
                  val reply = Promise[Unpublisher.ForwardUnsubAck.type]()
                  unpublisherPacketRouter !
                  RemotePacketRouter
                    .RouteViaConnection(connectionId, cp.packetId, Unpublisher.UnsubAckReceivedLocally(reply), reply)

                  reply.future.onComplete { result =>
                    completed
                      .foreach(_.complete(result.map(_ => Done)))
                  }

                  Source.future(reply.future.map(_ => cp.encode(ByteString.newBuilder).result())).recover {
                    case _: RemotePacketRouter.CannotRoute => ByteString.empty
                  }
                case Command(cp: PubAck, completed, _) =>
                  val reply = Promise[Consumer.ForwardPubAck.type]()
                  consumerPacketRouter ! RemotePacketRouter.RouteViaConnection(connectionId,
                    cp.packetId,
                    Consumer.PubAckReceivedLocally(reply),
                    reply)

                  reply.future.onComplete { result =>
                    completed
                      .foreach(_.complete(result.map(_ => Done)))
                  }

                  Source.future(reply.future.map(_ => cp.encode(ByteString.newBuilder).result())).recover {
                    case _: RemotePacketRouter.CannotRoute => ByteString.empty
                  }
                case Command(cp: PubRec, completed, _) =>
                  val reply = Promise[Consumer.ForwardPubRec.type]()
                  consumerPacketRouter ! RemotePacketRouter.RouteViaConnection(connectionId,
                    cp.packetId,
                    Consumer.PubRecReceivedLocally(reply),
                    reply)

                  reply.future.onComplete { result =>
                    completed
                      .foreach(_.complete(result.map(_ => Done)))
                  }

                  Source.future(reply.future.map(_ => cp.encode(ByteString.newBuilder).result())).recover {
                    case _: RemotePacketRouter.CannotRoute => ByteString.empty
                  }
                case Command(cp: PubComp, completed, _) =>
                  val reply = Promise[Consumer.ForwardPubComp.type]()
                  consumerPacketRouter ! RemotePacketRouter.RouteViaConnection(connectionId,
                    cp.packetId,
                    Consumer.PubCompReceivedLocally(reply),
                    reply)

                  reply.future.onComplete { result =>
                    completed
                      .foreach(_.complete(result.map(_ => Done)))
                  }

                  Source.future(reply.future.map(_ => cp.encode(ByteString.newBuilder).result())).recover {
                    case _: RemotePacketRouter.CannotRoute => ByteString.empty
                  }
                case c: Command[A] => throw new IllegalStateException(s"$c is not a server command")
              })
            .recover {
              case _: WatchedActorTerminatedException => ByteString.empty
            }
            .filter(_.nonEmpty)
            .log("server-commandFlow", _.iterator.decodeControlPacket(settings.maxPacketSize)) // we decode here so we can see the generated packet id
            .withAttributes(ActorAttributes.logLevels(onFailure = Logging.DebugLevel)))
      }
      .mapMaterializedValue(_ => NotUsed)

  override def eventFlow[A](connectionId: ByteString): EventFlow[A] =
    Flow[ByteString]
      .watch(serverConnector.toClassic)
      .watchTermination {
        case (_, terminated) =>
          terminated.onComplete {
            case Failure(_: WatchedActorTerminatedException) =>
            case _                                           =>
              serverConnector ! ServerConnector.ConnectionLost(connectionId)
          }
          NotUsed
      }
      .via(new MqttFrameStage(settings.maxPacketSize))
      .map(_.iterator.decodeControlPacket(settings.maxPacketSize))
      .log("server-events")
      .mapAsync[Either[MqttCodec.DecodeError, Event[A]]](settings.eventParallelism) {
        case Right(cp: Connect) =>
          val reply = Promise[ClientConnection.ForwardConnect.type]()
          serverConnector ! ServerConnector.ConnectReceivedFromRemote(connectionId, cp, reply)
          reply.future.map(_ => Right(Event(cp)))
        case Right(cp: Subscribe) =>
          val reply = Promise[Publisher.ForwardSubscribe.type]()
          serverConnector ! ServerConnector.SubscribeReceivedFromRemote(connectionId, cp, reply)
          reply.future.map(_ => Right(Event(cp)))
        case Right(cp: Unsubscribe) =>
          val reply = Promise[Unpublisher.ForwardUnsubscribe.type]()
          serverConnector ! ServerConnector.UnsubscribeReceivedFromRemote(connectionId, cp, reply)
          reply.future.map(_ => Right(Event(cp)))
        case Right(cp: Publish) =>
          val reply = Promise[Consumer.ForwardPublish.type]()
          serverConnector ! ServerConnector.PublishReceivedFromRemote(connectionId, cp, reply)
          reply.future.map(_ => Right(Event(cp)))
        case Right(cp: PubAck) =>
          val reply = Promise[Producer.ForwardPubAck]()
          producerPacketRouter ! LocalPacketRouter.Route(cp.packetId, Producer.PubAckReceivedFromRemote(reply), reply)
          reply.future.map {
            case Producer.ForwardPubAck(carry: Option[A] @unchecked) => Right(Event(cp, carry))
          }
        case Right(cp: PubRec) =>
          val reply = Promise[Producer.ForwardPubRec]()
          producerPacketRouter ! LocalPacketRouter.Route(cp.packetId, Producer.PubRecReceivedFromRemote(reply), reply)
          reply.future.map {
            case Producer.ForwardPubRec(carry: Option[A] @unchecked) => Right(Event(cp, carry))
          }
        case Right(cp: PubRel) =>
          val reply = Promise[Consumer.ForwardPubRel.type]()
          consumerPacketRouter ! RemotePacketRouter.RouteViaConnection(connectionId,
            cp.packetId,
            Consumer.PubRelReceivedFromRemote(reply),
            reply)
          reply.future.map(_ => Right(Event(cp)))
        case Right(cp: PubComp) =>
          val reply = Promise[Producer.ForwardPubComp]()
          producerPacketRouter ! LocalPacketRouter.Route(cp.packetId, Producer.PubCompReceivedFromRemote(reply), reply)
          reply.future.map {
            case Producer.ForwardPubComp(carry: Option[A] @unchecked) => Right(Event(cp, carry))
          }
        case Right(PingReq) =>
          val reply = Promise[ClientConnection.ForwardPingReq.type]()
          serverConnector ! ServerConnector.PingReqReceivedFromRemote(connectionId, reply)
          reply.future.map(_ => Right(Event(PingReq)))
        case Right(Disconnect) =>
          val reply = Promise[ClientConnection.ForwardDisconnect.type]()
          serverConnector ! ServerConnector.DisconnectReceivedFromRemote(connectionId, reply)
          reply.future.map(_ => Right(Event(Disconnect)))
        case Right(cp) => Future.failed(new IllegalStateException(s"$cp is not a server event"))
        case Left(de)  => Future.successful(Left(de))
      }
      .withAttributes(ActorAttributes.supervisionStrategy {
        // Benign exceptions
        case _: LocalPacketRouter.CannotRoute | _: RemotePacketRouter.CannotRoute =>
          Supervision.Resume
        case _ =>
          Supervision.Stop
      })
      .recoverWithRetries(-1,
        {
          case _: WatchedActorTerminatedException => Source.empty
        })
      .withAttributes(ActorAttributes.logLevels(onFailure = Logging.DebugLevel))
}
