/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.nifi.processors.enrich;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.naming.Context;
import javax.naming.NameNotFoundException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;

import org.apache.commons.lang3.StringUtils;

import org.apache.nifi.annotation.behavior.EventDriven;
import org.apache.nifi.annotation.behavior.InputRequirement;
import org.apache.nifi.annotation.behavior.SideEffectFree;
import org.apache.nifi.annotation.behavior.SupportsBatching;
import org.apache.nifi.annotation.behavior.WritesAttribute;
import org.apache.nifi.annotation.behavior.WritesAttributes;
import org.apache.nifi.annotation.documentation.CapabilityDescription;
import org.apache.nifi.annotation.documentation.Tags;
import org.apache.nifi.annotation.lifecycle.OnScheduled;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.flowfile.FlowFile;
import org.apache.nifi.processor.ProcessContext;
import org.apache.nifi.processor.ProcessSession;
import org.apache.nifi.processor.Relationship;
import org.apache.nifi.processor.exception.ProcessException;
import org.apache.nifi.processor.util.StandardValidators;


@EventDriven
@SideEffectFree
@SupportsBatching
@Tags({"dns", "enrich", "ip"})
@InputRequirement(InputRequirement.Requirement.INPUT_REQUIRED)
@CapabilityDescription("A powerful DNS query processor primary designed to enrich DataFlows with DNS based APIs " +
        "(e.g. RBLs, ShadowServer's ASN lookup) but that can be also used to perform regular DNS lookups.")
@WritesAttributes({
        @WritesAttribute(attribute = "enrich.dns.record*.group*", description = "The captured fields of the DNS query response for each of the records received"),
})
public class QueryDNS extends AbstractEnrichProcessor {

    public static final PropertyDescriptor DNS_QUERY_TYPE = new PropertyDescriptor.Builder()
            .name("DNS_QUERY_TYPE")
            .displayName("DNS Query Type")
            .description("The DNS query type to be used by the processor (e.g. TXT, A)")
            .required(true)
            .defaultValue("TXT")
            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
            .build();

    public static final PropertyDescriptor DNS_SERVER = new PropertyDescriptor.Builder()
            .name("DNS_SERVER")
            .displayName("DNS Servers")
            .description("A comma separated list of  DNS servers to be used. (Defaults to system wide if none is used)")
            .required(false)
            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
            .build();

