// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you 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 org.apache.cloudstack.api;

import com.cloud.configuration.ConfigurationService;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.InsufficientCapacityException;
import com.cloud.exception.NetworkRuleConflictException;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.network.NetworkModel;
import com.cloud.network.NetworkService;
import com.cloud.network.NetworkUsageService;
import com.cloud.network.StorageNetworkService;
import com.cloud.network.VpcVirtualNetworkApplianceService;
import com.cloud.network.as.AutoScaleService;
import com.cloud.network.firewall.FirewallService;
import com.cloud.network.lb.LoadBalancingRulesService;
import com.cloud.network.rules.RulesService;
import com.cloud.network.security.SecurityGroupService;
import com.cloud.network.vpc.NetworkACLService;
import com.cloud.network.vpc.VpcProvisioningService;
import com.cloud.network.vpc.VpcService;
import com.cloud.network.vpn.RemoteAccessVpnService;
import com.cloud.network.vpn.Site2SiteVpnService;
import com.cloud.projects.ProjectService;
import com.cloud.resource.ResourceService;
import com.cloud.server.ManagementService;
import com.cloud.server.ResourceMetaDataService;
import com.cloud.server.TaggedResourceService;
import com.cloud.storage.DataStoreProviderApiService;
import com.cloud.storage.StorageService;
import com.cloud.storage.VolumeApiService;
import com.cloud.storage.snapshot.SnapshotApiService;
import com.cloud.template.TemplateApiService;
import com.cloud.user.Account;
import com.cloud.user.AccountService;
import com.cloud.user.DomainService;
import com.cloud.user.ResourceLimitService;
import com.cloud.utils.HttpUtils;
import com.cloud.utils.ReflectUtil;
import com.cloud.utils.db.EntityManager;
import com.cloud.utils.db.UUIDManager;
import com.cloud.vm.UserVmService;
import com.cloud.vm.snapshot.VMSnapshotService;
import org.apache.cloudstack.acl.RoleService;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.affinity.AffinityGroupService;
import org.apache.cloudstack.alert.AlertService;
import org.apache.cloudstack.annotation.AnnotationService;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.network.element.InternalLoadBalancerElementService;
import org.apache.cloudstack.network.lb.ApplicationLoadBalancerService;
import org.apache.cloudstack.network.lb.InternalLoadBalancerVMService;
import org.apache.cloudstack.query.QueryService;
import org.apache.cloudstack.usage.UsageService;
import org.apache.log4j.Logger;

import javax.inject.Inject;
import java.lang.reflect.Field;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

public abstract class BaseCmd {
    private static final Logger s_logger = Logger.getLogger(BaseCmd.class.getName());
    public static final String RESPONSE_SUFFIX = "response";
    public static final String RESPONSE_TYPE_XML = HttpUtils.RESPONSE_TYPE_XML;
    public static final String RESPONSE_TYPE_JSON = HttpUtils.RESPONSE_TYPE_JSON;
    public static final String USER_ERROR_MESSAGE = "Internal error executing command, please contact your system administrator";
    public static Pattern newInputDateFormat = Pattern.compile("[\\d]+-[\\d]+-[\\d]+ [\\d]+:[\\d]+:[\\d]+");
    private static final DateFormat s_outputFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
    protected static final Map<Class<?>, List<Field>> fieldsForCmdClass = new HashMap<Class<?>, List<Field>>();

    public static enum HTTPMethod {
        GET, POST, PUT, DELETE
    }
    public static enum CommandType {
        BOOLEAN, DATE, FLOAT, DOUBLE, INTEGER, SHORT, LIST, LONG, OBJECT, MAP, STRING, TZDATE, UUID
    }

    private Object _responseObject;
    private Map<String, String> fullUrlParams;
    private HTTPMethod httpMethod;
    @Parameter(name = "response", type = CommandType.STRING)
    private String responseType;

