package io.cattle.platform.api.resource;

import io.cattle.platform.api.action.ActionHandler;
import io.cattle.platform.api.auth.Policy;
import io.cattle.platform.api.link.LinkHandler;
import io.cattle.platform.api.utils.ApiUtils;
import io.cattle.platform.archaius.util.ArchaiusUtil;
import io.cattle.platform.engine.manager.ProcessNotFoundException;
import io.cattle.platform.engine.process.ExitReason;
import io.cattle.platform.engine.process.ProcessInstanceException;
import io.cattle.platform.engine.process.impl.ProcessCancelException;
import io.cattle.platform.object.ObjectManager;
import io.cattle.platform.object.meta.ActionDefinition;
import io.cattle.platform.object.meta.MapRelationship;
import io.cattle.platform.object.meta.ObjectMetaDataManager;
import io.cattle.platform.object.meta.Relationship;
import io.cattle.platform.object.process.ObjectProcessManager;
import io.cattle.platform.object.process.StandardProcess;
import io.cattle.platform.object.util.DataAccessor;
import io.cattle.platform.util.type.CollectionUtils;
import io.cattle.platform.util.type.InitializationTask;
import io.cattle.platform.util.type.NamedUtils;
import io.github.ibuildthecloud.gdapi.condition.Condition;
import io.github.ibuildthecloud.gdapi.condition.ConditionType;
import io.github.ibuildthecloud.gdapi.context.ApiContext;
import io.github.ibuildthecloud.gdapi.exception.ClientVisibleException;
import io.github.ibuildthecloud.gdapi.factory.SchemaFactory;
import io.github.ibuildthecloud.gdapi.id.IdFormatter;
import io.github.ibuildthecloud.gdapi.id.IdentityFormatter;
import io.github.ibuildthecloud.gdapi.model.Action;
import io.github.ibuildthecloud.gdapi.model.Field;
import io.github.ibuildthecloud.gdapi.model.Include;
import io.github.ibuildthecloud.gdapi.model.ListOptions;
import io.github.ibuildthecloud.gdapi.model.Resource;
import io.github.ibuildthecloud.gdapi.model.Schema;
import io.github.ibuildthecloud.gdapi.model.Schema.Method;
import io.github.ibuildthecloud.gdapi.request.ApiRequest;
import io.github.ibuildthecloud.gdapi.request.resource.ResourceManager;
import io.github.ibuildthecloud.gdapi.request.resource.impl.AbstractBaseResourceManager;
import io.github.ibuildthecloud.gdapi.url.UrlBuilder;
import io.github.ibuildthecloud.gdapi.util.ResponseCodes;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.inject.Inject;

import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.netflix.config.DynamicIntProperty;

public abstract class AbstractObjectResourceManager extends AbstractBaseResourceManager implements InitializationTask {

    private static final Logger log = LoggerFactory.getLogger(AbstractObjectResourceManager.class);

    public static final String SCHEDULE_UPDATE = "scheduleUpdate";

    private static final DynamicIntProperty REMOVE_DELAY = ArchaiusUtil.getInt("api.show.removed.for.seconds");
    private static final IdFormatter IDENTITY_FORMATTER = new IdentityFormatter();

    ObjectManager objectManager;
    ObjectProcessManager objectProcessManager;
    ObjectMetaDataManager metaDataManager;
    Map<String, ActionHandler> actionHandlersMap;
    List<ActionHandler> actionHandlers;
    Map<String, List<LinkHandler>> linkHandlersMap;
    List<LinkHandler> linkHandlers;

    @Override
    protected Object authorize(Object object) {
        return ApiUtils.authorize(object);
    }

    @Override
    protected Object createInternal(String type, ApiRequest request) {
        Class<?> clz = getClassForType(request.getSchemaFactory(), type);
        if (clz == null) {
            return null;
        }

        return doCreate(type, clz, CollectionUtils.toMap(request.getRequestObject()));
    }

