/*_############################################################################
  _## 
  _##  SNMP4J-Agent 3 - NotificationOriginatorImpl.java  
  _## 
  _##  Copyright (C) 2005-2025  Frank Fock (SNMP4J.org)
  _##  
  _##  Licensed 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.snmp4j.agent.mo.snmp;

import java.util.*;

import org.snmp4j.*;
import org.snmp4j.agent.*;
import org.snmp4j.agent.mo.*;
import org.snmp4j.agent.security.*;
import org.snmp4j.event.*;
import org.snmp4j.smi.*;
import org.snmp4j.log.LogFactory;
import org.snmp4j.log.LogAdapter;
import org.snmp4j.mp.MessageProcessingModel;
import org.snmp4j.agent.mo.snmp.SnmpTargetMIB.SnmpTargetAddrEntryRow;

import java.io.IOException;

import org.snmp4j.mp.SnmpConstants;
import org.snmp4j.transport.TransportType;

import java.net.InetAddress;

/**
 * The {@code NotificationOriginatorImpl} class implements a notification originator application for SNMP4J.
 * See also RFC 3411 for a description of notification originators.
 *
 *
 * @author Frank Fock
 * @version 3.0.0
 */
public class NotificationOriginatorImpl implements NotificationOriginator {

    private static final LogAdapter logger =
            LogFactory.getLogger(NotificationOriginatorImpl.class);

    private Session session;
    private final VACM vacm;
    private final SnmpTargetMIB targetMIB;
    private final SnmpNotificationMIB notificationMIB;
    private SnmpCommunityMIB communityMIB;
    private final SysUpTime sysUpTime;
    private transient List<NotificationLogListener> notificationLogListeners;
    private long notificationEventID = 0;

    private static final OctetString EMPTY_CONTEXT_ENGINE_ID = new OctetString();

    /**
     * Creates a notification originator.
     *
     * @param session
     *         the SNMP Session instance to be used to send the notifications/informs.
     * @param vacm
     *         the VACM to be used to check access for notifications.
     * @param sysUpTime
     *         the sysUpTime instance to be used to determine sysUpTime.0 when sending notifications without
     *         specifically specified sysUpTime.
     * @param targetMIB
     *         the SnmpTargetMIB containing notification target information.
     * @param notificationMIB
     *         SnmpNotificationMIB
     */
    public NotificationOriginatorImpl(Session session,
                                      VACM vacm,
                                      SysUpTime sysUpTime,
                                      SnmpTargetMIB targetMIB,
                                      SnmpNotificationMIB notificationMIB) {
        this.session = session;
        this.sysUpTime = sysUpTime;
        this.vacm = vacm;
        this.targetMIB = targetMIB;
        this.notificationMIB = notificationMIB;
    }

    /**
     * Creates a notification originator.
     *
     * @param session
     *         the Snmp instance to be used to send the notifications/informs.
     * @param vacm
     *         the VACM to be used to check access for notifications.
     * @param sysUpTime
     *         the sysUpTime instance to be used to determine sysUpTime.0 when sending notifications without
     *         specifically specified sysUpTime.
     * @param targetMIB
     *         the SnmpTargetMIB containing notification target information.
     * @param notificationMIB
     *         the SnmpNotificationMIB containing notification filtering information.
     * @param communityMIB
     *         the community MIB for coexistence information.
     */
    public NotificationOriginatorImpl(Session session,
                                      VACM vacm,
                                      SysUpTime sysUpTime,
                                      SnmpTargetMIB targetMIB,
                                      SnmpNotificationMIB notificationMIB,
                                      SnmpCommunityMIB communityMIB) {
        this(session, vacm, sysUpTime, targetMIB, notificationMIB);
        this.communityMIB = communityMIB;
    }

    /**
     * Sends notifications (traps) to all appropriate notification targets with {@code sysUpTime.get()} as common
     * timestamp.
     *
     * @param context
     *         the context name of the context on whose behalf this notification has been generated.
     * @param notificationID
     *         the object ID that uniquely identifies this notification. For SNMPv1 traps, the notification ID has to be
     *         build using the rules provided by RFC 2576.
     * @param vbs
     *         an array of {@code VariableBinding} instances representing the payload of the notification.
     *
     * @return an array of ResponseEvent instances. Since the
     * {@code NotificationOriginator} determines on behalf of the
     * SNMP-NOTIFICATION-MIB contents whether a notification is sent as trap/notification or as inform request, the
     * returned array contains an element for each addressed target, but only a response PDU for inform targets.
     */
    public Object notify(OctetString context, OID notificationID,
                         VariableBinding[] vbs) {
        return notify(context, notificationID, sysUpTime.get(), vbs);
    }

