/*
 * sbt
 * Copyright 2023, Scala center
 * Copyright 2011 - 2022, Lightbend, Inc.
 * Copyright 2008 - 2010, Mark Harrah
 * Licensed under Apache License 2.0 (see LICENSE)
 */

package sbt
package internal
package librarymanagement

import java.nio.charset.StandardCharsets.UTF_8
import java.nio.file.Files

import lmcoursier.definitions.{ Configuration, Project }
import org.apache.ivy.core.module.id.ModuleRevisionId
import Def.Setting
import sbt.Keys.{ csrProject, csrPublications, publishLocalConfiguration, publishConfiguration }
import sbt.ProjectExtra.*
import sbt.librarymanagement.PublishConfiguration
import scala.jdk.CollectionConverters.*
import scala.xml.{ Node, PrefixedAttribute }

object IvyXml {

  private def rawContent(
      currentProject: Project,
      shadedConfigOpt: Option[Configuration],
      bomForcedDeps: Seq[(String, String, String)]
  ): String = {

    // Important: width = Int.MaxValue, so that no tag gets truncated.
    // In particular, that prevents things like <foo /> to be split to
    // <foo>
    // </foo>
    // by the pretty-printer.
    // See https://github.com/sbt/sbt/issues/3412.
    val printer = new scala.xml.PrettyPrinter(Int.MaxValue, 2)

    """<?xml version="1.0" encoding="UTF-8"?>""" + '\n' +
      printer.format(content(currentProject, shadedConfigOpt, bomForcedDeps))
  }

  // These are required for publish to be fine, later on.
  private[sbt] def writeFiles(
      currentProject: Project,
      shadedConfigOpt: Option[Configuration],
      ivySbt: IvySbt,
      log: sbt.util.Logger,
      resolvedDeps: Seq[sbt.librarymanagement.ModuleID]
  ): Unit = {
    val bomForcedDeps = resolvedDeps.map(m => (m.organization, m.name, m.revision))

    val ivyCacheManager = ivySbt.withIvy(log)(ivy => ivy.getResolutionCacheManager)

    val ivyModule = ModuleRevisionId.newInstance(
      currentProject.module.organization.value,
      currentProject.module.name.value,
      currentProject.version,
      currentProject.module.attributes.asJava
    )

    val cacheIvyFile = ivyCacheManager.getResolvedIvyFileInCache(ivyModule)
    val cacheIvyPropertiesFile = ivyCacheManager.getResolvedIvyPropertiesInCache(ivyModule)

    val content0 = rawContent(currentProject, shadedConfigOpt, bomForcedDeps)
    cacheIvyFile.getParentFile.mkdirs()
    log.debug(s"writing Ivy file $cacheIvyFile")
    Files.write(cacheIvyFile.toPath, content0.getBytes(UTF_8))

    // Just writing an empty file here... Are these only used?
    cacheIvyPropertiesFile.getParentFile.mkdirs()
    Files.write(cacheIvyPropertiesFile.toPath, Array.emptyByteArray)
    ()
  }

