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

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.naming.ConfigurationException;


import com.cloud.agent.IAgentControl;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.UpdateBcfRouterCommand;
import com.cloud.agent.api.BcfAnswer;
import com.cloud.agent.api.CacheBcfTopologyCommand;
import com.cloud.agent.api.Command;
import com.cloud.agent.api.CreateBcfRouterCommand;
import com.cloud.agent.api.CreateBcfRouterInterfaceCommand;
import com.cloud.agent.api.CreateBcfSegmentCommand;
import com.cloud.agent.api.CreateBcfAttachmentCommand;
import com.cloud.agent.api.CreateBcfStaticNatCommand;
import com.cloud.agent.api.DeleteBcfSegmentCommand;
import com.cloud.agent.api.DeleteBcfAttachmentCommand;
import com.cloud.agent.api.DeleteBcfStaticNatCommand;
import com.cloud.agent.api.GetControllerDataAnswer;
import com.cloud.agent.api.GetControllerDataCommand;
import com.cloud.agent.api.MaintainAnswer;
import com.cloud.agent.api.MaintainCommand;
import com.cloud.agent.api.PingCommand;
import com.cloud.agent.api.ReadyAnswer;
import com.cloud.agent.api.ReadyCommand;
import com.cloud.agent.api.StartupBigSwitchBcfCommand;
import com.cloud.agent.api.StartupCommand;
import com.cloud.agent.api.SyncBcfTopologyCommand;
import com.cloud.agent.api.UpdateBcfAttachmentCommand;
import com.cloud.host.Host;
import com.cloud.host.Host.Type;
import com.cloud.network.bigswitch.AclData;
import com.cloud.network.bigswitch.BigSwitchBcfApi;
import com.cloud.network.bigswitch.BigSwitchBcfApiException;
import com.cloud.network.bigswitch.Capabilities;
import com.cloud.network.bigswitch.ControlClusterStatus;
import com.cloud.network.bigswitch.ControllerData;
import com.cloud.network.bigswitch.FloatingIpData;
import com.cloud.network.bigswitch.NetworkData;
import com.cloud.network.bigswitch.AttachmentData;
import com.cloud.network.bigswitch.RouterData;
import com.cloud.network.bigswitch.RouterInterfaceData;
import com.cloud.network.bigswitch.TopologyData;
import com.cloud.resource.ServerResource;
import com.cloud.utils.component.ManagerBase;

public class BigSwitchBcfResource extends ManagerBase implements ServerResource {

    private String _name;
    private String _guid;
    private String _zoneId;
    private int _numRetries;

    private BigSwitchBcfApi _bigswitchBcfApi;
    private TopologyData _latestTopology = null;
    private boolean initTopologySyncDone = false;

    protected BigSwitchBcfApi createBigSwitchBcfApi() {
        return new BigSwitchBcfApi();
    }

    @Override
    public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {

        _name = (String)params.get("name");
        if (_name == null) {
            throw new ConfigurationException("Unable to find name");
        }

        _guid = (String)params.get("guid");
        if (_guid == null) {
            throw new ConfigurationException("Unable to find the guid");
        }

        _zoneId = (String)params.get("zoneId");
        if (_zoneId == null) {
            throw new ConfigurationException("Unable to find zone");
        }

        _numRetries = 2;

        String hostname = (String)params.get("hostname");
        if (hostname == null) {
            throw new ConfigurationException("Missing host name from params: " + params);
        }

        String username = (String) params.get("username");
        if (username == null) {
            throw new ConfigurationException("Missing user name from params: " + params);
        }

        String password = (String) params.get("password");
        if (password == null) {
            throw new ConfigurationException("Missing password from params: " + params);
        }

        Boolean nat = Boolean.parseBoolean((String) params.get("nat"));
        if (nat == null) {
            throw new ConfigurationException("Missing password from params: " + params);
        }

        _bigswitchBcfApi = createBigSwitchBcfApi();
        _bigswitchBcfApi.setControllerAddress(hostname);
        _bigswitchBcfApi.setControllerUsername(username);
        _bigswitchBcfApi.setControllerPassword(password);
        _bigswitchBcfApi.setControllerNat(nat);
        _bigswitchBcfApi.setZoneId(_zoneId);

        return true;
    }

    @Override
    public boolean start() {
        return true;
    }

    @Override
    public boolean stop() {
        return true;
    }

    @Override
    public String getName() {
        return _name;
    }

    @Override
    public Type getType() {
        return Host.Type.L2Networking;
    }

