/*_############################################################################
  _## 
  _##  SNMP4J-Agent-DB 3 - MOXodusPersistenceProvider.java  
  _## 
  _##  Copyright (C) 2017-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.db;

import jetbrains.exodus.ArrayByteIterable;
import jetbrains.exodus.ByteIterable;
import jetbrains.exodus.bindings.IntegerBinding;
import jetbrains.exodus.bindings.StringBinding;
import jetbrains.exodus.env.*;
import org.snmp4j.agent.MOServer;
import org.snmp4j.agent.audit.AgentAuditPolicy;
import org.snmp4j.agent.audit.FileAuditEvent;
import org.snmp4j.cfg.EngineBootsProvider;
import org.snmp4j.cfg.EngineIdProvider;
import org.snmp4j.agent.io.ImportMode;
import org.snmp4j.agent.io.MOPersistenceProvider;
import org.snmp4j.agent.mo.MOPriorityProvider;
import org.snmp4j.smi.OctetString;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;

/**
 * The {@link MOXodusPersistenceProvider} implements a {@link MOPersistenceProvider} that uses a JetBrains Xodus
 * database for persistent storage of {@link org.snmp4j.agent.RandomAccessManagedObject}s.
 * Using {@link org.snmp4j.agent.RandomAccessManagedObject} persistence with this {@link MOXodusPersistenceProvider} has
 * the following advantages:
 * <ul>
 *     <li>Less disk space usage than with {@link org.snmp4j.agent.io.DefaultMOPersistenceProvider}</li>
 *     <li>Any Managed object changes are store via {@link org.snmp4j.agent.mo.MOChangeListener} and
 *     {@link org.snmp4j.agent.mo.MOTableRowListener} persistently into the database immediately.</li>
 *     <li>Concurrent access to storage is safe.</li>
 * </ul>
 * See {@link org.snmp4j.agent.io.MOServerPersistence} for sample code and further usage details.
 * To release the locks created on the Xodus database hold by the default persistence provider, {@link #close()} need
 * to be called explicitly.
 *
 * @author Frank Fock
 * @version 3.7.2
 */
public class MOXodusPersistenceProvider implements MOPersistenceProvider, EngineBootsProvider, EngineIdProvider {

    /**
     * Engine store ID must be longer than 32 characters - otherwise the name could clash with an SNMP context.
     */
    private final static String ENGINE_STORE_ID = "_SNMP4J-Agent_EngineStore_______v3.0";
    private static final String BOOTS_COUNTER_KEY = "BootsCounter";
    private static final String ENGINE_ID_KEY = "EngineID";

    private final MOXodusPersistence defaultPersistence;
    private final AgentAuditPolicy auditPolicy;

    /**
     * Creates a {@link MOXodusPersistenceProvider} based with a provided default {@link MOXodusPersistence} handler
     * that provides the default storage access operations and listens for
     * {@link org.snmp4j.agent.RandomAccessManagedObject} update events
     * (see {@link MOXodusPersistence#registerChangeListenersWithServer(MOServer)} for details).
     * <p>
     * Additional {@link MOXodusPersistence} can be created on the fly, with {@link #store(String)}.
     *
     * @param defaultPersistence the default persistence handler.
     * @deprecated use {@link #MOXodusPersistenceProvider(MOXodusPersistence, AgentAuditPolicy)} instead.
     */
    @Deprecated
    public MOXodusPersistenceProvider(MOXodusPersistence defaultPersistence) {
        this(defaultPersistence, null);
    }

    /**
     * Creates a {@link MOXodusPersistenceProvider} based with a provided default {@link MOXodusPersistence} handler
     * that provides the default storage access operations and listens for
     * {@link org.snmp4j.agent.RandomAccessManagedObject} update events
     * (see {@link MOXodusPersistence#registerChangeListenersWithServer(MOServer)} for details).
     * <p>
     * Additional {@link MOXodusPersistence} can be created on the fly, with {@link #store(String)}.
     *
     * @param defaultPersistence the default persistence handler.
     * @param auditPolicy the audit policy to use for the agent or {@code null} if auditing is not required.
     * @since 3.9.0
     */
    public MOXodusPersistenceProvider(MOXodusPersistence defaultPersistence, AgentAuditPolicy auditPolicy) {
        this.defaultPersistence = defaultPersistence;
        this.auditPolicy = auditPolicy;
    }

    /**
     * Restore (load) agent state from the specified URI (can be as simple as
     * a file path).
     *
     * @param uri        a string pointing to the persistent storage from which the agent state
     *                   should be restored from. The format of the string is specified by the
     *                   persistence provider. A {@code null} value can be specified to
     *                   let the persistence provider use its default URI. If that default URI
     *                   is {@code null} too, a {@code NullPointerException} will be
     *                   thrown.
     * @param importMode specifies how the agent's current state should be update while
     *                   restoring a previous state.
     * @throws IOException if the restore operation fails.
     * @since 1.2
     */
    @Override
    public void restore(String uri, int importMode) throws IOException {
        restore(uri, importMode, null);
    }