    private <A extends Address> ResponseEvent<A> sendNotification(A address, Integer32 timeout, Integer32 retries,
                                                                  SnmpTargetAddrEntryRow targetAddrEntryRow,
                                                                  MOTableRow paramsRow, OctetString context,
                                                                  OID notificationID, TimeTicks sysUpTime,
                                                                  VariableBinding[] vbs, int type,
                                                                  long notificationEventID) {
        Integer32 mpModel = (Integer32) paramsRow.getValue(SnmpTargetMIB.idxSnmpTargetParamsMPModel);
        OctetString secName = (OctetString) paramsRow.getValue(SnmpTargetMIB.idxSnmpTargetParamsSecurityName);
        Integer32 secLevel = (Integer32) paramsRow.getValue(SnmpTargetMIB.idxSnmpTargetParamsSecurityLevel);
//    Integer32 secModel = (Integer32)
//        paramsRow.getValue(SnmpTargetMIB.idxSnmpTargetParamsSecurityModel);
        OctetString community = secName;
        try {
            if (secName == null) {
                throw new IllegalArgumentException("Security name must not be null");
            }
            if (communityMIB != null) {
                community = communityMIB.getCommunity(secName, null, context);
            }
            if (address == null) {
                String errMessage = "Null address, notification skipped";
                logger.debug(errMessage);
                throw new IllegalArgumentException(errMessage);
            }
            if ((mpModel.getValue() == MessageProcessingModel.MPv1 ||
                    mpModel.getValue() == MessageProcessingModel.MPv2c) &&
                    (community == null)) {
                throw new IllegalArgumentException("Security name '" + secName +
                        "' is not mapped to any community, notification not sent (RFC 3584 §5.2.3)");
            }
        } catch (IllegalArgumentException iaex) {
            logger.warn("Notification not sent due to configuration error: " + iaex.getMessage());
            //return new ResponseEvent((Snmp)session, address, null, null, notificationID, iaex);
            return null;
        }
        Target<A> t;
        PDU pdu;
        switch (mpModel.getValue()) {
            case MessageProcessingModel.MPv1: {
                t = new CommunityTarget<A>(address, community);
                PDUv1 trap = new PDUv1();
                pdu = trap;
                if (sysUpTime != null) {
                    trap.setTimestamp(sysUpTime.getValue());
                } else {
                    trap.setTimestamp(this.sysUpTime.get().getValue());
                }
                int genericID = SnmpConstants.getGenericTrapID(notificationID);
                if (genericID < 0) {
                    trap.setGenericTrap(6);
                    if ((notificationID.size() > 2) &&
                            (notificationID.get(notificationID.size() - 2) == 0)) {
                        OID enterprise = new OID(notificationID.getValue(), 0, notificationID.size() - 2);
                        trap.setEnterprise(enterprise);
                    } else {
                        OID enterprise = new OID(notificationID.getValue(), 0, notificationID.size() - 1);
                        trap.setEnterprise(enterprise);
                    }
                    trap.setSpecificTrap(notificationID.last());
                } else {
                    trap.setGenericTrap(genericID);
                    trap.setEnterprise(new OID(new int[]{0, 0}));
                }
                if (session instanceof Snmp) {
                    TransportMapping<?> tm =
                            ((Snmp) session).getMessageDispatcher().getTransport(address, TransportType.sender);
                    if ((tm != null) && (tm.getListenAddress() instanceof IpAddress)) {
                        InetAddress localAddress = ((IpAddress) tm.getListenAddress()).getInetAddress();
                        if (localAddress == null) {
                            trap.setAgentAddress(new IpAddress("0.0.0.0"));
                        } else {
                            trap.setAgentAddress(new IpAddress(localAddress));
                        }
                    }
                }
                break;
            }
            case MessageProcessingModel.MPv2c: {
                t = new CommunityTarget<A>(address, community);
                pdu = new PDU();
                break;
            }
            default: {
                byte[] authEngineID =
                        (type == SnmpNotificationMIB.SnmpNotifyTypeEnum.inform) ?
                                new byte[0] : targetMIB.getLocalEngineID();
                // check for TLS
                OID transportDomain = (OID) targetAddrEntryRow.getValue(SnmpTargetMIB.idxSnmpTargetAddrTDomain);
                if (SnmpTlsTmMib.oidSnmpTLSTCPDomain.equals(transportDomain) ||
                        SnmpTlsTmMib.oidSnmpDTLSUDPDomain.equals(transportDomain)) {
                    t = targetAddrEntryRow.getTarget(null, context, address);
                }
                else {
                    t = new UserTarget<A>(address, secName, authEngineID, secLevel.getValue());
                }
                ScopedPDU scopedPdu = new ScopedPDU();
                scopedPdu.setContextName(context);
                // context engine ID is always ID of the local engine if not overwritten
                // by a subclass
                setContextEngineID(scopedPdu, context, notificationID);
                pdu = scopedPdu;
            }
        }
        if (t == null) {
            return null;
        }
        t.setVersion(mpModel.getValue());
        t.setTimeout(timeout.getValue() * 10L);
        t.setRetries(retries.getValue());
        if (mpModel.getValue() != MessageProcessingModel.MPv1) {
            if (sysUpTime != null) {
                pdu.add(new VariableBinding(SnmpConstants.sysUpTime, sysUpTime));
            } else {
                pdu.add(new VariableBinding(SnmpConstants.sysUpTime, this.sysUpTime.get()));
            }
            pdu.add(new VariableBinding(SnmpConstants.snmpTrapOID, notificationID));
        }
        pdu.addAll(vbs);
        pdu.setType((type == SnmpNotificationMIB.SnmpNotifyTypeEnum.inform) ?
                PDU.INFORM : (mpModel.getValue() == MessageProcessingModel.MPv1)
                ? PDU.V1TRAP : PDU.TRAP);
        try {
            OctetString localEngineID = new OctetString();
            OctetString contextEngineID = new OctetString();
            if (pdu instanceof ScopedPDU) {
                localEngineID.setValue(targetMIB.getLocalEngineID());
                contextEngineID = ((ScopedPDU) pdu).getContextEngineID();
            }
            ResponseEvent<A> response = session.send(pdu, t);
            fireNotificationLogEvent(new NotificationLogEvent<A>(this,
                    localEngineID,
                    t,
                    contextEngineID,
                    context, notificationID,
                    sysUpTime,
                    vbs, notificationEventID, true));
            if (pdu.isConfirmedPdu()) {
                if (logger.isInfoEnabled()) {
                    if (response != null && response.getResponse() != null)
                        logger.info("Sent INFORM request with ID " + notificationEventID +
                                " " + pdu + " to " + t + " successfully");
                    else {
                        logger.info("INFORM request with ID " + notificationEventID +
                                " " + pdu + " to " + t + " timed out");
                    }
                }
            }
            else {
                logger.info("Sent notification with ID " + notificationEventID +
                        " " + pdu + " to " + t);
            }
            return response;
        } catch (IOException iox) {
            logger.error("Failed to send notification: " + iox.getMessage(), iox);
        }
        return null;
    }