    @Override
    public StartupCommand[] initialize() {
        StartupBigSwitchBcfCommand sc = new StartupBigSwitchBcfCommand();
        sc.setGuid(_guid);
        sc.setName(_name);
        sc.setDataCenter(_zoneId);
        sc.setPod("");
        sc.setPrivateIpAddress("");
        sc.setStorageIpAddress("");
        sc.setVersion("");
        return new StartupCommand[] {sc};
    }

    @Override
    public PingCommand getCurrentStatus(long id) {
        if(!initTopologySyncDone && _latestTopology != null){
            initTopologySyncDone = true;
            if(_bigswitchBcfApi.isNatEnabled()){
                try{
                    executeRequest(new SyncBcfTopologyCommand(true, true), _numRetries);
                } catch(Exception e){
                    logger.error("BigSwitch BCF sync error", e);
                }
            } else {
                try{
                    executeRequest(new SyncBcfTopologyCommand(true, false), _numRetries);
                } catch (Exception e){
                    logger.error("BigSwitch BCF sync error", e);
                }
            }
        }
        try {
            ControlClusterStatus ccs = _bigswitchBcfApi.getControlClusterStatus();
            if (!ccs.getStatus()) {
                logger.error("ControlCluster state is not ready: " + ccs.getStatus());
                return null;
            }
            if (ccs.isTopologySyncRequested()) {
                if(_latestTopology != null) {
                    if(_bigswitchBcfApi.isNatEnabled()){
                        executeRequest(new SyncBcfTopologyCommand(true, true), _numRetries);
                    } else {
                        executeRequest(new SyncBcfTopologyCommand(true, false), _numRetries);
                    }
                } else {
                    logger.debug("topology sync needed but no topology history");
                }
            }
        } catch (BigSwitchBcfApiException e) {
            logger.error("getControlClusterStatus failed", e);
            return null;
        }
        try {
            Capabilities cap = _bigswitchBcfApi.getCapabilities();

            // TODO: update controller status display, enable/disable service accordingly
            if (cap.isTopologySyncRequested()) {
                if(_latestTopology != null) {
                    if(_bigswitchBcfApi.isNatEnabled()){
                        executeRequest(new SyncBcfTopologyCommand(true, true), _numRetries);
                    } else {
                        executeRequest(new SyncBcfTopologyCommand(true, false), _numRetries);
                    }
                }
            }

        } catch (BigSwitchBcfApiException e) {
            logger.error("getCapabilities failed", e);
        }
        return new PingCommand(Host.Type.L2Networking, id);
    }

    @Override
    public Answer executeRequest(Command cmd) {
        return executeRequest(cmd, _numRetries);
    }

    public Answer executeRequest(Command cmd, int numRetries) {
        if (cmd instanceof ReadyCommand) {
            return executeRequest((ReadyCommand)cmd);
        } else if (cmd instanceof MaintainCommand) {
            return executeRequest((MaintainCommand)cmd);
        } else if (cmd instanceof CreateBcfSegmentCommand) {
            _latestTopology = ((CreateBcfSegmentCommand) cmd).getTopology();
            return executeRequest((CreateBcfSegmentCommand)cmd, numRetries);
        } else if (cmd instanceof DeleteBcfSegmentCommand) {
            _latestTopology = ((DeleteBcfSegmentCommand) cmd).getTopology();
            return executeRequest((DeleteBcfSegmentCommand)cmd, numRetries);
        } else if (cmd instanceof CreateBcfAttachmentCommand) {
            _latestTopology = ((CreateBcfAttachmentCommand) cmd).getTopology();
            return executeRequest((CreateBcfAttachmentCommand)cmd, numRetries);
        } else if (cmd instanceof DeleteBcfAttachmentCommand) {
            _latestTopology = ((DeleteBcfAttachmentCommand) cmd).getTopology();
            return executeRequest((DeleteBcfAttachmentCommand)cmd, numRetries);
        } else if (cmd instanceof UpdateBcfAttachmentCommand) {
            _latestTopology = ((UpdateBcfAttachmentCommand) cmd).getTopology();
            return executeRequest((UpdateBcfAttachmentCommand)cmd, numRetries);
        } else if (cmd instanceof CreateBcfRouterCommand) {
            _latestTopology = ((CreateBcfRouterCommand) cmd).getTopology();
            return executeRequest((CreateBcfRouterCommand)cmd, numRetries);
        } else if (cmd instanceof CreateBcfRouterInterfaceCommand) {
            _latestTopology = ((CreateBcfRouterInterfaceCommand) cmd).getTopology();
            return executeRequest((CreateBcfRouterInterfaceCommand)cmd, numRetries);
        } else if (cmd instanceof CreateBcfStaticNatCommand) {
            _latestTopology = ((CreateBcfStaticNatCommand) cmd).getTopology();
            return executeRequest((CreateBcfStaticNatCommand)cmd, numRetries);
        } else if (cmd instanceof DeleteBcfStaticNatCommand) {
            _latestTopology = ((DeleteBcfStaticNatCommand) cmd).getTopology();
            return executeRequest((DeleteBcfStaticNatCommand)cmd, numRetries);
        } else if (cmd instanceof UpdateBcfRouterCommand) {
            _latestTopology = ((UpdateBcfRouterCommand) cmd).getTopology();
            return executeRequest((UpdateBcfRouterCommand)cmd, numRetries);
        } else if (cmd instanceof SyncBcfTopologyCommand) {
            return executeRequest((SyncBcfTopologyCommand)cmd, numRetries);
        } else if (cmd instanceof CacheBcfTopologyCommand) {
            return executeRequest((CacheBcfTopologyCommand)cmd, numRetries);
        } else if (cmd instanceof GetControllerDataCommand) {
            return executeRequest((GetControllerDataCommand)cmd, numRetries);
        }
        logger.debug("Received unsupported command " + cmd.toString());
        return Answer.createUnsupportedCommandAnswer(cmd);
    }

