/*_############################################################################
  _## 
  _##  SNMP4J-AgentX - AgentXRequest.java  
  _## 
  _##  Copyright (C) 2005-2026  Frank Fock (SNMP4J.org)
  _##  
  _##  This program is free software; you can redistribute it and/or modify
  _##  it under the terms of the GNU General Public License version 2 as 
  _##  published by the Free Software Foundation.
  _##
  _##  This program is distributed in the hope that it will be useful,
  _##  but WITHOUT ANY WARRANTY; without even the implied warranty of
  _##  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  _##  GNU General Public License for more details.
  _##
  _##  You should have received a copy of the GNU General Public License
  _##  along with this program; if not, write to the Free Software
  _##  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 
  _##  MA  02110-1301  USA
  _##  
  _##########################################################################*/


package org.snmp4j.agent.agentx.subagent;

import java.util.*;

import org.snmp4j.*;
import org.snmp4j.agent.*;
import org.snmp4j.agent.request.*;
import org.snmp4j.mp.*;
import org.snmp4j.security.SecurityLevel;
import org.snmp4j.smi.*;
import org.snmp4j.log.LogAdapter;
import org.snmp4j.log.LogFactory;
import org.snmp4j.agent.agentx.AgentXResponsePDU;
import org.snmp4j.agent.agentx.AgentXGetBulkPDU;
import org.snmp4j.agent.agentx.AgentXRequestPDU;
import org.snmp4j.agent.agentx.AgentXVariableBindingPDU;
import org.snmp4j.agent.agentx.AgentXPDU;
import org.snmp4j.agent.agentx.AgentXContextPDU;
import org.snmp4j.agent.agentx.AgentXCommandEvent;

/**
 * The {@code AgentXRequest} class represents AgentX sub-agent requests.
 * AgentX sub-agent requests are similar to SNMP requests but especially for
 * SET request processing and query scopes differences apply.
 *
 * @author Frank Fock
 * @version 1.0
 */
