// 
// Decompiled by Procyon v0.6.0
// 

package io.sentry.cache;

import java.util.concurrent.TimeUnit;
import java.util.List;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.io.BufferedInputStream;
import java.util.ArrayList;
import java.util.Iterator;
import io.sentry.ISentryLifecycleToken;
import io.sentry.SentryUUID;
import java.io.Writer;
import java.io.BufferedWriter;
import java.io.OutputStreamWriter;
import java.io.ByteArrayInputStream;
import io.sentry.SentryItemType;
import io.sentry.SentryEnvelopeItem;
import java.io.OutputStream;
import java.io.FileOutputStream;
import java.util.Date;
import io.sentry.DateUtils;
import io.sentry.ISerializer;
import io.sentry.Session;
import java.io.Reader;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.FileInputStream;
import io.sentry.UncaughtExceptionHandlerIntegration;
import io.sentry.SentryCrashLastRunState;
import java.io.File;
import io.sentry.hints.SessionStart;
import io.sentry.hints.AbnormalExit;
import io.sentry.util.HintUtils;
import io.sentry.hints.SessionEnd;
import io.sentry.util.Objects;
import io.sentry.Hint;
import java.util.WeakHashMap;
import io.sentry.transport.NoOpEnvelopeCache;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.util.AutoClosableReentrantLock;
import org.jetbrains.annotations.NotNull;
import io.sentry.SentryEnvelope;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import org.jetbrains.annotations.ApiStatus;

@ApiStatus.Internal
public class EnvelopeCache extends CacheStrategy implements IEnvelopeCache
{
    public static final String SUFFIX_ENVELOPE_FILE = ".envelope";
    public static final String PREFIX_CURRENT_SESSION_FILE = "session";
    public static final String PREFIX_PREVIOUS_SESSION_FILE = "previous_session";
    static final String SUFFIX_SESSION_FILE = ".json";
    public static final String CRASH_MARKER_FILE = "last_crash";
    public static final String NATIVE_CRASH_MARKER_FILE = ".sentry-native/last_crash";
    public static final String STARTUP_CRASH_MARKER_FILE = "startup_crash";
    private final CountDownLatch previousSessionLatch;
    @NotNull
    private final Map<SentryEnvelope, String> fileNameMap;
    @NotNull
    protected final AutoClosableReentrantLock cacheLock;
    @NotNull
    protected final AutoClosableReentrantLock sessionLock;
    
    @NotNull
    public static IEnvelopeCache create(@NotNull final SentryOptions options) {
        final String cacheDirPath = options.getCacheDirPath();
        final int maxCacheItems = options.getMaxCacheItems();
        if (cacheDirPath == null) {
            options.getLogger().log(SentryLevel.WARNING, "cacheDirPath is null, returning NoOpEnvelopeCache", new Object[0]);
            return NoOpEnvelopeCache.getInstance();
        }
        return new EnvelopeCache(options, cacheDirPath, maxCacheItems);
    }
    
    public EnvelopeCache(@NotNull final SentryOptions options, @NotNull final String cacheDirPath, final int maxCacheItems) {
        super(options, cacheDirPath, maxCacheItems);
        this.fileNameMap = new WeakHashMap<SentryEnvelope, String>();
        this.cacheLock = new AutoClosableReentrantLock();
        this.sessionLock = new AutoClosableReentrantLock();
        this.previousSessionLatch = new CountDownLatch(1);
    }
    
    @Override
    public void store(@NotNull final SentryEnvelope envelope, @NotNull final Hint hint) {
        this.storeInternal(envelope, hint);
    }
    
    @Override
    public boolean storeEnvelope(@NotNull final SentryEnvelope envelope, @NotNull final Hint hint) {
        return this.storeInternal(envelope, hint);
    }
    