    public static final PropertyDescriptor DNS_TIMEOUT = new PropertyDescriptor.Builder()
            .name("DNS_TIMEOUT")
            .displayName("DNS Query Timeout")
            .description("The amount of time to wait until considering a query as failed")
            .required(true)
            .defaultValue("1500 ms")
            .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR)
            .build();

    public static final PropertyDescriptor DNS_RETRIES = new PropertyDescriptor.Builder()
            .name("DNS_RETRIES")
            .displayName("DNS Query Retries")
            .description("The number of attempts before giving up and moving on")
            .required(true)
            .defaultValue("1")
            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
            .build();


    private final static List<PropertyDescriptor> propertyDescriptors;
    private final static Set<Relationship> relationships;

    private DirContext ictx;

    // Assign the default and generally used contextFactory value
    private String contextFactory = "com.sun.jndi.dns.DnsContextFactory";

    static {
        List<PropertyDescriptor> props = new ArrayList<>();
        props.add(QUERY_INPUT);
        props.add(QUERY_PARSER);
        props.add(QUERY_PARSER_INPUT);
        props.add(DNS_RETRIES);
        props.add(DNS_TIMEOUT);
        props.add(DNS_SERVER);
        props.add(DNS_QUERY_TYPE);
        propertyDescriptors = Collections.unmodifiableList(props);

        Set<Relationship> rels = new HashSet<>();
        rels.add(REL_FOUND);
        rels.add(REL_NOT_FOUND);
        relationships = Collections.unmodifiableSet(rels);
    }
    private AtomicBoolean initialized = new AtomicBoolean(false);


    @Override
    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
        return propertyDescriptors;
    }


    @Override
    public Set<Relationship> getRelationships() {
        return relationships;
    }


    @Override
    public void onTrigger(ProcessContext context, ProcessSession session) throws ProcessException {
        if (!initialized.get()) {
            initializeResolver(context);
            getLogger().warn("Resolver was initialized at onTrigger instead of onScheduled");

        }

        FlowFile flowFile = session.get();
        if (flowFile == null) {
            return;
        }

        final String queryType = context.getProperty(DNS_QUERY_TYPE).getValue();
        final String queryInput = context.getProperty(QUERY_INPUT).evaluateAttributeExpressions(flowFile).getValue();
        final String queryParser = context.getProperty(QUERY_PARSER).getValue();
        final String queryRegex = context.getProperty(QUERY_PARSER_INPUT).getValue();

        boolean found = false;
        try {
            Attributes results = doLookup(queryInput, queryType);
            // NOERROR & NODATA seem to return empty Attributes handled bellow
            // but defaulting to not found in any case
            if (results.size() < 1) {
                found = false;
            } else {
                int recordNumber = 0;
                NamingEnumeration<?> dnsEntryIterator = results.get(queryType).getAll();

                while (dnsEntryIterator.hasMoreElements()) {
                    String dnsRecord = dnsEntryIterator.next().toString();
                    // While NXDOMAIN is being generated by doLookup catch

                    if (dnsRecord != "NXDOMAIN") {
                        // Map<String, String> parsedResults = parseResponse(recordNumber, dnsRecord, queryParser, queryRegex, "dns");
                        Map<String, String> parsedResults = parseResponse(String.valueOf(recordNumber), dnsRecord, queryParser, queryRegex, "dns");
                        flowFile = session.putAllAttributes(flowFile, parsedResults);
                        found = true;
                    } else {
                        // Otherwise treat as not found
                        found = false;
                    }

                    // Increase the counter and iterate over next record....
                    recordNumber++;
                }
            }
        } catch (NamingException e) {
            context.yield();
            throw new ProcessException("Unexpected NamingException while processing records. Please review your configuration.", e);

        }

        // Finally prepare to send the data down the pipeline
        if (found) {
            // Sending the resulting flowfile (with attributes) to REL_FOUND
            session.transfer(flowFile, REL_FOUND);
        } else {
            // NXDOMAIN received, accepting the fate but forwarding
            // to REL_NOT_FOUND
            session.transfer(flowFile, REL_NOT_FOUND);
        }
    }


    @OnScheduled
    public void onScheduled(ProcessContext context) {
        try {
            initializeResolver(context);
        } catch (Exception e) {
            context.yield();
            throw new ProcessException("Failed to initialize the JNDI DNS resolver server", e);
        }
    }


    protected void initializeResolver(final ProcessContext context ) {

        final String dnsTimeout = context.getProperty(DNS_TIMEOUT).asTimePeriod(TimeUnit.MILLISECONDS).toString();
        final String dnsServer = context.getProperty(DNS_SERVER).getValue();
        final String dnsRetries = context.getProperty(DNS_RETRIES).getValue();


        String finalServer = "";
        Hashtable<String,String> env = new Hashtable<String,String>();
        env.put("java.naming.factory.initial", contextFactory);
        env.put("com.sun.jndi.dns.timeout.initial", dnsTimeout);
        env.put("com.sun.jndi.dns.timeout.retries", dnsRetries);
        if (StringUtils.isNotEmpty(dnsServer)) {
            for (String server : dnsServer.split(",")) {
                finalServer = finalServer + "dns://" + server + "/. ";
            }
            env.put(Context.PROVIDER_URL, finalServer);
        }

        try {
            initializeContext(env);
            initialized.set(true);
        } catch (NamingException e) {
            getLogger().error("Could not initialize JNDI context", e);
        }
    }


    /**
     * This method performs a simple DNS lookup using JNDI
     * @param queryInput String containing the query body itself (e.g. 4.3.3.1.in-addr.arpa);
     * @param queryType String containing the query type (e.g. TXT);
     */
    protected Attributes doLookup(String queryInput, String queryType) throws NamingException {
        // This is a simple DNS lookup attempt

        Attributes attrs;

        try {
            // Uses pre-existing context to resolve
            attrs = ictx.getAttributes(queryInput, new String[]{queryType});
            return attrs;
        } catch ( NameNotFoundException e) {
            getLogger().debug("Resolution for domain {} failed due to {}", new Object[]{queryInput, e});
            attrs = new BasicAttributes(queryType, "NXDOMAIN",true);
            return attrs;
        }
    }

    // This was separated from main code to ease the creation of test units injecting fake JNDI data
    // back into the processor.
    protected void initializeContext(Hashtable<String,String> env) throws NamingException {
        this.ictx = new InitialDirContext(env);
        this.initialized =  new AtomicBoolean(false);
        initialized.set(true);
    }

}
