/*
 * Copyright (c) 2013 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.cloud.backend.spi;

import com.google.api.server.spi.response.UnauthorizedException;
import com.google.appengine.api.NamespaceManager;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.memcache.MemcacheService;
import com.google.appengine.api.memcache.MemcacheServiceFactory;
import com.google.appengine.api.users.User;
import com.google.apphosting.api.ApiProxy;
import com.google.cloud.backend.beans.EntityDto;
import com.google.cloud.backend.config.BackendConfigManager;
import com.google.cloud.backend.config.BackendConfigManager.AuthMode;
import com.google.cloud.backend.config.CloudEndpointsConfigManager;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * Manager class that provides utility methods for access control on CloudEntities.
 */
public class SecurityChecker {

  public static final String KIND_NAME_USERS = "_Users";

  public static final String USERS_PROP_USERID = "userId";

  public static final String USER_ID_PREFIX = "USER:";

  public static final String USER_ID_FOR_ANONYMOUS = USER_ID_PREFIX + "<anonymous>";

  public static final String NAMESPACE_DEFAULT = ""; // empty namespace

  public static final String KIND_PREFIX_PRIVATE = "[private]";

  public static final String KIND_PREFIX_PUBLIC = "[public]";

  private static final SecurityChecker _instance = new SecurityChecker();

  /**
   * Returns the Singleton instance.
   */
  public static SecurityChecker getInstance() {
    return _instance;
  }

  private static final DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

  private static final MemcacheService memcache = MemcacheServiceFactory.getMemcacheService();

  private static final BackendConfigManager backendConfigManager = new BackendConfigManager();

  private static final Map<String, String> userIdCache = new HashMap<String, String>();

  private SecurityChecker() {
  }

  /**
   * Checks if the user is allowed to use the backend. The method throws
   * {@link UnauthorizedException} if the backend is locked down or if the user
   * is null and the authentication through Client IDs is enabled.
   *
   * @param user
   *          {@link User} on behalf of which the call is made from the client.
   * @throws UnauthorizedException
   *           if the call is not authenticated because of the status of the
   *           authMode or the User.
   */
  protected void checkIfUserIsAvailable(User user) throws UnauthorizedException {

    AuthMode authMode = backendConfigManager.getAuthMode();
    switch (authMode) {
    case OPEN: // no check
      return;
    case CLIENT_ID: // error if User is null
      if (user == null) {
        throw new UnauthorizedException("Unauthenticated calls are not allowed");
      } else {
        return;
      }
    case LOCKED: // always error
    default:
      throw new UnauthorizedException("The backend is locked down. The administrator can change "
          + "the authentication/authorization settings on https://" + getHostname() + "/");
    }
  }

  private String getHostname() {
    return (String) ApiProxy.getCurrentEnvironment().getAttributes()
        .get("com.google.appengine.runtime.default_version_hostname");
  }

  /**
   * Checks ACL of the specified CloudEntity to see if the specified user can
   * write on it.
   *
   * @param e
   *          {@link Entity} of CloudEntity
   * @param user
   *          User object representing the caller.
   * @throws UnauthorizedException
   *           if the user does not have permission to write on the entity
   */
  protected void checkAclForWrite(Entity e, User user) throws UnauthorizedException {

    // get ACL
    String userId = getUserId(user);
    String ownerId = (String) e.getProperty(EntityDto.PROP_OWNER);

    // check ACL
    boolean isOwner = userId.equals(ownerId);
    boolean isPublic = e.getKind().startsWith(KIND_PREFIX_PUBLIC);
    boolean isWritable = isOwner || isPublic;
    if (!isWritable) {
      String id = e.getKey().getName();
      throw new UnauthorizedException("Insuffient permission for updating a CloudEntity: " + id
          + " by: " + userId);
    }
  }

  /**
   * Sets default security properties on the specified {@link EntityDto}.
   *
   * @param cd
   *          {@link EntityDto}
   * @param user
   *          {@link User} of the creator of CloudEntity
   */
  protected void setDefaultSecurityProps(EntityDto cd, User user) {

    // set createdBy and updatedBy
    if (user != null) {
      cd.setCreatedBy(user.getEmail());
      cd.setUpdatedBy(user.getEmail());
    }

    // set owner
    cd.setOwner(getUserId(user));
  }

