/*
 * Copyright 2016 Heiko Seeberger
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package de.heikoseeberger.gabbler.chat

import akka.actor.{ ActorLogging, Props }
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.Uri
import akka.persistence.{ PersistentActor, RecoveryCompleted }
import akka.stream.ActorMaterializer
import akka.stream.alpakka.sse.scaladsl.EventSource
import de.heikoseeberger.akkasse.ServerSentEvent
import io.circe.parser.decode

object UserRepository {

  private sealed trait UserEvent

  final case class FindUserByUsername(username: String)
  final case class UsernameUnknown(username: String)

  private final case class AddUser(id: Long, username: String, nickname: String)
  private final case class UserAdded(eventId: String, user: User)

  private final case class RemoveUser(id: Long)
  private final case class UserRemoved(eventId: String, user: User)

  final case class User(id: Long, username: String, nickname: String)

  final val Name = "user-repository"

  def apply(userEventsEndpoint: Uri): Props =
    Props(new UserRepository(userEventsEndpoint))
}

final class UserRepository(userEventsEndpoint: Uri) extends PersistentActor with ActorLogging {
  import UserRepository._
  import io.circe.generic.auto._

  override val persistenceId = Name

  private implicit val mat = ActorMaterializer()

  private var users = Map.empty[String, User]

  private var lastEventId = Option.empty[String]

  override def receiveCommand = {
    case FindUserByUsername(n)               => handleFindUserByUsername(n)
    case (eventId: String, AddUser(i, u, n)) => handleAddUser(eventId, i, u, n)
    case (eventId: String, RemoveUser(i))    => handleRemoveUser(eventId, i)
  }

  override def receiveRecover = {
    case RecoveryCompleted =>
      userEvents(lastEventId).runForeach(self ! _)

    case UserAdded(eventId, user) =>
      lastEventId = Some(eventId)
      users += user.username -> user
      log.info("Added user with username {}", user.username)

    case UserRemoved(eventId, user) =>
      lastEventId = Some(eventId)
      users -= user.username
      log.info("Removed user with username {}", user.username)
  }

  private def handleFindUserByUsername(username: String) =
    users.get(username) match {
      case Some(user) => sender() ! user
      case None       => sender() ! UsernameUnknown(username)
    }

  private def handleAddUser(eventId: String, id: Long, username: String, nickname: String) =
    persist(UserAdded(eventId, User(id, username, nickname)))(receiveRecover)

  private def handleRemoveUser(eventId: String, id: Long) =
    users.values.find(_.id == id) match {
      case Some(user) => persist(UserRemoved(eventId, user))(receiveRecover)
      case None       => log.warning("User with id {} does not exist!", id)
    }

  private def userEvents(lastEventId: Option[String]) =
    EventSource(userEventsEndpoint, Http(context.system).singleRequest(_), lastEventId)
      .collect {
        case ServerSentEvent(Some(data), Some("user-added"), Some(eventId), _) =>
          eventId -> decode[AddUser](data)
        case ServerSentEvent(Some(data), Some("user-removed"), Some(eventId), _) =>
          eventId -> decode[RemoveUser](data)
      }
      .collect { case (eventId, Right(userEvent)) => eventId -> userEvent }
}