package mdr.pdb.app
package state

import zio.*
import com.raquo.airstream.core.{EventStream, Signal}
import com.raquo.airstream.state.{Val, Var}
import mdr.pdb.OsobniCislo
import mdr.pdb.users.query.*
import com.raquo.airstream.core.Observer
import scala.scalajs.js
import scala.scalajs.js.JSON
import zio.json.{*, given}
import com.raquo.airstream.eventbus.EventBus
import com.raquo.airstream.ownership.Owner
import com.raquo.waypoint.Router
import mdr.pdb.parameters.*
import fiftyforms.services.files.File
import sttp.tapir.DecodeResult
import com.raquo.airstream.ownership.OneTimeOwner
import scala.annotation.unused
import com.raquo.airstream.ownership.Subscription
import mdr.pdb.users.query.client.UsersRepository
import mdr.pdb.proof.command.client.ProofCommandApi

trait AppState
    extends components.AppPage.AppState
    with connectors.DirectoryPageConnector.AppState
    with connectors.DetailPageConnector.AppState
    with connectors.DetailParametruPageConnector.AppState
    with connectors.DetailKriteriaPageConnector.AppState
    with pages.detail.UpravDukaz.State:

  def online: Signal[Boolean]
  def users: EventStream[List[UserInfo]]
  def details: EventStream[UserInfo]
  def parameters: EventStream[List[Parameter]]
  def actionBus: Observer[Action]

object AppStateLive:
  def layer: URLayer[
    ZEnv & AppConfig & Api & UsersRepository & ProofCommandApi & Router[Page],
    AppState
  ] = {
    (ZLayer.fromZIO(ZIO.runtime[ZEnv]) ++ ZIOOwner.layer) >>> (
        (
            appConfig: AppConfig,
            api: Api,
            usersRepository: UsersRepository,
            proofCommandApi: ProofCommandApi,
            router: Router[Page],
            runtime: Runtime[ZEnv],
            owner: Owner
        ) =>
          AppStateLive(
            appConfig,
            api,
            usersRepository,
            proofCommandApi,
            router,
            runtime
          )(using owner)
    ).toLayer[AppState]
  }

class AppStateLive(
    appConfig: AppConfig,
    api: Api,
    usersRepository: UsersRepository,
    proofCommandApi: ProofCommandApi,
    router: Router[Page],
    runtime: Runtime[ZEnv]
)(using
    owner: Owner
) extends AppState:

  given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply)
  given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen
  given JsonDecoder[UserContract] = DeriveJsonDecoder.gen
  given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen

  given JsonDecoder[ParameterCriterion] = DeriveJsonDecoder.gen
  given JsonDecoder[Parameter] = DeriveJsonDecoder.gen

  private val actions = EventBus[Action]()
  private val (parametersStream, pushParameters) =
    EventStream.withCallback[List[Parameter]]
  private val (usersStream, pushUsers) =
    EventStream.withCallback[List[UserInfo]]
  private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo]
  private val (filesStream, pushFiles) = EventStream.withCallback[List[File]]
  private val isOnline = Var(true)

  private val mockParameters: List[Parameter] =
    pdbParams
      .asInstanceOf[js.Dictionary[js.Object]]
      .values
      .map(o => JSON.stringify(o).fromJson[Parameter])
      .collect { case Right(p) => p }
      .toList

  private def scheduleOnlineCheck(): Unit =
    appConfig.onlineCheckMs.foreach(d =>
      actions.writer.delay(d).onNext(CheckOnlineState)
    )

  // TODO: Extract to separate event handler
  private val handler: Action => Task[Unit] =
    case CheckOnlineState =>
      for
        o <- api.alive()
        _ <- Task.attempt {
          isOnline.set(o)
          scheduleOnlineCheck()
        }
      yield ()
    case FetchDirectory =>
      for
        users <- usersRepository.matching(AllUsers)
        _ <- Task.attempt(pushUsers(users))
      yield ()
    case FetchUserDetails(osc) =>
      for user <- usersRepository.matching(UserWithOsobniCislo(osc))
      yield for o <- user do
        pushDetails(o)
        router.replaceState(Page.Detail(o))
    case FetchParameters(osc) =>
      Task.attempt(pushParameters(mockParameters))
    case FetchParameter(osc, paramId) =>
      for user <- usersRepository.matching(UserWithOsobniCislo(osc))
      yield for
        o <- user
        p <- mockParameters.find(_.id == paramId)
      do
        pushDetails(o)
        pushParameters(mockParameters)
        router.replaceState(Page.DetailParametru(o, p))
    case FetchParameterCriterion(osc, paramId, critId, page) =>
      for user <- usersRepository.matching(UserWithOsobniCislo(osc))
      yield for
        o <- user
        p <- mockParameters.find(_.id == paramId)
        c <- p.criteria.find(_.id == critId)
      do
        pushDetails(o)
        pushParameters(mockParameters)
        router.replaceState(page(o, p, c))
    case FetchAvailableFiles(osc) =>
      Task.attempt {
        pushFiles(
          List(
            File("https://tc163.cmi.cz/here", "Example file")
          )
        )
      }
    case SubmitProofCommand(cmd, next) =>
      for
        _ <- proofCommandApi.submitCommand(cmd)
        _ <- Task.attempt(router.pushState(next))
      yield ()
    case NavigateTo(page) => Task.attempt { router.pushState(page) }

  actions.events.foreach(action => runtime.unsafeRunAsync(handler(action)))

  scheduleOnlineCheck()

  override def online: Signal[Boolean] = isOnline.signal
  override def users: EventStream[List[UserInfo]] =
    usersStream.debugWithName("users")

  override def details: EventStream[UserInfo] =
    detailsStream.debugWithName("details")

  override def parameters: EventStream[List[Parameter]] =
    parametersStream.debugWithName("parameters")

  override def availableFiles: EventStream[List[File]] =
    filesStream.debugWithName("available files")

  override def actionBus: Observer[Action] =
    actions.writer.debugWithName("actions writer")