    protected <T> T doCreate(String type, Class<T> clz, Map<Object, Object> data) {
        Map<String, Object> properties = getObjectManager().convertToPropertiesFor(clz, data);
        if (!properties.containsKey(ObjectMetaDataManager.KIND_FIELD)) {
            properties.put(ObjectMetaDataManager.KIND_FIELD, type);
        }

        return createAndScheduleObject(clz, properties);
    }

    @SuppressWarnings("unchecked")
    protected <T> T createAndScheduleObject(Class<T> clz, Map<String, Object> properties) {
        Object result = objectManager.create(clz, properties);
        try {
            scheduleProcess(StandardProcess.CREATE, result, properties);
            result = objectManager.reload(result);
        } catch (ProcessNotFoundException e) {
        }

        return (T) result;
    }

    protected Class<?> getClassForType(SchemaFactory schemaFactory, String type) {
        Class<?> clz = schemaFactory.getSchemaClass(type);
        if (clz == null) {
            Schema schema = schemaFactory.getSchema(type);
            if (schema != null && schema.getParent() != null) {
                return getClassForType(schemaFactory, schema.getParent());
            }
        }

        return clz;
    }

    @Override
    protected Object deleteInternal(String type, String id, Object obj, ApiRequest request) {
        try {
            scheduleProcess(StandardProcess.REMOVE, obj, null);
            return objectManager.reload(obj);
        } catch (ProcessCancelException e) {
            throw new ClientVisibleException(ResponseCodes.METHOD_NOT_ALLOWED);
        } catch (ProcessNotFoundException e) {
            return removeFromStore(type, id, obj, request);
        }
    }

    protected abstract Object removeFromStore(String type, String id, Object obj, ApiRequest request);

    @Override
    protected Object updateInternal(String type, String id, Object obj, ApiRequest request) {
        Map<String, Object> updates = CollectionUtils.toMap(request.getRequestObject());
        Map<String, Object> existingValues = new HashMap<>();
        Map<String, Object> filteredUpdates = new HashMap<String, Object>();
        Map<String, Object> existing = createResource(obj, IDENTITY_FORMATTER, request).getFields();
        Schema schema = request.getSchemaFactory().getSchema(type);
        Map<String, Field> fields = schema.getResourceFields();

        boolean schedule = false;
        for (Map.Entry<String, Object> entry : updates.entrySet()) {
            String key = entry.getKey();
            Object existingValue = existing.get(key);
            if (!ObjectUtils.equals(existingValue, entry.getValue())) {
                filteredUpdates.put(key, entry.getValue());
                existingValues.put(key, existingValue);
                Field field = fields.get(key);
                if (field != null) {
                    schedule |= Boolean.TRUE.equals(field.getAttributes().get(SCHEDULE_UPDATE));
                }
            }

        }

        Object result = objectManager.setFields(schema, obj, filteredUpdates);
        if (schedule) {
            filteredUpdates.put("old", existingValues);
            objectProcessManager.scheduleStandardProcess(StandardProcess.UPDATE, obj, filteredUpdates);
            result = objectManager.reload(result);
        }

        return result;
    }

    @Override
    protected Object getLinkInternal(String type, String id, String link, ApiRequest request) {
        List<LinkHandler> linkHandlers = linkHandlersMap.get(type);
        if (linkHandlers != null) {
            for (LinkHandler linkHandler : linkHandlers) {
                if (linkHandler.handles(type, id, link, request)) {
                    Object currentObject = getById(type, id, new ListOptions(request));
                    if (currentObject == null) {
                        return null;
                    }

                    try {
                        return linkHandler.link(link, currentObject, request);
                    } catch (IOException e) {
                        log.error("Failed to process link [{}] for [{}:{}]", link, type, id, e);
                        return null;
                    }
                }
            }
        }

        Class<?> clz = request.getSchemaFactory().getSchemaClass(type, true);
        Relationship relationship;
        if (clz != null) {
            relationship = getRelationship(clz, link);
        } else {
            relationship = getRelationship(type, link);
        }

        if (relationship == null) {
            return null;
        }

        switch (relationship.getRelationshipType()) {
        case CHILD:
            return getChildLink(type, id, relationship, request);
        case REFERENCE:
            return getReferenceLink(type, id, relationship, request);
        case MAP:
            return getMapLink(type, id, (MapRelationship) relationship, request);
        }

        return null;
    }