    @Inject
    public ConfigurationService _configService;
    @Inject
    public AccountService _accountService;
    @Inject
    public RoleService roleService;
    @Inject
    public UserVmService _userVmService;
    @Inject
    public ManagementService _mgr;
    @Inject
    public StorageService _storageService;
    @Inject
    public VolumeApiService _volumeService;
    @Inject
    public ResourceService _resourceService;
    @Inject
    public NetworkService _networkService;
    @Inject
    public TemplateApiService _templateService;
    @Inject
    public SecurityGroupService _securityGroupService;
    @Inject
    public SnapshotApiService _snapshotService;
    @Inject
    public VpcVirtualNetworkApplianceService _routerService;
    @Inject
    public ResponseGenerator _responseGenerator;
    @Inject
    public EntityManager _entityMgr;
    @Inject
    public RulesService _rulesService;
    @Inject
    public AutoScaleService _autoScaleService;
    @Inject
    public LoadBalancingRulesService _lbService;
    @Inject
    public RemoteAccessVpnService _ravService;
    @Inject
    public ProjectService _projectService;
    @Inject
    public FirewallService _firewallService;
    @Inject
    public DomainService _domainService;
    @Inject
    public ResourceLimitService _resourceLimitService;
    @Inject
    public StorageNetworkService _storageNetworkService;
    @Inject
    public TaggedResourceService _taggedResourceService;
    @Inject
    public ResourceMetaDataService _resourceMetaDataService;
    @Inject
    public VpcService _vpcService;
    @Inject
    public NetworkACLService _networkACLService;
    @Inject
    public Site2SiteVpnService _s2sVpnService;
    @Inject
    public QueryService _queryService;
    @Inject
    public UsageService _usageService;
    @Inject
    public NetworkUsageService _networkUsageService;
    @Inject
    public VMSnapshotService _vmSnapshotService;
    @Inject
    public DataStoreProviderApiService dataStoreProviderApiService;
    @Inject
    public VpcProvisioningService _vpcProvSvc;
    @Inject
    public ApplicationLoadBalancerService _newLbSvc;
    @Inject
    public ApplicationLoadBalancerService _appLbService;
    @Inject
    public AffinityGroupService _affinityGroupService;
    @Inject
    public InternalLoadBalancerElementService _internalLbElementSvc;
    @Inject
    public InternalLoadBalancerVMService _internalLbSvc;
    @Inject
    public NetworkModel _ntwkModel;
    @Inject
    public AlertService _alertSvc;
    @Inject
    public UUIDManager _uuidMgr;
    @Inject
    public AnnotationService annotationService;

    public abstract void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException,
        ResourceAllocationException, NetworkRuleConflictException;

    public void configure() {
    }

    public HTTPMethod getHttpMethod() {
        return httpMethod;
    }

    public void setHttpMethod(final String method) {
        if (method != null) {
            if (method.equalsIgnoreCase("GET"))
                httpMethod = HTTPMethod.GET;
            else if (method.equalsIgnoreCase("PUT"))
                httpMethod = HTTPMethod.PUT;
            else if (method.equalsIgnoreCase("POST"))
                httpMethod = HTTPMethod.POST;
            else if (method.equalsIgnoreCase("DELETE"))
                httpMethod = HTTPMethod.DELETE;
        } else {
            httpMethod = HTTPMethod.GET;
        }
    }

    public String getResponseType() {
        if (responseType == null) {
            return RESPONSE_TYPE_XML;
        }
        return responseType;
    }

    public void setResponseType(final String responseType) {
        this.responseType = responseType;
    }

    /**
     * For some reason this method does not return the actual command name, but more a name that
     * is used to create the response. So you can expect for a XCmd a value like xcmdresponse. Anyways
     * this methods is used in too many places so for now instead of changing it we just create another
     * method {@link BaseCmd#getActualCommandName()} that returns the value from {@link APICommand#name()}
     *
     * @return
     */
    public abstract String getCommandName();


    /**
     * Gets the CommandName based on the class annotations: the value from {@link APICommand#name()}
     *
     * @return the value from {@link APICommand#name()}
     */
    public String getActualCommandName() {
        String cmdName = null;
        if (this.getClass().getAnnotation(APICommand.class) != null) {
            cmdName = this.getClass().getAnnotation(APICommand.class).name();
        } else {
            cmdName = this.getClass().getName();
        }
       return cmdName;
    }

    /**
     * For commands the API framework needs to know the owner of the object being acted upon. This method is
     * used to determine that information.
     *
     * @return the id of the account that owns the object being acted upon
     */
    public abstract long getEntityOwnerId();

    public Object getResponseObject() {
        return _responseObject;
    }

    public void setResponseObject(final Object responseObject) {
        _responseObject = responseObject;
    }

    public static String getDateString(final Date date) {
        if (date == null) {
            return "";
        }
        String formattedString = null;
        synchronized (s_outputFormat) {
            formattedString = s_outputFormat.format(date);
        }
        return formattedString;
    }

