/*_############################################################################
  _##
  _##  SNMP4J-Agent 3 - PropertyMOTreeBuilder.java
  _##
  _##  Copyright (C) 2005-2026  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.io.prop;

import org.snmp4j.agent.io.prop.MIBTree.Row;
import org.snmp4j.log.LogAdapter;
import org.snmp4j.log.LogFactory;
import org.snmp4j.smi.OID;
import org.snmp4j.smi.OctetString;
import org.snmp4j.smi.Variable;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.TreeSet;
import java.util.regex.Pattern;

/**
 * Builds an in-memory {@link MIBTree} from a {@link org.snmp4j.agent.io.MOInput}-formatted
 * properties configuration (see {@link PropertyMOInput} for the format description).
 * <p>
 * The builder reuses {@link PropertyMOInput#createVariableFromString(String, Class)} for the actual
 * value/OID parsing (including all {@code {type}value} format codes, embedded OID references and
 * randomly generated secrets), so it is fully faithful to how an agent would interpret the same
 * file. It enumerates the configured scalar and table objects per context and inserts them into an
 * OID tree.
 * <p>
 * Because no running agent is available to supply referenced instance values, all {@code {$name}}
 * value references (and {@code ${name}}/{@code $#{name}} index references) are resolved to
 * {@link UnresolvedReference} placeholders via a {@link ReferenceVariableProvider}, preserving the
 * configured reference for display.
 * <p>
 * The resulting tree can be rendered to a human readable overview using
 * {@code org.snmp4j.agent.util.MarkdownMIBTreeFormatter}.
 *
 * @author SNMP4J.org
 * @version 3.9.2
 * @since 3.9.2
 */
public class PropertyMOTreeBuilder {

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

    /** Matches a table dimension value of the form {@code <rows>:<cols>} (optionally spaced). */
    private static final Pattern DIMENSION = Pattern.compile("\\s*\\d+\\s*:\\s*\\d+\\s*");
    private static final Pattern DOTTED_OID = Pattern.compile("[0-9]+(\\.[0-9]+)*");

    private final Properties properties;
    private final PropertyMOInput parser;

    /**
     * Creates a builder for the given configuration properties.
     *
     * @param properties
     *    the {@link PropertyMOInput}-formatted properties. The properties are not modified; a
     *    private copy is used internally.
     */
    public PropertyMOTreeBuilder(Properties properties) {
        // Copy so we can guarantee the mandatory 'contexts' key exists (PropertyMOInput requires it)
        // without mutating the caller's properties.
        Properties copy = new Properties();
        copy.putAll(properties);
        String contextsKey = PropertyMOInput.CONFIG_PREFIX + PropertyMOInput.CONTEXTS_ID;
        if (copy.getProperty(contextsKey) == null) {
            copy.setProperty(contextsKey, "");
        }
        this.properties = copy;
        this.parser = new PropertyMOInput(copy, new ReferenceVariableProvider());
    }

    /**
     * Creates a builder by loading properties from the given input stream. The stream is closed
     * by this method.
     *
     * @param in
     *    the input stream to read the properties from.
     * @return a new builder.
     * @throws IOException
     *    if the stream cannot be read.
     */
    public static PropertyMOTreeBuilder from(InputStream in) throws IOException {
        Properties props = new Properties();
        try (InputStream stream = in) {
            props.load(stream);
        }
        return new PropertyMOTreeBuilder(props);
    }

    /**
     * Creates a builder by loading properties from the given reader. The reader is closed by this
     * method.
     *
     * @param reader
     *    the reader to read the properties from.
     * @return a new builder.
     * @throws IOException
     *    if the reader cannot be read.
     */
    public static PropertyMOTreeBuilder from(Reader reader) throws IOException {
        Properties props = new Properties();
        try (Reader r = reader) {
            props.load(r);
        }
        return new PropertyMOTreeBuilder(props);
    }

    /**
     * Creates a builder by loading properties from the given file.
     *
     * @param file
     *    the properties file to read.
     * @return a new builder.
     * @throws IOException
     *    if the file cannot be read.
     */
    public static PropertyMOTreeBuilder from(File file) throws IOException {
        return from(new FileInputStream(file));
    }

