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

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.List;

import javax.inject.Inject;

import org.apache.log4j.Logger;
import org.springframework.stereotype.Component;

import org.apache.cloudstack.api.command.admin.network.CreateStorageNetworkIpRangeCmd;
import org.apache.cloudstack.api.command.admin.network.DeleteStorageNetworkIpRangeCmd;
import org.apache.cloudstack.api.command.admin.network.ListStorageNetworkIpRangeCmd;
import org.apache.cloudstack.api.command.admin.network.UpdateStorageNetworkIpRangeCmd;

import com.cloud.dc.HostPodVO;
import com.cloud.dc.StorageNetworkIpAddressVO;
import com.cloud.dc.StorageNetworkIpRange;
import com.cloud.dc.StorageNetworkIpRangeVO;
import com.cloud.dc.dao.HostPodDao;
import com.cloud.dc.dao.StorageNetworkIpAddressDao;
import com.cloud.dc.dao.StorageNetworkIpRangeDao;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.network.Networks.TrafficType;
import com.cloud.network.dao.NetworkDao;
import com.cloud.network.dao.NetworkVO;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.db.DB;
import com.cloud.utils.db.QueryBuilder;
import com.cloud.utils.db.SearchCriteria.Op;
import com.cloud.utils.db.Transaction;
import com.cloud.utils.db.TransactionCallbackNoReturn;
import com.cloud.utils.db.TransactionCallbackWithException;
import com.cloud.utils.db.TransactionLegacy;
import com.cloud.utils.db.TransactionStatus;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.net.NetUtils;
import com.cloud.vm.SecondaryStorageVmVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.dao.SecondaryStorageVmDao;

@Component
public class StorageNetworkManagerImpl extends ManagerBase implements StorageNetworkManager, StorageNetworkService {
    private static final Logger s_logger = Logger.getLogger(StorageNetworkManagerImpl.class);

    @Inject
    StorageNetworkIpAddressDao _sNwIpDao;
    @Inject
    StorageNetworkIpRangeDao _sNwIpRangeDao;
    @Inject
    NetworkDao _networkDao;
    @Inject
    HostPodDao _podDao;
    @Inject
    SecondaryStorageVmDao _ssvmDao;

    private void checkOverlapPrivateIpRange(long podId, String startIp, String endIp) {
        HostPodVO pod = _podDao.findById(podId);
        if (pod == null) {
            throw new CloudRuntimeException("Cannot find pod " + podId);
        }

        final String[] existingPodIpRanges = pod.getDescription().split(",");

        for(String podIpRange: existingPodIpRanges) {
            final String[] existingPodIpRange = podIpRange.split("-");

            if (existingPodIpRange.length > 1) {
                if (!NetUtils.isValidIp4(existingPodIpRange[0]) || !NetUtils.isValidIp4(existingPodIpRange[1])) {
                    continue;
                }

                if (NetUtils.ipRangesOverlap(startIp, endIp, existingPodIpRange[0], existingPodIpRange[1])) {
                    throw new InvalidParameterValueException("The Storage network Start IP and endIP address range overlap with private IP :" + existingPodIpRange[0] + ":" + existingPodIpRange[1]);
                }
            }
        }
    }

    private void checkOverlapStorageIpRange(long podId, String startIp, String endIp) {
        List<StorageNetworkIpRangeVO> curRanges = _sNwIpRangeDao.listByPodId(podId);
        for (StorageNetworkIpRangeVO range : curRanges) {
            if (NetUtils.ipRangesOverlap(startIp, endIp, range.getStartIp(), range.getEndIp())) {
                throw new InvalidParameterValueException("The Storage network Start IP and endIP address range overlap with private IP :" + range.getStartIp() + " - " +
                    range.getEndIp());
            }
        }
    }

    private void createStorageIpEntires(TransactionLegacy txn, long rangeId, String startIp, String endIp, long zoneId) throws SQLException {
        long startIPLong = NetUtils.ip2Long(startIp);
        long endIPLong = NetUtils.ip2Long(endIp);
        String insertSql =
            "INSERT INTO `cloud`.`op_dc_storage_network_ip_address` (range_id, ip_address, mac_address, taken) VALUES (?, ?, (select mac_address from `cloud`.`data_center` where id=?), ?)";
        String updateSql = "UPDATE `cloud`.`data_center` set mac_address = mac_address+1 where id=?";
        Connection conn = txn.getConnection();
        while (startIPLong <= endIPLong) {
            try (PreparedStatement stmt_insert = conn.prepareStatement(insertSql);) {
                stmt_insert.setLong(1, rangeId);
                stmt_insert.setString(2, NetUtils.long2Ip(startIPLong++));
                stmt_insert.setLong(3, zoneId);
                stmt_insert.setNull(4, java.sql.Types.DATE);
                stmt_insert.executeUpdate();
            }

            try (PreparedStatement stmt_update = txn.prepareStatement(updateSql);) {
                stmt_update.setLong(1, zoneId);
                stmt_update.executeUpdate();
            }
        }
    }

