package ru.bitel.bgbilling.modules.inet.dyn.device.snmp;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.LongStream;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import ru.bitel.common.TimeUtils;
import ru.bitel.bgbilling.kernel.base.server.logger.BGLogger;
import ru.bitel.bgbilling.kernel.container.managed.ServerContext;
import ru.bitel.bgbilling.modules.inet.common.bean.InetDevice;
import ru.bitel.bgbilling.modules.inet.common.bean.InetDeviceType;
import ru.bitel.bgbilling.modules.inet.common.event.manage.InetDeviceManageEvent;
import ru.bitel.bgbilling.server.util.Setup;
import ru.bitel.common.ParameterMap;
import ru.bitel.common.Preferences;
import ru.bitel.common.Utils;
import ru.bitel.common.worker.ThreadContext;
import ru.bitel.oss.systems.inventory.resource.common.bean.Device;
import ru.bitel.oss.systems.inventory.resource.common.bean.DeviceInterface;
import ru.bitel.oss.systems.inventory.resource.common.bean.DeviceInterfaceIndex;
import ru.bitel.oss.systems.inventory.resource.common.bean.DeviceType;
import ru.bitel.oss.systems.inventory.resource.common.dm.DeviceManager;
import ru.bitel.oss.systems.inventory.resource.common.service.DeviceInterfaceService;
import ru.bitel.oss.systems.inventory.resource.server.DeviceManagerMethod;
import uk.co.westhawk.snmp.stack.AsnObjectId;


/**
 * @author cromeshnic@gmail.com
 * patched by stark
 *
 * Класс для работы с интерфейсами их индексами по SNMP
 * <pre>
 * команды:
 *      ifsync - синхронизация интерфейсов и их ifIndex на роутере и в биллинге:
 *          1. Получаем по SNMP список соответствий: ifIndex->ifName интерфейсов на устройстве
 *          2. Для каждого полученного интерфейса проверяем,
 *              есть ли в биллинге на устройсве интерфейс с таким именем?
 *          2.1. Если нет, и он проходит через фильтр snmp.ifNameRegexpFilter - создаём с правильным ifIndex и категорией ip = snmp.ipCategory и номер интерфейса находим
 *          как group(1) у заданного snmp.ifNameRegexpFilter.
 *          2.2. Если есть, то совпадает ли ifIndex?
 *          2.2.1. Если да, то ничего не делаем, всё ок.
 *          2.2.2. Если нет, то меняем ifIndex
 *          3. Оставшиеся необработанными интерфейсы в BG: если их нет на устройстве, то
 *              помечаем как отключенный и устанавливаем ifIndex=0
 * параметры:
 *      snmp.ifNameOid=1.3.6.1.2.1.2.2.1.1
 *          - oid для получения списка интерфейсов устройства
 *      snmp.ifNameRegexpFilter=.*
 *          - regexp для фильтрации интерфейсов. Например, если нам в биллинге нужны только интерфейсы вида GigabitEthernet0/0/2.1122,
 *          то указываем snmp.ifNameRegexpFilter=GigabitEthernet0/0/2\.(\d+)
 *          Группировку тут надо обязательно указывать, подразумевается что в ней идет номер индекса, мы его обновляем на интерфейсе.
 *      snmp.ipCategory=0
 *          - id категории ресурсов, которая будет указана для создаваемых интерфейсов
 *      snmp.substractUptime=1
 *          - если 1, то меняем индексы не с текущего момента, а со времени старта устройства, если это произошло сегодня.
 *              Предполагаем, что ifIndex могут меняться только при рестарте устройства.
 *      snmp.enable=1
 *          - включает работу менеджера на этом устройстве
 * </pre>
 * @see SnmpDeviceManager
 */