    protected Object getMapLink(String type, String id, MapRelationship relationship, ApiRequest request) {
        return null;
    }

    protected Object getChildLink(String type, String id, Relationship relationship, ApiRequest request) {
        Schema otherSchema = request.getSchemaFactory().getSchema(relationship.getObjectType());
        if (otherSchema == null) {
            return Collections.EMPTY_LIST;
        }

        Object currentObject = getById(type, id, new ListOptions(request));
        if (currentObject == null) {
            return null;
        }

        String otherType = otherSchema.getId();
        Field field = otherSchema.getResourceFields().get(relationship.getPropertyName());
        if (field == null) {
            return Collections.EMPTY_LIST;
        }

        if (otherSchema.getCollectionMethods().contains(Method.POST.toString())) {
            Map<String, Object> createDefaults = new HashMap<String, Object>();
            IdFormatter idFormatter = ApiContext.getContext().getIdFormatter();
            createDefaults.put(relationship.getPropertyName(), idFormatter.formatId(type, id));
            request.setCreateDefaults(createDefaults);
        }

        Map<Object, Object> criteria = getDefaultCriteria(false, true, otherType);
        criteria.put(relationship.getPropertyName(), id);

        ResourceManager resourceManager = locator.getResourceManagerByType(otherType);
        return resourceManager.list(otherType, criteria, null);
    }

    protected Object getReferenceLink(String type, String id, Relationship relationship, ApiRequest request) {
        SchemaFactory schemaFactory = request.getSchemaFactory();
        Schema schema = schemaFactory.getSchema(type);
        Schema otherSchema = schemaFactory.getSchema(relationship.getObjectType());
        Field field = schema.getResourceFields().get(relationship.getPropertyName());
        if (field == null || otherSchema == null) {
            return null;
        }

        ListOptions options = new ListOptions(request);
        Object currentObject = getById(type, id, options);
        if (currentObject == null) {
            return null;
        }

        Object fieldValue = field.getValue(currentObject);

        if (fieldValue == null) {
            return null;
        }

        Map<Object, Object> criteria = getDefaultCriteria(false, true, otherSchema.getId());
        criteria.put(ObjectMetaDataManager.ID_FIELD, fieldValue);

        ResourceManager resourceManager = locator.getResourceManagerByType(otherSchema.getId());
        return ApiUtils.getFirstFromList(resourceManager.list(otherSchema.getId(), criteria, options));
    }

    protected Map<String, Relationship> getLinkRelationships(SchemaFactory schemaFactory, String type, Include include) {
        if (include == null)
            return Collections.emptyMap();

        Map<String, Relationship> result = new HashMap<String, Relationship>();
        Map<String, Relationship> links = metaDataManager.getLinkRelationships(schemaFactory, type);
        for (String link : include.getLinks()) {
            link = link.toLowerCase();
            if (links.containsKey(link)) {
                result.put(link, links.get(link));
            }
        }

        return result;
    }

    @Override
    protected Map<Object, Object> getDefaultCriteria(boolean byId, boolean byLink, String type) {
        Map<Object, Object> criteria = super.getDefaultCriteria(byId, byLink, type);
        Policy policy = ApiUtils.getPolicy();

        addAccountAuthorization(byId, byLink, type, criteria, policy);

        if (!showRemoved() && !byId) {
            /* removed is null or removed >= (NOW() - delay) */
            Condition or = new Condition(new Condition(ConditionType.NULL), new Condition(ConditionType.GTE, removedTime()));
            criteria.put(ObjectMetaDataManager.REMOVED_FIELD, or);

            /* remove_time is null or remove_time > NOW() */
            or = new Condition(new Condition(ConditionType.NULL), new Condition(ConditionType.GT, new Date()));
            criteria.put(ObjectMetaDataManager.REMOVE_TIME_FIELD, or);
        }

        return criteria;
    }

    protected boolean showRemoved() {
        ApiRequest request = ApiContext.getContext().getApiRequest();
        if (request == null) {
            return false;
        }
        return request.getOptions().containsKey("_removed");
    }

