package com.oguerreiro.resilient.security.custom;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import com.oguerreiro.resilient.security.AuthoritiesConstants;
import com.oguerreiro.resilient.security.SecurityUtils;
import com.oguerreiro.resilient.security.resillient.SecurityGroup;
import com.oguerreiro.resilient.security.resillient.SecurityGroupPermission;
import com.oguerreiro.resilient.security.resillient.SecurityPermission;
import com.oguerreiro.resilient.service.AbstractResilientService;
import com.oguerreiro.resilient.web.rest.AbstractResilientResource;

/**
 * For this to work, an Aspect must run before @PreAuthorize.
 * Use this in methods like this:
 * 
 * <pre>
 * {@literal @}PreAuthorize("{@literal @}resilientSecurity.canAccess()")
 * </pre>
 * 
 * OR
 * 
 * <pre>
 * {@literal @}PreAuthorize("{@literal @}resilientSecurity.canAccess('CUSTOM_AUTHORITY')")
 * </pre>
 * 
 * @see ResilientSecurityContextAspect
 */
@Component
public class ResilientSecurity {
  protected final Logger log = LoggerFactory.getLogger(this.getClass());

  /**
   * Resilient security annotation that forces a base permission type of {@link ResilientBasePermission#CUSTOM}, and
   * supplies a customPermission code to evaluate user permissions.
   * 
   * Usage:
   * 
   * <pre>
   * {@literal @}PreAuthorize("{@literal @}resilientSecurity.canAccess('CUSTOM_AUTHORITY')")
   * </pre>
   * 
   * @param  customPermission
   * @return
   */
  public boolean canAccess(String customPermission) {
    return this.internalCanAccess(customPermission, null);
  }

  /**
   * Resilient security annotation that forces a base permission type of {@link ResilientBasePermission#CUSTOM}, and
   * supplies a customPermission code to evaluate user permissions.
   * 
   * Usage:
   * 
   * <pre>
   * {@literal @}PreAuthorize("{@literal @}resilientSecurity.canAccess('CUSTOM_AUTHORITY')")
   * </pre>
   * 
   * @param  customPermission
   * @return
   */
  public boolean canAccess(String customPermission, String customResource) {
    return this.internalCanAccess(customPermission, customResource);
  }

  /**
   * Resilient security annotation that infer's the base permission type of {@link ResilientBasePermission permission
   * type} from the {@link ResilientSecurityContext}.
   * 
   * Usage:
   * 
   * <pre>
   * {@literal @}PreAuthorize("{@literal @}resilientSecurity.canAccess()")
   * </pre>
   * 
   * In a simply way, this automatically sets the {@link ResilientBasePermission} for the secured
   * {@link AbstractResilientResource} method names.
   * 
   * @param  customPermission
   * @return
   */
  public boolean canAccess() {
    return this.internalCanAccess(null, null);
  }

  private boolean internalCanAccess(String customPermission, String customResource) {
    ResilientSecurityContext targetInvocationContext = ResilientSecurityContextAspect.getCurrentTargetInstance();
    if (targetInvocationContext == null) {
      // Must have the invocation context. The secured method and its class instance. Without it, can't evaluate security,
      log.error("Unauthorized. No ResilientSecurityContext was provided.");
      return false;
    }

    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    if (authentication == null) {
      // Authentication must exist.
      log.error("Unauthorized access to . Authentication doesn't exist in the context.");
      return false;
    } else if (!authentication.isAuthenticated()) {
      // And must be authenticated
      log.error("Unauthorized. Authentication is NOT authenticated.");
      return false;
    }

    //Distinguish the target instance
    Object targetInstance = targetInvocationContext.getTargetInstance();
    if (targetInstance == null) {
      // Must have the targetInstance filled in the invocation context. Something is WRONG in this request.
      // TODO : Log this event
      return false;
    }

    // Admin user doesn't need a SecurityGroup and Have access to EVERYTHING
    if (SecurityUtils.hasCurrentUserThisAuthority(AuthoritiesConstants.ADMIN)) {
      return true;
    }

    // Must have a SecurityGroup assigned
    SecurityGroup group = SecurityUtils.getCurrentUserSecurityGroup();
    if (group == null) {
      return false;
    }

    // Calculate user permission allowance
    SecurityPermission allowedPermission = SecurityPermission.NONE;
    if (targetInstance instanceof AbstractResilientResource) {
      // Resource endpoint secured method
      allowedPermission = this.evaluateResourceAccess(authentication, (AbstractResilientResource) targetInstance,
          targetInvocationContext.getTargetMethodName(), customPermission, customResource, group);
    } else if (targetInstance instanceof AbstractResilientService) {
      // Service secured method      
    } else {
      // Unknowned target object. The security method annotation (canAccess()) is used in a ilegal place
      // It must have customPermission && customResource defined
      allowedPermission = evaluateAccess(ResilientBasePermission.valueOf(customPermission), customResource, group);
    }

    // Evaluate the result. NOTE: HIERARCHY will be re-check in the Service, for more complex business security validations
    if (allowedPermission.equals(SecurityPermission.ALL) || allowedPermission.equals(SecurityPermission.HIERARCHY)) {
      return true;
    }

    return false;
  }

