/*
 * Licensed to The OpenNMS Group, Inc (TOG) under one or more
 * contributor license agreements.  See the LICENSE.md file
 * distributed with this work for additional information
 * regarding copyright ownership.
 *
 * TOG licenses this file to You under the GNU Affero General
 * Public License Version 3 (the "License") or (at your option)
 * any later version.  You may not use this file except in
 * compliance with the License.  You may obtain a copy of the
 * License at:
 *
 *      https://www.gnu.org/licenses/agpl-3.0.txt
 *
 * 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.opennms.netmgt.snmp.snmp4j;

import java.io.IOException;

import org.opennms.netmgt.snmp.CollectionTracker;
import org.opennms.netmgt.snmp.SnmpException;
import org.opennms.netmgt.snmp.SnmpObjId;
import org.opennms.netmgt.snmp.SnmpValue;
import org.opennms.netmgt.snmp.SnmpWalker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.snmp4j.PDU;
import org.snmp4j.Snmp;
import org.snmp4j.Target;
import org.snmp4j.event.ResponseEvent;
import org.snmp4j.event.ResponseListener;
import org.snmp4j.mp.SnmpConstants;
import org.snmp4j.smi.OID;
import org.snmp4j.smi.VariableBinding;

public class Snmp4JWalker extends SnmpWalker {
	
	private static final transient Logger LOG = LoggerFactory.getLogger(Snmp4JWalker.class);
	
	public static abstract class Snmp4JPduBuilder extends WalkerPduBuilder {
        public Snmp4JPduBuilder(int maxVarsPerPdu) {
            super(maxVarsPerPdu);
        }
        
        public abstract PDU getPdu();
    }
    
    public class GetNextBuilder extends Snmp4JPduBuilder {

        private PDU m_nextPdu = null;

        private GetNextBuilder(int maxVarsPerPdu) {
            super(maxVarsPerPdu);
            reset();
        }
        
        @Override
        public void reset() {
            m_nextPdu = m_agentConfig.createPdu(PDU.GETNEXT);
        }

        @Override
        public PDU getPdu() {
            return m_nextPdu;
        }
        
        @Override
        public void addOid(SnmpObjId snmpObjId) {
            VariableBinding varBind = new VariableBinding(new OID(snmpObjId.getIds()));
            m_nextPdu.add(varBind);
        }

        @Override
        public void setNonRepeaters(int numNonRepeaters) {
        }

        @Override
        public void setMaxRepetitions(int maxRepititions) {
        }
        
    }
    
    public class GetBulkBuilder extends Snmp4JPduBuilder {

        private PDU m_bulkPdu;

        public GetBulkBuilder(int maxVarsPerPdu) {
            super(maxVarsPerPdu);
            reset();
        }
        
        @Override
        public void reset() {
            m_bulkPdu = m_agentConfig.createPdu(PDU.GETBULK);
        }

        @Override
        public PDU getPdu() {
            return m_bulkPdu;
        }

        @Override
        public void addOid(SnmpObjId snmpObjId) {
            VariableBinding varBind = new VariableBinding(new OID(snmpObjId.getIds()));
            m_bulkPdu.add(varBind);
        }

        @Override
        public void setNonRepeaters(int numNonRepeaters) {
            m_bulkPdu.setNonRepeaters(numNonRepeaters);
        }

        @Override
        public void setMaxRepetitions(int maxRepetitions) {
            m_bulkPdu.setMaxRepetitions(maxRepetitions);
        }
        
    }

    /**
     * TODO: Merge this logic with {@link org.opennms.netmgt.snmp.snmp4j.Snmp4JStrategy} processResponse()
     */
    public class Snmp4JResponseListener implements ResponseListener {

        private void processResponse(final PDU response) throws SnmpException {
            try {
                LOG.debug("Received a tracker PDU of type {} from {} of size {}, errorStatus = {}, errorStatusText = {}, errorIndex = {}", PDU.getTypeString(response.getType()), getAddress(), response.size(), response.getErrorStatus(), response.getErrorStatusText(), response.getErrorIndex());
                if (response.getType() == PDU.REPORT) {
                    handleAuthError("A REPORT PDU was returned from the agent.  This is most likely an authentication problem.  Please check the config");
                } else {
                    if (!processErrors(response.getErrorStatus(), response.getErrorIndex())) {
                        if (response.size() == 0) { // NMS-6484
                            handleError("A PDU with no errors and 0 varbinds was returned from the agent at " + getAddress() + ". This seems to be related with a broken SNMP agent.");
                        } else {
                            for (int i = 0; i < response.size(); i++) {
                                final VariableBinding vb = response.get(i);
                                final SnmpObjId receivedOid = SnmpObjId.get(vb.getOid().getValue());
                                final SnmpValue val = new Snmp4JValue(vb.getVariable());
                                Snmp4JWalker.this.processResponse(receivedOid, val);
                            }
                        }
                    }
                    buildAndSendNextPdu();
                }
            } catch (final RuntimeException|SnmpException e) {
                handleFatalError(e);
            }
        }

        @Override
        public void onResponse(ResponseEvent responseEvent) {
            // need to cancel the request here otherwise SNMP4J Keeps it around forever... go figure
            m_session.cancel(responseEvent.getRequest(), this);

            // Check to see if we got an interrupted exception
            if (responseEvent.getError() instanceof InterruptedException) {
                LOG.debug("Interruption event.  We have probably tried to close the session due to an error", responseEvent.getError());
            // Check to see if the response is null, indicating a timeout
            } else if (responseEvent.getResponse() == null) {
                handleTimeout(getName()+": snmpTimeoutError for: " + getAddress());
            // Check to see if we got any kind of error
            } else if (responseEvent.getError() != null){
                handleError(getName()+": snmpInternalError: " + responseEvent.getError() + " for: " + getAddress(), responseEvent.getError());
            // If we have a PDU in the response, process it
            } else {
                try {
                    processResponse(responseEvent.getResponse());
                } catch (final SnmpException e) {
                    handleFatalError(e);
                }
            }
            
        }
        
        
    }
    
    private Snmp m_session;
    private final Target m_tgt;
    private final ResponseListener m_listener;
    private final Snmp4JAgentConfig m_agentConfig;

    public Snmp4JWalker(Snmp4JAgentConfig agentConfig, String name, CollectionTracker tracker) {
        super(agentConfig.getInetAddress(), name, agentConfig.getMaxVarsPerPdu(), agentConfig.getMaxRepetitions(), agentConfig.getRetries(), tracker);
        
        m_agentConfig = agentConfig;
        
        m_tgt = agentConfig.getTarget();
        m_listener = new Snmp4JResponseListener();
    }
    
        @Override
    public void start() {
        
        LOG.debug("Walking {} for {} using version {} with config: {}", getName(), getAddress(), m_agentConfig.getVersionString(), m_agentConfig);
            
        super.start();
    }

        @Override
    protected WalkerPduBuilder createPduBuilder(int maxVarsPerPdu) {
        return (getVersion() == SnmpConstants.version1 
                ? (WalkerPduBuilder)new GetNextBuilder(maxVarsPerPdu) 
                : (WalkerPduBuilder)new GetBulkBuilder(maxVarsPerPdu));
    }

        @Override
    protected void sendNextPdu(WalkerPduBuilder pduBuilder) throws SnmpException {
        Snmp4JPduBuilder snmp4JPduBuilder = (Snmp4JPduBuilder)pduBuilder;
        try {
            if (m_session == null) {
                m_session = m_agentConfig.createSnmpSession();
                Snmp4JStrategy.trackSession(m_session);
                m_session.listen();
            }
        } catch (final IOException e) {
            close();
            throw new SnmpException(e);
        }

        LOG.debug("Sending tracker pdu of size {}", snmp4JPduBuilder.getPdu().size());
        try {
            m_session.send(snmp4JPduBuilder.getPdu(), m_tgt, null, m_listener);
        } catch (final IOException e) {
            LOG.debug("Failed to send pdu of size {}", snmp4JPduBuilder.getPdu().size(), e);
            close();
            throw new SnmpException(e);
        }
    }
    
    protected int getVersion() {
        return m_tgt.getVersion();
    }

    @Override
    public void close() {
        if (m_session != null) {
            try {
                m_session.close();
            } catch (IOException e) {
                LOG.error("{}: Unexpected Error occured closing SNMP session for: {}", getName(), m_agentConfig, e);
            } finally {
                Snmp4JStrategy.reapSession(m_session);
            }
            m_session = null;
        }
    }
}