    @Override
    public void disconnected() {
    }

    @Override
    public IAgentControl getAgentControl() {
        return null;
    }

    @Override
    public void setAgentControl(IAgentControl agentControl) {
    }

    public void setTopology(TopologyData topology){
        _latestTopology = topology;
    }

    public TopologyData getTopology(){
        return _latestTopology;
    }

    private Answer executeRequest(CreateBcfSegmentCommand cmd, int numRetries) {
        NetworkData network = new NetworkData();
        network.getNetwork().setTenantId(cmd.getTenantId());
        network.getNetwork().setTenantName(cmd.getTenantName());
        network.getNetwork().setId(cmd.getNetworkId());
        network.getNetwork().setName(truncate("segment-cloudstack-" + cmd.getNetworkName(), 64));
        network.getNetwork().setVlan(cmd.getVlan());

        try {
            String hash =_bigswitchBcfApi.createNetwork(network);
            return new BcfAnswer(cmd, true, "Segment " + network.getNetwork().getId() + " created", hash);
        } catch (BigSwitchBcfApiException e) {
            if (e.is_topologySyncRequested()) {
                cmd.setTopologySyncRequested(true);
                return new BcfAnswer(cmd, true, "Segment " + network.getNetwork().getId() + " created; topology sync required.");
            } else {
                if (numRetries > 0) {
                    return retry(cmd, --numRetries);
                } else {
                    return new BcfAnswer(cmd, e);
                }
            }
        }

    }

    private Answer executeRequest(DeleteBcfSegmentCommand cmd, int numRetries) {
        try {
            String hash = _bigswitchBcfApi.deleteNetwork(cmd.getTenantUuid(), cmd.getNetworkUuid());
            return new BcfAnswer(cmd, true, "Segment " + cmd.getNetworkUuid() + " deleted", hash);
        } catch (BigSwitchBcfApiException e) {
            if (e.is_topologySyncRequested()) {
                cmd.setTopologySyncRequested(true);
                return new BcfAnswer(cmd, true, "Segment " + cmd.getNetworkUuid() + " deleted; topology sync required.");
            } else {
                if (numRetries > 0) {
                    return retry(cmd, --numRetries);
                } else {
                    return new BcfAnswer(cmd, e);
                }
            }
        }
    }

    private Answer executeRequest(CreateBcfAttachmentCommand cmd, int numRetries) {
        AttachmentData attachment = new AttachmentData();
        attachment.getAttachment().setId(cmd.getNicId());
        attachment.getAttachment().setHostId(cmd.getPortId());
        attachment.getAttachment().setTenantName(cmd.getTenantName());
        attachment.getAttachment().setVlan(cmd.getVlan());
        attachment.getAttachment().addIpv4(cmd.getIpv4());
        attachment.getAttachment().setMac(cmd.getMac());

        try {
            String hash = _bigswitchBcfApi.createAttachment(cmd.getTenantId(), cmd.getNetworkId(), attachment);
            return new BcfAnswer(cmd, true, "network attachment " + cmd.getPortId() + " created", hash);
        } catch (BigSwitchBcfApiException e) {
            if (e.is_topologySyncRequested()) {
                cmd.setTopologySyncRequested(true);
                return new BcfAnswer(cmd, true, "network attachment " + cmd.getPortId() + " created; topology sync required.");
            } else {
                if (numRetries > 0) {
                    return retry(cmd, --numRetries);
                } else {
                    return new BcfAnswer(cmd, e);
                }
            }
        }
    }