    /**
     * Predefines a named random value (secret) that is used instead of generating a new random
     * {@code OctetString} wherever the configuration references it via a {@code {r}name}/{@code {R}name}
     * value. This mirrors {@link PropertyMOInput#setRandomValue(String, OctetString)} and must be called
     * before {@link #build()} so that the generated overview reflects the supplied secrets (for example
     * pass keys provided on an agent command line) rather than freshly generated random values.
     *
     * @param name
     *    the name of the random value as referenced by a {@code {r}name}/{@code {R}name} value.
     * @param value
     *    the value to use for that name; a {@code null} value removes any predefined value.
     */
    public void setRandomValue(String name, OctetString value) {
        parser.setRandomValue(name, value);
    }

    /**
     * Builds the MIB tree from the configuration.
     *
     * @return the resulting {@link MIBTree}.
     */
    public MIBTree build() {
        MIBTree tree = new MIBTree();
        TreeSet<String> keys = new TreeSet<>(properties.stringPropertyNames());

        // Context-independent (default) data first.
        buildContext(tree, keys, null, "");

        // Then each declared context, in declaration order.
        for (OctetString context : readContexts()) {
            String ctxSuffix = PropertyMOInput.CTX_ID + context + ".";
            buildContext(tree, keys, context, ctxSuffix);
        }
        return tree;
    }

    private List<OctetString> readContexts() {
        List<OctetString> contexts = new ArrayList<>();
        String line = properties.getProperty(PropertyMOInput.CONFIG_PREFIX + PropertyMOInput.CONTEXTS_ID);
        if (line == null || line.trim().isEmpty()) {
            return contexts;
        }
        StringTokenizer st = new StringTokenizer(line, ",");
        while (st.hasMoreTokens()) {
            String token = st.nextToken().trim();
            if (token.isEmpty()) {
                continue;
            }
            Variable v = parser.createVariableFromString(token, OctetString.class);
            if (v instanceof OctetString) {
                contexts.add((OctetString) v);
            }
        }
        return contexts;
    }

    private void buildContext(MIBTree tree, TreeSet<String> keys, OctetString context, String ctxSuffix) {
        String oidPrefix = PropertyMOInput.CONFIG_PREFIX + PropertyMOInput.OID_ID + ctxSuffix;
        // For the default context we must not pick up the per-context entries ("oid.ctx....").
        String ctxExclusion = (context == null)
                ? PropertyMOInput.CONFIG_PREFIX + PropertyMOInput.OID_ID + PropertyMOInput.CTX_ID
                : null;

        for (String key : keys) {
            if (!key.startsWith(oidPrefix)) {
                continue;
            }
            if (ctxExclusion != null && key.startsWith(ctxExclusion)) {
                continue;
            }
            String oidStr = key.substring(oidPrefix.length());
            if (!DOTTED_OID.matcher(oidStr).matches()) {
                logger.warn("Skipping managed object with non-numeric OID key: " + key);
                continue;
            }
            OID oid = new OID(oidStr);
            String value = properties.getProperty(key);
            if (value != null && DIMENSION.matcher(value).matches()) {
                addTable(tree, context, ctxSuffix, oid, oidStr, value);
            } else {
                Variable v = parser.createVariableFromString(value, Variable.class);
                tree.addScalar(context, oid, v);
            }
        }
    }

    private void addTable(MIBTree tree, OctetString context, String ctxSuffix, OID oid, String oidStr,
                          String dimension) {
        StringTokenizer st = new StringTokenizer(dimension, ": ");
        int rows = Integer.parseInt(st.nextToken());
        int cols = Integer.parseInt(st.nextToken());
        String indexPrefix = PropertyMOInput.CONFIG_PREFIX + PropertyMOInput.INDEX_ID + ctxSuffix + oidStr;
        String valuePrefix = PropertyMOInput.CONFIG_PREFIX + PropertyMOInput.VALUE_ID + ctxSuffix + oidStr;
        List<Row> rowList = new ArrayList<>(rows);
        for (int i = 0; i < rows; i++) {
            String indexString = properties.getProperty(indexPrefix + "." + i);
            OID index = new OID();
            if (indexString != null) {
                Variable parsed = parser.createVariableFromString(indexString, OID.class);
                if (parsed instanceof OID) {
                    index = (OID) parsed;
                }
            }
            Variable[] values = new Variable[cols];
            for (int j = 0; j < cols; j++) {
                String cell = properties.getProperty(valuePrefix + "." + i + "." + j);
                if (cell != null) {
                    values[j] = parser.createVariableFromString(cell, Variable.class);
                }
            }
            rowList.add(new Row(index, values));
        }
        tree.addTable(context, oid, cols, rowList);
    }
}