  /**
   * Creates a {@link Key} from the specified kindName and CloudEntity id. If
   * the kindName has _private suffix, the key will be created under a namespace
   * for the specified {@link User}.
   *
   * @param kindName
   *          Name of kind
   * @param id
   *          CloudEntity id
   * @param user
   *          {@link User} of the requestor
   * @return {@link Key}
   */
  public Key createKeyWithNamespace(String kindName, String id, User user) {

    // save the original namespace
    String origNamespace = NamespaceManager.get();

    // set namespace based on the kind suffix
    if (kindName.startsWith(SecurityChecker.KIND_PREFIX_PRIVATE)) {
      String userId = getUserId(user);
      NamespaceManager.set(userId);
    } else {
      NamespaceManager.set(SecurityChecker.NAMESPACE_DEFAULT);
    }

    // create a key
    Key k = KeyFactory.createKey(kindName, id);

    // restore the original namespace
    NamespaceManager.set(origNamespace);
    return k;
  }

  /**
   * Creates {@link Query} from the specified kindName. If the kindName has
   * _private suffix, the key will be created under a namespace for the
   * specified {@link User}.
   *
   * @param kindName
   *          Name of kind
   * @param user
   *          {@link User} of the requestor
   * @return {@link Query}
   */
  public Query createKindQueryWithNamespace(String kindName, User user) {

    // save the original namespace
    String origNamespace = NamespaceManager.get();

    // set namespace based on the kind suffix
    if (kindName.startsWith(KIND_PREFIX_PRIVATE)) {
      String userId = getUserId(user);
      NamespaceManager.set(userId);
    } else {
      NamespaceManager.set(NAMESPACE_DEFAULT);
    }

    // create a key
    Query q = new Query(kindName);

    // restore the original namespace
    NamespaceManager.set(origNamespace);
    return q;
  }

  private String getUserId(User u) {

    // check if User is available
    if (u == null) {
      return USER_ID_FOR_ANONYMOUS;
    }

    // check if valid email is available
    String email = u.getEmail();
    if (email == null || email.trim().length() == 0) {
      throw new IllegalArgumentException("Illegal email: " + email);
    }

    // try to find it on local cache
    String memKey = USER_ID_PREFIX + email;
    String id = userIdCache.get(memKey);
    if (id != null) {
      return id;
    }

    // try to find it on memcache
    id = (String) memcache.get(memKey);
    if (id != null) {
      userIdCache.put(memKey, id);
      return id;
    }

    // create a key to find the user on Datastore
    String origNamespace = NamespaceManager.get();
    NamespaceManager.set(NAMESPACE_DEFAULT);
    Key key = KeyFactory.createKey(KIND_NAME_USERS, email);
    NamespaceManager.set(origNamespace);

    // try to find it on Datastore
    Entity e;
    try {
      e = datastore.get(key);
      id = (String) e.getProperty(USERS_PROP_USERID);
    } catch (EntityNotFoundException ex) {
      // when the user has not been registered
      e = new Entity(key);
      id = USER_ID_PREFIX + UUID.randomUUID().toString();
      e.setProperty(USERS_PROP_USERID, id);
      datastore.put(e);
    }

    // put the user on memcache and local cache
    userIdCache.put(memKey, id);
    memcache.put(memKey, id);
    return id;
  }

  /**
   * Checks if the specified kind name is not one of system configuration kinds
   * and is allowed to access.
   *
   * @param kindName
   * @throws IllegalArgumentException
   *           if the kind name is not accessible
   */
  public void checkIfKindNameAccessible(String kindName) {
    if (BackendConfigManager.CONFIGURATION_ENTITY_KIND.equals(kindName)
        || CloudEndpointsConfigManager.ENDPOINT_CONFIGURATION_KIND.equals(kindName)
        || BlobMetadata.ENTITY_KIND.equals(kindName)) {
      throw new IllegalArgumentException("save/saveAll: the kind name is not allowed to access: "
          + kindName);
    }
  }

}