    private Answer executeRequest(DeleteBcfAttachmentCommand cmd, int numRetries) {
        String nicName = cmd.getAttachmentId();
        try {
            String hash = _bigswitchBcfApi.deleteAttachment(cmd.getTenantId(), cmd.getNetworkId(), nicName);
            return new BcfAnswer(cmd, true, "network attachment " + nicName + " deleted", hash);
        } catch (BigSwitchBcfApiException e) {
            if (e.is_topologySyncRequested()) {
                cmd.setTopologySyncRequested(true);
                return new BcfAnswer(cmd, true, "network attachment " + nicName + " deleted; topology sync required.");
            } else {
                if (numRetries > 0) {
                    return retry(cmd, --numRetries);
                } else {
                    return new BcfAnswer(cmd, e);
                }
            }
        }
    }

    private Answer executeRequest(UpdateBcfAttachmentCommand cmd, int numRetries) {
        AttachmentData attachment = new AttachmentData();
        attachment.getAttachment().setId(cmd.getAttachmentId());
        attachment.getAttachment().setTenantName(cmd.getTenantId());

        try {
            String hash = _bigswitchBcfApi.modifyAttachment(cmd.getTenantId(), cmd.getNetworkId(), attachment);
            return new BcfAnswer(cmd, true, "Network attachment  " + cmd.getAttachmentId() + " updated", hash);
        } catch (BigSwitchBcfApiException e) {
            if (e.is_topologySyncRequested()) {
                cmd.setTopologySyncRequested(true);
                return new BcfAnswer(cmd, true, "Network attachment  " + cmd.getAttachmentId() + " updated; topology sync required.");
            } else {
                if (numRetries > 0) {
                    return retry(cmd, --numRetries);
                } else {
                    return new BcfAnswer(cmd, e);
                }
            }
        }
    }

    private Answer executeRequest(CreateBcfStaticNatCommand cmd, int numRetries) {
        FloatingIpData fip = new FloatingIpData();
        fip.setTenantId(cmd.getTenantId());
        fip.setNetworkId(cmd.getNetworkId());
        fip.setFixedIp(cmd.getPrivateIp());
        fip.setFloatingIpAndId(cmd.getPublicIp());
        fip.setMac(cmd.getMac());

        try {
            String hash = _bigswitchBcfApi.createFloatingIp(cmd.getTenantId(), fip);
            return new BcfAnswer(cmd, true, "floating ip " + cmd.getPublicIp() + ":" +
                    cmd.getPrivateIp() + " created", hash);
        } catch (BigSwitchBcfApiException e) {
            if (e.is_topologySyncRequested()) {
                cmd.setTopologySyncRequested(true);
                return new BcfAnswer(cmd, true, "floating ip " + cmd.getPublicIp() + ":" +
                        cmd.getPrivateIp() + " created; topology sync required.");
            } else {
                if (numRetries > 0) {
                    return retry(cmd, --numRetries);
                } else {
                    return new BcfAnswer(cmd, e);
                }
            }
        }
    }

    private Answer executeRequest(DeleteBcfStaticNatCommand cmd, int numRetries) {
        try {
            String hash = _bigswitchBcfApi.deleteFloatingIp(cmd.getTenantId(), cmd.getFloatingIpId());
            return new BcfAnswer(cmd, true, "floating ip " + cmd.getPublicIp() + " deleted", hash);
        } catch (BigSwitchBcfApiException e) {
            if (e.is_topologySyncRequested()) {
                cmd.setTopologySyncRequested(true);
                return new BcfAnswer(cmd, true, "floating ip " + cmd.getPublicIp() + " deleted; topology sync required.");
            } else {
                if (numRetries > 0) {
                    return retry(cmd, --numRetries);
                } else {
                    return new BcfAnswer(cmd, e);
                }
            }
        }
    }