@SuppressWarnings("unchecked")
public class AgentXRequest
        extends AbstractRequest<AgentXRequest.AgentXSubRequest, AgentXCommandEvent<?>, AgentXResponsePDU> {

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

    /**
     * The default context (an empty {@link OctetString}) that is used when the
     * underlying AgentX PDU does not specify a context.
     */
    public static final OctetString DEFAULT_CONTEXT = new OctetString();

    /**
     * Map of user objects that are associated with the processing of this request
     * and can be shared between the processing steps. It is lazily initialized on
     * first use.
     */
    protected Map<Object, Object> processingUserObjects;

    private static int nextTransactionID = 0;

    /**
     * Creates an {@code AgentXRequest} from the given AgentX command event. The
     * request values are corrected according to the command type and a new unique
     * transaction ID is assigned.
     *
     * @param request the {@link AgentXCommandEvent} that triggered this request.
     */
    public AgentXRequest(AgentXCommandEvent<?> request) {
        super(request);
        this.source = request;
        correctRequestValues();
        this.transactionID = nextTransactionID();
    }

    /**
     * Returns the next unique transaction ID and increments the internal
     * transaction ID counter.
     *
     * @return a new transaction ID for an AgentX request.
     */
    public static int nextTransactionID() {
        return nextTransactionID++;
    }

    public int size() {
        if (source.getCommand() instanceof AgentXRequestPDU) {
            return ((AgentXRequestPDU) source.getCommand()).size();
        } else if (source.getCommand() instanceof AgentXVariableBindingPDU) {
            return ((AgentXVariableBindingPDU) source.getCommand()).size();
        }
        return 0;
    }

    public boolean isBulkRequest() {
        return source.getCommand().getType() == AgentXPDU.AGENTX_GETBULK_PDU;
    }

    private void correctRequestValues() {
        AgentXPDU request = source.getCommand();
        if (request instanceof AgentXGetBulkPDU) {
            repeaterStartIndex = getNonRepeaters();
            repeaterRowSize =
                    Math.max(size() - repeaterStartIndex, 0);
        } else {
            repeaterStartIndex = 0;
            repeaterRowSize = size();
        }
    }

    protected void setupSubRequests() {
        int capacity = size();
        int totalRepetitions = 0;
        if (source.getCommand() instanceof AgentXGetBulkPDU) {
            totalRepetitions = repeaterRowSize * getMaxRepetitions();
        }
        subrequests = new ArrayList<AgentXSubRequest>(capacity + totalRepetitions);
        if (response == null) {
            response = createResponse();
        }
        if (source.getCommand() instanceof AgentXRequestPDU) {
            AgentXRequestPDU rangeRequest =
                    (AgentXRequestPDU) source.getCommand();
            MOScope[] ranges = rangeRequest.getRanges();
            for (int i = 0; i < ranges.length; i++) {
                AgentXSubRequest subReq =
                        new AgentXSubRequest(
                                new DefaultMOContextScope(getContext(), ranges[i]), i);
                addSubRequest(subReq);
            }
        } else if (source.getCommand() instanceof AgentXVariableBindingPDU) {
            AgentXVariableBindingPDU vbRequest =
                    (AgentXVariableBindingPDU) source.getCommand();
            VariableBinding[] vbs = vbRequest.getVariableBindings();
            for (int i = 0; i < vbs.length; i++) {
                AgentXSubRequest subReq = new AgentXSubRequest(vbs[i], i);
                addSubRequest(subReq);
            }
        }
        if (logger.isDebugEnabled()) {
            logger.debug("AgentXSubRequests initialized: " + subrequests);
        }
    }

    /**
     * Returns the maximum number of repetitions requested for the repeater variable
     * bindings, which only applies to GETBULK requests.
     *
     * @return the max-repetitions value of the underlying GETBULK PDU, or {@code 0}
     * if this is not a GETBULK request.
     */
    public int getMaxRepetitions() {
        if (source.getCommand() instanceof AgentXGetBulkPDU) {
            return ((AgentXGetBulkPDU) source.getCommand()).getMaxRepetitions()
                    & 0xFFFF;
        }
        return 0;
    }

    /**
     * Returns the number of non-repeater variable bindings of the request, which
     * only applies to GETBULK requests.
     *
     * @return the non-repeaters value of the underlying GETBULK PDU, or {@code 0}
     * if this is not a GETBULK request.
     */
    public int getNonRepeaters() {
        if (source.getCommand() instanceof AgentXGetBulkPDU) {
            return ((AgentXGetBulkPDU) source.getCommand()).getNonRepeaters()
                    & 0xFFFF;
        }
        return 0;
    }

    private void addSubRequest(AgentXSubRequest subReq) {
        subrequests.add(subReq);
        response.add(subReq.getVariableBinding());
    }

    protected int getMaxPhase() {
        return (is2PC()) ? PHASE_2PC_CLEANUP : PHASE_1PC;
    }

    /**
     * Sets the AgentX command event that is the source of this request.
     *
     * @param source the {@link AgentXCommandEvent} that triggered this request.
     */
    public void setRequestEvent(AgentXCommandEvent<?> source) {
        this.source = source;
    }

    /**
     * Copies the current error status and error index of this request into the
     * associated response PDU.
     */
    protected void assignErrorStatus2Response() {
        int errStatus = getErrorStatus();
        response.setErrorStatus((short) errStatus);
        response.setErrorIndex((short) getErrorIndex());
    }

    private AgentXResponsePDU createResponse() {
        AgentXResponsePDU resp = new AgentXResponsePDU(0, (short) 0, (short) 0);
        resp.setTransactionID(transactionID);
        return resp;
    }

    /**
     * Returns the AgentX response PDU for this request. This is a convenience
     * method that delegates to {@link #getResponse()}.
     *
     * @return the {@link AgentXResponsePDU} associated with this request, or
     * {@code null} for the cleanup and undo phases of a two-phase commit SET.
     */
    public AgentXResponsePDU getResponsePDU() {
        return getResponse();
    }

    public AgentXResponsePDU getResponse() {
        if (response == null) {
            response = createResponse();
            assignErrorStatus2Response();
        } else {
            assignErrorStatus2Response();
        }
        if (is2PC()) {
            response.clear();
            if ((source.getCommand().getType() == AgentXPDU.AGENTX_CLEANUPSET_PDU) ||
                    (source.getCommand().getType() == AgentXPDU.AGENTX_UNDOSET_PDU)) {
                return null;
            }
        }
        return response;
    }

    public Iterator<AgentXRequest.AgentXSubRequest> iterator() {
        initSubRequests();
        return new AgentXSubRequestIterator();
    }


    /**
     * Indicates whether this request is processed using the AgentX two-phase commit
     * (2PC) SET protocol, i.e. whether its command is one of the TestSet, CommitSet,
     * UndoSet or CleanupSet PDUs.
     *
     * @return {@code true} if this is a two-phase commit SET request, {@code false} otherwise.
     */
    protected boolean is2PC() {
        return ((source.getCommand().getType() >= AgentXPDU.AGENTX_TESTSET_PDU) &&
                (source.getCommand().getType() <= AgentXPDU.AGENTX_CLEANUPSET_PDU));
    }

    public OctetString getContext() {
        if (source.getCommand() instanceof AgentXContextPDU) {
            return ((AgentXContextPDU) source.getCommand()).getContext();
        }
        return DEFAULT_CONTEXT;
    }

    public OctetString getViewName() {
        throw new UnsupportedOperationException();
    }

    public void setViewName(OctetString viewName) {
        throw new UnsupportedOperationException();
    }

    /**
     * Returns the security level for the request. The security level determines the
     * authentication and encryption requirements for processing the request.
     * For {@link AgentXRequest} always returns {@link SecurityLevel#NOAUTH_NOPRIV} because AgentX protocol
     * does not support authentication and privacy (this needs to be implemented by the transport mapping).
     *
     * @return the security level as an integer, where:
     *         1 represents "No Authentication, No Privacy" ({@link SecurityLevel#NOAUTH_NOPRIV}),
     *         2 represents "Authentication, No Privacy" ({@link SecurityLevel#AUTH_NOPRIV}),
     *         3 represents "Authentication and Privacy" ({@link SecurityLevel#AUTH_PRIV}).
     */
    public int getSecurityLevel() {
        return SecurityLevel.NOAUTH_NOPRIV;
    }

    public int getSecurityModel() {
        throw new UnsupportedOperationException();
    }

    public OctetString getSecurityName() {
        throw new UnsupportedOperationException();
    }

    public int getViewType() {
        throw new UnsupportedOperationException();
    }

    /**
     * Appends a new repeater sub-request to the sub-requests list by continuing the
     * repetition row of the corresponding predecessor sub-request. This is used to
     * dynamically grow the sub-requests of a GETBULK request while iterating.
     */
    protected synchronized void addRepeaterSubRequest() {
        int predecessorIndex = subrequests.size() - repeaterRowSize;
        AgentXSubRequest sreq =
                new AgentXSubRequest(subrequests.get(predecessorIndex),
                        subrequests.size());
        addSubRequest(sreq);
    }

    /**
     * Returns the last repetition row that is complete (regarding the number
     * of elements in the row).
     *
     * @return a sub list of the sub-requests list that contains the row's elements.
     * If no such row exists {@code null} is returned.
     */
    private List<AgentXSubRequest> lastRow() {
        if (repeaterRowSize == 0) {
            return null;
        }
        int rows = (subrequests.size() - repeaterStartIndex) / repeaterRowSize;
        int startIndex = repeaterStartIndex + (repeaterRowSize * (rows - 1));
        int endIndex = repeaterStartIndex + (repeaterRowSize * rows);
        return subrequests.subList(startIndex, endIndex);
    }

    public int getMessageProcessingModel() {
        throw new UnsupportedOperationException();
    }

    public String toString() {
        return getClass().getName() + "[subrequests=" + subrequests + ",phase=" + phase +
                ",source=" + source + "]";
    }

    public boolean isPhaseComplete() {
        if (errorStatus == SnmpConstants.SNMP_ERROR_SUCCESS) {
            initSubRequests();
            for (SubRequest<?> subrequest : subrequests) {
                RequestStatus status = subrequest.getStatus();
                if (status.getErrorStatus() != SnmpConstants.SNMP_ERROR_SUCCESS) {
                    return true;
                } else if (!status.isPhaseComplete()) {
                    return false;
                }
            }
        }
        return true;
    }

    public synchronized Object getProcessingUserObject(Object key) {
        if (processingUserObjects != null) {
            return processingUserObjects.get(key);
        }
        return null;
    }

    public synchronized Object setProcessingUserObject(Object key, Object value) {
        if (processingUserObjects == null) {
            processingUserObjects = new HashMap<>(5);
        }
        return processingUserObjects.put(key, value);
    }

    /**
     * The AgentXSubRequestIterator iterates over the subrequests in a AgentX
     * request. In case of bulk operations, it also may physically append
     * new sub-request instances while iterating, until the bulk operations
     * limits are reached.
     *
     * @author Frank Fock
     * @version 3.0.0
     */
    public class AgentXSubRequestIterator implements SubRequestIterator<AgentXSubRequest> {

        private int cursor = 0;
        private int increment = 1;

        /**
         * Creates a sub-request iterator that iterates over all sub-requests
         * starting at the first one with an increment of one.
         */
        protected AgentXSubRequestIterator() {
        }

        /**
         * Creates a sub-request iterator that starts at the given offset and
         * advances by the given increment on each step.
         *
         * @param offset    the zero-based index of the first sub-request to return.
         * @param increment the number of sub-requests to advance on each iteration step.
         */
        protected AgentXSubRequestIterator(int offset, int increment) {
            this.cursor = offset;
            this.increment = increment;
        }

        private int getRepeaterCount() {
            AgentXPDU pdu = source.getCommand();
            if (pdu instanceof AgentXGetBulkPDU bulkPDU) {
                return Math.max(bulkPDU.size() - bulkPDU.getNonRepeaters(), 0);
            }
            return 0;
        }

        public boolean hasNext() {
            AgentXPDU reqPDU = source.getCommand();
            if (reqPDU.getType() == AgentXPDU.AGENTX_GETBULK_PDU) {
                AgentXGetBulkPDU bulkPDU = (AgentXGetBulkPDU) reqPDU;
                if (cursor < Math.min(bulkPDU.size(), bulkPDU.getNonRepeaters())) {
                    return true;
                } else {
                    if (cursor < bulkPDU.getNonRepeaters() +
                            bulkPDU.getMaxRepetitions() * getRepeaterCount()) {
                        List<AgentXSubRequest> lastRow = lastRow();
                        if (lastRow != null) {
                            boolean allEndOfMibView = true;
                            for (SubRequest<?> sreq : lastRow) {
                                if (sreq.getVariableBinding().getSyntax() !=
                                        SMIConstants.EXCEPTION_END_OF_MIB_VIEW) {
                                    allEndOfMibView = false;
                                    break;
                                }
                            }
                            if (allEndOfMibView) {
                                return false;
                            }
                        }
                        return true;
                    }
                }
                return false;
            }
            return (cursor < size());
        }

        public AgentXSubRequest next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            if ((isBulkRequest()) &&
                    (cursor >= subrequests.size())) {
                while (cursor >= subrequests.size()) {
                    addRepeaterSubRequest();
                }
            }
            AgentXSubRequest sreq = subrequests.get(cursor);
            cursor += increment;
            return sreq;
        }

        public void remove() {
            throw new UnsupportedOperationException("Remove is not supported on sub-requests");
        }

        public boolean equals(Object other) {
            if (other instanceof AgentXRequest) {
                return ((AgentXRequest) other).getTransactionID() == getTransactionID();
            }
            return false;
        }

        public int hashCode() {
            return getTransactionID();
        }
    }


    /**
     * The {@code AgentXSubRequest} implements the {@code SubRequest}
     * interface for AgentX sub-requests.
     *
     * @author Frank Fock
     * @version 1.0
     */
    public class AgentXSubRequest implements SubRequest<AgentXSubRequest>, RequestStatusListener {

        private RequestStatus status;
        private VariableBinding vb;
        private Object undoValue;
        private MOScope scope;
        private MOQuery query;
        private ManagedObject<? super AgentXSubRequest> targetMO;
        private final int index;

        private MOServerLookupEvent lookupEvent;

        private volatile Object userObject;

        private AgentXSubRequest(int index) {
            this.index = index;
            status = new RequestStatus();
            status.addRequestStatusListener(this);
        }

        /**
         * Creates a sub-request for the given search range (used for GET, GETNEXT
         * and GETBULK requests). The variable binding is initialized with the
         * lower bound OID of the supplied scope.
         *
         * @param searchRange the {@link MOScope} that defines the search range of this
         *                    sub-request.
         * @param index       the zero-based position of this sub-request within its request.
         */
        protected AgentXSubRequest(MOScope searchRange, int index) {
            this(index);
            this.scope = searchRange;
            this.vb = new VariableBinding(searchRange.getLowerBound());
        }

        /**
         * Creates a sub-request for the given variable binding (used for SET and
         * exact GET requests). The scope is initialized to match exactly the OID
         * of the supplied variable binding within the request's context.
         *
         * @param subrequest the {@link VariableBinding} to be processed by this sub-request.
         * @param index      the zero-based position of this sub-request within its request.
         */
        protected AgentXSubRequest(VariableBinding subrequest, int index) {
            this(index);
            this.vb = subrequest;
            OID oid = this.vb.getOid();
            this.scope = new DefaultMOContextScope(getContext(),
                    oid, true, oid, true);
        }

        /**
         * Creates a repeater sub-request that continues the processing of the given
         * predecessor sub-request. For GETNEXT and GETBULK commands the scope starts
         * after the predecessor's current OID; for other command types the
         * predecessor's scope is reused.
         *
         * @param predecessor the sub-request whose repetition is continued by this sub-request.
         * @param index       the zero-based position of this sub-request within its request.
         */
        protected AgentXSubRequest(AgentXSubRequest predecessor, int index) {
            this(index);
            this.vb = new VariableBinding(predecessor.getVariableBinding().getOid());
            switch (source.getCommand().getType()) {
                case AgentXPDU.AGENTX_GETBULK_PDU:
                case AgentXPDU.AGENTX_GETNEXT_PDU: {
                    this.scope =
                            new DefaultMOContextScope(getContext(),
                                    predecessor.getVariableBinding().getOid(),
                                    false,
                                    predecessor.getScope().getUpperBound(),
                                    predecessor.getScope().isUpperIncluded());
                    break;
                }
                default: {
                    this.scope = new DefaultMOContextScope(getContext(),
                            predecessor.getScope());
                }
            }
//    Do not copy queries because they need to be updated externally only!
//    this.query = predecessor.getQuery();
        }

        @Override
        public Request<?, ?, ?> getRequest() {
            return AgentXRequest.this;
        }

        public RequestStatus getStatus() {
            return status;
        }

        public VariableBinding getVariableBinding() {
            return vb;
        }

        /**
         * Sets the request status object that tracks the processing state and
         * error status of this sub-request.
         *
         * @param status the new {@link RequestStatus} for this sub-request.
         */
        public void setStatus(RequestStatus status) {
            this.status = status;
        }

        public Object getUndoValue() {
            return undoValue;
        }

        public void setUndoValue(Object undoInformation) {
            this.undoValue = undoInformation;
        }

        public void requestStatusChanged(RequestStatusEvent event) {
            int newStatus = event.getStatus().getErrorStatus();
            AgentXRequest.this.setErrorStatus(newStatus);
            if (logger.isDebugEnabled() &&
                    (newStatus != SnmpConstants.SNMP_ERROR_SUCCESS)) {
                new Exception("Error " + event.getStatus().getErrorStatus() +
                        " generated at: " + vb).printStackTrace();
            }
        }

        public MOScope getScope() {
            return scope;
        }

        public void completed() {
            status.setPhaseComplete(true);
        }

        public boolean hasError() {
            return getStatus().getErrorStatus() != SnmpConstants.SNMP_ERROR_SUCCESS;
        }

        public boolean isComplete() {
            return status.isPhaseComplete();
        }

        public void setTargetMO(ManagedObject<? super AgentXSubRequest> managedObject) {
            this.targetMO = managedObject;
        }

        public ManagedObject<? super AgentXSubRequest> getTargetMO() {
            return targetMO;
        }

        public int getIndex() {
            return index;
        }

        public void setQuery(MOQuery query) {
            this.query = query;
        }

        public MOQuery getQuery() {
            return query;
        }

        public String toString() {
            return getClass().getName() + "[scope=" + scope +
                    ",vb=" + vb + ",status=" + status + ",query=" + query + ",index=" + index +
                    ",targetMO=" + targetMO + "]";
        }

        public SubRequestIterator<AgentXSubRequest> repetitions() {
            initSubRequests();
            if (source.getCommand().getType() == AgentXPDU.AGENTX_GETBULK_PDU) {
                AgentXGetBulkPDU getBulk = (AgentXGetBulkPDU) source.getCommand();
                int repeaters = getBulk.size() - getBulk.getNonRepeaters();
                if (repeaters > 0) {
                    return new AgentXSubRequestIterator(Math.max(getIndex(), getBulk.getNonRepeaters()), repeaters);
                }
                // return empty iterator
            }
            return new SubRequestIteratorSupport<>(Collections.EMPTY_LIST.iterator());
        }

        public void updateNextRepetition() {
            SubRequestIterator<AgentXSubRequest> repetitions = repetitions();
            // skip this one
            repetitions.next();
            if (repetitions.hasNext()) {
                if ((getStatus().getErrorStatus() == PDU.noError) &&
                        (!this.vb.isException())) {
                    AgentXSubRequest nsreq = repetitions.next();
                    nsreq.query = null;
                    nsreq.scope =
                            new DefaultMOContextScope(getContext(),
                                    this.getVariableBinding().getOid(),
                                    false,
                                    this.getScope().getUpperBound(),
                                    this.getScope().isUpperIncluded());
                } else if (this.vb.isException()) {
                    while (repetitions.hasNext()) {
                        AgentXSubRequest nsreq = repetitions.next();
                        nsreq.query = null;
                        nsreq.getVariableBinding().setOid(this.vb.getOid());
                        nsreq.getVariableBinding().setVariable(this.vb.getVariable());
                        nsreq.getStatus().setPhaseComplete(true);
                    }
                }
            }
        }

        public final void setErrorStatus(int errorStatus) {
            getStatus().setErrorStatus(errorStatus);
        }

        public final int getErrorStatus() {
            return getStatus().getErrorStatus();
        }

        public Object getUserObject() {
            return userObject;
        }

        public void setUserObject(Object userObject) {
            this.userObject = userObject;
        }

        /**
         * Returns the {@link MOServerLookupEvent} associated with this sub-request,
         * describing the managed object lookup performed for it.
         *
         * @return the lookup event, or {@code null} if no lookup event has been set.
         */
        public MOServerLookupEvent getLookupEvent() {
            return lookupEvent;
        }

        /**
         * Sets the {@link MOServerLookupEvent} associated with this sub-request.
         *
         * @param lookupEvent the lookup event describing the managed object lookup
         *                    performed for this sub-request.
         */
        public void setLookupEvent(MOServerLookupEvent lookupEvent) {
            this.lookupEvent = lookupEvent;
        }

    }
}