    private boolean storeInternal(@NotNull final SentryEnvelope envelope, @NotNull final Hint hint) {
        Objects.requireNonNull(envelope, "Envelope is required.");
        this.rotateCacheIfNeeded(this.allEnvelopeFiles());
        final File currentSessionFile = getCurrentSessionFile(this.directory.getAbsolutePath());
        final File previousSessionFile = getPreviousSessionFile(this.directory.getAbsolutePath());
        if (HintUtils.hasType(hint, SessionEnd.class) && !currentSessionFile.delete()) {
            this.options.getLogger().log(SentryLevel.WARNING, "Current envelope doesn't exist.", new Object[0]);
        }
        if (HintUtils.hasType(hint, AbnormalExit.class)) {
            this.tryEndPreviousSession(hint);
        }
        if (HintUtils.hasType(hint, SessionStart.class)) {
            this.movePreviousSession(currentSessionFile, previousSessionFile);
            this.updateCurrentSession(currentSessionFile, envelope);
            boolean crashedLastRun = false;
            final File crashMarkerFile = new File(this.options.getCacheDirPath(), ".sentry-native/last_crash");
            if (crashMarkerFile.exists()) {
                crashedLastRun = true;
            }
            if (!crashedLastRun) {
                final File javaCrashMarkerFile = new File(this.options.getCacheDirPath(), "last_crash");
                if (javaCrashMarkerFile.exists()) {
                    this.options.getLogger().log(SentryLevel.INFO, "Crash marker file exists, crashedLastRun will return true.", new Object[0]);
                    crashedLastRun = true;
                    if (!javaCrashMarkerFile.delete()) {
                        this.options.getLogger().log(SentryLevel.ERROR, "Failed to delete the crash marker file. %s.", javaCrashMarkerFile.getAbsolutePath());
                    }
                }
            }
            SentryCrashLastRunState.getInstance().setCrashedLastRun(crashedLastRun);
            this.flushPreviousSession();
        }
        final File envelopeFile = this.getEnvelopeFile(envelope);
        if (envelopeFile.exists()) {
            this.options.getLogger().log(SentryLevel.WARNING, "Not adding Envelope to offline storage because it already exists: %s", envelopeFile.getAbsolutePath());
            return true;
        }
        this.options.getLogger().log(SentryLevel.DEBUG, "Adding Envelope to offline storage: %s", envelopeFile.getAbsolutePath());
        final boolean didWriteToDisk = this.writeEnvelopeToDisk(envelopeFile, envelope);
        if (HintUtils.hasType(hint, UncaughtExceptionHandlerIntegration.UncaughtExceptionHint.class)) {
            this.writeCrashMarkerFile();
        }
        return didWriteToDisk;
    }
    
    private void tryEndPreviousSession(@NotNull final Hint hint) {
        final Object sdkHint = HintUtils.getSentrySdkHint(hint);
        if (sdkHint instanceof AbnormalExit) {
            final File previousSessionFile = getPreviousSessionFile(this.directory.getAbsolutePath());
            if (previousSessionFile.exists()) {
                this.options.getLogger().log(SentryLevel.WARNING, "Previous session is not ended, we'd need to end it.", new Object[0]);
                try (final Reader reader = new BufferedReader(new InputStreamReader(new FileInputStream(previousSessionFile), EnvelopeCache.UTF_8))) {
                    final Session session = this.serializer.getValue().deserialize(reader, Session.class);
                    if (session != null) {
                        final AbnormalExit abnormalHint = (AbnormalExit)sdkHint;
                        final Long abnormalExitTimestamp = abnormalHint.timestamp();
                        Date timestamp = null;
                        if (abnormalExitTimestamp != null) {
                            timestamp = DateUtils.getDateTime(abnormalExitTimestamp);
                            final Date sessionStart = session.getStarted();
                            if (sessionStart == null || timestamp.before(sessionStart)) {
                                this.options.getLogger().log(SentryLevel.WARNING, "Abnormal exit happened before previous session start, not ending the session.", new Object[0]);
                                reader.close();
                                return;
                            }
                        }
                        final String abnormalMechanism = abnormalHint.mechanism();
                        session.update(Session.State.Abnormal, null, true, abnormalMechanism);
                        session.end(timestamp);
                        this.writeSessionToDisk(previousSessionFile, session);
                    }
                }
                catch (final Throwable e) {
                    this.options.getLogger().log(SentryLevel.ERROR, "Error processing previous session.", e);
                }
            }
            else {
                this.options.getLogger().log(SentryLevel.DEBUG, "No previous session file to end.", new Object[0]);
            }
        }
    }
    
