/* See LICENSE for licensing and NOTICE for copyright. */
package org.ldaptive.auth.ext;

import java.time.ZonedDateTime;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.security.auth.login.AccountException;
import javax.security.auth.login.AccountExpiredException;
import javax.security.auth.login.AccountLockedException;
import javax.security.auth.login.AccountNotFoundException;
import javax.security.auth.login.CredentialExpiredException;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;
import org.ldaptive.auth.AccountState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Represents the state of an Active Directory account. Note that the warning returned by this implementation always
 * returns -1 for logins remaining.
 *
 * @author  Middleware Services
 */
public class ActiveDirectoryAccountState extends AccountState
{


  /**
   * Enum to define active directory errors. See http://ldapwiki.willeke.com/wiki/
   * Common%20Active%20Directory%20Bind%20Errors
   */
  public enum Error implements AccountState.Error {

    /** no such user. 0x525. */
    NO_SUCH_USER(1317),

    /** logon failure. 0x52e. */
    LOGON_FAILURE(1326),

    /** invalid logon hours. 0x530. */
    INVALID_LOGON_HOURS(1328),

    /** invalid workstation. 0x531. */
    INVALID_WORKSTATION(1329),

    /** password expired. 0x532. */
    PASSWORD_EXPIRED(1330),

    /** account disabled. 0x533. */
    ACCOUNT_DISABLED(1331),

    /** account expired. 0x701. */
    ACCOUNT_EXPIRED(1793),

    /** password must change. 0x773. */
    PASSWORD_MUST_CHANGE(1907),

    /** account locked out. 0x775. */
    ACCOUNT_LOCKED_OUT(1909);

    /** hex radix for hex to decimal conversion. */
    private static final int HEX_RADIX = 16;

    /** pattern to find hex code in active directory messages. */
    private static final Pattern PATTERN = Pattern.compile("data (\\w+)");

    /** underlying error code. */
    private final int code;


    /**
     * Creates a new active directory error.
     *
     * @param  i  error code
     */
    Error(final int i)
    {
      code = i;
    }


    @Override
    public int getCode()
    {
      return code;
    }


    @Override
    public String getMessage()
    {
      return name();
    }


    @Override
    public void throwSecurityException()
      throws LoginException
    {
      switch (this) {

      case NO_SUCH_USER:
        throw new AccountNotFoundException(name());

      case LOGON_FAILURE:
        throw new FailedLoginException(name());

      case INVALID_LOGON_HOURS:

      case ACCOUNT_DISABLED:

      case ACCOUNT_LOCKED_OUT:
        throw new AccountLockedException(name());

      case INVALID_WORKSTATION:
        throw new AccountException(name());

      case PASSWORD_EXPIRED:

      case PASSWORD_MUST_CHANGE:
        throw new CredentialExpiredException(name());

      case ACCOUNT_EXPIRED:
        throw new AccountExpiredException(name());

      default:
        throw new IllegalStateException("Unknown active directory error: " + this);
      }
    }


    /**
     * Returns the error for the supplied integer constant.
     *
     * @param  code  to find error for
     *
     * @return  error
     */
    public static Error valueOf(final int code)
    {
      for (Error e : Error.values()) {
        if (e.getCode() == code) {
          return e;
        }
      }
      return null;
    }


    /**
     * Parses the supplied error messages and returns the corresponding error enum. Attempts to find {@link #PATTERN}
     * and parses the first group match as a hexadecimal integer.
     *
     * @param  message  to parse
     *
     * @return  active directory error
     */
    public static Error parse(final String message)
    {
      if (message != null) {
        final Matcher matcher = PATTERN.matcher(message);
        if (matcher.find()) {
          try {
            return Error.valueOf(Integer.parseInt(matcher.group(1).toUpperCase(), HEX_RADIX));
          } catch (NumberFormatException e) {
            final Logger l = LoggerFactory.getLogger(Error.class);
            l.warn("Error parsing active directory error", e);
          }
        }
      }
      return null;
    }
  }

  /** active directory specific enum. */
  private final Error adError;


  /**
   * Creates a new active directory account state.
   *
   * @param  exp  account expiration
   */
  public ActiveDirectoryAccountState(final ZonedDateTime exp)
  {
    super(new AccountState.DefaultWarning(exp, -1));
    adError = null;
  }


  /**
   * Creates a new active directory account state.
   *
   * @param  error  containing authentication failure details
   */
  public ActiveDirectoryAccountState(final ActiveDirectoryAccountState.Error error)
  {
    super(error);
    adError = error;
  }


  /**
   * Returns the active directory error for this account state.
   *
   * @return  active directory error
   */
  public ActiveDirectoryAccountState.Error getActiveDirectoryError()
  {
    return adError;
  }
}
