package wow.doge.mygame.subsystems.moddingsystem
import java.io.FileNotFoundException
import java.nio.file.NoSuchFileException

import scala.collection.View
import scala.collection.immutable.ArraySeq
import scala.util.Try

import cats.implicits._
import io.circe._
import io.circe.generic.JsonCodec
import io.circe.generic.semiauto._
import io.circe.parser._
import monix.bio.IO
import monix.bio.UIO
import monix.reactive.Consumer
import monix.reactive.Observable
import wow.doge.mygame.utils.IOUtils

@JsonCodec
final case class Test1(hello1: String, hello2: String)
@JsonCodec
final case class Test2(hello1: String)
final case class Plugin(name: String, priority: Int)
object Plugin {
  implicit val pluginFormat: Decoder[Plugin] = deriveDecoder
}

object ModdingSystem {
  sealed trait Error extends Serializable with Product
  final case class CouldNotDecode(cause: String) extends Error
  final case class ParseFailure(cause: String) extends Error
  final case class FileNotFound(fileName: String) extends Error
  case object GenericError extends Error

  def readPluginsList(dir: os.Path): Try[Either[Error, ArraySeq[Plugin]]] =
    Try(
      parse(os.read(dir / "plugins.json"))
        .map(
          _.as[ArraySeq[Plugin]]
            .leftMap(e => CouldNotDecode(e.getMessage()))
        )
        .leftMap((e: ParsingFailure) => ParseFailure(e.message))
        .flatten
    )
  // .toValidated

  def findPluginFiles(dir: os.Path): View[os.Path] =
    os.list(dir)
      .view
      .filter(f => f.ext == "json" && f.baseName.endsWith("plugin"))

  def findAndReadPluginFiles(
      dir: os.Path,
      plugins: ArraySeq[Plugin]
  ) =
    plugins
      .sortBy(_.priority)
      .view
      .map(p =>
        p ->
          Either
            .catchNonFatal {
              val path = dir / os.RelPath(p.name + ".plugin.json")
              os.read(path)
            }
            .leftMap {
              case _: FileNotFoundException =>
                FileNotFound(p.name)
              case _: NoSuchFileException => FileNotFound(p.name)
              case e                      => GenericError
            }
      )
      .partitionMap {
        case (p, either) =>
          either match {
            case Left(value)  => Left(p -> value)
            case Right(value) => Right(p -> value)
          }
      }

  def readPluginFiles(filePaths: View[os.Path]) = {
    filePaths.map(path => os.read(path))
  }

  def parsePluginFiles(files: View[(Plugin, String)]) =
    files
      .map {
        case (p, s) => p -> parse(s)
      }
      .partitionMap {
        case (p, Left(value))  => Left(p -> value)
        case (p, Right(value)) => Right(p -> value)
      }

  def foldMerge(iterable: Iterable[Json]) =
    iterable.foldLeft(Json.fromString("empty")) {
      case (json, io.circe.Json.Null) => json //ignore null values
      case (json, value)              => json.deepMerge(value)
    }

  def mergePluginData(plugins: View[(Plugin, Json)]) = {
    foldMerge(plugins.map {
      case (p, json) => json
    })
  }

  def mergePluginDataConsumer =
    Consumer.foldLeft[Json, Json](Json.fromString("empty")) {
      case (json, io.circe.Json.Null) => json
      case (json, that)               => json.deepMerge(that)
    }

  def loadBalancedPluginDataMerger =
    Consumer
      .loadBalance(parallelism = 2, mergePluginDataConsumer)
      .map(foldMerge)

//   def test =
//     for {
//       filePaths <- Task(findPluginFiles(os.pwd))
//       files <- Task(readPluginFiles(filePaths))
//       (failures, successes) <- Task(parsePluginFiles(files))
//       merged <- Task(mergePluginData(successes))
//       _ <- Task {
//         println(s"Successes = ${successes.to(Seq)}")
//         println(s"Failure = ${failures.to(Seq)}")
//         println(s"Merged = $merged")
//       }
//     } yield ()

  def test(wd: os.Path = os.pwd) =
    for {
      plugins <- IO.fromTryEither(readPluginsList(wd))
      (readFailures, readSuccesses) <- UIO(findAndReadPluginFiles(wd, plugins))
      (parseFailures, parseSuccesses) <- UIO(parsePluginFiles(readSuccesses))
      // res <- UIO(mergePluginData(parseSuccesses))
      res <-
        IOUtils
          .toIO(
            Observable
              .fromIterable(parseSuccesses)
              .map { case (p, json) => json }
              .consumeWith(loadBalancedPluginDataMerger)
          )
          .onErrorHandle(e => GenericError)
      _ <- UIO {
        println(s"Read Successes = ${readSuccesses.to(Seq)}")
        println(s"Read Failures = ${readFailures.to(Seq)}")
        println(s"Parse Successes = ${parseSuccesses.to(Seq)}")
        println(s"Parse Failures = ${parseFailures.to(Seq)}")
        println(s"Merged = $res")
      }
    } yield ()

  // monix.eval.Task.deferAction(implicit s =>
  //   ModdingSystem
  //     .test()
  //     .leftMap(e => new Throwable(e.toString()))
  //     .to[monix.eval.Task]
  // )

  // def test3(wd: os.Path = os.pwd) = {
  //   (readPluginsList(os.pwd).toValidatedNec)
  // }
}