  private SecurityPermission evaluateResourceAccess(Authentication authentication, AbstractResilientResource resource,
      String endpointMethodName, String customPermission, String customResource, SecurityGroup group) {

    // By method name, infer the needed permission
    ResilientBasePermission neededPermission = null;

    if (customPermission != null) {
      //This is a custom permission, defined in security annotation
      neededPermission = ResilientBasePermission.valueOf(customPermission);
    } else {
      //Try to automatically infer the permission from method name
      neededPermission = this.inferPermission(endpointMethodName);
    }

    // Evaluate permission using the {group}
    String neededResource;
    if (customResource != null) {
      neededResource = customResource;
    } else {
      neededResource = resource.getDomainClass().getName();
    }

    return this.evaluateAccess(neededPermission, neededResource, group);
  }

  private SecurityPermission evaluateAccess(ResilientBasePermission neededPermission, String resource,
      SecurityGroup group) {
    SecurityPermission allowedSecurityPermission = SecurityPermission.NONE;

    // Evaluate permission using the {group}
    SecurityGroupPermission permission = this.findPermission(resource, group);

    if (permission == null) {
      // If the method is secured, the user/group must define a permission for it.
      return allowedSecurityPermission;
    }

    switch (neededPermission) {
    case CREATE: {
      allowedSecurityPermission = permission.getCreatePermission();
      break;
    }
    case READ: {
      allowedSecurityPermission = permission.getReadPermission();
      break;
    }
    case UPDATE: {
      allowedSecurityPermission = permission.getUpdatePermission();
      break;
    }
    case DELETE: {
      allowedSecurityPermission = permission.getDeletePermission();
      break;
    }
    default:
      allowedSecurityPermission = SecurityPermission.NONE;
      break;
    }

    return allowedSecurityPermission;
  }

  private SecurityGroupPermission findPermission(String resourceCode, SecurityGroup securityGroup) {
    return securityGroup.getSecurityGroupPermissions().stream().filter(
        (perm) -> perm.getSecurityResource().getCode().equals(resourceCode)).findAny().orElse(null);
  }

  private ResilientBasePermission inferPermission(String endpointMethodName) {
    ResilientBasePermission neededPermission = null;

    switch (endpointMethodName) {
    case AbstractResilientResource.ENDPOINT_METHOD_NAME_CREATE: {
      neededPermission = ResilientBasePermission.CREATE;
      break;
    }
    case AbstractResilientResource.ENDPOINT_METHOD_NAME_READ:
    case AbstractResilientResource.ENDPOINT_METHOD_NAME_LIST:
    case AbstractResilientResource.ENDPOINT_METHOD_NAME_CRITERIA_LIST:
      neededPermission = ResilientBasePermission.READ;
      break;
    case AbstractResilientResource.ENDPOINT_METHOD_NAME_DELETE: {
      neededPermission = ResilientBasePermission.DELETE;
      break;
    }
    case AbstractResilientResource.ENDPOINT_METHOD_NAME_UPDATE:
    case AbstractResilientResource.ENDPOINT_METHOD_NAME_PARTIAL:
      neededPermission = ResilientBasePermission.CREATE;
      break;
    default:
      // This means that the invoked endpoint its not implemented at the level of AbstractResilientResource. 
      // Its a custom endpoint. For this, a custom permission code must be provided in the annotation
      neededPermission = ResilientBasePermission.CUSTOM;
    }

    return neededPermission;
  }
}