    protected List<Field> getAllFieldsForClass(final Class<?> clazz) {
        List<Field> filteredFields = fieldsForCmdClass.get(clazz);

        // If list of fields was not cached yet
        if (filteredFields == null) {
            final List<Field> allFields = ReflectUtil.getAllFieldsForClass(this.getClass(), BaseCmd.class);
            filteredFields = new ArrayList<Field>();

            for (final Field field : allFields) {
                final Parameter parameterAnnotation = field.getAnnotation(Parameter.class);
                if ((parameterAnnotation != null) && parameterAnnotation.expose()) {
                    filteredFields.add(field);
                    }
                }

            // Cache the prepared list for future use
            fieldsForCmdClass.put(clazz, filteredFields);
                    }
        return filteredFields;
                }

    /**
     * This method doesn't return all the @{link Parameter}, but only the ones exposed
     * and allowed for current @{link RoleType}. This method will get the fields for a given
     * Cmd class only once and never again, so in case of a dynamic update the result would
     * be obsolete (this might be a plugin update. It is agreed upon that we will not do
     * upgrades dynamically but in case we come back on that decision we need to revisit this)
     *
     * @return
     */
    public List<Field> getParamFields() {
        final List<Field> allFields = getAllFieldsForClass(this.getClass());
        final List<Field> validFields = new ArrayList<Field>();
        final Account caller = CallContext.current().getCallingAccount();

        for (final Field field : allFields) {
            final Parameter parameterAnnotation = field.getAnnotation(Parameter.class);

            //TODO: Annotate @Validate on API Cmd classes, FIXME how to process Validate
            final RoleType[] allowedRoles = parameterAnnotation.authorized();
            boolean roleIsAllowed = true;
            if (allowedRoles.length > 0) {
                roleIsAllowed = false;
                for (final RoleType allowedRole : allowedRoles) {
                    if (allowedRole.getAccountType() == caller.getType()) {
                        roleIsAllowed = true;
                        break;
                    }
                }
            }

            if (roleIsAllowed) {
                validFields.add(field);
            } else {
                s_logger.debug("Ignoring paremeter " + parameterAnnotation.name() + " as the caller is not authorized to pass it in");
            }
        }

        return validFields;
    }

    public void setFullUrlParams(final Map<String, String> map) {
        fullUrlParams = map;
    }

    public Map<String, String> getFullUrlParams() {
        return fullUrlParams;
    }

    /**
     * To be overwritten by any class who needs specific validation
     */
    public void validateSpecificParameters(final Map<String, String> params){
        // To be overwritten by any class who needs specific validation
    }

    /**
     * display flag is used to control the display of the resource only to the end user. It doesnt affect Root Admin.
     * @return display flag
     */
    public boolean isDisplay(){
        CallContext context = CallContext.current();
        Map<Object, Object> contextMap = context.getContextParameters();
        boolean isDisplay = true;

        // Iterate over all the first class entities in context and check their display property.
        for(Map.Entry<Object, Object> entry : contextMap.entrySet()){
            try{
                Object key = entry.getKey();
                Class clz = Class.forName((String)key);
                if(Displayable.class.isAssignableFrom(clz)){
                    final Object objVO = getEntityVO(clz, entry.getValue());
                    isDisplay = ((Displayable) objVO).isDisplay();
                }

                // If the flag is false break immediately
                if(!isDisplay)
                    break;
            } catch (Exception e){
                s_logger.trace("Caught exception while checking first class entities for display property, continuing on", e);
            }
        }

        context.setEventDisplayEnabled(isDisplay);
        return isDisplay;

    }

    private Object getEntityVO(Class entityType, Object entityId){

        // entityId can be internal db id or UUID so accordingly call findbyId or findByUUID

        if (entityId instanceof Long){
            // Its internal db id - use findById
            return _entityMgr.findById(entityType, (Long)entityId);
        } else if(entityId instanceof String){
            try{
                // In case its an async job the internal db id would be a string because of json deserialization
                Long internalId = Long.valueOf((String) entityId);
                return _entityMgr.findById(entityType, internalId);
            } catch (NumberFormatException e){
               // It is uuid - use findByUuid`
               return _entityMgr.findByUuid(entityType, (String)entityId);
            }
        }

        return null;
    }

}