    /**
     * Sets the context engine ID of the scoped PDU to the local engine ID provided by the {@code targetMIB}
     * member.
     *
     * @param scopedPDU
     *         the scopedPDU to modify.
     * @param context
     *         the context associated with the notification/inform PDU.
     * @param notificationID
     *         the notification ID of the notification/inform PDU.
     *
     * @since 1.2.1
     */
    protected void setContextEngineID(ScopedPDU scopedPDU,
                                      OctetString context,
                                      OID notificationID) {
        scopedPDU.setContextEngineID(new OctetString(targetMIB.getLocalEngineID()));
    }

    private boolean isAccessGranted(MOTableRow addr, MOTableRow paramsRow,
                                    OctetString context,
                                    OID notificationID,
                                    VariableBinding[] vbs) {
        if (!notificationMIB.passesFilter(paramsRow.getIndex(), notificationID, vbs)) {
            if (logger.isInfoEnabled()) {
                logger.info("Notification " + notificationID + " did not pass filter " +
                        paramsRow.getIndex());
            }
            return false;
        }
//    Integer32 mpModel = (Integer32)
//        paramsRow.getValue(SnmpTargetMIB.idxSnmpTargetParamsMPModel);
        OctetString secName = (OctetString)
                paramsRow.getValue(SnmpTargetMIB.idxSnmpTargetParamsSecurityName);
        Integer32 secLevel = (Integer32)
                paramsRow.getValue(SnmpTargetMIB.idxSnmpTargetParamsSecurityLevel);
        Integer32 secModel = (Integer32)
                paramsRow.getValue(SnmpTargetMIB.idxSnmpTargetParamsSecurityModel);
        int status = vacm.isAccessAllowed(context, secName,
                secModel.getValue(), secLevel.getValue(),
                VACM.VIEW_NOTIFY, notificationID);
        for (int i = 0; (status == VACM.VACM_OK) && (i < vbs.length); i++) {
            status = vacm.isAccessAllowed(context, secName,
                    secModel.getValue(), secLevel.getValue(),
                    VACM.VIEW_NOTIFY, vbs[i].getOid());
            if ((status != VACM.VACM_OK) && logger.isDebugEnabled()) {
                logger.debug("Notification "+notificationID+ " to target '"+
                        OctetString.fromIndex(addr.getIndex(), 0, true)+
                        "': VACM access not granted for OID "+vbs[i].getOid()+": "+status);
            }
        }
        return (status == VACM.VACM_OK);
    }

