package sbt
package ivyint

import java.lang.reflect.Field
import java.lang.reflect.Method
import org.apache.ivy.util.Credentials
import org.apache.ivy.util.Message
import org.apache.ivy.util.url.IvyAuthenticator
import org.apache.ivy.util.url.CredentialsStore

 * Helper to install an Authenticator that works with the IvyAuthenticator to provide better error messages when
 * credentials don't line up.
object ErrorMessageAuthenticator {
  private var securityWarningLogged = false

  private def originalAuthenticator: Option[Authenticator] = {
    try {
      val f = classOf[Authenticator].getDeclaredField("theAuthenticator")
    } catch {
      // TODO - Catch more specific errors.
      case t: Throwable =>
        Message.debug("Error occurred while getting the original authenticator: " + t.getMessage)

  private lazy val ivyOriginalField = {
    val field = classOf[IvyAuthenticator].getDeclaredField("original")
  // Attempts to get the original authenticator form the ivy class or returns null.
  private def installIntoIvy(ivy: IvyAuthenticator): Option[Authenticator] = {
    // Here we install ourselves as the IvyAuthenticator's default so we get called AFTER Ivy has a chance to run.
    def installIntoIvyImpl(original: Option[Authenticator]): Unit = {
      val newOriginal = new ErrorMessageAuthenticator(original)
      ivyOriginalField.set(ivy, newOriginal)

    try Option(ivyOriginalField.get(ivy).asInstanceOf[Authenticator]) match {
      case Some(alreadyThere: ErrorMessageAuthenticator) => // We're already installed, no need to do the work again.
      case originalOpt                                   => installIntoIvyImpl(originalOpt)
    } catch {
      case t: Throwable =>
        Message.debug("Error occurred while trying to install debug messages into Ivy Authentication" + t.getMessage)

  /** Installs the error message authenticator so we have nicer error messages when using java's URL for downloading. */
  def install(): Unit = {
    // Actually installs the error message authenticator.
    def doInstall(original: Option[Authenticator]): Unit =
      try Authenticator.setDefault(new ErrorMessageAuthenticator(original))
      catch {
        case e: SecurityException if !securityWarningLogged =>
          securityWarningLogged = true
          Message.warn("Not enough permissions to set the ErorrMessageAuthenticator. "
            + "Helpful debug messages disabled!");
    // We will try to use the original authenticator as backup authenticator.
    // Since there is no getter available, so try to use some reflection to
    // obtain it. If that doesn't work, assume there is no original authenticator
    def doInstallIfIvy(original: Option[Authenticator]): Unit =
      original match {
        case Some(installed: ErrorMessageAuthenticator) => // Ignore, we're already installed
        case Some(ivy: IvyAuthenticator)                => installIntoIvy(ivy)
        case original                                   => doInstall(original)
 * An authenticator which just delegates to a previous authenticator and issues *nice*
 * error messages on failure to find credentials.
 * Since ivy installs its own credentials handler EVERY TIME it resolves or publishes, we want to
 * install this one at some point and eventually ivy will capture it and use it.
private[sbt] final class ErrorMessageAuthenticator(original: Option[Authenticator]) extends Authenticator {

  protected override def getPasswordAuthentication(): PasswordAuthentication = {
    // We're guaranteed to only get here if Ivy's authentication fails
    if (!isProxyAuthentication) {
      val host = getRequestingHost
      // TODO - levenshtein distance "did you mean" message.
      Message.error(s"Unable to find credentials for [${getRequestingPrompt} @ ${host}].")
      val configuredRealms = IvyCredentialsLookup.realmsForHost.getOrElse(host, Set.empty)
      if (configuredRealms.nonEmpty) {
        Message.error(s"  Is one of these realms misspelled for host [${host}]:")
        configuredRealms foreach { realm =>
          Message.error(s"  * ${realm}")
    // TODO - Maybe we should work on a helpful proxy message...

    // TODO - To be more maven friendly, we may want to also try to grab the "first" authentication that shows up for a server and try it.
    //        or maybe allow that behavior to be configured, since maven users aren't used to realms (which they should be).

    // Grabs the authentication that would have been provided had we not been installed...
    def originalAuthentication: Option[PasswordAuthentication] = {
      try Option(Authenticator.requestPasswordAuthentication(
      finally Authenticator.setDefault(this)

   * Returns true if this authentication if for a proxy and not for an HTTP server.
   *  We want to display different error messages, depending.
  private def isProxyAuthentication: Boolean =
    getRequestorType == Authenticator.RequestorType.PROXY