package lmcoursier.internal

import coursier.{ Resolution, Resolve }
import coursier.cache.internal.ThreadUtil
import coursier.cache.loggers.{ FallbackRefreshDisplay, ProgressBarRefreshDisplay, RefreshLogger }
import coursier.core.*
import coursier.error.ResolutionError
import coursier.error.ResolutionError.CantDownloadModule
import coursier.ivy.IvyRepository
import coursier.maven.MavenRepositoryLike
import coursier.params.rule.RuleResolution
import coursier.util.Task
import sbt.util.Logger

import scala.annotation.nowarn
import scala.concurrent.duration.FiniteDuration
import scala.collection.mutable

// private[coursier]
object ResolutionRun {

  private def resolution(
      params: ResolutionParams,
      verbosityLevel: Int,
      log: Logger,
      configs: Set[Configuration],
      startingResolutionOpt: Option[Resolution]
  ): Either[coursier.error.ResolutionError, Resolution] = {

    val isScalaToolConfig = configs(Configuration("scala-tool"))
    // Ref coursier/coursier#1340 coursier/coursier#1442
    // This treats ScalaTool as a sandbox configuration isolated from other subprojects.
    // Likely this behavior is needed only for ScalaTool configuration where the scala-xml
    // build's ScalaTool configuration transitively loops back to scala-xml's Compile artifacts.
    // In most other cases, it's desirable to allow "x->compile" relationship.
    def isSandboxConfig: Boolean = isScalaToolConfig

    val repositories =
      params.internalRepositories.drop(if (isSandboxConfig) 1 else 0) ++
        params.mainRepositories ++
        params.fallbackDependenciesRepositories

    val rules =
      params.params.rules ++ params.strictOpt.map(s => Seq((s, RuleResolution.Fail))).getOrElse(Nil)

    val printOptionalMessage = verbosityLevel >= 0 && verbosityLevel <= 1

    @nowarn
    def depsRepr(deps: Seq[(Configuration, Dependency)]) =
      deps
        .map { (config, dep) =>
          s"${dep.module}:${dep.version}:${config.value}->${dep.configuration.value}"
        }
        .sorted
        .distinct

    val initialMessage =
      Seq(
        if (verbosityLevel >= 0)
          Seq(
            s"Updating ${params.projectName}" + (if (params.sbtClassifiers) " (sbt classifiers)"
                                                 else "")
          )
        else
          Nil,
        if (verbosityLevel >= 2)
          depsRepr(params.dependencies).map(depRepr => s"  $depRepr")
        else
          Nil
      ).flatten.mkString("\n")

    if (verbosityLevel >= 2) {
      val repoReprs = repositories.map {
        case r: IvyRepository =>
          s"ivy:${r.pattern}"
        case _: InterProjectRepository =>
          "inter-project"
        case r: MavenRepositoryLike =>
          r.root
        case r =>
          // should not happen
          r.toString
      }

      log.info(
        "Repositories:\n" +
          repoReprs.map("  " + _).mkString("\n")
      )
    }

    if (verbosityLevel >= 2)
      log.info(initialMessage)

    @nowarn
    val resolveTask: Resolve[Task] = {
      Resolve()
        // re-using various caches from a resolution of a configuration we extend
        .withInitialResolution(startingResolutionOpt)
        .withDependencies(
          params.dependencies.collect {
            case (config, dep) if configs(config) =>
              dep
          }
        )
        .withBoms(params.boms)
        .withRepositories(repositories)
        .withResolutionParams(
          params.params
            .addForceVersion(
              (if (isSandboxConfig) Nil
               else params.interProjectDependencies.map(_.moduleVersion))*
            )
            .withForceScalaVersion(params.autoScalaLibOpt.nonEmpty)
            .withScalaVersionOpt(params.autoScalaLibOpt.map(_._2))
            .withTypelevel(params.params.typelevel)
            .withRules(rules)
        )
        .withCache(
          params.cache
            .withLogger(
              params.loggerOpt.getOrElse {
                RefreshLogger.create(
                  if (RefreshLogger.defaultFallbackMode)
                    new FallbackRefreshDisplay()
                  else
                    ProgressBarRefreshDisplay.create(
                      if (printOptionalMessage) log.info(initialMessage),
                      if (printOptionalMessage || verbosityLevel >= 2)
                        log.info(s"Resolved ${params.projectName} dependencies")
                    )
                )
              }
            )
        )
    }

    val (period, maxAttempts) = params.retry
    val finalResult: Either[ResolutionError, Resolution] = {

      def retry(
          attempt: Int,
          waitOnError: FiniteDuration
      ): Task[Either[ResolutionError, Resolution]] =
        resolveTask.io.attempt
          .flatMap {
            case Left(e: ResolutionError) =>
              if (isTransientResolutionError(e))
                if (attempt + 1 >= maxAttempts) {
                  log.error(s"Failed, maximum iterations ($maxAttempts) reached")
                  Task.point(Left(e))
                } else {
                  log.warn(s"Attempt ${attempt + 1} failed: $e")
                  Task.completeAfter(retryScheduler, waitOnError).flatMap { _ =>
                    retry(attempt + 1, waitOnError * 2)
                  }
                }
              else
                Task.point(Left(e))
            case Left(ex) =>
              Task.fail(ex)
            case Right(value) =>
              Task.point(Right(value))
          }

      retry(0, period).unsafeRun()(using resolveTask.cache.ec)
    }

    finalResult match {
      case Left(err) if params.missingOk => Right(err.resolution)
      case others                        => others
    }
  }

