package ru.pavkin.booking

import java.time.Instant
import java.util.concurrent.TimeUnit

import aecor.data.{ EitherK, Enriched }
import aecor.runtime.akkapersistence.serialization.{
  DecodingFailure,
  PersistentDecoder,
  PersistentEncoder,
  PersistentRepr
}
import aecor.runtime.akkapersistence.{ AkkaPersistenceRuntime, CassandraJournalAdapter }
import akka.actor.ActorSystem
import cats.effect._
import cats.implicits._
import ru.pavkin.booking.common.models.BookingKey
import ru.pavkin.booking.booking.entity._
import ru.pavkin.booking.booking.entity.BookingWireCodecs._
import ru.pavkin.booking.booking.serialization.BookingEventSerializer
import ru.pavkin.booking.common.effect.TimedOutBehaviour

import scala.concurrent.duration._

/**
  * This is an example of wiring an AkkaPersistenceRuntime entity deployment.
  * It's only a demo and is not used in the application.
  *
  * For this deployment to work properly, you'll need to configure akka-persistence-cassandra plugin properly
  */
final class AkkaPersistenceRuntimeWirings[F[_]](
  val bookings: BookingKey => EitherK[Booking, BookingCommandRejection, F]
)

object AkkaPersistenceRuntimeWirings {
  def apply[F[_]: ConcurrentEffect: Timer](system: ActorSystem,
                                           clock: Clock[F]): F[AkkaPersistenceRuntimeWirings[F]] = {

    val journalAdapter = CassandraJournalAdapter(system)
    val runtime = AkkaPersistenceRuntime(system, journalAdapter)

    implicit val eventEncoder: PersistentEncoder[Enriched[EventMetadata, BookingEvent]] =
      PersistentEncoder.instance { evt =>
        val (manifest, bytes) = BookingEventSerializer.serialize(evt)
        PersistentRepr(manifest, bytes)
      }

    implicit val eventDecoder: PersistentDecoder[Enriched[EventMetadata, BookingEvent]] =
      PersistentDecoder.instance { repr =>
        BookingEventSerializer
          .deserialize(repr.manifest, repr.payload)
          .leftMap(ex => DecodingFailure(ex.getMessage, Some(ex)))
      }

    val generateTimestamp: F[EventMetadata] =
      clock.realTime(TimeUnit.MILLISECONDS).map(Instant.ofEpochMilli).map(EventMetadata)

    val bookingsBehavior =
      TimedOutBehaviour(
        EventsourcedBooking.behavior[F](clock).enrich[EventMetadata](generateTimestamp)
      )(2.seconds)

    val bookings: F[BookingKey => EitherK[Booking, BookingCommandRejection, F]] = runtime
      .deploy(EventsourcedBooking.entityName, bookingsBehavior, EventsourcedBooking.tagging)

    bookings.map(new AkkaPersistenceRuntimeWirings(_))
  }
}