// 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.network.contrail.management;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.inject.Inject;

import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.network.contrail.api.response.ServiceInstanceResponse;
import org.apache.cloudstack.network.contrail.model.ServiceInstanceModel;
import org.apache.cloudstack.network.contrail.model.VirtualMachineModel;
import org.apache.cloudstack.network.contrail.model.VirtualNetworkModel;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;

import com.cloud.api.ApiDBUtils;
import com.cloud.dc.DataCenter;
import com.cloud.deploy.DataCenterDeployment;
import com.cloud.event.ActionEvent;
import com.cloud.event.EventTypes;
import com.cloud.exception.InsufficientCapacityException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.network.Network;
import com.cloud.network.NetworkModel;
import com.cloud.network.Networks.TrafficType;
import com.cloud.network.dao.NetworkVO;
import com.cloud.offering.ServiceOffering;
import com.cloud.projects.Project;
import com.cloud.template.VirtualMachineTemplate;
import com.cloud.user.Account;
import com.cloud.user.AccountService;
import com.cloud.user.UserVO;
import com.cloud.user.dao.UserDao;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.NicProfile;
import com.cloud.vm.UserVmVO;
import com.cloud.vm.VirtualMachineManager;
import com.cloud.vm.VirtualMachineName;
import com.cloud.vm.dao.UserVmDao;
import com.google.gson.Gson;

import net.juniper.contrail.api.ApiConnector;
import net.juniper.contrail.api.types.ServiceInstance;

public class ServiceManagerImpl implements ServiceManager {
    protected Logger logger = LogManager.getLogger(getClass());

    @Inject
    UserDao _userDao;
    @Inject
    UserVmDao _vmDao;
    @Inject
    VirtualMachineManager _vmManager;
    @Inject
    NetworkModel _networkModel;
    @Inject
    AccountService _accountService;
    @Inject
    ContrailManager _manager;

    /**
     * In the case of service instance the primary object is in the contrail API server. This object stores the
     * service instance parameters in the database.
     *
     * @param owner     Used to determine the project.
     * @param name      Service instance name (user specified).
     * @param template  Image to execute.
     * @param serviceOffering
     * @param left      Inside network.
     * @param right     Outside network.
     * @return
     */

    /**
     * create a new ServiceVM object.
     * @return
     */
    @ActionEvent(eventType = EventTypes.EVENT_VM_CREATE, eventDescription = "createServiceInstance", create = true)
    private ServiceVirtualMachine createServiceVM(DataCenter zone, Account owner, VirtualMachineTemplate template, ServiceOffering serviceOffering, String name,
        ServiceInstance siObj, Network left, Network right) {
        long id = _vmDao.getNextInSequence(Long.class, "id");

        DataCenterDeployment plan = new DataCenterDeployment(zone.getId());
        LinkedHashMap<NetworkVO, List<? extends NicProfile>> networks = new LinkedHashMap<NetworkVO, List<? extends NicProfile>>();
        NetworkVO linklocal = (NetworkVO) _networkModel.getSystemNetworkByZoneAndTrafficType(zone.getId(),
                TrafficType.Management);
        networks.put(linklocal, new ArrayList<NicProfile>());
        networks.put((NetworkVO)left, new ArrayList<NicProfile>());
        networks.put((NetworkVO)right, new ArrayList<NicProfile>());
        String instanceName = VirtualMachineName.getVmName(id, owner.getId(), "SRV");

        long userId = CallContext.current().getCallingUserId();
        if (CallContext.current().getCallingAccount().getId() != owner.getId()) {
            List<UserVO> userVOs = _userDao.listByAccount(owner.getAccountId());
            if (!userVOs.isEmpty()) {
                userId =  userVOs.get(0).getId();
            }
        }

        ServiceVirtualMachine svm =
            new ServiceVirtualMachine(id, instanceName, name, template.getId(), serviceOffering.getId(), template.getHypervisorType(), template.getGuestOSId(),
                zone.getId(), owner.getDomainId(), owner.getAccountId(), userId, false);

        // database synchronization code must be able to distinguish service instance VMs.
        Map<String, String> kvmap = new HashMap<String, String>();
        kvmap.put("service-instance", siObj.getUuid());
        Gson json = new Gson();
        String userData = json.toJson(kvmap);
        svm.setUserData(userData);

        try {
            _vmManager.allocate(instanceName, template, serviceOffering, networks, plan, template.getHypervisorType(), null, null);
        } catch (InsufficientCapacityException ex) {
            throw new CloudRuntimeException("Insufficient capacity", ex);
        }
        CallContext.current().setEventDetails("Vm Id: " + svm.getId());
        return svm;
    }