    protected Date removedTime() {
        return new Date(System.currentTimeMillis() - REMOVE_DELAY.get() * 1000);
    }

    protected void addAccountAuthorization(boolean byId, boolean byLink, String type, Map<Object, Object> criteria, Policy policy) {
        if (!policy.isOption(Policy.LIST_ALL_ACCOUNTS)) {
            if (policy.isOption(Policy.AUTHORIZED_FOR_ALL_ACCOUNTS) && (byId || byLink)) {
                return;
            }

            if ("account".equals(type)) {
                criteria.put(ObjectMetaDataManager.ID_FIELD, policy.getAccountId());
            } else {
                criteria.put(ObjectMetaDataManager.ACCOUNT_FIELD, policy.getAccountId());
            }
        }
    }

    @Override
    protected String getCollectionType(List<?> list, ApiRequest request) {
        String link = request.getLink();
        if (link == null) {
            return request.getType();
        } else {
            Relationship relationship = getRelationship(request.getSchemaFactory()
                    .getSchemaClass(request.getType(), true), link);
            return request.getSchemaFactory().getSchemaName(relationship.getObjectType());
        }
    }

    @Override
    protected Schema getSchemaForDisplay(SchemaFactory schemaFactory, Object obj) {
        String schemaId = ApiUtils.getSchemaIdForDisplay(schemaFactory, obj);
        Schema schema = schemaFactory.getSchema(schemaId);

        if (schema == null) {
            /* Check core schema because the parent might not be authorized */
            schemaId = ApiUtils.getSchemaIdForDisplay(getObjectManager().getSchemaFactory(), obj);

            /* Still get schema from request's schemaFactory */
            schema = schemaFactory.getSchema(schemaId);
        }

        return schema;
    }

    @Override
    protected Resource constructResource(IdFormatter idFormatter, SchemaFactory schemaFactory, Schema schema, Object obj, ApiRequest apiRequest) {
        Map<String, Object> transitioningFields = metaDataManager.getTransitionFields(schema, obj);
        return ApiUtils.createResourceWithAttachments(this, apiRequest, idFormatter, schemaFactory, schema, obj, transitioningFields);
    }

    @Override
    protected Object resourceActionInternal(Object obj, ApiRequest request) {
        String processName = getProcessName(obj, request);
        ActionHandler handler = actionHandlersMap.get(processName);
        if (handler != null) {
            return handler.perform(processName, obj, request);
        }

        Map<String, Object> data = CollectionUtils.toMap(request.getRequestObject());

        try {
            scheduleProcess(getProcessName(obj, request), obj, data);
        } catch (ProcessNotFoundException e) {
            throw new ClientVisibleException(ResponseCodes.NOT_FOUND);
        }

        request.setResponseCode(ResponseCodes.ACCEPTED);
        return objectManager.reload(obj);
    }

    protected String getProcessName(Object obj, ApiRequest request) {
        String baseType = request.getSchemaFactory().getBaseType(request.getType());
        return String.format("%s.%s", baseType == null ? request.getType() : baseType, request.getAction()).toLowerCase();
    }

    protected void scheduleProcess(final String processName, final Object resource, final Map<String, Object> data) {
        scheduleProcess(new Runnable() {
            @Override
            public void run() {
                objectProcessManager.scheduleProcessInstance(processName, resource, data);
            }
        });
    }

    protected void scheduleProcess(final StandardProcess process, final Object resource, final Map<String, Object> data) {
        scheduleProcess(new Runnable() {
            @Override
            public void run() {
                objectProcessManager.scheduleStandardProcess(process, resource, data);
            }
        });
    }

    protected void scheduleProcess(Runnable run) {
        try {
            run.run();
        } catch (ProcessInstanceException e) {
            if (e.getExitReason() == ExitReason.RESOURCE_BUSY || e.getExitReason() == ExitReason.CANCELED) {
                throw new ClientVisibleException(ResponseCodes.CONFLICT);
            } else {
                throw e;
            }
        }
    }

