// 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.element;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import javax.inject.Inject;
import javax.naming.ConfigurationException;

import org.apache.log4j.Logger;

import org.apache.cloudstack.api.commands.AddSspCmd;
import org.apache.cloudstack.api.commands.DeleteSspCmd;
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.network.dao.SspCredentialDao;
import org.apache.cloudstack.network.dao.SspCredentialVO;
import org.apache.cloudstack.network.dao.SspTenantDao;
import org.apache.cloudstack.network.dao.SspTenantVO;
import org.apache.cloudstack.network.dao.SspUuidDao;
import org.apache.cloudstack.network.dao.SspUuidVO;

import com.cloud.dc.dao.DataCenterDao;
import com.cloud.deploy.DeployDestination;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.InsufficientCapacityException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.host.Host;
import com.cloud.host.HostVO;
import com.cloud.host.dao.HostDao;
import com.cloud.network.Network;
import com.cloud.network.Network.Capability;
import com.cloud.network.Network.Provider;
import com.cloud.network.Network.Service;
import com.cloud.network.NetworkMigrationResponder;
import com.cloud.network.NetworkModel;
import com.cloud.network.Networks.BroadcastDomainType;
import com.cloud.network.PhysicalNetwork;
import com.cloud.network.PhysicalNetworkServiceProvider;
import com.cloud.network.PhysicalNetworkServiceProvider.State;
import com.cloud.network.dao.NetworkServiceMapDao;
import com.cloud.network.dao.PhysicalNetworkDao;
import com.cloud.network.dao.PhysicalNetworkServiceProviderDao;
import com.cloud.network.dao.PhysicalNetworkServiceProviderVO;
import com.cloud.network.element.ConnectivityProvider;
import com.cloud.offering.NetworkOffering;
import com.cloud.resource.ResourceManager;
import com.cloud.utils.component.AdapterBase;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.NicProfile;
import com.cloud.vm.NicVO;
import com.cloud.vm.ReservationContext;
import com.cloud.vm.VirtualMachineProfile;
import com.cloud.vm.dao.NicDao;

/**
 * Stratosphere sdn platform network element
 *
 * This class will be called per network setup operations.
 * This class also have ssp specific methods.
 *
 * Current implementation use HostVO for storage of api endpoint information,
 * but note this is not necessary. The other way is create our own database
 * table for that information.
 */
public class SspElement extends AdapterBase implements ConnectivityProvider, SspManager, SspService, NetworkMigrationResponder {
    private static final Logger s_logger = Logger.getLogger(SspElement.class);
    public static final String s_SSP_NAME = "StratosphereSsp";
    private static final Provider s_ssp_provider = new Provider(s_SSP_NAME, false);

    @Inject
    NetworkServiceMapDao _ntwkSrvcDao;
    @Inject
    NetworkModel _networkModel;
    @Inject
    NetworkOrchestrationService _networkMgr;
    @Inject
    ResourceManager _resourceMgr;
    @Inject
    PhysicalNetworkDao _physicalNetworkDao;
    @Inject
    PhysicalNetworkServiceProviderDao _physicalNetworkServiceProviderDao;
    @Inject
    SspCredentialDao _sspCredentialDao;
    @Inject
    SspTenantDao _sspTenantDao;
    @Inject
    SspUuidDao _sspUuidDao;
    @Inject
    DataCenterDao _dcDao;
    @Inject
    HostDao _hostDao;
    @Inject
    ConfigurationDao _configDao;
    @Inject
    NicDao _nicDao = null;