    public Object notify(OctetString context, OID notificationID, TimeTicks sysUpTime, VariableBinding[] vbs) {
        // Make a copy for better multi-threading support
        OID notificationIDCopy = new OID(notificationID);
        if (logger.isInfoEnabled()) {
            logger.info("Notification " + notificationIDCopy + " reported with " +
                    Arrays.asList(vbs) + " for context " + context);
        }
        if (targetMIB == null) {
            logger.error("TargetMIB is null, notification cannot be sent: "+notificationIDCopy);
            return new ResponseEvent<?>[0];
        }
        if (context == null) {
            context = new OctetString();
        }
        List<ResponseEvent<?>> responses = new LinkedList<>();
        long notificationEventIndex;
        synchronized (this) {
            notificationEventIndex = this.notificationEventID++;
        }
        OctetString localEngineID = null;
        if (targetMIB.getLocalEngineID() != null) {
            localEngineID = new OctetString(targetMIB.getLocalEngineID());
        }
        fireNotificationLogEvent(new NotificationLogEvent<Address>(this, localEngineID, null,
                EMPTY_CONTEXT_ENGINE_ID, context, notificationIDCopy, sysUpTime, vbs, notificationEventIndex, false));
        for (Iterator<DefaultMOMutableRow2PC> it = notificationMIB.getNotifyTable().getModel().iterator();
             it.hasNext(); ) {
            MOTableRow notifyRow = it.next();
            OctetString tag = (OctetString) notifyRow.getValue(SnmpNotificationMIB.idxSnmpNotifyTag);
            Integer32 type = (Integer32) notifyRow.getValue(SnmpNotificationMIB.idxSnmpNotifyType);
            Collection<SnmpTargetAddrEntryRow> addresses = targetMIB.getTargetAddrRowsForTag(tag);
            MOTableRowFilter<SnmpTargetAddrEntryRow> aFilter =
                    new RowStatus.ActiveRowsFilter<SnmpTargetAddrEntryRow>(SnmpTargetMIB.idxSnmpTargetAddrRowStatus);
            for (SnmpTargetAddrEntryRow address : addresses) {
                if (aFilter.passesFilter(address)) {
                    OctetString params = (OctetString) address.getValue(SnmpTargetMIB.idxSnmpTargetAddrParams);
                    MOTableRow paramsRow = targetMIB.getTargetParamsRow(params);
                    if (paramsRow != null) {
                        if (RowStatus.isRowActive(paramsRow, SnmpTargetMIB.idxSnmpTargetParamsRowStatus)) {
                            if (isAccessGranted(address, paramsRow, context, notificationIDCopy, vbs)) {
                                Integer32 timeout = (Integer32)
                                        address.getValue(SnmpTargetMIB.idxSnmpTargetAddrTimeout);
                                Integer32 retries = (Integer32)
                                        address.getValue(SnmpTargetMIB.idxSnmpTargetAddrRetryCount);
                                ResponseEvent<?> response =
                                        sendNotification(address.getAddress(), timeout, retries, address, paramsRow,
                                                context, notificationIDCopy,
                                                sysUpTime,
                                                vbs, type.getValue(), notificationEventIndex);
                                responses.add(response);
                            }
                        } else {
                            logger.warn("Found active target address but corresponding params are not active");
                        }
                    } else {
                        if (logger.isWarnEnabled()) {
                            logger.warn("Found active target address but not corresponding params row: '" + params + "'");
                        }
                    }
                }
            }
        }
        return responses.toArray(new ResponseEvent<?>[0]);
    }

    /**
     * Sets the SNMP session to used by this notification originator for sending notifications.
     *
     * @param snmpSession
     *         the Snmp instance to be used to send the notifications/informs.
     *
     * @since 1.9.1
     */
    public void setSession(Session snmpSession) {
        this.session = snmpSession;
    }

    public SysUpTime getSysUpTime() {
        return sysUpTime;
    }

    public synchronized void addNotificationLogListener(NotificationLogListener l) {
        if (notificationLogListeners == null) {
            notificationLogListeners = new ArrayList<>(2);
        }
        notificationLogListeners.add(l);
    }

    public synchronized void removeNotificationLogListener(NotificationLogListener l) {
        if (notificationLogListeners != null) {
            notificationLogListeners.remove(l);
        }
    }

    protected synchronized void fireNotificationLogEvent(NotificationLogEvent<?> event) {
        List<NotificationLogListener> listeners = notificationLogListeners;
        if (listeners != null) {
            for (NotificationLogListener listener : listeners) {
                listener.notificationLogEvent(event);
            }
        }
    }

}
