package be.iminds.ilabt.jfed.call_log_output;

import be.iminds.ilabt.jfed.util.IOUtils;
import org.slf4j.LoggerFactory;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
import java.io.*;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * LogOutput
 */
public class LogOutput {
    protected static final org.slf4j.Logger LOG = LoggerFactory.getLogger(LogOutput.class);
    protected static boolean disableRemoveSecurityRisks = false;

    public static List<SecurityRiskDescription> getAllSecurityRiskDescriptions() {
        List<SecurityRiskDescription> res = new ArrayList<>();
        //Credentials are not really security risks.
//        res.add(new SecurityRiskDescription("<signed-credential", "</signed-credential>"));
//        res.add(new SecurityRiskDescription("<owner_gid", "</owner_gid>"));
//        res.add(new SecurityRiskDescription("<target_gid", "</target_gid>"));
//        res.add(new SecurityRiskDescription("<X509Certificate", "<X509Certificate>"));
//
//        res.add(new SecurityRiskDescription("&lt;signed-credential", "&lt;/signed-credential&gt;"));
//        res.add(new SecurityRiskDescription("&lt;owner_gid", "&lt;/owner_gid&gt;"));
//        res.add(new SecurityRiskDescription("&lt;target_gid", "&lt;/target_gid&gt;"));
//        res.add(new SecurityRiskDescription("&lt;X509Certificate", "&lt;X509Certificate&gt;"));

        //private keys are always security risks
        res.add(new SecurityRiskDescription("-----BEGIN RSA PRIVATE KEY-----", "-----END RSA PRIVATE KEY-----"));
        res.add(new SecurityRiskDescription("-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----"));

        //certificates are not security risks
//        res.add(new SecurityRiskDescription("MIID", "[^A-Za-z0-9+/=._: \n-]")); //PEM Base64 certificate block detection (starts with MIID due to header)

//        res.add(new SecurityRiskDescription("", ""));
        return res;
    }

    public void copyToFile(URL in, File fileOut) throws IOException {
        assert in != null;
        assert fileOut != null;
        OutputStream out = new FileOutputStream(fileOut);
        InputStream is = in.openStream();
        byte[] buffer = new byte[1024];
        int len;
        while ((len = is.read(buffer)) != -1) {
            out.write(buffer, 0, len);
        }
        out.close();
    }

    /* Scans the input string for credentials, and removes them. */
    public String removeSecurityRisks(String text) {
        if (disableRemoveSecurityRisks) return text;
        if (text == null) return null;

        StringBuilder sb = new StringBuilder();
        List<SecurityRiskDescription> secRisks = getAllSecurityRiskDescriptions();

//        System.out.println("Scanning string of size "+text.length()+" for "+secRisks.size()+" security risks.");

        while (text.length() > 0) {
            int firstStringIndex = -1;
            int secRiskIndex = -1;
            int i = 0;
            for (SecurityRiskDescription secRisk : secRisks) {
                int stringIndex = text.indexOf(secRisk.start);
                if (stringIndex >= 0 && (stringIndex < firstStringIndex || firstStringIndex == -1)) {
                    firstStringIndex = stringIndex;
                    secRiskIndex = i;
                }
                i++;
            }

            if (secRiskIndex != -1) {
                //add part before security risk
                String safePart = text.substring(0, firstStringIndex);
                sb.append(safePart);
                text = text.substring(firstStringIndex);

                //cut out security risk
                SecurityRiskDescription secRisk = secRisks.get(secRiskIndex);
//                System.out.println("   ********* Found security risk: "+secRisk.start+" @ position "+firstStringIndex);
                Pattern pattern = Pattern.compile(secRisk.end);
                Matcher matcher = pattern.matcher(text);
                if (matcher.find()) {
                    //int startIndex = matcher.start();
                    int endIndex = matcher.end();
                    String unsafe = text.substring(0, endIndex);
                    text = text.substring(endIndex);
                    sb.append("**** SECURITY: removed text that looked like a security risk. (").append(unsafe.length()).append(" characters) ****");
                } else {
                    //problem: out of paranoia, lose the rest of the string
                    sb.append("**** SECURITY: removed text that looked like a security risk. WARNING: could not find end, so removed rest of text (").append(text.length()).append(" characters) ****");
                    text = "";
                }

            } else {
//                System.out.println("   No (more) security risks found.");
                //add entire string
                sb.append(text);
                text = "";
            }
        }

        return sb.toString();
    }

