/*-
 * ~~~~~~licensing~~~~~~
 * cef-logger-layout
 * ==========
 * Copyright (C) 2020 - 2025 EmDev LLC
 * ==========
 * You may not use this file except in accordance with the License Terms of the Copyright
 * Holder located at: https://entaxy.ru/eula . All copyrights, all intellectual property
 * rights to the Software and any copies are the property of the Copyright Holder. Unless
 * it is explicitly allowed the Copyright Holder, the User is prohibited from using the
 * Software for commercial purposes to provide services to third parties.
 * 
 * The Copyright Holder hereby declares that the Software is provided on an "AS IS".
 * Under no circumstances does the Copyright Holder guarantee or promise that the
 * Software provided by him will be suitable or not suitable for the specific purposes
 * of the User, that the Software will meet all commercial and personal subjective
 * expectations of the User, that the Software will work properly, without technical
 * errors, quickly and uninterruptedly.
 * 
 * Under no circumstances shall the Copyright Holder or its Affiliates is not liable
 * to the User for any direct or indirect losses of the User, his expenses or actual
 * damage, including, downtime; loss of bussines; lost profit; lost earnings; loss
 * or damage to data, property, etc.
 * ~~~~~~/licensing~~~~~~
 */
package ru.emdev.cef;

import static java.util.Objects.nonNull;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.layout.AbstractStringLayout;
import org.apache.logging.log4j.core.lookup.StrSubstitutor;
import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.util.Strings;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

public class AuditLayout extends AbstractStringLayout {
    /**
     * Формат сообщения и версия
     */
    public static final String CEF_VERSION = "cefVersion";

    /**
     * Производитель ПО
     */
    public static final String VENDOR = "vendor";
    /**
     * Имя компонента
     */
    public static final String PRODUCT = "product";
    /**
     * Версия компонента
     */
    public static final String VERSION = "version";
    /**
     * Версия компонента
     */
    public static final String EVENT_TYPE = "typ";
    /**
     * Название события
     */
    public static final String EVENT_NAME = "eventName";
    /**
     * Важность события
     */
    public static final String SEVERITY = "severity";

    public static final List<String> HEADER_FIELDS =
            Arrays.asList(CEF_VERSION, VENDOR, PRODUCT, VERSION, EVENT_TYPE, EVENT_NAME, SEVERITY);
    /**
     * Дополнительные поля
     */
    public static final String EXTENSION = "extension";

    /**
     * IP пользователя
     */
    public static final String EXT_SRC = "src";
    /**
     * IP сервиса
     */
    public static final String EXT_DST = "dst";

    /**
     * хост пользователя
     */
    public static final String EXT_SHOST = "shost";

    /**
     * SID пользователя
     */
    public static final String EXT_SUID = "suid";
    /**
     * имя пользователя
     */
    public static final String EXT_SOURCE_USER_NAME = "suser";
    /**
     * описание события
     */
    public static final String EXT_MSG = "msg";
    /**
     * время события
     */
    public static final String EXT_END = "end";

    public static final List<String> EXT_FIELDS =
            Arrays.asList(EXT_SRC, EXT_DST, EXT_SHOST, EXT_SUID, EXT_SOURCE_USER_NAME, EXT_MSG, EXT_END);

    public static final List<String> ALL_FIELDS =
            Stream.concat(HEADER_FIELDS.stream(), EXT_FIELDS.stream()).collect(Collectors.toList());

    public static final Map<String, String> DEFAULTS = new HashMap<String, String>() {
        {
            put(CEF_VERSION, "0");
            put(VENDOR, "EMDEV");
            put(PRODUCT, "ENTAXY");
            put(VERSION, "1.11.0");
            put(EVENT_TYPE, "common");
            put(EVENT_NAME, "common event");

            put(EXT_SRC, "");
            put(EXT_DST, "");
            put(EXT_SHOST, "");
            put(EXT_SUID, "");
            put(EXT_SOURCE_USER_NAME, "");
            put(EXT_MSG, "");
            put(EXT_END, "");
        }
    };

