// 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 com.cloud.resourceicon;

import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

import javax.inject.Inject;
import javax.persistence.EntityExistsException;

import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.context.CallContext;

import com.cloud.domain.PartOf;
import com.cloud.event.ActionEvent;
import com.cloud.event.EventTypes;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.network.security.SecurityGroupRuleVO;
import com.cloud.network.security.SecurityGroupVO;
import com.cloud.network.vpc.NetworkACLItemVO;
import com.cloud.network.vpc.NetworkACLVO;
import com.cloud.network.vpc.VpcVO;
import com.cloud.projects.ProjectVO;
import com.cloud.resource.icon.ResourceIconVO;
import com.cloud.resource.icon.dao.ResourceIconDao;
import com.cloud.server.ResourceIcon;
import com.cloud.server.ResourceIconManager;
import com.cloud.server.ResourceManagerUtil;
import com.cloud.server.ResourceTag;
import com.cloud.storage.SnapshotPolicyVO;
import com.cloud.storage.VolumeVO;
import com.cloud.tags.ResourceManagerUtilImpl;
import com.cloud.user.Account;
import com.cloud.user.AccountService;
import com.cloud.user.AccountVO;
import com.cloud.user.OwnedBy;
import com.cloud.user.dao.AccountDao;
import com.cloud.utils.Pair;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.db.DB;
import com.cloud.utils.db.EntityManager;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
import com.cloud.utils.db.Transaction;
import com.cloud.utils.db.TransactionCallbackNoReturn;
import com.cloud.utils.db.TransactionStatus;
import com.cloud.utils.exception.CloudRuntimeException;

public class ResourceIconManagerImpl extends ManagerBase implements ResourceIconManager {

    @Inject
    AccountService accountService;
    @Inject
    ResourceManagerUtil resourceManagerUtil;
    @Inject
    ResourceIconDao resourceIconDao;
    @Inject
    EntityManager entityMgr;
    @Inject
    AccountDao accountDao;

    private Pair<Long, Long> getAccountDomain(long resourceId, ResourceTag.ResourceObjectType resourceType) {
        Class<?> clazz = ResourceManagerUtilImpl.s_typeMap.get(resourceType);

        Object entity = entityMgr.findById(clazz, resourceId);
        Long accountId = null;
        Long domainId = null;

        // if the resource type is a security group rule, get the accountId and domainId from the security group itself
        if (resourceType == ResourceTag.ResourceObjectType.SecurityGroupRule) {
            SecurityGroupRuleVO rule = (SecurityGroupRuleVO)entity;
            Object SecurityGroup = entityMgr.findById(ResourceManagerUtilImpl.s_typeMap.get(ResourceTag.ResourceObjectType.SecurityGroup), rule.getSecurityGroupId());

            accountId = ((SecurityGroupVO)SecurityGroup).getAccountId();
            domainId = ((SecurityGroupVO)SecurityGroup).getDomainId();
        }

        if (resourceType == ResourceTag.ResourceObjectType.Account) {
            AccountVO account = (AccountVO)entity;
            accountId = account.getId();
            domainId = account.getDomainId();
        }

        // if the resource type is network acl, get the accountId and domainId from VPC following: NetworkACLItem -> NetworkACL -> VPC
        if (resourceType == ResourceTag.ResourceObjectType.NetworkACL) {
            NetworkACLItemVO aclItem = (NetworkACLItemVO)entity;
            Object networkACL = entityMgr.findById(ResourceManagerUtilImpl.s_typeMap.get(ResourceTag.ResourceObjectType.NetworkACLList), aclItem.getAclId());
            Long vpcId = ((NetworkACLVO)networkACL).getVpcId();

            if (vpcId != null && vpcId != 0) {
                Object vpc = entityMgr.findById(ResourceManagerUtilImpl.s_typeMap.get(ResourceTag.ResourceObjectType.Vpc), vpcId);

                accountId = ((VpcVO)vpc).getAccountId();
                domainId = ((VpcVO)vpc).getDomainId();
            }
        }

        if (resourceType == ResourceTag.ResourceObjectType.Project) {
            accountId = ((ProjectVO)entity).getProjectAccountId();
        }

        if (resourceType == ResourceTag.ResourceObjectType.SnapshotPolicy) {
            accountId = entityMgr.findById(VolumeVO.class, ((SnapshotPolicyVO)entity).getVolumeId()).getAccountId();
        }

        if (entity instanceof OwnedBy) {
            accountId = ((OwnedBy)entity).getAccountId();
        }

        if (entity instanceof PartOf) {
            domainId = ((PartOf)entity).getDomainId();
        }

        if (accountId == null) {
            accountId = Account.ACCOUNT_ID_SYSTEM;
        }

        if ((domainId == null) || ((accountId != null) && (domainId.longValue() == -1))) {
            domainId = accountDao.getDomainIdForGivenAccountId(accountId);
        }
        return new Pair<>(accountId, domainId);
    }

    private void updateResourceDetailsInContext(Long resourceId, ResourceTag.ResourceObjectType resourceType) {
        Class<?> clazz = ResourceManagerUtilImpl.s_typeMap.get(resourceType);
        ApiCommandResourceType type = ApiCommandResourceType.valueFromAssociatedClass(clazz);
        int depth = 5;
        while (type == null && depth > 0) {
            Class<?>[] clazzes = clazz.getInterfaces();
            if (clazzes.length == 0) {
                break;
            }
            depth--;
            clazz = clazzes[0];
            type = ApiCommandResourceType.valueFromAssociatedClass(clazz);
        }
        CallContext.current().setEventResourceId(resourceId);
        if (!ApiCommandResourceType.None.equals(type)) {
            CallContext.current().setEventResourceType(type);
        }
    }