    @Override
    public ServiceVirtualMachine createServiceInstance(DataCenter zone, Account owner, VirtualMachineTemplate template, ServiceOffering serviceOffering, String name,
        Network left, Network right) {
        logger.debug("createServiceInstance by " + owner.getAccountName());
        // TODO: permission model.
        // service instances need to be able to access the public network.
        if (left.getTrafficType() == TrafficType.Guest) {
            _networkModel.checkNetworkPermissions(owner, left);
        }
        if (right.getTrafficType() == TrafficType.Guest) {
            _networkModel.checkNetworkPermissions(owner, right);
        }

        final ApiConnector api = _manager.getApiConnector();
        VirtualNetworkModel leftModel = _manager.getDatabase().lookupVirtualNetwork(left.getUuid(),
                _manager.getCanonicalName(left), left.getTrafficType());
        if (leftModel == null) {
            throw new CloudRuntimeException("Unable to read virtual-network object");
        }
        VirtualNetworkModel rightModel = _manager.getDatabase().lookupVirtualNetwork(right.getUuid(),
                _manager.getCanonicalName(right), right.getTrafficType());
        if (rightModel == null) {
            throw new CloudRuntimeException("Unable to read virtual-network object");
        }

        net.juniper.contrail.api.types.Project project;
        try {
            project = _manager.getVncProject(owner.getDomainId(), owner.getAccountId());
        } catch (IOException ex) {
            logger.warn("read project", ex);
            throw new CloudRuntimeException(ex);
        }

        try {
            final String srvid = api.findByName(ServiceInstance.class, project, name);
            if (srvid != null) {
                throw new InvalidParameterValueException("service-instance " + name + " already exists uuid=" + srvid);
            }
        } catch (IOException ex) {
            logger.warn("service-instance lookup", ex);
            throw new CloudRuntimeException(ex);
        }

        // 1. Create service-instance.
        ServiceInstanceModel serviceModel = new ServiceInstanceModel(project, name, template, serviceOffering,
                leftModel, rightModel);

        try {
            serviceModel.update(_manager.getModelController());
        } catch (Exception ex) {
            logger.warn("service-instance update", ex);
            throw new CloudRuntimeException(ex);
        }

        logger.debug("service-instance object created");

        ServiceInstance siObj;
        try {
            _manager.getDatabase().getServiceInstances().add(serviceModel);
            siObj = serviceModel.getServiceInstance();
        } catch (Exception ex) {
            logger.warn("DB add", ex);
            throw new CloudRuntimeException(ex);
        }

        // 2. Create one virtual-machine.
        String svmName = name.replace(" ", "_") + "-1";
        ServiceVirtualMachine svm = createServiceVM(zone, owner, template, serviceOffering, svmName, siObj, left, right);

        logger.debug("created VMInstance " + svm.getUuid());

        // 3. Create the virtual-machine model and push the update.
        VirtualMachineModel instanceModel = new VirtualMachineModel(svm, svm.getUuid());
        _manager.getDatabase().getVirtualMachines().add(instanceModel);
        try {
            instanceModel.setServiceInstance(_manager.getModelController(), svm, serviceModel);
            instanceModel.update(_manager.getModelController());
        } catch (Exception ex) {
            logger.warn("service virtual-machine update", ex);
            throw new CloudRuntimeException(ex);
        }

        return svm;
    }

    @Override
    public void startServiceInstance(long instanceId) {
        logger.debug("start service instance " + instanceId);

        UserVmVO vm = _vmDao.findById(instanceId);
        _vmManager.start(vm.getUuid(), null);
    }

    @Override
    public ServiceInstanceResponse createServiceInstanceResponse(long instanceId) {
        logger.debug("ServiceInstance response for id: " + instanceId);

        UserVmVO vm = _vmDao.findById(instanceId);
        ServiceInstanceResponse response = new ServiceInstanceResponse();
        response.setId(vm.getUuid());
        Account owner = _accountService.getAccount(vm.getAccountId());

        if (owner.getType() == Account.Type.PROJECT) {
            Project project = ApiDBUtils.findProjectByProjectAccountIdIncludingRemoved(owner.getAccountId());
            response.setProjectId(project.getUuid());
            response.setProjectName(project.getName());
        } else {
            response.setAccountName(owner.getAccountName());
        }
        return response;
    }

}