    private void writeCrashMarkerFile() {
        final File crashMarkerFile = new File(this.options.getCacheDirPath(), "last_crash");
        try (final OutputStream outputStream = new FileOutputStream(crashMarkerFile)) {
            final String timestamp = DateUtils.getTimestamp(DateUtils.getCurrentDateTime());
            outputStream.write(timestamp.getBytes(EnvelopeCache.UTF_8));
            outputStream.flush();
        }
        catch (final Throwable e) {
            this.options.getLogger().log(SentryLevel.ERROR, "Error writing the crash marker file to the disk", e);
        }
    }
    
    private void updateCurrentSession(@NotNull final File currentSessionFile, @NotNull final SentryEnvelope envelope) {
        final Iterable<SentryEnvelopeItem> items = envelope.getItems();
        if (items.iterator().hasNext()) {
            final SentryEnvelopeItem item = items.iterator().next();
            if (SentryItemType.Session.equals(item.getHeader().getType())) {
                try (final Reader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(item.getData()), EnvelopeCache.UTF_8))) {
                    final Session session = this.serializer.getValue().deserialize(reader, Session.class);
                    if (session == null) {
                        this.options.getLogger().log(SentryLevel.ERROR, "Item of type %s returned null by the parser.", item.getHeader().getType());
                    }
                    else {
                        this.writeSessionToDisk(currentSessionFile, session);
                    }
                }
                catch (final Throwable e) {
                    this.options.getLogger().log(SentryLevel.ERROR, "Item failed to process.", e);
                }
            }
            else {
                this.options.getLogger().log(SentryLevel.INFO, "Current envelope has a different envelope type %s", item.getHeader().getType());
            }
        }
        else {
            this.options.getLogger().log(SentryLevel.INFO, "Current envelope %s is empty", currentSessionFile.getAbsolutePath());
        }
    }
    
    private boolean writeEnvelopeToDisk(@NotNull final File file, @NotNull final SentryEnvelope envelope) {
        if (file.exists()) {
            this.options.getLogger().log(SentryLevel.DEBUG, "Overwriting envelope to offline storage: %s", file.getAbsolutePath());
            if (!file.delete()) {
                this.options.getLogger().log(SentryLevel.ERROR, "Failed to delete: %s", file.getAbsolutePath());
            }
        }
        try (final OutputStream outputStream = new FileOutputStream(file)) {
            this.serializer.getValue().serialize(envelope, outputStream);
        }
        catch (final Throwable e) {
            this.options.getLogger().log(SentryLevel.ERROR, e, "Error writing Envelope %s to offline storage", file.getAbsolutePath());
            return false;
        }
        return true;
    }
    
    private void writeSessionToDisk(@NotNull final File file, @NotNull final Session session) {
        try (final OutputStream outputStream = new FileOutputStream(file);
             final Writer writer = new BufferedWriter(new OutputStreamWriter(outputStream, EnvelopeCache.UTF_8))) {
            this.options.getLogger().log(SentryLevel.DEBUG, "Overwriting session to offline storage: %s", session.getSessionId());
            this.serializer.getValue().serialize(session, writer);
        }
        catch (final Throwable e) {
            this.options.getLogger().log(SentryLevel.ERROR, e, "Error writing Session to offline storage: %s", session.getSessionId());
        }
    }
    
    @Override
    public void discard(@NotNull final SentryEnvelope envelope) {
        Objects.requireNonNull(envelope, "Envelope is required.");
        final File envelopeFile = this.getEnvelopeFile(envelope);
        if (envelopeFile.delete()) {
            this.options.getLogger().log(SentryLevel.DEBUG, "Discarding envelope from cache: %s", envelopeFile.getAbsolutePath());
        }
        else {
            this.options.getLogger().log(SentryLevel.DEBUG, "Envelope was not cached or could not be deleted: %s", envelopeFile.getAbsolutePath());
        }
    }
    
    @NotNull
    private File getEnvelopeFile(@NotNull final SentryEnvelope envelope) {
        try (final ISentryLifecycleToken ignored = this.cacheLock.acquire()) {
            String fileName;
            if (this.fileNameMap.containsKey(envelope)) {
                fileName = this.fileNameMap.get(envelope);
            }
            else {
                fileName = SentryUUID.generateSentryId() + ".envelope";
                this.fileNameMap.put(envelope, fileName);
            }
            return new File(this.directory.getAbsolutePath(), fileName);
        }
    }
    
    @NotNull
    public static File getCurrentSessionFile(@NotNull final String cacheDirPath) {
        return new File(cacheDirPath, "session.json");
    }
    
    @NotNull
    public static File getPreviousSessionFile(@NotNull final String cacheDirPath) {
        return new File(cacheDirPath, "previous_session.json");
    }
    
    @NotNull
    @Override
    public Iterator<SentryEnvelope> iterator() {
        final File[] allCachedEnvelopes = this.allEnvelopeFiles();
        final List<SentryEnvelope> ret = new ArrayList<SentryEnvelope>(allCachedEnvelopes.length);
        for (final File file : allCachedEnvelopes) {
            try (final InputStream is = new BufferedInputStream(new FileInputStream(file))) {
                ret.add(this.serializer.getValue().deserializeEnvelope(is));
            }
            catch (final FileNotFoundException e) {
                this.options.getLogger().log(SentryLevel.DEBUG, "Envelope file '%s' disappeared while converting all cached files to envelopes.", file.getAbsolutePath());
            }
            catch (final IOException e2) {
                this.options.getLogger().log(SentryLevel.ERROR, String.format("Error while reading cached envelope from file %s", file.getAbsolutePath()), e2);
            }
        }
        return ret.iterator();
    }
    
    @NotNull
    private File[] allEnvelopeFiles() {
        if (this.isDirectoryValid()) {
            final File[] files = this.directory.listFiles((__, fileName) -> fileName.endsWith(".envelope"));
            if (files != null) {
                return files;
            }
        }
        return new File[0];
    }
    
    public boolean waitPreviousSessionFlush() {
        try {
            return this.previousSessionLatch.await(this.options.getSessionFlushTimeoutMillis(), TimeUnit.MILLISECONDS);
        }
        catch (final InterruptedException e) {
            Thread.currentThread().interrupt();
            this.options.getLogger().log(SentryLevel.DEBUG, "Timed out waiting for previous session to flush.", new Object[0]);
            return false;
        }
    }
    
    public void flushPreviousSession() {
        this.previousSessionLatch.countDown();
    }
    
    public void movePreviousSession(@NotNull final File currentSessionFile, @NotNull final File previousSessionFile) {
        try (final ISentryLifecycleToken ignored = this.sessionLock.acquire()) {
            if (previousSessionFile.exists()) {
                this.options.getLogger().log(SentryLevel.DEBUG, "Previous session file already exists, deleting it.", new Object[0]);
                if (!previousSessionFile.delete()) {
                    this.options.getLogger().log(SentryLevel.WARNING, "Unable to delete previous session file: %s", previousSessionFile);
                }
            }
            if (currentSessionFile.exists()) {
                this.options.getLogger().log(SentryLevel.INFO, "Moving current session to previous session.", new Object[0]);
                try {
                    final boolean renamed = currentSessionFile.renameTo(previousSessionFile);
                    if (!renamed) {
                        this.options.getLogger().log(SentryLevel.WARNING, "Unable to move current session to previous session.", new Object[0]);
                    }
                }
                catch (final Throwable e) {
                    this.options.getLogger().log(SentryLevel.ERROR, "Error moving current session to previous session.", e);
                }
            }
        }
    }
}