    public static final Map<String, String> SEVERITY_BY_EVENT_NAME = new HashMap<String, String>() {
        {
            put("Add-User", "6");
            put("Delete-User", "8");
            put("Delete-User-Claim-Values", "8");
            put("Delete-User-Claim-Value", "8");
            put("Disable user account", "8");
            put("Enable user account", "6");
            put("bulk_user_import", "6");
            put("Remove local user account association with federated account", "6");
            put("Change-Password-by-User", "6");
            put("Change-Password-by-Administrator", "6");
            put("Login", "6");
            put("Login success", "6");
            put("LoginStepSuccess", "6");
            put("Login success from ip", "6");
            put("Unauthorized login attempt", "8");
            put("Null domain login attempt", "8");
            put("User authenticated to JMX", "6");
            put("User authorized to JMX", "6");
            put("Unauthorized access attempt to JMX", "8");
            put("Unauthorized attempt to read the resource", "8");
            put("DELETED", "8");
            put("PRODUCT_DELETED", "8");
            put("APIPRODUCT_DELETED", "8");
            put("APPLICATION_DELETED", "8");
            put("SUBSCRIPTION_DELETED", "8");
            put("Owner of application updated", "6");
            put("Possible Signature Wrapping Attack", "9");
            put("Possible CSRF attack", "9");
            put("Delete-Role", "8");
            put("delete", "8");
            put("Add-Role", "3");
            put("Update-Role-Name", "3");
            put("Update-Users-of-Role", "3");
            put("Update-Roles-of-User", "3");
            put("Threat detected", "9");
        }
    };


    public static final String CEF_FORMAT =
            "CEF:${" + CEF_VERSION + "}|${" + VENDOR + "}|${" + PRODUCT + "}|${" + VERSION + "}|${"
                    + EVENT_TYPE + "}|${" + EVENT_NAME + "}|${" + SEVERITY + "}|${" + EXTENSION + "}";

    private static final ObjectMapper objectMapper = new ObjectMapper();
    private final String pattern;
    public static final String ANY_NOT_WORD_SYMBOLS = "\\W";

    public AuditLayout(String pattern) {
        super(StandardCharsets.UTF_8);
        this.pattern = pattern;
    }

    @SuppressWarnings("unused")
    // @PluginFactory
    public static AuditLayout createLayout(
            // @PluginAttribute(value = "pattern", defaultString = CEF_FORMAT) String pattern) {
            String pattern) {
        return new AuditLayout(pattern);
    }

    @Override
    public String toSerializable(LogEvent logEvent) {
        return Optional.ofNullable(logEvent).map(LogEvent::getMessage).map(Message::getFormattedMessage)
                .map(this::convert).orElse(null);
    }

    public Map<String, String> jsonMap(LogEvent logEvent) {
        return Optional.ofNullable(logEvent).map(LogEvent::getMessage).map(Message::getFormattedMessage)
                .map(this::asMap).orElse(Collections.emptyMap());
    }


    private Map<String, String> asMap(String message) {
        final Map<String, String> allFields = new HashMap<>();
        parseByRegexp(message, allFields);
        if (allFields.isEmpty()) {
            parseByJackson(message, allFields);
        }
        if (!allFields.isEmpty()) {
            prepareAllFields(allFields);

            prepareExtension(allFields);

            prepareHeader(allFields);

            enhance(allFields);
        }
        return allFields;
    }

    private String convert(String message) {
        final Map<String, String> allFields = asMap(message);

        parseByRegexp(message, allFields);
        if (allFields.isEmpty()) {
            parseByJackson(message, allFields);
        }

        if (!allFields.isEmpty()) {
            message = StrSubstitutor.replace(pattern, allFields);
        }
        return message + Strings.LINE_SEPARATOR;
    }

    private void prepareAllFields(final Map<String, String> allFields) {
        ALL_FIELDS.forEach(field -> {
            String value = allFields.get(field);
            if (nonNull(value) && !value.isEmpty()) {
                if (HEADER_FIELDS.contains(field)) {
                    value = value.replace("|", "\\|");
                }
                if (EXT_FIELDS.contains(field)) {
                    value = value.replace("\\", "\\\\")
                            .replace("=", "\\=")
                            .replaceAll("\\n", " ");

                }
                if (field.equals(EXT_SRC)) {
                    value = DnsResolver.tryResolve(value);
                }
                allFields.put(field, value);
            }
        });
    }

    private void prepareExtension(final Map<String, String> allFields) {
        StringBuilder extension = new StringBuilder();
        EXT_FIELDS.stream().filter(f -> !f.equals(EXT_END)).forEach(key -> {
            String value1 = allFields.get(key);
            if (nonNull(value1) && !value1.isEmpty()) {
                extension.append(key).append("=").append(value1).append(" ");
            }
        });
        extension.append(EXT_END).append("=").append(ZonedDateTime.now().toInstant().toEpochMilli());

        allFields.put(EXTENSION, extension.toString().trim());
    }