    @Override
    @DB
    @ActionEvent(eventType = EventTypes.EVENT_RESOURCE_ICON_UPLOAD, eventDescription = "uploading resource icon")
    public boolean uploadResourceIcon(List<String> resourceIds, ResourceTag.ResourceObjectType resourceType, String base64Image) {
        final Account caller = CallContext.current().getCallingAccount();

        Transaction.execute(new TransactionCallbackNoReturn() {
            @Override
            public void doInTransactionWithoutResult(TransactionStatus status) {
                for (String resourceId : resourceIds) {
                    if (!resourceType.resourceIconSupport()) {
                        throw new InvalidParameterValueException("The resource type " + resourceType + " doesn't support resource icons");
                    }

                    if (base64Image == null) {
                        throw new InvalidParameterValueException("No icon provided to be uploaded for resource: " + resourceId);
                    }

                    long id = resourceManagerUtil.getResourceId(resourceId, resourceType);
                    String resourceUuid = resourceManagerUtil.getUuid(resourceId, resourceType);
                    updateResourceDetailsInContext(id, resourceType);
                    ResourceIconVO existingResourceIcon = resourceIconDao.findByResourceUuid(resourceUuid, resourceType);
                    ResourceIconVO resourceIcon = null;
                    Pair<Long, Long> accountDomainPair = getAccountDomain(id, resourceType);
                    Long domainId = accountDomainPair.second();
                    Long accountId = accountDomainPair.first();
                    resourceManagerUtil.checkResourceAccessible(accountId, domainId, String.format("Account ' %s ' doesn't have permissions to upload icon for resource [id: %s, uuid: %s] ", caller, id, resourceUuid));

                    if (existingResourceIcon == null) {
                        resourceIcon = new ResourceIconVO(id, resourceType, resourceUuid, base64Image);
                    } else {
                        resourceIcon = existingResourceIcon;
                        resourceIcon.setIcon(base64Image);
                        resourceIcon.setUpdated(new Date());
                    }
                    try {
                        resourceIconDao.persist(resourceIcon);
                    } catch (EntityExistsException e) {
                        throw new CloudRuntimeException(String.format("Image already uploaded for resource type: %s with id %s",  resourceType.toString(), resourceId),e);
                    }
                }
            }
        });

        return true;
    }

    @Override
    @ActionEvent(eventType = EventTypes.EVENT_RESOURCE_ICON_DELETE, eventDescription = "deleting resource icon")
    public boolean deleteResourceIcon(List<String> resourceIds, ResourceTag.ResourceObjectType resourceType) {
        Account caller = CallContext.current().getCallingAccount();
        List<? extends ResourceIcon> resourceIcons = searchResourceIcons(resourceIds, resourceType);
        if (resourceIcons.isEmpty()) {
            logger.debug("No resource Icon(s) uploaded for the specified resources");
            return false;
        }
        Transaction.execute(new TransactionCallbackNoReturn() {
            @Override
            public void doInTransactionWithoutResult(TransactionStatus status) {
                for (ResourceIcon resourceIcon : resourceIcons) {
                    String resourceId = resourceIcon.getResourceUuid();
                    long id = resourceManagerUtil.getResourceId(resourceId, resourceType);
                    updateResourceDetailsInContext(id, resourceType);
                    Pair<Long, Long> accountDomainPair = getAccountDomain(id, resourceType);
                    Long domainId = accountDomainPair.second();
                    Long accountId = accountDomainPair.first();
                    resourceManagerUtil.checkResourceAccessible(accountId, domainId, String.format("Account ' %s ' doesn't have permissions to upload icon for resource [id: %s, uuid: %s]", caller, id, resourceId));
                    resourceIconDao.remove(resourceIcon.getId());
                    logger.debug("Removed icon for resources (" +
                            String.join(", ", resourceIds) + ")");
                }
            }
        });
        return true;
    }

    @Override
    public ResourceIcon getByResourceTypeAndUuid(ResourceTag.ResourceObjectType type, String resourceId) {
        return resourceIconDao.findByResourceUuid(resourceId, type);
    }

    private List<? extends ResourceIcon> searchResourceIcons(List<String> resourceIds, ResourceTag.ResourceObjectType resourceType) {
        List<String> resourceUuids = resourceIds.stream().map(resourceId -> resourceManagerUtil.getUuid(resourceId, resourceType)).collect(Collectors.toList());
        SearchBuilder<ResourceIconVO> sb = resourceIconDao.createSearchBuilder();
        sb.and("resourceUuid", sb.entity().getResourceUuid(), SearchCriteria.Op.IN);
        sb.and("resourceType", sb.entity().getResourceType(), SearchCriteria.Op.EQ);

        SearchCriteria<ResourceIconVO> sc = sb.create();
        sc.setParameters("resourceUuid", resourceUuids.toArray());
        sc.setParameters("resourceType", resourceType);
        return resourceIconDao.search(sc, null);
    }


    @Override
    public Map<Long, ResourceIcon> getByResourceTypeAndIds(ResourceTag.ResourceObjectType resourceType, Collection<Long> resourceIds) {
        List<ResourceIconVO> icons = resourceIconDao.listByResourceTypeAndIds(resourceType, resourceIds);
        return icons.stream().collect(Collectors.toMap(ResourceIconVO::getResourceId, Function.identity()));
    }


    @Override
    public Map<String, ResourceIcon> getByResourceTypeAndUuids(ResourceTag.ResourceObjectType resourceType, Collection<String> resourceUuids) {
        List<ResourceIconVO> icons = resourceIconDao.listByResourceTypeAndUuids(resourceType, resourceUuids);
        return icons.stream().collect(Collectors.toMap(ResourceIconVO::getResourceUuid, Function.identity()));
    }
}
