/*
 * Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * 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 io.helidon.security.spi;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import io.helidon.common.StackWalker;
import io.helidon.security.AuditEvent;

/**
 * Audit provider, storing audit events.
 * If no custom audit provider is defined (using
 * {@link io.helidon.security.Security.Builder#addAuditProvider(AuditProvider)}) a default provider will be used.
 * <p>
 * Default audit provider logs most events in {@link java.util.logging.Level#FINEST}.
 * {@link AuditEvent.AuditSeverity#AUDIT_FAILURE}
 * and {@link AuditEvent.AuditSeverity#ERROR}
 * are logged in {@link java.util.logging.Level#SEVERE} and {@link AuditEvent.AuditSeverity#WARN} is logged in {@link
 * java.util.logging.Level#WARNING} level.
 *
 * <p>
 * Format of default audit provider log record (all end of lines are removed from message, not from stack trace):
 * {@code
 * year.month.day hour(24):minute:second LogLevel AUDIT auditSeverity tracingId auditEventType auditEventClassName location(class)
 * location(method) location(sourceFile) location(line) :: "audit message"
 * }
 */
@FunctionalInterface
public interface AuditProvider extends SecurityProvider {
    /**
     * Return your subscriber for audit events. The method is invoked synchronously, so if you want to have low impact on
     * performance, you should handle possible asynchronous processing in the provider implementation.
     *
     * @return Consumer that will receive all audit events of this security realm
     */
    Consumer<TracedAuditEvent> auditConsumer();

    /**
     * Audit event sent to Audit provider. Wraps tracing id and AuditEvent sent by
     * a component/user.
     */
    interface TracedAuditEvent extends AuditEvent {
        /**
         * Tracing id of the current audit event, generated by SecurityContext.
         *
         * @return String with tracing id
         */
        String tracingId();

        /**
         * Source of this audit event (class, method, line number etc.).
         *
         * @return Source of the audit event
         */
        AuditSource auditSource();

        /**
         * Creates a formatted message from this events message format and parameters.
         *
         * @return formatted message
         */
        default String formatMessage() {
            List<AuditParam> params = params();
            List<Object> msgParams = new ArrayList<>(params.size());

            params.forEach(auditParam -> msgParams.add(auditParam.value().orElse("")));

            return String.format(messageFormat(), msgParams.toArray(new Object[0]));
        }
    }

    /**
     * Source of an audit source (as in "where this audit event originated").
     */
    interface AuditSource {
        /**
         * Build an audit source.
         *
         * @return AuditSource for current stack trace
         */
        static AuditSource create() {
            StackWalker walker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);
            //I want to filter out all methods in the root of security package
            //including security builder
            Optional<StackTraceElement> frame = walker
                    .walk(stream -> stream
                            .filter(f -> !f.getClassName().startsWith("sun."))
                            .filter(f -> !f.getClassName().startsWith("java."))
                            .filter(f -> {
                                // if this is a unit test class, return it
                                if (f.getClassName().endsWith("Test")) {
                                    return true;
                                }
                                // filter out security classes
                                return !isSecurityClass(f);
                            })
                            .findFirst());

            if (!frame.isPresent()) {
                frame = walker
                        .walk(stream -> {
                                  List<StackTraceElement> elems = stream
                                          .filter(f -> !f.getClassName().startsWith("sun."))
                                          .filter(f -> !f.getClassName().startsWith("java."))
                                          .collect(Collectors.toList());

                                  if (elems.isEmpty()) {
                                      return Optional.empty();
                                  }
                                  return Optional.of(elems.get(elems.size() - 1));
                              }
                        );
            }

            if (frame.isPresent()) {
                StackTraceElement stackFrame = frame.get();

                return new AuditSource() {
                    @Override
                    public Optional<String> className() {
                        return Optional.of(stackFrame.getClassName());
                    }

                    @Override
                    public Optional<String> methodName() {
                        return Optional.of(stackFrame.getMethodName());
                    }

                    @Override
                    public Optional<String> fileName() {
                        return Optional.ofNullable(stackFrame.getFileName());
                    }

                    @Override
                    public OptionalInt lineNumber() {
                        return (stackFrame.getLineNumber() < 0)
                                ? OptionalInt.empty()
                                : OptionalInt.of(stackFrame.getLineNumber());
                    }
                };
            } else {
                // class name, method name, source file, line number
                return new AuditSource() { };
            }
        }

        static boolean isSecurityClass(StackTraceElement element) {
            String className = element.getClassName();
            int last = className.lastIndexOf('.');
            String packageName = (last > 0) ? className.substring(0, last) : "";

            if (packageName.equals(AuditEvent.class.getPackage().getName())) {
                return true;
            }

            return packageName.equals(AuditProvider.class.getPackage().getName());
        }

        /**
         * Name of the class that caused this event.
         * @return class name or empty if not provided
         */
        default Optional<String> className() {
            return Optional.empty();
        }

        /**
         * Method name that caused this event.
         * @return method name or empty if not provided
         */
        default Optional<String> methodName() {
            return Optional.empty();
        }

        /**
         * File name of the source that caused this event.
         * @return file name or empty if not provided
         */
        default Optional<String> fileName() {
            return Optional.empty();
        }

        /**
         * Line number within the source file that caused this event.
         * @return line number or empty if not provided
         */
        default OptionalInt lineNumber() {
            return OptionalInt.empty();
        }
    }
}