    /**
     * Restore (load) agent state from the specified URI (can be as simple as
     * a file path).
     *
     * @param uri        a string pointing to the persistent storage from which the agent state
     *                   should be restored from. The format of he string is specified by the
     *                   persistence provider. A {@code null} value can be specified to
     *                   let the persistence provider use its default URI. If that default URI
     *                   is {@code null} too, a {@code NullPointerException} will be
     *                   thrown.
     * @param importMode specifies how the agent's current state should be update while
     *                   restoring a previous state.
     * @param priorityProvider
     *                   if not {@code null}, this parameter specifies in which order the
     *                   {@link org.snmp4j.agent.ManagedObject}s are restored and which
     *                   object contains the latest restore configuration which has to be
     *                   restored at first to be able to restore the rest.
     * @throws IOException if the restore operation fails.
     * @since 3.5.0
     */
    @Override
    public void restore(String uri, int importMode, MOPriorityProvider priorityProvider) throws IOException {
        MOXodusPersistence persistence = defaultPersistence;
        boolean specialURI = (uri != null && !uri.equals(getDefaultURI()));
        String filePath = (specialURI) ? getFilePath(uri) : persistence.getEnvironment().getLocation();
        if (auditPolicy == null || auditPolicy.fileAudit(
                new FileAuditEvent(this, FileAuditEvent.FileOperation.READ, filePath))) {
            if (specialURI) {
                persistence = new MOXodusPersistence(defaultPersistence.getMOServer(),
                        Environments.newInstance(getFilePath(uri)));
            }
            persistence.load(ImportMode.values()[importMode], priorityProvider);
            if (specialURI) {
                persistence.getEnvironment().close();
            }
        }
    }

    /**
     * Stores the current agent state to persistent storage specified by the
     * supplied URI.
     *
     * @param uri a string pointing to the persistent storage from which the agent state
     *            should be stored to. The format of the string is specified by the
     *            persistence provider. A {@code null} value can be specified to
     *            let the persistence provider use its default URI. If that default URI
     *            is {@code null} too, a {@code NullPointerException} will be
     *            thrown.
     * @throws IOException if the store operation fails.
     * @since 1.2
     */
    @Override
    public void store(String uri) throws IOException {
        store(uri, null);
    }

    @Override
    public void store(String uri, MOPriorityProvider priorityProvider) throws IOException {
        MOXodusPersistence persistence = defaultPersistence;
        boolean specialURI = (uri != null && !uri.equals(getDefaultURI()));
        String fileLocation = (specialURI) ? getFilePath(uri) : persistence.getEnvironment().getLocation();
        if (auditPolicy == null || auditPolicy.fileAudit(new FileAuditEvent(this,
                (new File(fileLocation).exists() ?
                        FileAuditEvent.FileOperation.CREATE : FileAuditEvent.FileOperation.WRITE),
                fileLocation))) {
            if (specialURI) {
                persistence = new MOXodusPersistence(defaultPersistence.getMOServer(),
                        Environments.newInstance(getFilePath(uri)));
            }
            persistence.save(priorityProvider);
            if (specialURI) {
                persistence.getEnvironment().close();
            }
        }
    }

    /**
     * Checks whether the supplied URI string is valid for this persistence
     * provider.
     *
     * @param uriString a string identifying a persistent storage location for this storage
     *            provider.
     * @return {@code true} if the {@code uri} is valid, {@code false}
     * otherwise.
     * @since 1.2
     */
    @Override
    public boolean isValidPersistenceURI(String uriString) {
        try {
            URI uri = new URI(uriString);
            return uri.getScheme() == null || uri.getScheme().equals("file");
        } catch (URISyntaxException e) {
            return false;
        }
    }

    /**
     * Returns a unique ID of the persistence provider which should identify the
     * format and type of the persistence provider.
     *
     * @return an 1-32 character long string that identifies the persistence provider.
     * @since 1.2
     */
    @Override
    public String getPersistenceProviderID() {
        return "default";
    }

    /**
     * Gets the URI of the default persistent storage for this provider.
     *
     * @return the URI (e.g. file path) for the default persistent storage location of
     * this provider. A provider may use a different one. A {@code null}
     * value indicates that there is no default location.
     */
    @Override
    public String getDefaultURI() {
        return defaultPersistence.getEnvironment().getLocation();
    }

    private String getFilePath(String uri) {
        File f;
        if (uri.toUpperCase().startsWith("FILE:")) {
            URI u = URI.create(uri);
            f = new File(u);
        }
        else {
            f = new File(uri);
        }
        return f.getPath();
    }

