package com.spr.blog

import akka.actor.Props
import akka.pattern.pipe
import akka.persistence.{PersistentActor, SnapshotOffer}

import scala.concurrent.{Future, Promise}

/**
  * Aggregate root entity for holding the state of a blog.
  */
class BlogEntity extends PersistentActor {

  import BlogEntity._
  import context._

  private var state = BlogState()

  // in order to really match the Lagom example properly, we'd create an actor for every blog post, though that
  // sounds a bit overkill to me. then our persistenceId would be something like s"blog-$id".
  override def persistenceId: String = "blog"

  override def receiveCommand: Receive = {
    case GetPost(id) =>
      sender() ! state(id)
    case AddPost(content) =>
      handleEvent(PostAdded(PostId(), content)) pipeTo sender()
      ()
    case UpdatePost(id, content) =>
      state(id) match {
        case response @ Left(_) => sender() ! response
        case Right(_) =>
          handleEvent(PostUpdated(id, content)) pipeTo sender()
          ()
      }
  }

  private def handleEvent[E <: BlogEvent](e: => E): Future[E] = {
    val p = Promise[E]
    persist(e) { event =>
      p.success(event)
      state += event
      system.eventStream.publish(event)
      if (lastSequenceNr != 0 && lastSequenceNr % 1000 == 0) saveSnapshot(state)
    }
    p.future
  }

  override def receiveRecover: Receive = {
    case event: BlogEvent =>
      state += event
    case SnapshotOffer(_, snapshot: BlogState) =>
      state = snapshot
  }

}

object BlogEntity {
  def props = Props(new BlogEntity)

  sealed trait BlogCommand

  final case class GetPost(id: PostId) extends BlogCommand

  final case class AddPost(content: PostContent) extends BlogCommand

  final case class UpdatePost(id: PostId, content: PostContent) extends BlogCommand

  sealed trait BlogEvent {
    val id: PostId
    val content: PostContent
  }

  final case class PostAdded(id: PostId, content: PostContent) extends BlogEvent

  final case class PostUpdated(id: PostId, content: PostContent) extends BlogEvent

  final case class PostNotFound(id: PostId) extends RuntimeException(s"Blog post not found with id $id")

  type MaybePost[+A] = Either[PostNotFound, A]

  final case class BlogState(posts: Map[PostId, PostContent]) {
    def apply(id: PostId): MaybePost[PostContent] = posts.get(id).toRight(PostNotFound(id))

    def +(event: BlogEvent): BlogState = BlogState(posts.updated(event.id, event.content))
  }

  object BlogState {
    def apply(): BlogState = BlogState(Map.empty)
  }

}