    @Override
    public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
        return super.configure(name, params);
    }

    @Override
    public Map<Service, Map<Capability, String>> getCapabilities() {
        Map<Service, Map<Capability, String>> capabilities = new HashMap<Service, Map<Capability, String>>();
        capabilities.put(Service.Connectivity, new HashMap<Capability, String>()); // XXX: We might need some more category here.
        return capabilities;
    }

    @Override
    public Provider getProvider() {
        return s_ssp_provider;
    }

    private List<SspClient> fetchSspClients(Long physicalNetworkId, Long dataCenterId, boolean enabledOnly) {
        ArrayList<SspClient> clients = new ArrayList<SspClient>();

        boolean provider_found = false;
        PhysicalNetworkServiceProviderVO provider = _physicalNetworkServiceProviderDao.findByServiceProvider(physicalNetworkId, s_SSP_NAME);
        if (enabledOnly) {
            if (provider != null && provider.getState() == State.Enabled) {
                provider_found = true;
            }
        } else {
            provider_found = true;
        }

        if (physicalNetworkId != null && provider_found) {
            SspCredentialVO credential = _sspCredentialDao.findByZone(dataCenterId);
            List<HostVO> hosts = _resourceMgr.listAllHostsInOneZoneByType(Host.Type.L2Networking, dataCenterId);
            for (HostVO host : hosts) {
                assert (credential != null);
                _hostDao.loadDetails(host);
                if ("v1Api".equals(host.getDetail("sspHost"))) {
                    clients.add(new SspClient(host.getDetail("url"), credential.getUsername(), credential.getPassword()));
                }
            }
        }
        if (clients.size() == 0) {
            String global_apiUrl = _configDao.getValueAndInitIfNotExist("ssp.url", "Network", null);
            String global_username = _configDao.getValueAndInitIfNotExist("ssp.username", "Network", null);
            String global_password = _configDao.getValueAndInitIfNotExist("ssp.password", "Network", null);
            if (global_apiUrl != null && global_username != null && global_password != null) {
                clients.add(new SspClient(global_apiUrl, global_username, global_password));
            }
        }
        return clients;
    }

    /* (non-Javadoc)
     * @see org.apache.cloudstack.network.element.NetworkElement#isReady(com.cloud.network.PhysicalNetworkServiceProvider)
     */
    @Override
    public boolean isReady(PhysicalNetworkServiceProvider provider) {
        PhysicalNetwork physicalNetwork = _physicalNetworkDao.findById(provider.getPhysicalNetworkId());
        assert (physicalNetwork != null);
        if (fetchSspClients(physicalNetwork.getId(), physicalNetwork.getDataCenterId(), false).size() > 0) {
            return true;
        }
        s_logger.warn("Ssp api endpoint not found. " + physicalNetwork.toString());
        return false;
    }

    /* (non-Javadoc)
     * If this element is ready, then it can be enabled.
     * @see org.apache.cloudstack.network.element.SspManager#isEnabled(com.cloud.network.PhysicalNetwork)
     */
    @Override
    public boolean canHandle(PhysicalNetwork physicalNetwork) {
        if (physicalNetwork != null) {
            if (fetchSspClients(physicalNetwork.getId(), physicalNetwork.getDataCenterId(), true).size() > 0) {
                return true;
            }
            s_logger.warn("enabled Ssp api endpoint not found. " + physicalNetwork.toString());
        } else {
            s_logger.warn("PhysicalNetwork is NULL.");
        }
        return false;
    }

    private boolean canHandle(Network network) {
        if (canHandle(_physicalNetworkDao.findById(network.getPhysicalNetworkId()))) {
            if (!_ntwkSrvcDao.canProviderSupportServiceInNetwork(network.getId(), Service.Connectivity, getProvider())) {
                s_logger.info("SSP is implicitly active for " + network);
            }
            return true;
        }
        return false;
    }

    @Override
    public Host addSspHost(AddSspCmd cmd) {
        SspClient client = new SspClient(cmd.getUrl(), cmd.getUsername(), cmd.getPassword());
        if (!client.login()) {
            throw new CloudRuntimeException("Ssp login failed.");
        }

        long zoneId = cmd.getZoneId();
        SspCredentialVO credential = _sspCredentialDao.findByZone(zoneId);
        if (credential == null) {
            if (cmd.getUsername() == null || cmd.getPassword() == null) {
                throw new InvalidParameterValueException("Initial credential required for zone: " + zoneId);
            }
            credential = new SspCredentialVO();
            credential.setZoneId(zoneId);
            credential.setUsername(cmd.getUsername());
            credential.setPassword(cmd.getPassword());
            _sspCredentialDao.persist(credential);
        } else {
            if (cmd.getUsername() != null || cmd.getPassword() != null) {
                s_logger.warn("Tenant credential already configured for zone:" + zoneId);
            }
        }

        String tenantUuid = _sspTenantDao.findUuidByZone(zoneId);
        if (tenantUuid == null) {
            if (cmd.getTenantUuid() == null) {
                throw new InvalidParameterValueException("Initial tenant uuid required for zone: " + zoneId);
            }
            SspTenantVO tenant = new SspTenantVO();
            tenant.setZoneId(zoneId);
            tenant.setUuid(cmd.getTenantUuid());
            _sspTenantDao.persist(tenant);
        } else {
            if (cmd.getTenantUuid() != null) {
                s_logger.warn("Tenant uuid already configured for zone:" + zoneId);
            }
        }

        String normalizedUrl = null;
        String hostname = null;
        try {
            URL url = new URL(cmd.getUrl());
            normalizedUrl = url.toString();
            hostname = url.getHost();
        } catch (MalformedURLException e1) {
            throw new CloudRuntimeException("Invalid url " + cmd.getUrl());
        }

        List<HostVO> hosts = _resourceMgr.listAllHostsInOneZoneByType(Host.Type.L2Networking, zoneId);
        for (HostVO host : hosts) {
            assert (credential != null);
            _hostDao.loadDetails(host);
            if ("v1Api".equals(host.getDetail("sspHost"))) {
                if (normalizedUrl.equals(host.getDetail("url"))) {
                    s_logger.warn("Ssp host already registered " + normalizedUrl);
                    return host;
                }
            }
        }
        // SspHost HostVO will be created per zone and url.
        HostVO host = new HostVO(UUID.randomUUID().toString());
        host.setDataCenterId(zoneId);
        host.setType(Host.Type.L2Networking);
        host.setPrivateIpAddress(hostname); // db schema not null. It may be a name, not IP address.
        //        host.setPrivateMacAddress(""); // db schema nullable
        //        host.setPrivateNetmask(""); // db schema nullable
        host.setVersion("1"); // strange db schema not null
        host.setName(cmd.getName());

        host.setDetails(new HashMap<String, String>());
        host.setDetail("sspHost", "v1Api");
        host.setDetail("url", normalizedUrl);
        return _hostDao.persist(host);
    }

    @Override
    public boolean deleteSspHost(DeleteSspCmd cmd) {
        s_logger.info("deleteStratosphereSsp");
        return _hostDao.remove(cmd.getHostId());
    }

    @Override
    public boolean createNetwork(Network network, NetworkOffering offering, DeployDestination dest, ReservationContext context) {
        if (_sspUuidDao.findUuidByNetwork(network) != null) {
            s_logger.info("Network already has ssp TenantNetwork uuid :" + network.toString());
            return true;
        }
        if (!canHandle(network)) {
            return false;
        }

        String tenantUuid = _sspTenantDao.findUuidByZone(network.getDataCenterId());
        if (tenantUuid == null) {
            tenantUuid = _configDao.getValueAndInitIfNotExist("ssp.tenant", "Network", null);
        }

        boolean processed = false;
        for (SspClient client : fetchSspClients(network.getPhysicalNetworkId(), network.getDataCenterId(), true)) {
            SspClient.TenantNetwork sspNet = client.createTenantNetwork(tenantUuid, network.getName());
            if (sspNet != null) {
                SspUuidVO uuid = new SspUuidVO();
                uuid.setUuid(sspNet.uuid);
                uuid.setObjClass(SspUuidVO.objClassNetwork);
                uuid.setObjId(network.getId());
                _sspUuidDao.persist(uuid);
                return true;
            }
            processed = true;
        }
        if (processed) {
            s_logger.error("Could not allocate an uuid for network " + network.toString());
            return false;
        } else {
            s_logger.error("Skipping #createNetwork() for " + network.toString());
            return true;
        }
    }

    @Override
    public boolean deleteNetwork(Network network) {
        String tenantNetworkUuid = _sspUuidDao.findUuidByNetwork(network);
        if (tenantNetworkUuid != null) {
            boolean processed = false;
            for (SspClient client : fetchSspClients(network.getPhysicalNetworkId(), network.getDataCenterId(), true)) {
                if (client.deleteTenantNetwork(tenantNetworkUuid)) {
                    _sspUuidDao.removeUuid(tenantNetworkUuid);
                    processed = true;
                    break;
                }
            }
            if (!processed) {
                s_logger.error("Ssp api tenant network deletion failed " + network.toString());
            }
        } else {
            s_logger.debug("Silently skipping #deleteNetwork() for " + network.toString());
        }
        return true;
    }

    // we use context.reservationId for dedup of guru & element operations.
    @Override
    public boolean createNicEnv(Network network, NicProfile nic, DeployDestination dest, ReservationContext context) {
        String tenantNetworkUuid = _sspUuidDao.findUuidByNetwork(network);
        if (tenantNetworkUuid == null) {
            s_logger.debug("Skipping #createNicEnv() for nic on " + network.toString());
            return true;
        }

        String reservationId = context.getReservationId();
        List<SspUuidVO> tenantPortUuidVos = _sspUuidDao.listUUidVoByNicProfile(nic);
        for (SspUuidVO tenantPortUuidVo : tenantPortUuidVos) {
            if (reservationId.equals(tenantPortUuidVo.getReservationId())) {
                s_logger.info("Skipping because reservation found " + reservationId);
                return true;
            }
        }

        String tenantPortUuid = null;
        for (SspClient client : fetchSspClients(network.getPhysicalNetworkId(), network.getDataCenterId(), true)) {
            SspClient.TenantPort sspPort = client.createTenantPort(tenantNetworkUuid);
            if (sspPort != null) {
                tenantPortUuid = sspPort.uuid;
                nic.setReservationId(reservationId);

                SspUuidVO uuid = new SspUuidVO();
                uuid.setUuid(tenantPortUuid);
                uuid.setObjClass(SspUuidVO.objClassNicProfile);
                uuid.setObjId(nic.getId());
                uuid.setReservationId(reservationId);
                _sspUuidDao.persist(uuid);
                break;
            }
        }
        if (tenantPortUuid == null) {
            s_logger.debug("#createNicEnv() failed for nic on " + network.toString());
            return false;
        }

        for (SspClient client : fetchSspClients(network.getPhysicalNetworkId(), network.getDataCenterId(), true)) {
            SspClient.TenantPort sspPort = client.updateTenantVifBinding(tenantPortUuid, dest.getHost().getPrivateIpAddress());
            if (sspPort != null) {
                if (sspPort.vlanId != null) {
                    nic.setBroadcastType(BroadcastDomainType.Vlan);
                    nic.setBroadcastUri(BroadcastDomainType.Vlan.toUri(String.valueOf(sspPort.vlanId)));
                }
                return true;
            }
        }
        s_logger.error("Updating vif failed " + nic.toString());
        return false;
    }

    @Override
    public boolean deleteNicEnv(Network network, NicProfile nic, ReservationContext context) {
        if (context == null) {
            s_logger.error("ReservationContext was null for " + nic + " " + network);
            return false;
        }
        String reservationId = context.getReservationId();

        SspUuidVO deleteTarget = null;
        SspUuidVO remainingTarget = null;
        List<SspUuidVO> tenantPortUuidVos = _sspUuidDao.listUUidVoByNicProfile(nic);
        for (SspUuidVO tenantPortUuidVo : tenantPortUuidVos) {
            if (reservationId.equals(tenantPortUuidVo.getReservationId())) {
                deleteTarget = tenantPortUuidVo;
            } else {
                remainingTarget = tenantPortUuidVo;
            }
        }

        if (deleteTarget != null) { // delete the target ssp uuid (tenant-port)
            String tenantPortUuid = deleteTarget.getUuid();
            boolean processed = false;
            for (SspClient client : fetchSspClients(network.getPhysicalNetworkId(), network.getDataCenterId(), true)) {
                SspClient.TenantPort sspPort = client.updateTenantVifBinding(tenantPortUuid, null);
                if (sspPort != null) {
                    processed = true;
                    break;
                }
            }
            if (!processed) {
                s_logger.warn("Ssp api nic detach failed " + nic.toString());
            }
            processed = false;
            for (SspClient client : fetchSspClients(network.getPhysicalNetworkId(), network.getDataCenterId(), true)) {
                if (client.deleteTenantPort(tenantPortUuid)) {
                    _sspUuidDao.removeUuid(tenantPortUuid);
                    processed = true;
                    break;
                }
            }
            if (!processed) {
                s_logger.warn("Ssp api tenant port deletion failed " + nic.toString());
            }
            _sspUuidDao.removeUuid(tenantPortUuid);
        }
        if (remainingTarget != null) {
            NicVO nicVo = _nicDao.findById(nic.getId());
            nicVo.setReservationId(remainingTarget.getReservationId());
            _nicDao.persist(nicVo); // persist the new reservationId
        }
        return true;
    }

    /* (non-Javadoc)
     * Implements a network using ssp element.
     *
     * This method will be called right after NetworkGuru#implement().
     * see also {@link #shutdown(Network, ReservationContext, boolean)}
     * @see org.apache.cloudstack.network.element.NetworkElement#implement(com.cloud.network.Network, com.cloud.offering.NetworkOffering, com.cloud.deploy.DeployDestination, com.cloud.vm.ReservationContext)
     */
    @Override
    public boolean implement(Network network, NetworkOffering offering, DeployDestination dest, ReservationContext context) throws ConcurrentOperationException,
        ResourceUnavailableException, InsufficientCapacityException {
        s_logger.info("implement");
        return createNetwork(network, offering, dest, context);
    }

    /* (non-Javadoc)
     * Shutdown the network implementation
     *
     * This method will be called right BEFORE NetworkGuru#shutdown().
     * The entities was acquired by {@link #implement(Network, NetworkOffering, DeployDestination, ReservationContext)}
     * @see org.apache.cloudstack.network.element.NetworkElement#shutdown(com.cloud.network.Network, com.cloud.vm.ReservationContext, boolean)
     */
    @Override
    public boolean shutdown(Network network, ReservationContext context, boolean cleanup) throws ConcurrentOperationException, ResourceUnavailableException {
        s_logger.trace("shutdown");
        return deleteNetwork(network);
    }

    /* (non-Javadoc)
     * Prepares a network environment for a VM nic.
     *
     * This method will be called right after NetworkGuru#reserve().
     * The entities will be released by {@link #release(Network, NicProfile, VirtualMachineProfile, ReservationContext)}
     * @see org.apache.cloudstack.network.element.NetworkElement#prepare(com.cloud.network.Network, com.cloud.vm.NicProfile, com.cloud.vm.VirtualMachineProfile, com.cloud.deploy.DeployDestination, com.cloud.vm.ReservationContext)
     */
    @Override
    public boolean prepare(Network network, NicProfile nic, VirtualMachineProfile vm, DeployDestination dest, ReservationContext context)
        throws ConcurrentOperationException, ResourceUnavailableException, InsufficientCapacityException {
        s_logger.trace("prepare");
        return createNicEnv(network, nic, dest, context);
    }

    /* (non-Javadoc)
     * Release the network environment that was prepared for a VM nic.
     *
     * This method will be called right AFTER NetworkGuru#release().
     * The entities was acquired in {@link #prepare(Network, NicProfile, VirtualMachineProfile, DeployDestination, ReservationContext)}
     * @see org.apache.cloudstack.network.element.NetworkElement#release(com.cloud.network.Network, com.cloud.vm.NicProfile, com.cloud.vm.VirtualMachineProfile, com.cloud.vm.ReservationContext)
     */
    @Override
    public boolean release(Network network, NicProfile nic, VirtualMachineProfile vm, ReservationContext context) throws ConcurrentOperationException,
        ResourceUnavailableException {
        s_logger.trace("release");
        return deleteNicEnv(network, nic, context);
    }

    /* (non-Javadoc)
     * Destroy a network implementation.
     *
     * This method will be called right BEFORE NetworkGuru#trash() in "Expunge" phase.
     * @see org.apache.cloudstack.network.element.NetworkElement#destroy(com.cloud.network.Network)
     */
    @Override
    public boolean destroy(Network network, ReservationContext context) throws ConcurrentOperationException, ResourceUnavailableException {
        s_logger.trace("destroy");
        // nothing to do here.
        return true;
    }

    @Override
    public boolean shutdownProviderInstances(PhysicalNetworkServiceProvider provider, ReservationContext context) throws ConcurrentOperationException,
        ResourceUnavailableException {
        s_logger.trace("shutdownProviderInstances");
        return true;
    }

    @Override
    public boolean canEnableIndividualServices() {
        s_logger.trace("canEnableIndividualServices");
        return true; // because there is only Connectivity
    }

    @Override
    public boolean verifyServicesCombination(Set<Service> services) {
        s_logger.trace("verifyServicesCombination " + services.toString());
        return true;
    }

    @Override
    public boolean prepareMigration(NicProfile nic, Network network, VirtualMachineProfile vm, DeployDestination dest, ReservationContext context) {
        try {
            prepare(network, nic, vm, dest, context);
        } catch (ConcurrentOperationException e) {
            s_logger.error("prepareForMigration failed.", e);
            return false;
        } catch (ResourceUnavailableException e) {
            s_logger.error("prepareForMigration failed.", e);
            return false;
        } catch (InsufficientCapacityException e) {
            s_logger.error("prepareForMigration failed.", e);
            return false;
        }
        return true;
    }

    @Override
    public void rollbackMigration(NicProfile nic, Network network, VirtualMachineProfile vm, ReservationContext src, ReservationContext dst) {
        try {
            release(network, nic, vm, dst);
        } catch (ConcurrentOperationException e) {
            s_logger.error("rollbackMigration failed.", e);
        } catch (ResourceUnavailableException e) {
            s_logger.error("rollbackMigration failed.", e);
        }
    }

    @Override
    public void commitMigration(NicProfile nic, Network network, VirtualMachineProfile vm, ReservationContext src, ReservationContext dst) {
        try {
            release(network, nic, vm, src);
        } catch (ConcurrentOperationException e) {
            s_logger.error("commitMigration failed.", e);
        } catch (ResourceUnavailableException e) {
            s_logger.error("commitMigration failed.", e);
        }
    }

    @Override
    public List<Class<?>> getCommands() {
        return Arrays.<Class<?>> asList(AddSspCmd.class, DeleteSspCmd.class);
    }
}