    private void prepareHeader(final Map<String, String> allFields) {
        HEADER_FIELDS.forEach(s -> {
            String value = Optional.ofNullable(allFields.get(s)).orElse(DEFAULTS.get(s));
            allFields.put(s, value);
        });
    }

    private void enhance(Map<String, String> allFields) {

        final String eventName = allFields.get(EVENT_NAME);

        String severity = SEVERITY_BY_EVENT_NAME.keySet().stream()
                .filter(s -> eventEquals(eventName, s)).findFirst().map(s -> {
                    allFields.put(EVENT_NAME, s);
                    return s;
                }).map(SEVERITY_BY_EVENT_NAME::get).orElse("3");

        allFields.put(SEVERITY, severity);


        if (eventEquals(eventName, "Login") && nonNull(allFields.get(EXT_MSG))) {
            String msq = allFields.get(EXT_MSG).toLowerCase();

            if (msq.contains("result : success")) {
                allFields.put(SEVERITY, "6");
            } else if (msq.contains("result : failed")) {
                allFields.put(SEVERITY, "8");
            }
        }
    }

    private boolean eventEquals(String left, String right) {
        if (left != null) {
            left = left.replaceAll(ANY_NOT_WORD_SYMBOLS, "");
            left = left.toUpperCase();
        }
        if (right != null) {
            right = right.replaceAll(ANY_NOT_WORD_SYMBOLS, "");
            right = right.toUpperCase();
        }
        return Objects.equals(left, right);
    }

    private void parseByJackson(String message, Map<String, String> reqFields) {
        try {
            JsonNode jsonNode = objectMapper.readTree(message);
            if (jsonNode.isObject()) {

                String typ = Optional.ofNullable(jsonNode.get("typ")).map(JsonNode::asText).orElse("");

                String action =
                        Optional.ofNullable(jsonNode.get("action")).map(JsonNode::asText).orElse("");

                if (!typ.isEmpty() && !action.isEmpty()) {
                    reqFields.put(EVENT_TYPE, "API");
                    if (!typ.equalsIgnoreCase("api")) {
                        action = typ + "_" + action;
                    }
                }
                if (!action.isEmpty()) {
                    reqFields.put(EVENT_NAME, action.toUpperCase());
                }

                Optional.ofNullable(jsonNode.get("performedBy")).map(JsonNode::asText)
                        .ifPresent(s -> reqFields.put(EXT_SOURCE_USER_NAME, s));

                Optional.ofNullable(jsonNode.get("info")).map(JsonNode::toString)
                        .ifPresent(s -> reqFields.put(EXT_MSG, s));
            }
        } catch (IOException ignore) {
            // ignore
        }
    }

    public static void reverse(Object[] input) {
        if (input == null || input.length == 0) {
            return;
        }
        int i = 0;
        int j = input.length - 1;
        Object tmp;
        while (j > i) {
            tmp = input[j];
            input[j] = input[i];
            input[i] = tmp;
            j--;
            i++;
        }
    }

    private void parseByRegexp(String message, Map<String, String> allFields) {
        AuditEvent[] values = AuditEvent.values();
        reverse(values);
        for (AuditEvent auditEvent : values) {
            Pattern pattern = auditEvent.getPattern();
            Matcher matcher = pattern.matcher(message);
            if (matcher.matches()) {
                ALL_FIELDS.forEach(field -> {
                    String value = "";
                    try {
                        value = matcher.group(field);
                    } catch (IllegalArgumentException ignored) {
                    }
                    if (!value.isEmpty()) {
                        allFields.put(field, value);
                        allFields.put(EVENT_TYPE, auditEvent.getEventType());
                        if (Strings.isEmpty(allFields.get(EVENT_NAME))) {
                            allFields.put(EVENT_NAME, auditEvent.getEventName());
                        }
                    }
                    if (allFields.containsKey(EXT_SRC)) {
                        allFields.replace(EXT_SRC, DnsResolver.tryResolve(allFields.get(EXT_SRC)));
                    }
                });
                break;
            }
        }
    }

    public void addOverrides(Map<String, String> override) {
        DEFAULTS.putAll(override);
    }
}