    @Override
    protected void addActions(Object obj, SchemaFactory schemaFactory, Schema schema, Resource resource) {
        Object state = resource.getFields().get(ObjectMetaDataManager.STATE_FIELD);
        Map<String, ActionDefinition> defs = metaDataManager.getActionDefinitions(obj);

        if (state == null || defs == null) {
            super.addActions(obj, schemaFactory, schema, resource);
            return;
        }

        Map<String, Action> actions = schema.getResourceActions();

        if (actions == null || actions.size() == 0) {
            return;
        }

        UrlBuilder urlBuilder = ApiContext.getUrlBuilder();

        for (Map.Entry<String, Action> entry : actions.entrySet()) {
            String name = entry.getKey();
            Action action = entry.getValue();

            if ("restore".equals(name) || !isValidAction(obj, action)) {
                continue;
            }

            ActionDefinition def = defs.get(name);
            if (def == null || def.getValidStates().contains(state)) {
                resource.getActions().put(name, urlBuilder.actionLink(resource, name));
            }
        }
    }

    @SuppressWarnings("unchecked")
    protected boolean isValidAction(Object obj, Action action) {
        Map<String, Object> attributes = action.getAttributes();

        if (attributes == null || attributes.size() == 0) {
            return true;
        }

        String capability = ObjectUtils.toString(attributes.get("capability"), null);
        String state = ObjectUtils.toString(attributes.get(ObjectMetaDataManager.STATE_FIELD), null);
        String currentState = io.cattle.platform.object.util.ObjectUtils.getState(obj);

        if (!StringUtils.isBlank(capability) && !(ApiContext.getContext().getCapabilities(obj) != null ?
                ApiContext.getContext().getCapabilities(obj).contains(capability) :
                DataAccessor.fieldStringList(obj, ObjectMetaDataManager.CAPABILITIES_FIELD).contains(capability))) {
            return false;
        }

        if (!StringUtils.isBlank(state) && !state.equals(currentState)) {
            return false;
        }
        List<String> states = ((List<String>) attributes.get(ObjectMetaDataManager.STATES_FIELD));
        if (states != null && !states.contains(currentState)){
            return false;
        }

        return true;
    }

    @Override
    public void start() {
        actionHandlersMap = NamedUtils.createMapByName(actionHandlers);
        linkHandlersMap = new HashMap<String, List<LinkHandler>>();

        for (LinkHandler handler : linkHandlers) {
            for (String type : handler.getTypes()) {
                CollectionUtils.addToMap(linkHandlersMap, type, handler, ArrayList.class);
            }
        }
    }

    protected Relationship getRelationship(String type, String linkName) {
        return metaDataManager.getRelationship(type, linkName);
    }

    protected Relationship getRelationship(Class<?> clz, String linkName) {
        return metaDataManager.getRelationship(clz, linkName);
    }

    @Override
    protected Object collectionActionInternal(Object resources, ApiRequest request) {
        return null;
    }

    @Override
    protected Map<String, String> getLinks(SchemaFactory schemaFactory, Resource resource) {
        return metaDataManager.getLinks(schemaFactory, resource.getType());
    }

    public ObjectManager getObjectManager() {
        return objectManager;
    }

    @Inject
    public void setObjectManager(ObjectManager objectManager) {
        this.objectManager = objectManager;
    }

    public ObjectMetaDataManager getMetaDataManager() {
        return metaDataManager;
    }

    @Inject
    public void setMetaDataManager(ObjectMetaDataManager metaDataManager) {
        this.metaDataManager = metaDataManager;
    }

    public ObjectProcessManager getObjectProcessManager() {
        return objectProcessManager;
    }

    @Inject
    public void setObjectProcessManager(ObjectProcessManager objectProcessManager) {
        this.objectProcessManager = objectProcessManager;
    }

    public List<ActionHandler> getActionHandlers() {
        return actionHandlers;
    }

    @Inject
    public void setActionHandlers(List<ActionHandler> actionHandlers) {
        this.actionHandlers = actionHandlers;
    }

    public List<LinkHandler> getLinkHandlers() {
        return linkHandlers;
    }

    @Inject
    public void setLinkHandlers(List<LinkHandler> linkHandlers) {
        this.linkHandlers = linkHandlers;
    }
}
