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 cats.Show
import cats.implicits._
import cats.kernel.Eq
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

import IOUtils.toIO

@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 decoder: Decoder[Plugin] = deriveDecoder
  implicit val show = Show.fromToString[Plugin]
  implicit val eq = Eq.fromUniversalEquals[Plugin]
}

object ModdingSystem {
  sealed trait Error
  final case class CouldNotDecode(cause: String) extends Error
  final case class ParseFailure(cause: io.circe.ParsingFailure) extends Error
  final case class DecodingFailure(cause: io.circe.DecodingFailure)
      extends Error
  final case class FileNotFound(path: os.Path) extends Error
  object Error {
    implicit val show = Show.fromToString[Error]
    implicit val eq = Eq.fromUniversalEquals[Error]
  }

  def readPluginsList(dir: os.Path): IO[Error, ArraySeq[Plugin]] =
    IO(parse(os.read(dir / "plugins.json")))
      .onErrorHandleWith {
        case _: FileNotFoundException =>
          IO.raiseError(FileNotFound(dir / "plugins.json"))
        case _: NoSuchFileException =>
          IO.raiseError(FileNotFound(dir / "plugins.json"))
      }
      .flatMap(files =>
        IO.fromEither(files)
          .map(_.as[ArraySeq[Plugin]])
          .mapError(ParseFailure)
      )
      .flatMap(result => IO.fromEither(result).mapError(DecodingFailure))

  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]
  ): UIO[(View[(Plugin, Error)], View[(Plugin, String)])] =
    UIO(
      plugins
        .sortBy(_.priority)
        .view
        .map { p =>
          val path = dir / os.RelPath(p.name + ".plugin.json")
          p ->
            Either
              .catchNonFatal(os.read(path))
              .leftMap {
                case _: FileNotFoundException => FileNotFound(path)
                case _: NoSuchFileException   => FileNotFound(path)
              }

        }
        .partitionMap {
          case (p, either) =>
            either match {
              case Left(value)  => Left(p -> value)
              case Right(value) => Right(p -> value)
            }
        }
    )
  // : (View[(Plugin, Error)], View[(Plugin, String)])
  def findAndReadPluginFiles2(
      dir: os.Path,
      plugins: ArraySeq[Plugin]
  ) =
    // IO.parTraverse(plugins.sortBy(_.priority))(p =>
    //   IO {
    //     val path = dir / os.RelPath(p.name + ".plugin.json")
    //     os.read(path)
    //   }
    //     .onErrorHandleWith {
    //       case _: FileNotFoundException =>
    //         IO.raiseError(FileNotFound(dir.toString))
    //       case _: NoSuchFileException =>
    //         IO.raiseError(FileNotFound(dir.toString))
    //     }
    //     .flatMap(r => UIO(p -> r))
    // ).map {
    //   _.partitionMap {
    //     case (p, either) =>
    //       either match {
    //         case Left(value)  => Left(p -> value)
    //         case Right(value) => Right(p -> value)
    //       }
    //   }
    // }
    plugins
      .sortBy(_.priority)
      .view
      .map { p =>
        val path = dir / os.RelPath(p.name + ".plugin.json")
        p -> IO(os.read(path))
          .onErrorHandleWith {
            case _: FileNotFoundException => IO.raiseError(FileNotFound(path))
            case _: NoSuchFileException   => IO.raiseError(FileNotFound(path))
          }
      // .map(r => p -> r)

      }
      .map {
        case (p, io) =>
          io.attempt.map {
            case Left(value)  => Left(p -> value)
            case Right(value) => Right(p -> value)
          }
      }
      .to(List)
      .parSequence

  // .partitionMap {
  //   _.map {
  //     case l @ Left(value)  => l
  //     case r @ Right(value) => r
  //   }
  // }
  // .sequence

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

  // def readPluginFiles2(filePaths: View[os.Path]) =
  //   filePaths
  //     .map(path =>
  //       IO(os.read(path)).onErrorHandleWith {
  //         case _: FileNotFoundException =>
  //           IO.raiseError(FileNotFound(path))
  //         case _: NoSuchFileException =>
  //           IO.raiseError(FileNotFound(path))
  //       }
  //     )
  //     .to(List)
  //     .parSequence
  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)
      }

  val emptyJson = Json.fromString("empty")

  val foldFn: (Json, Json) => Json = {
    case (json, Json.Null) => json //ignore null values
    case (json, value)     => json.deepMerge(value)
  }

  def mergePluginDataConsumer =
    Consumer.foldLeft[Json, Json](emptyJson)(foldFn)

  def loadBalancedPluginDataMerger =
    Consumer
      .loadBalance(parallelism = 2, mergePluginDataConsumer)
      .map(_.foldLeft(emptyJson)(foldFn))

  def run(wd: os.Path = os.pwd) =
    for {
      plugins <- readPluginsList(wd)
      (readFailures, readSuccesses) <- findAndReadPluginFiles(wd, plugins)
      (parseFailures, parseSuccesses) <- UIO(parsePluginFiles(readSuccesses))
      res <- UIO.parMap5(
        UIO(readFailures.to(List)),
        UIO(readSuccesses.to(List)),
        UIO(parseFailures.to(List)),
        UIO(parseSuccesses.to(List)),
        toIO(
          Observable
            .fromIterable(parseSuccesses)
            .map { case (p, json) => json }
            .consumeWith(loadBalancedPluginDataMerger)
        ).hideErrors
      )(Result.apply)
    } yield res

  def log(res: Result) =
    UIO {
      pprint.log(show"Read Successes = ${res.readSuccesses}")
      pprint.log(show"Read Failures = ${res.readFailures}")
      pprint.log(show"Parse Successes = ${res.parseSuccesses}")
      pprint.log(show"Parse Failures = ${res.parseFailures}")
      pprint.log(show"Merged = ${res.pluginJson}")
    }

  case class Result(
      readFailures: List[(Plugin, Error)],
      readSuccesses: List[(Plugin, String)],
      parseFailures: List[(Plugin, ParsingFailure)],
      parseSuccesses: List[(Plugin, Json)],
      pluginJson: Json
  )
  object Result {
    implicit val show = Show.fromToString[Result]
    // implicit val eq = Eq.fromUniversalEquals[Error]
  }

}
