import com.typesafe.config.{Config, ConfigFactory}

import scala.xml._
import java.nio.file.{Files, Path, Paths, StandardOpenOption}
import java.io.InputStream
import java.nio.file.attribute.FileTime
import java.util.zip.{ZipEntry, ZipFile}
import java.time.LocalDateTime

import scala.util.{Failure, Success, Try}
import xmlvalid._
import templates._

object Main {
  val conf: Config = ConfigFactory.load
  val debugMode: Boolean = conf.getBoolean("debug")
  val logFileName: String = conf.getString("log")

  def main(args: Array[String]): Unit = {
    println("Converting NSD -> SPB has been started...")

    if (args.length != 2)
      println("Error. Converting hasn`t been finished: Undefined INPUT/OUTPUT directory.\nUsage: run INPUT OUTPUT")
    else if (!Files.exists(Paths.get(args(0))))
      println("Error. Converting hasn`t been finished: INPUT directory doesn`t exist")
    else if (!Files.exists(Paths.get(args(1))))
      println("Error. Converting hasn`t been finished: OUTPUT directory doesn`t exist")
    else {
      val p = new PrettyPrinter(160, 2)
      val nsdXmlValid = XMLValid(getClass.getResource("/nsd/nsd-ext-merged-schema.xsd"))
      val rtsXmlValid = XMLValid(getClass.getResource("/rts/fpml-recordkeeping-merged-schema.xsd"))
      var logString = ""

      val paths = Files.list(Paths.get(args(0)))
        .filter {
          p => !Files.isDirectory(p)
        }
        .toArray
        .map {
          _.asInstanceOf[Path]
        }

      val start = LocalDateTime.now.toString.replace(":", ".")

      val sourceNameValidated = s"${args(0)}/${start}_validated"
      val sourceValidated = Try { Files.createDirectory(Paths.get(sourceNameValidated)) }

      val sourceNameNotValidated = s"${args(0)}/${start}_not_validated"
      val sourceNotValidated = Try { Files.createDirectory(Paths.get(sourceNameNotValidated)) }

      val sourceNameError = s"${args(0)}/${start}_error"
      val sourceError = Try { Files.createDirectory(Paths.get(sourceNameError)) }

      val outputNameValidated = s"${args(1)}/${start}_validated"
      val outputValidated = Try { Files.createDirectory(Paths.get(outputNameValidated)) }

      val outputNameNotValidated = s"${args(1)}/${start}_not_validated"
      val outputNotValidated = Try { Files.createDirectory(Paths.get(outputNameNotValidated)) }

      val createDirRes = for {
        sv <- sourceValidated
        snv <- sourceNotValidated
        se <- sourceError
        ov <- outputValidated
        onv <- outputNotValidated
      } yield onv

      createDirRes match {
        case Success(_) =>

          val sourceArray = {
            for (p <- paths) yield {
              val name = p.getFileName.toString
              val ext = name.split('.').last.toLowerCase

              if (ext == "zip")
                Try {
                  val zip = new ZipFile(p.toString)

                  zip
                    .stream
                    .toArray
                    .map {
                      z => z.asInstanceOf[ZipEntry]
                    }
                    .map {
                      e =>
                        FileEntry(
                          s"${name}_${e.getName}",
                          //e.getName,
                          e.getLastModifiedTime,
                          Some(zip.getInputStream(e)),
                          zip.getInputStream(e),
                          Some(zip),
                          None)
                    }
                } match {
                  case Success(v) =>
                    v
                  case Failure(ex) =>
                    Array(
                      FileEntry(
                        name,
                        Files.getLastModifiedTime(p),
                        None,
                        Files.newInputStream(p),
                        None,
                        Some(ex.toString))
                    )
                }
              else
                Array(
                  FileEntry(
                    name,
                    Files.getLastModifiedTime(p),
                    Some(Files.newInputStream(p)),
                    Files.newInputStream(p),
                    None,
                    None)
                )
            }
          }
            .flatten
            .sortWith { (a, b) =>
              (a.modified.compareTo(b.modified) == 0 && a.name <= b.name) || (a.modified.compareTo(b.modified) < 0)
            }


          Try {
            for (f <- sourceArray) {
              val now = LocalDateTime.now.toString

              f.msg match {
                case Some(msg) =>
                  logString += s"'$now'\nImpossible to process '${f.name}': $msg\n\n"
                  Files.copy(f.streamMove, Paths.get(sourceNameError, f.name))

                case None =>
                  Try {
                    convert(
                      f.streamConvert.get,
                      f.name,
                      s"converted_${f.name}",
                      nsdXmlValid,
                      rtsXmlValid,
                      p)
                  } match {
                    case Success((oXml, ti)) if ti.contains("isn`t wellformed or invalid") =>
                      XML.save(
                        Paths.get(outputNameNotValidated, s"converted_${f.name}").toString,
                        oXml,
                        xmlDecl = true
                      )
                      logString += s"'$now'\n$ti"
                      Files.copy(f.streamMove, Paths.get(sourceNameNotValidated, f.name))

                    case Success((oXml, ti)) =>
                      XML.save(
                        Paths.get(outputNameValidated, s"converted_${f.name}").toString,
                        oXml,
                        xmlDecl = true
                      )
                      logString += s"'$now'\n$ti"
                      Files.copy(f.streamMove, Paths.get(sourceNameValidated, f.name))

                    case Failure(ex) =>
                      logString += s"'$now'\nImpossible to convert '${f.name}': ${ex.toString}\n\n"
                      Files.copy(f.streamMove, Paths.get(sourceNameError, f.name))
                  }
              }
            }

            sourceArray
              .filter { f => f.zip.nonEmpty }
              .groupBy { f => f.zip.get }
              .keys
              .foreach { _.close() }

            sourceArray
              .filter { f => f.zip.isEmpty }
              .foreach {
                f =>
                  f.streamMove.close()
                  if (f.streamConvert.nonEmpty) f.streamConvert.get.close()
              }

            paths.foreach {
              p => Files.delete(p)
            }

            Files.write(
              Paths.get(args(0), logFileName),
              logString.getBytes("utf-8"),
              StandardOpenOption.CREATE,
              StandardOpenOption.TRUNCATE_EXISTING)

            if (Files.list(sourceValidated.get).toArray.nonEmpty)
              Files.copy(Paths.get(args(0), logFileName), Paths.get(sourceNameValidated, logFileName))
            else
              Files.delete(sourceValidated.get)

            if (Files.list(sourceNotValidated.get).toArray.isEmpty) Files.delete(sourceNotValidated.get)
            if (Files.list(sourceError.get).toArray.isEmpty) Files.delete(sourceError.get)
            if (Files.list(outputValidated.get).toArray.isEmpty) Files.delete(outputValidated.get)
            if (Files.list(outputNotValidated.get).toArray.isEmpty) Files.delete(outputNotValidated.get)

          } match {
            case Success(_) => println(s"Converting has been finished. $logFileName located in ${args(0)}")
            case Failure(ex) => println(s"Error. Converting hasn`t been finished: ${ex.toString}")
          }

        case Failure(ex) =>
          println(s"Error. Converting hasn`t been finished: ${ex.toString}")
      }
    }
  }

