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

import scala.xml._
import java.nio.file.{Files, Paths, StandardOpenOption}
import java.io.{File, FileInputStream, InputStream}
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 files = new File(args(0))
        .listFiles
        .filter {
          !_.isDirectory
        }

      val start = LocalDateTime.now.toString

      val sourceNameValidated = s"${args(0)}/${start}_validated"
      val sourceValidated = new File(sourceNameValidated)
      val sourceNameNotValidated = s"${args(0)}/${start}_not_validated"
      val sourceNotValidated = new File(sourceNameNotValidated)
      val sourceNameError = s"${args(0)}/${start}_error"
      val sourceError = new File(sourceNameError)

      val outputNameValidated = s"${args(1)}/${start}_validated"
      val outputValidated = new File(outputNameValidated)
      val outputNameNotValidated = s"${args(1)}/${start}_not_validated"
      val outputNotValidated = new File(outputNameNotValidated)

      if (sourceValidated.mkdir
        && sourceNotValidated.mkdir
        && sourceError.mkdir
        && outputValidated.mkdir
        && outputNotValidated.mkdir) {

        val sourceArray = {
          for (f <- files) yield {
            val name = f.getName
            val ext = name.split('.').last.toLowerCase

            if (ext == "zip")
              Try {
                val zip = new ZipFile(f)

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


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

            f.msg match {
              case Some(msg) =>
                logString += s"'$now'\nImpossible to convert '${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))
                }
            }
          }

          //files.foreach { _.delete }

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

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

          if (sourceNotValidated.listFiles.isEmpty) sourceNotValidated.delete
          if (sourceError.listFiles.isEmpty) sourceError.delete
          if (outputValidated.listFiles.isEmpty) outputValidated.delete
          if (outputNotValidated.listFiles.isEmpty) outputNotValidated.delete

        } 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}")
        }

      } else
        println(s"Error. Converting hasn`t been finished: unable to create directories to move files.")
    }
  }

  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: Long,
    streamConvert: Option[InputStream],
    streamMove: InputStream,
    msg: Option[String]
  )
}