    @Override
    @DB
    public StorageNetworkIpRange updateIpRange(UpdateStorageNetworkIpRangeCmd cmd) {
        final Integer vlan = cmd.getVlan();
        final Long rangeId = cmd.getId();
        String startIp = cmd.getStartIp();
        String endIp = cmd.getEndIp();
        final String netmask = cmd.getNetmask();

        if (netmask != null && !NetUtils.isValidIp4Netmask(netmask)) {
            throw new CloudRuntimeException("Invalid netmask:" + netmask);
        }

        if (_sNwIpDao.countInUseIpByRangeId(rangeId) > 0) {
            throw new CloudRuntimeException("Cannot update the range," + getInUseIpAddress(rangeId));
        }

        StorageNetworkIpRangeVO range = _sNwIpRangeDao.findById(rangeId);
        if (range == null) {
            throw new CloudRuntimeException("Cannot find storage ip range " + rangeId);
        }

        if (startIp != null || endIp != null) {
            long podId = range.getPodId();
            startIp = startIp == null ? range.getStartIp() : startIp;
            endIp = endIp == null ? range.getEndIp() : endIp;
            checkOverlapPrivateIpRange(podId, startIp, endIp);
            checkOverlapStorageIpRange(podId, startIp, endIp);
        }

        final String startIpFinal = startIp;
        final String endIpFinal = endIp;
        Transaction.execute(new TransactionCallbackNoReturn() {
            @Override
            public void doInTransactionWithoutResult(TransactionStatus status) {
                StorageNetworkIpRangeVO range = null;
                try {
                    range = _sNwIpRangeDao.acquireInLockTable(rangeId);
                    if (range == null) {
                        throw new CloudRuntimeException("Cannot acquire lock on storage ip range " + rangeId);
                    }
                    StorageNetworkIpRangeVO vo = _sNwIpRangeDao.createForUpdate();
                    if (vlan != null) {
                        vo.setVlan(vlan);
                    }
                    if (startIpFinal != null) {
                        vo.setStartIp(startIpFinal);
                    }
                    if (endIpFinal != null) {
                        vo.setEndIp(endIpFinal);
                    }
                    if (netmask != null) {
                        vo.setNetmask(netmask);
                    }
                    _sNwIpRangeDao.update(rangeId, vo);
                } finally {
                    if (range != null) {
                        _sNwIpRangeDao.releaseFromLockTable(range.getId());
                    }
                }
            }
        });

        return _sNwIpRangeDao.findById(rangeId);
    }

    @Override
    @DB
    public StorageNetworkIpRange createIpRange(final CreateStorageNetworkIpRangeCmd cmd) throws SQLException {
        final Long podId = cmd.getPodId();
        final String startIp = cmd.getStartIp();
        String endIp = cmd.getEndIp();
        final Integer vlan = cmd.getVlan();
        final String netmask = cmd.getNetmask();

        if (endIp == null) {
            endIp = startIp;
        }

        if (!NetUtils.isValidIp4Netmask(netmask)) {
            throw new CloudRuntimeException("Invalid netmask:" + netmask);
        }

        HostPodVO pod = _podDao.findById(podId);
        if (pod == null) {
            throw new CloudRuntimeException("Cannot find pod " + podId);
        }
        final Long zoneId = pod.getDataCenterId();

        List<NetworkVO> nws = _networkDao.listByZoneAndTrafficType(zoneId, TrafficType.Storage);
        if (nws.size() == 0) {
            throw new CloudRuntimeException("Cannot find storage network in zone " + zoneId);
        }
        if (nws.size() > 1) {
            throw new CloudRuntimeException("Find more than one storage network in zone " + zoneId + "," + nws.size() + " found");
        }
        final NetworkVO nw = nws.get(0);

        checkOverlapPrivateIpRange(podId, startIp, endIp);
        checkOverlapStorageIpRange(podId, startIp, endIp);

        StorageNetworkIpRangeVO range = null;

        final String endIpFinal = endIp;
        return Transaction.execute(new TransactionCallbackWithException<StorageNetworkIpRangeVO, SQLException>() {
            @Override
            public StorageNetworkIpRangeVO doInTransaction(TransactionStatus status) throws SQLException {
                StorageNetworkIpRangeVO range = new StorageNetworkIpRangeVO(zoneId, podId, nw.getId(), startIp, endIpFinal, vlan, netmask, cmd.getGateWay());
                _sNwIpRangeDao.persist(range);
                try {
                    createStorageIpEntires(TransactionLegacy.currentTxn(), range.getId(), startIp, endIpFinal, zoneId);
                } catch (SQLException e) {
                    StringBuilder err = new StringBuilder();
                    err.append("Create storage network range failed.");
                    err.append("startIp=" + startIp);
                    err.append("endIp=" + endIpFinal);
                    err.append("netmask=" + netmask);
                    err.append("zoneId=" + zoneId);
                    s_logger.debug(err.toString(), e);
                    throw e;
                }

                return range;
            }
        });
    }