public class IndexSnmpDeviceManager
    extends SnmpDeviceManager
    implements DeviceManager
{
    protected static final Logger logger = LogManager.getLogger();
    //TODO - резервирование/закрытие датой расформированных интерфейсов : http://forum.bitel.ru/viewtopic.php?p=71924#p71924

    private long[] ifNameOid;
    /**
     * Фильтр имён интерфейсов
     * Если имя не соответствует регэкспу, то не заводим его в биллинге
     */
    private Pattern ifaceNamePattern;
    private int ipCategory;
    private boolean subtractUptimeForIfIndex;
    protected int mid;
    protected int deviceId;

    protected boolean enabled;

    @Override
    public Object init(Setup setup, int moduleId, Device<?, ?> device, DeviceType deviceType, ParameterMap deviceConfig) {
        super.init(setup, moduleId, device, deviceType, deviceConfig);
        this.mid = moduleId;
        this.deviceId = device.getId();
        //1.3.6.1.2.1.2.2.1.2.56 = STRING: "GigabitEthernet0/0/2.1100"
        this.ifNameOid =  new AsnObjectId( ( deviceConfig.get( "snmp.ifNameOid", "1.3.6.1.2.1.31.1.1.1.1" ) ) ).getOid();
        this.ifaceNamePattern = Pattern.compile( deviceConfig.get("snmp.ifNameRegexpFilter", ".*") );
        //Категория IP, выставляемая новым интерфейсам
        this.ipCategory = deviceConfig.getInt("snmp.ipCategory", 0);
        //Вычитать ли из текущего времени uptime устройства при проставлении даты/времени для ifIndex?
        //(не дальше, чем за границу суток)
        this.subtractUptimeForIfIndex = deviceConfig.getBoolean("snmp.substractUptime", true);
        this.enabled = deviceConfig.getBoolean("snmp.enable", true);

        return null;
    }

    @DeviceManagerMethod(title = "Cинхронизировать индексы" )
    public String ifsync()
    throws Exception
    {
        if(!this.enabled){
            return "SNMP manager disabled on device";
        }
        StringBuilder result = new StringBuilder();
        ServerContext ctx = ThreadContext.get();
        DeviceInterfaceService devicePortService = ctx.getService(DeviceInterfaceService.class, this.mid);
        //Интерфейсы устройства в биллинге
        Map<String, DeviceInterface> ifaces = new HashMap<String, DeviceInterface>();
        int maxPortNumber = 0;
        for (DeviceInterface deviceInterface : devicePortService.devicePortList(this.deviceId, false)) {
            //Не дублируются ли интерфейсы по именам?
            if(ifaces.get(deviceInterface.getTitle())!=null){
                logger.error("duplicate iface names on device " + this.deviceId + ": " + deviceInterface.getTitle() +
                             " (ports:" + deviceInterface.getPort() + "," + ifaces.get(deviceInterface.getTitle()).getPort() + ")");
                result.append("! duplicate bg ifaces: ")
                      .append(deviceInterface.getTitle())
                      .append(" (ports:")
                      .append(deviceInterface.getPort())
                      .append(",")
                      .append(ifaces.get(deviceInterface.getTitle()).getPort())
                      .append(")\n");
            }
            ifaces.put(deviceInterface.getTitle(), deviceInterface);
            //Определяем максимальный номер порта
            if(deviceInterface.getPort()>maxPortNumber){
                maxPortNumber=deviceInterface.getPort();
            }
        }

        //Интерфейсы, полученные с устройства
        final Map<String, Integer> ifaceTitleToIfIndexMap = new HashMap<String, Integer>();
        logger.info( "ifNameOid =" + LongStream.of( ifNameOid ).boxed().collect( Collectors.toList()) );

        this.snmpClient.walk(this.ifNameOid, String.class, ( o, v ) -> {
            //System.out.println( new AsnObjectId( o ) + " = " + v );
            //logger.info ( "v=" + v );
            ifaceTitleToIfIndexMap.put( v ,  (int)o[ o.length - 1 ] );
        } );

        Calendar now = Calendar.getInstance();

        //Получаем uptime (в TimeTicks - 0.01 секунды)
        Long uptime = (long) this.uptime();
        Date start;
        Calendar cal;
        if(uptime==null || !this.subtractUptimeForIfIndex){//нет данных оп uptime, либо мы их не должны использовать
            start = (Date)now.getTime().clone();
        }else{
            cal = (Calendar)now.clone();
            cal.add(Calendar.SECOND, (int)(uptime/-1000) );
            if(TimeUtils.daysDelta(now, cal)!=0){//Девайс был рестартован не сегодня
                start = (Date)now.getTime().clone(); //Меняем индексы с текущего момента
                logger.info("device "+deviceId+" started "+TimeUtils.formatDate(cal));
            }else{
                start = (Date)cal.getTime().clone();
            }
        }
        cal = Calendar.getInstance();
        cal.setTime(start);
        cal.add(Calendar.SECOND, -1);
        Date nowMinusSecond = cal.getTime();

        DeviceInterface iface;
        DeviceInterfaceIndex deviceInterfaceIndex;
        Integer ifaceIndex;
        List<DeviceInterfaceIndex> indexList;

//        int count = 0;
        //1. Перебираем все полученные интерфейсы
        for (Map.Entry<String, Integer> nameToIndex : ifaceTitleToIfIndexMap.entrySet())
        {
            //1.1 Ищем интерфейс в биллинге по его имени
            String ifName = nameToIndex.getKey();
            Integer ifIndexReal = nameToIndex.getValue();
            iface = ifaces.get( ifName );

            /*count ++;
            if ( count > 1000 )
            {
                break;
            }*/

            Matcher matcher = ifaceNamePattern.matcher( ifName );
            boolean find = matcher.find();

            if( iface == null )
            {
                //1.1.1 Не нашли - заводим в
                // Удовлетворяет ли интерфейс фильтру? Если нет - не заводим его
                if( !find )
                {
                    logger.info("skip snmp iface "+ifName+" on deviceId="+this.deviceId+" due to regexp "+ this.ifaceNamePattern.pattern() );
                    continue;
                }

                logger.info ( "ifName=" + ifName );
                int portNumber = Utils.parseInt(  matcher.group( 1 ), 0 );
                if ( portNumber == 0 )
                {
                    logger.info("portNumber is 0  for snmp iface "+ifName+" on deviceId="+this.deviceId+" due to regexp "+ this.ifaceNamePattern.pattern() );
                    continue;
                }


                iface = new DeviceInterface();
                iface.setPort( portNumber );//increment port number (port number != ifindex here !!!)
                iface.setTitle(ifName);
                iface.setComment("");
                iface.setStatus(1);//1 - Доступен, 0 - Зарезервирован
                iface.setDeviceId(this.deviceId);
                iface.setIpCategoryId(this.ipCategory);
                deviceInterfaceIndex = new DeviceInterfaceIndex();
                deviceInterfaceIndex.setId(0);//id<=0 => new
                deviceInterfaceIndex.setPort( portNumber );//вроде бы не обязательно, но пусть будет
                deviceInterfaceIndex.setDeviceId(this.deviceId);//вроде бы не обязательно, но пусть будет

                deviceInterfaceIndex.setIndex( ifIndexReal );
                deviceInterfaceIndex.setTimeFrom(now.getTime());
                deviceInterfaceIndex.setTimeTo(null);
                iface.setIndexList(  Collections.singletonList(deviceInterfaceIndex) );
                devicePortService.devicePortUpdate( iface, false );
                logger.info("adding new iface "+iface.getTitle()+" with ifIndex="+ifIndexReal+" from "+ TimeUtils.formatFullDate(now.getTime()));
                result.append("+ ")
                      .append(iface.getTitle())
                      .append(" (")
                      .append(ifIndexReal)
                      .append(") from ")
                      .append(TimeUtils.formatFullDate(now.getTime()))
                      .append("\n");
            }else
            {//1.1.2 Нашли такой интерфейс, проверяем, изменился ли у него ifindex
                //Если интерфейс при этом не удовлетворяет фильтру, то пишем об этом в лог
                if( !find )
                {
                    logger.warn("existing bg iface "+ifName+" on deviceId="+this.deviceId+" doesn't match regexp "+this.ifaceNamePattern.pattern() );
                    result.append("? ")
                          .append(iface.getTitle())
                          .append(" - нестандартный интерфейс в BG\n");
                }
                indexList = iface.getIndexList();
                deviceInterfaceIndex = this.getCurrentIndex(now.getTime(), indexList);
                if( deviceInterfaceIndex!=null )
                {
                    ifaceIndex = deviceInterfaceIndex.getIndex();
                }else //По-умолчанию, индекс интерфейса - это его порт
                {
                    ifaceIndex = iface.getPort();
                }

                if( !ifaceIndex.equals(ifIndexReal) ){//Индекс поменялся! Нужно обновить
                    //Закрываем датой предыдущий ifIndex, если есть
                    if( deviceInterfaceIndex!=null )
                    {
                        deviceInterfaceIndex.setTimeTo(nowMinusSecond);
                    }

                    if(indexList==null)
                    {
                        indexList = new ArrayList<DeviceInterfaceIndex>();
                        iface.setIndexList(indexList);
                    }
                    deviceInterfaceIndex = new DeviceInterfaceIndex();
                    deviceInterfaceIndex.setId(0);//id<=0 => new
                    deviceInterfaceIndex.setPort(iface.getPort());//вроде бы не обязательно, но пусть будет
                    deviceInterfaceIndex.setDeviceId(iface.getDeviceId());//вроде бы не обязательно, но пусть будет
                    deviceInterfaceIndex.setIndex( ifIndexReal );
                    deviceInterfaceIndex.setTimeFrom(start);
                    deviceInterfaceIndex.setTimeTo(null);
                    indexList.add(deviceInterfaceIndex);
                    devicePortService.devicePortUpdate( iface, false );
                    logger.info("updating iface "+iface.getTitle()+": ifIndex="+ifIndexReal+" (was "+ifaceIndex+") from "+ TimeUtils.formatFullDate(start));
                    result.append("* ")
                          .append(iface.getTitle())
                          .append(" (")
                          .append(ifaceIndex)
                          .append("->")
                          .append(ifIndexReal)
                          .append(") from ")
                          .append(TimeUtils.formatFullDate(start))
                          .append("\n");
                }
            }
        }
        logger.info ( "other" );

        //2. Теперь перебираем интерфейсы в BG, чтобы обработать оставшиеся
        for(Map.Entry<String, DeviceInterface> ifaceEntry : ifaces.entrySet()) {
            iface = ifaceEntry.getValue();//iface в BG
            //2.1 Ищем индекс интерфейса в полученном по SNMP списке
            ifaceIndex = ifaceTitleToIfIndexMap.get(ifaceEntry.getKey());
            if(ifaceIndex==null){//Есть в BG, но не получен с устройства
                //зануляем ifIndex, чтобы он гарантированно ни с кем не пересекался
                indexList = iface.getIndexList();
                deviceInterfaceIndex = getCurrentIndex(now.getTime(), indexList);
                if(deviceInterfaceIndex==null){
                    ifaceIndex = iface.getPort();
                }else{
                    ifaceIndex = deviceInterfaceIndex.getIndex();
                }
                if(ifaceIndex!=0){
                    //Зануляем!
                    if(deviceInterfaceIndex!=null){
                        deviceInterfaceIndex.setTimeTo(nowMinusSecond);
                    }
                    if(indexList==null){
                        indexList = new ArrayList<DeviceInterfaceIndex>();
                        iface.setIndexList(indexList);
                    }
                    deviceInterfaceIndex = new DeviceInterfaceIndex();
                    deviceInterfaceIndex.setId(0);//id<=0 => new
                    deviceInterfaceIndex.setPort(iface.getPort());//вроде бы не обязательно, но пусть будет
                    deviceInterfaceIndex.setDeviceId(iface.getDeviceId());//вроде бы не обязательно, но пусть будет
                    deviceInterfaceIndex.setIndex(0);
                    deviceInterfaceIndex.setTimeFrom(start);
                    deviceInterfaceIndex.setTimeTo(null);
                    indexList.add(deviceInterfaceIndex);
                    iface.setComment("deleted");
                    devicePortService.devicePortUpdate( iface, false );
                    logger.info("updating iface "+iface.getTitle()+": ifIndex=0 (was "+ifaceIndex+") from "+ TimeUtils.formatFullDate(start));
                    result.append("- ")
                          .append(iface.getTitle())
                          .append(" (")
                          .append(ifaceIndex)
                          .append("->0) from ")
                          .append(TimeUtils.formatFullDate(start))
                          .append("\n");
                }
            }
        }
        if(result.length()==0){
            return "no changes";
        }

        return result.toString();
    }

    protected DeviceInterfaceIndex getCurrentIndex(Date now, List<DeviceInterfaceIndex> deviceInterfaceIndexList){
        if(deviceInterfaceIndexList!=null){
            for (DeviceInterfaceIndex deviceInterfaceIndex : deviceInterfaceIndexList) {
                if(TimeUtils.timeInRange(now, deviceInterfaceIndex.getTimeFrom(), deviceInterfaceIndex.getTimeTo())){
                    return deviceInterfaceIndex;
                }
            }
        }
        return null;
    }

    public static void main( String argv[] )
    {
        try
        {
            logger.info( "hello" );
            IndexSnmpDeviceManager man = new IndexSnmpDeviceManager();
            int moduleId = 1;
            InetDevice device = new InetDevice();
            Preferences deviceConfig = new Preferences();
            /*
            //cisco
            int deviceId = 2;
            device.setId( deviceId );
            device.setHost( "10.10.1.42" );

            deviceConfig.set( "snmp.ifNameOid", "1.3.6.1.2.1.2.2.1.2" );
            deviceConfig.set( "snmp.ifNameRegexpFilter", "GigabitEthernet0/0/2\\.(\\d+)" );

            deviceConfig.set( SNMP_VERSION, "2" );
            deviceConfig.set( SNMP_HOST, "10.10.1.42" );
            */

            //juniper
            int deviceId = 3;
            device.setId( deviceId );
            device.setHost( "10.10.1.100" );

            deviceConfig.set( "snmp.ifNameOid", "1.3.6.1.2.1.2.2.1.2" );
            deviceConfig.set( "snmp.ifNameRegexpFilter", "ae0\\.(\\d+)" );

            deviceConfig.set( SNMP_VERSION, "2" );
            //deviceConfig.set( SNMP_HOST, "10.10.1.42" );

            man.init( Setup.getSetup(), moduleId, device, new InetDeviceType(), deviceConfig );
            man.connect();
            man.ifsync();

            //System.out.println ( "hello" );
        }
        catch( Exception ex )
        {
            BGLogger.error( ex );
        }
    }

    /**
     * Метод вызывается при обнаружении перезагрузки устройства.
     * @param e
     * @return
     * @throws Exception
     */
    public Object onReboot( InetDeviceManageEvent e )
        throws Exception
    {
        logger.info( "onReboot" );
        ifsync();
        return null;
    }
}