    /**
     * Returns the current engine boot counter value incremented by one. If
     * that number would by greater than 2^31-1 then one is returned. The
     * engine boots provider has to make sure that the returned value is
     * persistently stored before the method returns.
     *
     * @return the last engine boots counter incremented by one.
     */
    @Override
    public int updateEngineBoots() {
        Environment environment = defaultPersistence.getEnvironment();
        if (auditPolicy == null || (auditPolicy.fileAudit(
                new FileAuditEvent(this, FileAuditEvent.FileOperation.READ, environment.getLocation()))
        && auditPolicy.fileAudit(
                new FileAuditEvent(this, FileAuditEvent.FileOperation.WRITE, environment.getLocation())
        ))) {
            Transaction txn = environment.beginExclusiveTransaction();

            Store store = environment.openStore(ENGINE_STORE_ID, StoreConfig.WITHOUT_DUPLICATES, txn);
            ByteIterable bootsCounterKey = StringBinding.stringToEntry(BOOTS_COUNTER_KEY);
            ByteIterable bootsCounterRaw = store.get(txn, bootsCounterKey);
            int bootsCounter = 0;
            if (bootsCounterRaw != null) {
                bootsCounter = IntegerBinding.entryToInt(bootsCounterRaw);
                bootsCounter++;
                store.put(txn, bootsCounterKey, IntegerBinding.intToEntry(bootsCounter));
            }
            txn.flush();
            txn.commit();
            return bootsCounter;
        }
        return 0;
    }

    /**
     * Returns current engine boot counter value.
     *
     * @return the last engine boots counter.
     */
    @Override
    public int getEngineBoots() {
        Environment environment = defaultPersistence.getEnvironment();
        if (auditPolicy == null || (auditPolicy.fileAudit(
                new FileAuditEvent(this, FileAuditEvent.FileOperation.READ, environment.getLocation())))) {
            Transaction txn = environment.beginReadonlyTransaction();
            Store store = environment.openStore(ENGINE_STORE_ID, StoreConfig.WITHOUT_DUPLICATES, txn);
            ByteIterable bootsCounterKey = StringBinding.stringToEntry(BOOTS_COUNTER_KEY);
            ByteIterable bootsCounterRaw = store.get(txn, bootsCounterKey);
            int bootsCounter = 0;
            if (bootsCounterRaw != null) {
                bootsCounter = IntegerBinding.entryToInt(bootsCounterRaw);
            }
            txn.abort();
            return bootsCounter;
        }
        return 0;
    }

    @Override
    public OctetString getEngineId(OctetString defaultEngineID) {
        Environment environment = defaultPersistence.getEnvironment();
        if (auditPolicy == null || (auditPolicy.fileAudit(
                new FileAuditEvent(this, FileAuditEvent.FileOperation.READ, environment.getLocation())))) {
            Transaction txn = environment.beginExclusiveTransaction();

            Store store = environment.openStore(ENGINE_STORE_ID, StoreConfig.WITHOUT_DUPLICATES, txn);
            ByteIterable engineIdKey = StringBinding.stringToEntry(ENGINE_ID_KEY);
            ByteIterable engineIdRaw = store.get(txn, engineIdKey);
            byte[] engineID = null;
            if (engineIdRaw != null) {
                engineID = new byte[engineIdRaw.getLength()];
                System.arraycopy(engineIdRaw.getBytesUnsafe(), 0, engineID, 0, engineID.length);
            } else if (defaultEngineID != null) {
                engineID = defaultEngineID.getValue();
                resetEngineId(txn, store, engineID);
            }
            txn.flush();
            txn.commit();
            return (engineIdRaw != null) ? new OctetString(engineID) : defaultEngineID;
        }
        return defaultEngineID;
    }

    @Override
    public void resetEngineId(OctetString engineId) {
        Environment environment = defaultPersistence.getEnvironment();
        if (auditPolicy == null || (auditPolicy.fileAudit(
                new FileAuditEvent(this, FileAuditEvent.FileOperation.READ, environment.getLocation())))) {
            Transaction txn = environment.beginExclusiveTransaction();

            Store store = environment.openStore(ENGINE_STORE_ID, StoreConfig.WITHOUT_DUPLICATES, txn);
            resetEngineId(txn, store, engineId.getValue());
        }
    }

    private static void resetEngineId(Transaction txn, Store store, byte[] engineID) {
        ByteIterable engineIdKey = StringBinding.stringToEntry(ENGINE_ID_KEY);
        store.put(txn, engineIdKey, new ArrayByteIterable(engineID));
        ByteIterable bootsCounterKey = StringBinding.stringToEntry(BOOTS_COUNTER_KEY);
        store.put(txn, bootsCounterKey, IntegerBinding.intToEntry(1));
    }

    /**
     * Close the Xodus environment (DB) used by the default persistence located at {@link #getDefaultURI()}.
     * That environment is then no longer usable.
     * @throws Exception
     *    If there is an error on closing the environment. This should never happen with current Xodus releases.
     * @since 3.7.2
     */
    @Override
    public void close() throws Exception {
        MOXodusPersistence persistence = defaultPersistence;
        if (persistence != null) {
            persistence.getEnvironment().close();
        }
    }
}
