/*
 * 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.logging.log4j.status;

import static java.util.Objects.requireNonNull;
import static org.apache.logging.log4j.util.Chars.SPACE;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.io.Serializable;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.internal.annotation.SuppressFBWarnings;
import org.apache.logging.log4j.message.Message;
import org.jspecify.annotations.Nullable;

/**
 * The Status data.
 */
public class StatusData implements Serializable {

    private static final long serialVersionUID = -4341916115118014017L;

    private final Instant instant;

    private final DateTimeFormatter instantFormatter;

    @Nullable
    private final StackTraceElement caller;

    private final Level level;

    private final Message message;

    private final String threadName;

    @Nullable
    private final Throwable throwable;

    /**
     * Constructs the instance using given properties.
     *
     * @param caller the method that created the event
     * @param level a logging level
     * @param message a message
     * @param throwable the error occurred
     * @param threadName the thread name
     * @since 2.4
     */
    public StatusData(
            @Nullable final StackTraceElement caller,
            final Level level,
            final Message message,
            @Nullable final Throwable throwable,
            @Nullable final String threadName) {
        this(caller, level, message, throwable, threadName, null, Instant.now());
    }

    StatusData(
            @Nullable final StackTraceElement caller,
            final Level level,
            final Message message,
            @Nullable final Throwable throwable,
            @Nullable final String threadName,
            @Nullable final DateTimeFormatter instantFormatter,
            final Instant instant) {
        // DateTimeFormatter.ISO_INSTANT is the default used in instant.toString()
        this.instantFormatter = instantFormatter != null ? instantFormatter : DateTimeFormatter.ISO_INSTANT;
        this.instant = instant;
        this.caller = caller;
        this.level = requireNonNull(level, "level");
        this.message = requireNonNull(message, "message");
        this.throwable = throwable;
        this.threadName =
                threadName != null ? threadName : Thread.currentThread().getName();
    }

    /**
     * Returns the instant of the event.
     *
     * @return the event's instant
     * @since 2.23.0
     */
    public Instant getInstant() {
        return instant;
    }

    /**
     * Returns the instant of the event.
     *
     * @return the event's instant
     * @deprecated since 2.23.0, use {@link #getInstant()} instead.
     */
    @Deprecated
    public long getTimestamp() {
        return instant.toEpochMilli();
    }

    /**
     * Returns the method that created the event.
     *
     * @return the method that created the event
     */
    @Nullable
    public StackTraceElement getStackTraceElement() {
        return caller;
    }

    /**
     * Returns the logging level for the event.
     *
     * @return the event's logging level
     */
    public Level getLevel() {
        return level;
    }

    /**
     * Returns the message associated with the event.
     *
     * @return the message associated with the event
     */
    public Message getMessage() {
        return message;
    }

    /**
     * Returns the name of the thread associated with the event.
     *
     * @return the name of the thread associated with the event
     * @since 2.4
     */
    public String getThreadName() {
        return threadName;
    }

    /**
     * Returns the error associated with the event.
     *
     * @return the error associated with the event
     */
    @Nullable
    public Throwable getThrowable() {
        return throwable;
    }

    /**
     * Formats the event in to a log line for viewing.
     *
     * @return the formatted event
     */
    @SuppressFBWarnings(
            value = "INFORMATION_EXPOSURE_THROUGH_AN_ERROR_MESSAGE",
            justification = "Log4j prints stacktraces only to logs, which should be private.")
    @SuppressWarnings("DefaultCharset")
    public String getFormattedStatus() {
        final StringBuilder sb = new StringBuilder();
        instantFormatter.formatTo(instant, sb);
        sb.append(SPACE);
        sb.append(getThreadName());
        sb.append(SPACE);
        sb.append(level.toString());
        sb.append(SPACE);
        sb.append(message.getFormattedMessage());
        final Object[] parameters = message.getParameters();
        Throwable effectiveThrowable;
        if (throwable == null
                && parameters != null
                && parameters.length > 0
                && parameters[parameters.length - 1] instanceof Throwable) {
            effectiveThrowable = (Throwable) parameters[parameters.length - 1];
        } else {
            effectiveThrowable = throwable;
        }
        if (effectiveThrowable != null) {
            sb.append(System.lineSeparator());
            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
            effectiveThrowable.printStackTrace(new PrintStream(baos));
            /*
             * https://errorprone.info/bugpattern/DefaultCharset
             *
             * Since Java 9 we'll be able to provide a charset.
             */
            sb.append(baos);
        }
        return sb.toString();
    }

    @Override
    public String toString() {
        return getMessage().getFormattedMessage();
    }
}