    private String getInUseIpAddress(long rangeId) {
        List<String> ips = _sNwIpDao.listInUseIpByRangeId(rangeId);
        StringBuilder res = new StringBuilder();
        res.append("Below IP of range " + rangeId + " is still in use:");
        for (String ip : ips) {
            res.append(ip).append(",");
        }
        return res.toString();
    }

    @Override
    @DB
    public void deleteIpRange(DeleteStorageNetworkIpRangeCmd cmd) {
        final long rangeId = cmd.getId();
        StorageNetworkIpRangeVO range = _sNwIpRangeDao.findById(rangeId);
        if (range == null) {
            throw new CloudRuntimeException("Can not find storage network ip range " + rangeId);
        }

        if (_sNwIpDao.countInUseIpByRangeId(rangeId) > 0) {
            throw new CloudRuntimeException(getInUseIpAddress(rangeId));
        }

        Transaction.execute(new TransactionCallbackNoReturn() {
            @Override
            public void doInTransactionWithoutResult(TransactionStatus status) {
                StorageNetworkIpRangeVO range = null;
                try {
                    range = _sNwIpRangeDao.acquireInLockTable(rangeId);
                    if (range == null) {
                        String msg = "Unable to acquire lock on storage network ip range id=" + rangeId + ", delete failed";
                        s_logger.warn(msg);
                        throw new CloudRuntimeException(msg);
                    }
                    /*
                     * entries in op_dc_storage_network_ip_address will be deleted automatically due to
                     * fk_storage_ip_address__range_id constraint key
                     */
                    _sNwIpRangeDao.remove(rangeId);
                } finally {
                    if (range != null) {
                        _sNwIpRangeDao.releaseFromLockTable(rangeId);
                    }
                }
            }
        });

    }

    @Override
    public List<StorageNetworkIpRange> listIpRange(ListStorageNetworkIpRangeCmd cmd) {
        Long rangeId = cmd.getRangeId();
        Long podId = cmd.getPodId();
        Long zoneId = cmd.getZoneId();

        List result = null;
        if (rangeId != null) {
            result = _sNwIpRangeDao.listByRangeId(rangeId);
        } else if (podId != null) {
            result = _sNwIpRangeDao.listByPodId(podId);
        } else if (zoneId != null) {
            result = _sNwIpRangeDao.listByDataCenterId(zoneId);
        } else {
            result = _sNwIpRangeDao.listAll();
        }

        return result;
    }

    @Override
    public void releaseIpAddress(String ip) {
        _sNwIpDao.releaseIpAddress(ip);
    }

    @Override
    public StorageNetworkIpAddressVO acquireIpAddress(long podId) {
        List<StorageNetworkIpRangeVO> ranges = _sNwIpRangeDao.listByPodId(podId);
        for (StorageNetworkIpRangeVO r : ranges) {
            try {
                Long rangeId = r.getId();
                r = _sNwIpRangeDao.acquireInLockTable(rangeId);
                if (r == null) {
                    String msg = "Unable to acquire lock on storage network ip range id=" + rangeId + ", delete failed";
                    s_logger.warn(msg);
                    throw new CloudRuntimeException(msg);
                }

                StorageNetworkIpAddressVO ip = _sNwIpDao.takeIpAddress(r.getId());
                if (ip != null) {
                    return ip;
                }
            } finally {
                if (r != null) {
                    _sNwIpRangeDao.releaseFromLockTable(r.getId());
                }
            }
        }

        return null;
    }

    @Override
    public boolean isStorageIpRangeAvailable(long zoneId) {
        QueryBuilder<StorageNetworkIpRangeVO> sc = QueryBuilder.create(StorageNetworkIpRangeVO.class);
        sc.and(sc.entity().getDataCenterId(), Op.EQ, zoneId);
        List<StorageNetworkIpRangeVO> entries = sc.list();
        return entries.size() > 0;
    }

    @Override
    public List<SecondaryStorageVmVO> getSSVMWithNoStorageNetwork(long zoneId) {
        List<SecondaryStorageVmVO> ssvms =
            _ssvmDao.getSecStorageVmListInStates(null, zoneId, VirtualMachine.State.Starting, VirtualMachine.State.Running, VirtualMachine.State.Stopping);
        return ssvms;
    }

    @Override
    public boolean isAnyStorageIpInUseInZone(long zoneId) {
        List<StorageNetworkIpRangeVO> ranges = _sNwIpRangeDao.listByDataCenterId(zoneId);
        for (StorageNetworkIpRangeVO r : ranges) {
            if (_sNwIpDao.countInUseIpByRangeId(r.getId()) > 0) {
                return true;
            }
        }
        return false;
    }
}