    private Answer executeRequest(CreateBcfRouterCommand cmd, int numRetries) {
        RouterData router = new RouterData(cmd.getTenantId());
        try {
            String hash;
            hash = _bigswitchBcfApi.createRouter(cmd.getTenantId(), router);

            return new BcfAnswer(cmd, true, "router " + cmd.getTenantId() +
                    " created.", hash);
        } catch (BigSwitchBcfApiException e) {
            if (e.is_topologySyncRequested()) {
                cmd.setTopologySyncRequested(true);
                return new BcfAnswer(cmd, true, " created; topology sync required.");
            } else {
                if (numRetries > 0) {
                    return retry(cmd, --numRetries);
                } else {
                    return new BcfAnswer(cmd, e);
                }
            }
        }
    }

    private Answer executeRequest(CreateBcfRouterInterfaceCommand cmd, int numRetries) {
        RouterInterfaceData routerInterface = new RouterInterfaceData(cmd.getTenantId(),
                cmd.getGateway(), cmd.getCidr(), cmd.getNetworkId(), cmd.getNetworkName());
        try {
            String hash;
            hash = _bigswitchBcfApi.createRouterInterface(cmd.getTenantId(),
                    cmd.getTenantId(), routerInterface);

            return new BcfAnswer(cmd, true, "router " + cmd.getTenantId() +
                    " created.", hash);
        } catch (BigSwitchBcfApiException e) {
            if (e.is_topologySyncRequested()) {
                cmd.setTopologySyncRequested(true);
                return new BcfAnswer(cmd, true, " created; topology sync required.");
            } else {
                if (numRetries > 0) {
                    return retry(cmd, --numRetries);
                } else {
                    return new BcfAnswer(cmd, e);
                }
            }
        }
    }

    private Answer executeRequest(UpdateBcfRouterCommand cmd, int numRetries){
        RouterData routerData = new RouterData(cmd.getTenantId());

        List<AclData> acls = new ArrayList<AclData>();
        acls.addAll(cmd.getAcls());
        routerData.getRouter().getAcls().addAll(acls);

        routerData.getRouter().addExternalGateway(cmd.getPublicIp());

        try {
            String hash = _bigswitchBcfApi.modifyRouter(cmd.getTenantId(), routerData);
            return new BcfAnswer(cmd, true, "tenant " + cmd.getTenantId() + " router updated", hash);
        } catch (BigSwitchBcfApiException e) {
            if (e.is_topologySyncRequested()) {
                cmd.setTopologySyncRequested(true);
                return new BcfAnswer(cmd, true, "tenant " + cmd.getTenantId() + " router updated but topology sync required.");
            } else {
                if (numRetries > 0) {
                    return retry(cmd, --numRetries);
                } else {
                    return new BcfAnswer(cmd, e);
                }
            }
        } catch (IllegalArgumentException e1){
            return new BcfAnswer(cmd, false, "Illegal argument in BCF router update");
        }
    }

    private Answer executeRequest(SyncBcfTopologyCommand cmd, int numRetries) {
        try {
            TopologyData topo = _latestTopology;
            if (!cmd.isNetworkIncluded()) {
                topo.clearNetworks();
            }
            if(!cmd.isRouterIncluded()) {
                topo.clearRouters();
            }
            String hash = _bigswitchBcfApi.syncTopology(topo);
            if(!initTopologySyncDone){
                initTopologySyncDone=true;
            }
            return new BcfAnswer(cmd, true, "BCF topology synced", hash);
        } catch (BigSwitchBcfApiException e) {
            if (numRetries > 0) {
                return retry(cmd, --numRetries);
            } else {
                return new BcfAnswer(cmd, e);
            }
        } catch (IllegalArgumentException e1){
            return new BcfAnswer(cmd, false, "Illegal argument in BCF topology sync");
        }
    }

    private Answer executeRequest(CacheBcfTopologyCommand cmd, int numRetries) {
        _latestTopology = cmd.getTopology();
        return new Answer(cmd, true, "BCF topology cached");
    }

    private Answer executeRequest(GetControllerDataCommand cmd, int numRetries) {
        ControllerData controller = _bigswitchBcfApi.getControllerData();
        return new GetControllerDataAnswer(cmd,
                controller.getIpAddress(),
                controller.isPrimary());
    }

    private Answer executeRequest(ReadyCommand cmd) {
        return new ReadyAnswer(cmd);
    }

    private Answer executeRequest(MaintainCommand cmd) {
        return new MaintainAnswer(cmd);
    }

    private Answer retry(Command cmd, int numRetries) {
        logger.warn("Retrying " + cmd.getClass().getSimpleName() + ". Number of retries remaining: " + numRetries);
        return executeRequest(cmd, numRetries);
    }

    private String truncate(String string, int length) {
        if (string.length() <= length) {
            return string;
        } else {
            return string.substring(0, length);
        }
    }
}
