/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Portions Copyright 2011-2015 ForgeRock AS.
 *      Portions Copyright 2014 ForgeRock AS
 */
package org.opends.server.api;

import java.util.List;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ConditionResult;
import org.forgerock.opendj.ldap.GeneralizedTime;
import org.forgerock.opendj.ldap.ResultCode;
import org.opends.server.core.DirectoryServer;
import org.opends.server.types.*;

import static org.opends.messages.CoreMessages.*;
import static org.opends.server.config.ConfigConstants.*;
import static org.opends.server.util.StaticUtils.*;

/**
 * The authentication policy context associated with a user's entry, which is
 * responsible for managing the user's account, their password, as well as
 * authenticating the user.
 */
public abstract class AuthenticationPolicyState
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();



  /**
   * Returns the authentication policy state for the user provided user. This
   * method is equivalent to the following:
   *
   * <pre>
   * AuthenticationPolicy policy = AuthenticationPolicy.forUser(userEntry,
   *     useDefaultOnError);
   * AuthenticationPolicyState state = policy
   *     .createAuthenticationPolicyState(userEntry);
   * </pre>
   *
   * See the documentation of {@link AuthenticationPolicy#forUser} for a
   * description of the algorithm used to find a user's authentication policy.
   *
   * @param userEntry
   *          The user entry.
   * @param useDefaultOnError
   *          Indicates whether the server should fall back to using the default
   *          password policy if there is a problem with the configured policy
   *          for the user.
   * @return The password policy for the user.
   * @throws DirectoryException
   *           If a problem occurs while attempting to determine the password
   *           policy for the user.
   * @see AuthenticationPolicy#forUser(Entry, boolean)
   */
  public static AuthenticationPolicyState forUser(final Entry userEntry,
      final boolean useDefaultOnError) throws DirectoryException
  {
    final AuthenticationPolicy policy = AuthenticationPolicy.forUser(userEntry,
        useDefaultOnError);
    return policy.createAuthenticationPolicyState(userEntry);
  }



  /**
   * A utility method which may be used by implementations in order to obtain
   * the value of the specified attribute from the provided entry as a boolean.
   *
   * @param entry
   *          The entry whose attribute is to be parsed as a boolean.
   * @param attributeType
   *          The attribute type whose value should be parsed as a boolean.
   * @return The attribute's value represented as a ConditionResult value, or
   *         ConditionResult.UNDEFINED if the specified attribute does not exist
   *         in the entry.
   * @throws DirectoryException
   *           If the value cannot be decoded as a boolean.
   */
  protected static ConditionResult getBoolean(final Entry entry,
      final AttributeType attributeType) throws DirectoryException
  {
    final List<Attribute> attrList = entry.getAttribute(attributeType);
    if (attrList != null)
    {
      for (final Attribute a : attrList)
      {
        if (a.isEmpty())
        {
          continue;
        }

        final String valueString = toLowerCase(a.iterator().next().toString());

        if (valueString.equals("true") || valueString.equals("yes")
            || valueString.equals("on") || valueString.equals("1"))
        {
          if (logger.isTraceEnabled())
          {
            logger.trace("Attribute %s resolves to true for user entry %s",
                attributeType.getNameOrOID(), entry.getName());
          }

          return ConditionResult.TRUE;
        }

        if (valueString.equals("false") || valueString.equals("no")
            || valueString.equals("off") || valueString.equals("0"))
        {
          if (logger.isTraceEnabled())
          {
            logger.trace("Attribute %s resolves to false for user entry %s",
                attributeType.getNameOrOID(), entry.getName());
          }

          return ConditionResult.FALSE;
        }

        if (logger.isTraceEnabled())
        {
          logger.trace("Unable to resolve value %s for attribute %s "
              + "in user entry %s as a Boolean.", valueString,
              attributeType.getNameOrOID(), entry.getName());
        }

        final LocalizableMessage message = ERR_PWPSTATE_CANNOT_DECODE_BOOLEAN
            .get(valueString, attributeType.getNameOrOID(), entry.getName());
        throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
            message);
      }
    }

    if (logger.isTraceEnabled())
    {
      logger.trace("Returning %s because attribute %s does not exist "
          + "in user entry %s", ConditionResult.UNDEFINED,
          attributeType.getNameOrOID(), entry.getName());
    }

    return ConditionResult.UNDEFINED;
  }



  /**
   * A utility method which may be used by implementations in order to obtain
   * the value of the specified attribute from the provided entry as a time in
   * generalized time format.
   *
   * @param entry
   *          The entry whose attribute is to be parsed as a boolean.
   * @param attributeType
   *          The attribute type whose value should be parsed as a generalized
   *          time value.
   * @return The requested time, or -1 if it could not be determined.
   * @throws DirectoryException
   *           If a problem occurs while attempting to decode the value as a
   *           generalized time.
   */
  protected static long getGeneralizedTime(final Entry entry,
      final AttributeType attributeType) throws DirectoryException
  {
    long timeValue = -1;

    final List<Attribute> attrList = entry.getAttribute(attributeType);
    if (attrList != null)
    {
      for (final Attribute a : attrList)
      {
        if (a.isEmpty())
        {
          continue;
        }

        final ByteString v = a.iterator().next();
        try
        {
          timeValue = GeneralizedTime.valueOf(v.toString()).getTimeInMillis();
        }
        catch (final Exception e)
        {
          logger.traceException(e, "Unable to decode value %s for attribute %s in user entry %s",
              v, attributeType.getNameOrOID(), entry.getName());

          final LocalizableMessage message = ERR_PWPSTATE_CANNOT_DECODE_GENERALIZED_TIME
              .get(v, attributeType.getNameOrOID(), entry.getName(), e);
          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
              message, e);
        }
        break;
      }
    }

    if (timeValue == -1 && logger.isTraceEnabled())
     {
      logger.trace("Returning -1 because attribute %s does not "
          + "exist in user entry %s", attributeType.getNameOrOID(), entry.getName());
    }

    return timeValue;
  }



  /**
   * A boolean indicating whether or not the account associated with this
   * authentication state has been administratively disabled.
   */
  protected ConditionResult isDisabled = ConditionResult.UNDEFINED;

  /**
   * The user entry associated with this authentication policy state.
   */
  protected final Entry userEntry;



  /**
   * Creates a new abstract authentication policy context.
   *
   * @param userEntry
   *          The user's entry.
   */
  protected AuthenticationPolicyState(final Entry userEntry)
  {
    this.userEntry = userEntry;
  }



  /**
   * Performs any finalization required after a bind operation has completed.
   * Implementations may perform internal operations in order to persist
   * internal state to the user's entry if needed.
   *
   * @throws DirectoryException
   *           If a problem occurs during finalization.
   */
  public void finalizeStateAfterBind() throws DirectoryException
  {
    // Do nothing by default.
  }



  /**
   * Returns the authentication policy associated with this state.
   *
   * @return The authentication policy associated with this state.
   */
  public abstract AuthenticationPolicy getAuthenticationPolicy();



  /**
   * Returns {@code true} if this authentication policy state is associated with
   * a user whose account has been administratively disabled.
   * <p>
   * The default implementation is use the value of the "ds-pwp-account-disable"
   * attribute in the user's entry.
   *
   * @return {@code true} if this authentication policy state is associated with
   *         a user whose account has been administratively disabled.
   */
  public boolean isDisabled()
  {
    final AttributeType type = DirectoryServer.getAttributeTypeOrDefault(OP_ATTR_ACCOUNT_DISABLED);
    try
    {
      isDisabled = getBoolean(userEntry, type);
    }
    catch (final Exception e)
    {
      logger.traceException(e, "User %s is considered administratively "
          + "disabled because an error occurred while "
          + "attempting to make the determination.", userEntry.getName());

      isDisabled = ConditionResult.TRUE;
      return true;
    }

    if (isDisabled == ConditionResult.UNDEFINED)
    {
      isDisabled = ConditionResult.FALSE;
      if (logger.isTraceEnabled())
      {
        logger.trace("User %s is not administratively disabled since "
            + "the attribute \"%s\" is not present in the entry.", userEntry.getName(), OP_ATTR_ACCOUNT_DISABLED);
      }
      return false;
    }

    final boolean result = isDisabled == ConditionResult.TRUE;
    if (logger.isTraceEnabled())
    {
      logger.trace("User %s is%s administratively disabled.", userEntry.getName(),
          result ? "" : " not");
    }
    return result;
  }



  /**
   * Returns {@code true} if this authentication policy state is associated with
   * a password policy and the method {@link #getAuthenticationPolicy} will
   * return a {@code PasswordPolicy}.
   *
   * @return {@code true} if this authentication policy state is associated with
   *         a password policy, otherwise {@code false}.
   */
  public boolean isPasswordPolicy()
  {
    return getAuthenticationPolicy().isPasswordPolicy();
  }



  /**
   * Returns {@code true} if the provided password value matches any of the
   * user's passwords.
   *
   * @param password
   *          The user-provided password to verify.
   * @return {@code true} if the provided password value matches any of the
   *         user's passwords.
   * @throws DirectoryException
   *           If verification unexpectedly failed.
   */
  public abstract boolean passwordMatches(ByteString password)
      throws DirectoryException;
}