  implicit def templateFactory(source: Node): Template = {
    Try { (source \\ "trade").head.child
    .filter {
      case v: Elem => true
      case _ => false
    }(1).label } match {
      case Success(v) if v == "repo" =>
        if (debugMode) new RepoTemplate(source) with Logger else new RepoTemplate(source)
      case Success(v) if v == "bondBasketOption" =>
        if (debugMode) new BondBasketOptionTemplate(source) with Logger else new BondBasketOptionTemplate(source)
      case Success(v) if v == "bondOption" =>
        if (debugMode) new BondOptionTemplate(source) with Logger else new BondOptionTemplate(source)
      case Success(v) if v == "equityOption" =>
        if (debugMode) new EquityOptionTemplate(source) with Logger else new EquityOptionTemplate(source)
      case Success(v) if v == "commodityOption" =>
        if (debugMode) new CommodityOptionTemplate(source) with Logger else new CommodityOptionTemplate(source)
      case Success(v) if v == "bondForward" =>
        if (debugMode) new BondForwardTemplate(source) with Logger else new BondForwardTemplate(source)
      case Success(v) if v == "equityForward" =>
        if (debugMode) new EquityForwardTemplate(source) with Logger else new EquityForwardTemplate(source)
      case Success(v) if v == "commodityForward" =>
        if (debugMode) new CommodityForwardTemplate(source) with Logger else new CommodityForwardTemplate(source)
      case Success(v) if v == "commoditySwap" =>
        if (debugMode) new CommoditySwapTemplate(source) with Logger else new CommoditySwapTemplate(source)
      case _ =>
        Try { (source \\ "markToMarketValuation").head } match {
          case Success(_) =>
            if (debugMode) new FairValueReportTemplate(source) with Logger else new FairValueReportTemplate(source)
          case _ => Try { (source \\ "masterAgreementTerms").head } match {
            case Success(_) =>
              if (debugMode) new MasterAgreementTemplate(source) with Logger else new MasterAgreementTemplate(source)
            case _ => Try { (source \\ "masterAgreementTermination").head } match {
              case Success(_) =>
                if (debugMode) new MasterAgreementTemplate(source) with Logger else new MasterAgreementTemplate(source)
              case _ => Try { (source \\ "executionStatus").head } match {
                case Success(_) =>
                  if (debugMode) new ExecutionStatusReportTemplate(source) with Logger
                  else new ExecutionStatusReportTemplate(source)
                case _ => throw new Exception("Undefined trade type")
              }
            }
          }
        }
    }
  }

  def convert(
    inputStream: InputStream,
    sName: String,
    oName: String,
    nsdScheme: XMLValid,
    rtsScheme: XMLValid,
    pp: PrettyPrinter): (Node, String) = {

    val sourceXml = XML.load(inputStream)

    val template = Template(sourceXml)
    /* temporary */
    val outputXml = XML.loadString(pp.format(template.render)
      .replace("RP0058900001", "BROKN")
      .replace("NDC000000000", "SPB01")
      .replace("НКО АО НРД", "ПАО «Санкт-Петербургская биржа»"))

    val sourceXmlValid = if (template.isStraight) nsdScheme else rtsScheme
    val outputXmlValid = if (template.isStraight) rtsScheme else nsdScheme

    val msgCheckSource = Try {
      sourceXmlValid.loadString(sourceXml.mkString)
    } match {
      case Success(_) => s"'$sName' successfully checked against schema"
      case Failure(ex) => s"'$sName' isn`t wellformed or invalid: ${ex.toString}"
    }

    val msgCheckOutput = Try {
      outputXmlValid.loadString(outputXml.mkString)
    } match {
      case Success(_) => s"'$oName' successfully checked against schema"
      case Failure(ex) => s"'$oName' isn`t wellformed or invalid: ${ex.toString}"
    }

    (outputXml, template.info(s"$msgCheckSource\n$msgCheckOutput") + "\n\n")
  }

  case class FileEntry(
    name: String,
    modified: FileTime,
    streamConvert: Option[InputStream],
    streamMove: InputStream,
    zip: Option[ZipFile],
    msg: Option[String]
  )
}