  @nowarn
  def resolutions(
      params: ResolutionParams,
      verbosityLevel: Int,
      log: Logger
  ): Either[coursier.error.ResolutionError, Map[Configuration, Resolution]] = {

    // TODO Warn about possible duplicated modules from source repositories?

    if (verbosityLevel >= 2) {
      log.info("InterProjectRepository")
      for (p <- params.interProjectDependencies)
        log.info(s"  ${p.module}:${p.version}")
    }

    SbtCoursierCache.default.resolutionOpt(params.resolutionKey).map(Right(_)).getOrElse {
      val resOrError =
        Lock.maybeSynchronized(needsLock =
          Lock.progressBarActive(
            hasCustomLogger = params.loggerOpt.nonEmpty,
            fallbackMode = RefreshLogger.defaultFallbackMode
          )
        ) {
          val map = new mutable.HashMap[Configuration, Resolution]
          val either = params.orderedConfigs.foldLeft[Either[coursier.error.ResolutionError, Unit]](
            Right(())
          ) { case (acc, (config, extends0)) =>
            for {
              _ <- acc
              initRes = {
                val it = extends0.iterator.flatMap(map.get(_).iterator)
                if (it.hasNext) Some(it.next())
                else None
              }
              allExtends = params.allConfigExtends.getOrElse(config, Set.empty)
              res <- resolution(params, verbosityLevel, log, allExtends, initRes)
            } yield {
              map += config -> res
              ()
            }
          }
          either.map(_ => map.toMap)
        }
      for (res <- resOrError)
        SbtCoursierCache.default.putResolution(params.resolutionKey, res)
      resOrError
    }
  }

  def resolutionsWithLockFile(
      params: ResolutionParams,
      verbosityLevel: Int,
      log: Logger,
      lockFileOpt: Option[java.io.File],
      scalaVersion: Option[String]
  ): Either[coursier.error.ResolutionError, (Map[Configuration, Resolution], Boolean)] = {
    resolutionsWithLockFileData(params, verbosityLevel, log, lockFileOpt, scalaVersion)
      .map { case (res, lockDataOpt) => (res, lockDataOpt.isDefined) }
  }

  def resolutionsWithLockFileData(
      params: ResolutionParams,
      verbosityLevel: Int,
      log: Logger,
      lockFileOpt: Option[java.io.File],
      scalaVersion: Option[String]
  ): Either[
    coursier.error.ResolutionError,
    (Map[Configuration, Resolution], Option[LockFileData])
  ] = {
    lockFileOpt
      .flatMap { lockFile =>
        LockFile.read(lockFile) match {
          case Right(lockData) =>
            if (
              BuildClock.matches(
                lockData,
                params.dependencies,
                params.mainRepositories,
                scalaVersion,
                params
              )
            ) {
              if (verbosityLevel >= 1) {
                log.info(s"Using lock file: ${lockFile.getAbsolutePath}")
              }
              val reconstructed = ResolutionSerializer.reconstructResolutions(lockData, params)
              Some(Right((reconstructed, Some(lockData))))
            } else {
              if (verbosityLevel >= 1) {
                log.info(s"Lock file outdated, performing resolution")
              }
              None
            }
          case Left(err) =>
            if (verbosityLevel >= 2) {
              log.debug(s"Lock file error: $err")
            }
            None
        }
      }
      .getOrElse {
        resolutions(params, verbosityLevel, log).map(res => (res, None))
      }
  }

  private lazy val retryScheduler = ThreadUtil.fixedScheduledThreadPool(1)

  private[internal] def isTransientResolutionError(e: ResolutionError): Boolean =
    e.errors.exists {
      case err: CantDownloadModule => isTimeout(err) || isServerError(err)
      case _                       => false
    }

  private def isTimeout(err: CantDownloadModule): Boolean =
    err.perRepositoryErrors.exists(_.contains("Connection timed out"))

  private def isServerError(err: CantDownloadModule): Boolean =
    err.perRepositoryErrors.exists(_.contains("Server returned HTTP response code: 5"))
}