  private def content(
      project0: Project,
      shadedConfigOpt: Option[Configuration],
      bomForcedDeps: Seq[(String, String, String)]
  ): Node = {

    val filterOutDependencies =
      shadedConfigOpt.toSet[Configuration].flatMap { shadedConfig =>
        project0.dependencies
          .collect { case (conf, dep) if conf.value == shadedConfig.value => dep }
      }

    val project: Project = project0.withDependencies(project0.dependencies.collect {
      case p @ (_, dep) if !filterOutDependencies(dep) => p
    })

    val infoAttrs =
      (project.module.attributes.toSeq ++ project.properties).foldLeft[xml.MetaData](xml.Null) {
        case (acc, (k, v)) =>
          new PrefixedAttribute("e", k, v, acc)
      }

    val licenseElems = project.info.licenses.map { (name, urlOpt) =>
      val n = <license name={name} />

      urlOpt.fold(n) { url =>
        n % <x url={url} />.attributes
      }
    }

    val descriptionElem = {
      val n = <description>{project.info.description}</description>
      if (project.info.homePage.nonEmpty)
        n % <x homepage={project.info.homePage} />.attributes
      else
        n
    }

    val infoElem = {
      <info
        organisation={project.module.organization.value}
        module={project.module.name.value}
        revision={project.version}
      >
        {licenseElems}
        {descriptionElem}
      </info>
    } % infoAttrs

    val confElems = project.configurations.toVector.collect {
      case (name, extends0) if !shadedConfigOpt.exists(_.value == name.value) =>
        val extends1 = shadedConfigOpt.fold(extends0)(c => extends0.filter(_.value != c.value))
        val n = <conf name={name.value} visibility="public" description="" />
        if (extends1.nonEmpty)
          n % <x extends={extends1.map(_.value).mkString(",")} />.attributes
        else
          n
    }

    val publications = project.publications
      .groupBy { case (_, p) => p }
      .view
      .mapValues { _.map { case (cfg, _) => cfg } }

    val publicationElems = publications.map { (pub, configs) =>
      val n =
        <artifact name={pub.name} type={pub.`type`.value} ext={pub.ext.value} conf={
          configs.map(_.value).mkString(",")
        } />

      if (pub.classifier.value.nonEmpty)
        n % <x e:classifier={pub.classifier.value} />.attributes
      else
        n
    }

    val bomForcedSet = bomForcedDeps.toSet
    val dependencyElems = project.dependencies.toVector.map { (conf, dep) =>
      val classifier = {
        val pub = dep.publication
        if (pub.classifier.value.nonEmpty)
          Seq(
            <artifact name={pub.name} type={pub.`type`.value} ext={
              pub.ext.value
            } conf="*" e:classifier={
              pub.classifier.value
            } />
          )
        else
          Seq.empty
      }

      val excludes = dep.exclusions.toSeq.map { (org, name) =>
        <exclude org={org.value} module={
          name.value
        } name="*" type="*" ext="*" conf="" matcher="exact"/>
      }

      val org0 = dep.module.organization.value
      val name0 = dep.module.name.value
      val rev0 = dep.version
      val forced = bomForcedSet((org0, name0, rev0))
      val forceAttr =
        if (forced) new scala.xml.UnprefixedAttribute("force", "true", scala.xml.Null)
        else scala.xml.Null

      val n =
        <dependency org={org0} name={name0} rev={rev0} conf={
          s"${conf.value}->${dep.configuration.value}"
        }>
          {classifier}
          {excludes}
        </dependency>

      val moduleAttrs = dep.module.attributes.foldLeft[xml.MetaData](xml.Null) {
        case (acc, (k, v)) =>
          new PrefixedAttribute("e", k, v, acc)
      }

      n % moduleAttrs % forceAttr
    }

    <ivy-module version="2.0" xmlns:e="http://ant.apache.org/ivy/extra">
      {infoElem}
      <configurations>{confElems}</configurations>
      <publications>{publicationElems}</publications>
      <dependencies>{dependencyElems}</dependencies>
    </ivy-module>
  }

  private def makeIvyXmlBefore[T](
      task: TaskKey[T],
      shadedConfigOpt: Option[Configuration]
  ): Setting[Task[T]] =
    task := Def.uncached(task.dependsOnTask {
      Def.task {
        val currentProject = {
          val proj = csrProject.value
          val publications = csrPublications.value
          proj.withPublications(publications)
        }
        val resolved = sbt.Keys.resolvedDependencies.value
        IvyXml.writeFiles(
          currentProject,
          shadedConfigOpt,
          sbt.Keys.ivySbt.value,
          sbt.Keys.streams.value.log,
          resolved
        )
      }
    }.value)

  private lazy val needsIvyXmlLocal = Seq(publishLocalConfiguration) ++ getPubConf(
    "makeIvyXmlLocalConfiguration"
  )
  private lazy val needsIvyXml = Seq(publishConfiguration) ++ getPubConf(
    "makeIvyXmlConfiguration"
  )

  private def getPubConf(method: String): List[TaskKey[PublishConfiguration]] =
    try {
      val cls = sbt.Keys.getClass
      val m = cls.getMethod(method)
      val task = m.invoke(sbt.Keys).asInstanceOf[TaskKey[PublishConfiguration]]
      List(task)
    } catch {
      case _: Throwable => // FIXME Too wide
        Nil
    }

  def generateIvyXmlSettings(
      shadedConfigOpt: Option[Configuration] = None
  ): Seq[Setting[?]] =
    (needsIvyXml ++ needsIvyXmlLocal).map(makeIvyXmlBefore(_, shadedConfigOpt))

}
