/*
 * Copyright 2014-2015 Groupon, Inc
 * Copyright 2014-2015 The Billing Project, LLC
 *
 * The Billing Project 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.killbill.billing.server.log.obfuscators;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.annotation.Nullable;

import org.killbill.commons.utils.annotation.VisibleForTesting;
import org.killbill.commons.profiling.ProfilingFeature.ProfilingFeatureType;

import ch.qos.logback.classic.spi.ILoggingEvent;

public abstract class Obfuscator {

    @VisibleForTesting
    static final String LOGGING_FILTER_NAME = "org.glassfish.jersey.logging.LoggingFeature";

    protected static final int DEFAULT_PATTERN_FLAGS = Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL;

    protected static final char PAD_CHAR = '*';

    public abstract String obfuscate(final String originalString, final ILoggingEvent event);

    protected String obfuscate(final String originalString, final Iterable<Pattern> patterns, final ILoggingEvent event) {
        final StringBuilder obfuscatedStringBuilder = new StringBuilder(originalString);

        for (final Pattern pattern : patterns) {
            int currentOffset = 0;
            // Create a matcher with a copy of the current obfuscated String
            final Matcher matcher = pattern.matcher(obfuscatedStringBuilder.toString());
            while (matcher.find()) {
                for (int groupNb = 1; groupNb <= matcher.groupCount(); groupNb++) {
                    final String confidentialData = matcher.group(groupNb);

                    if (shouldObfuscate(confidentialData, event)) {
                        final String obfuscatedConfidentialData = obfuscateConfidentialData(confidentialData);

                        obfuscatedStringBuilder.replace(currentOffset + matcher.start(groupNb), currentOffset + matcher.end(groupNb), obfuscatedConfidentialData);

                        // The original String is modified in place, which will confuse the Matcher if it becomes bigger
                        if (obfuscatedConfidentialData.length() > confidentialData.length()) {
                            currentOffset += obfuscatedConfidentialData.length() - confidentialData.length();
                        }
                    }
                }
            }
        }

        return obfuscatedStringBuilder.toString();
    }

    private boolean shouldObfuscate(final String confidentialData, final ILoggingEvent event) {
        return !isProfilingHeader(confidentialData, event);
    }

    // Huge hack to avoid obfuscating the "name" in the X-Killbill-Profiling-Resp json. Unfortunately, we can't simply
    // filter-out c.s.j.a.c.filter.LoggingFilter because we do want to obfuscate requests (in case sensitive data is passed as
    // query parameters, e.g. in plugin properties)
    private boolean isProfilingHeader(final String confidentialData, final ILoggingEvent event) {
        if (!LOGGING_FILTER_NAME.equals(event.getLoggerName())) {
            return false;
        }

        for (final ProfilingFeatureType profileType : ProfilingFeatureType.values()) {
            // See ProfilingDataItem#getKey
            if (confidentialData.startsWith("\"" + profileType.name() + ":")) {
                return true;
            }
        }
        return false;
    }

    private String obfuscateConfidentialData(final CharSequence confidentialSequence) {
        return obfuscateConfidentialData(confidentialSequence, null);
    }

    /**
     * Get a mask string for masking the given `confidentialSequence`.
     *
     * @param confidentialSequence the string to be obfuscated
     * @param unmasked             the section of `confidentialSequence` to be left unmasked
     * @return a mask string
     */
    @VisibleForTesting
    String obfuscateConfidentialData(final CharSequence confidentialSequence, @Nullable final CharSequence unmasked) {
        final int maskedLength = unmasked == null ? confidentialSequence.length() : confidentialSequence.length() - unmasked.length();
        return new String(new char[maskedLength]).replace('\0', PAD_CHAR);
    }
}