    public enum TestResultState {SUCCESS, WARN, SKIPPED, FAILED}

    public enum LogLineType {TRACE, DEBUG, NOTE, WARN, ERROR, FATAL_ERROR, EXCEPTION}

    @XmlRootElement(name = "logentry")
    @XmlType(name="logentry")
    public static class LogEntry {
        LogLineType type;

        String text;

        long timeMs;

        String exception;

        boolean formatAsCode;

        public LogEntry() {
         /* only to be used by JAXB */
            this.type = null;
            this.text = null;
            this.exception = null;
            this.timeMs = -1;
            this.formatAsCode = false;
        }

        public LogEntry(LogLineType type, String text, long timeMs) {
            this(type, text, timeMs, (Throwable) null, false);
        }

        public LogEntry(LogLineType type, String text, long timeMs, String exception, boolean formatAsCode) {
            assert type != null;
            assert text != null;
            this.type = type;
            this.text = text;
            this.timeMs = timeMs;
            this.exception = exception;
            this.formatAsCode = formatAsCode;
        }

        public LogEntry(LogLineType type, String text, long timeMs, Throwable exception, boolean formatAsCode) {
            assert type != null;
            assert text != null;
            this.type = type;
            this.text = text;
            this.timeMs = timeMs;
            this.exception = exception == null ? null : IOUtils.exceptionToStacktraceString(exception);
            this.formatAsCode = formatAsCode;
        }

        public LogEntry(LogLineType type, String text) {
            this(type, text, System.currentTimeMillis(), (Throwable) null, false);
        }
        public LogEntry(LogLineType type, String text, Throwable exception) {
            this(type, text, System.currentTimeMillis(), exception, false);
        }
        public LogEntry(LogLineType type, String text, boolean formatAsCode) {
            this(type, text, System.currentTimeMillis(), (Throwable) null, formatAsCode);
        }
        public LogEntry(LogLineType type, String text, Throwable exception, boolean formatAsCode) {
            this(type, text, System.currentTimeMillis(), exception, formatAsCode);
        }

        @XmlElement(name = "type")
        public LogLineType getType() {
            return type;
        }

        @XmlElement(name = "text")
        public String getText() {
            return text;
        }

        @XmlElement(name = "timeInMs")
        public long getTimeMs() {
            return timeMs;
        }

        @XmlElement(name = "exception")
        public String getExceptionStackTrace() {
            return exception;
        }

        @XmlElement(name = "formatAsCode")
        public boolean isFormatAsCode() {
            return formatAsCode;
        }


        public void setType(LogLineType type) {
            this.type = type;
        }

        public void setText(String text) {
            this.text = text;
        }

        public void setTimeMs(long timeMs) {
            this.timeMs = timeMs;
        }

        public void setException(String exception) {
            this.exception = exception;
        }

        public void setFormatAsCode(boolean formatAsCode) {
            this.formatAsCode = formatAsCode;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof LogEntry)) return false;

            LogEntry logEntry = (LogEntry) o;

            if (timeMs != logEntry.timeMs) return false;
            if (exception != null ? !Objects.equals(exception, logEntry.exception) : logEntry.exception != null) return false;
            if (text != null ? !Objects.equals(text, logEntry.text) : logEntry.text != null) return false;
            if (type != logEntry.type) return false;

            return true;
        }

        @Override
        public int hashCode() {
            int result = type.hashCode();
            result = 31 * result + (text != null ? text.hashCode() : 0);
            result = 31 * result + (int) (timeMs ^ (timeMs >>> 32));
            result = 31 * result + (exception != null ? exception.hashCode() : 0);
            return result;
        }
    }

    private static class SecurityRiskDescription {
        public final String start;
        public final String end;

        private SecurityRiskDescription(String start, String end) {
            this.start = start;
            this.end = end;
        }
    }
}
