package eu.siacs.conversations.crypto;

import android.app.PendingIntent;
import android.content.Intent;
import android.util.Log;
import eu.siacs.conversations.AppSettings;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.MimeUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayDeque;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import org.openintents.openpgp.OpenPgpMetadata;
import org.openintents.openpgp.util.OpenPgpApi;

public class PgpDecryptionService {

    protected final ArrayDeque<Message> messages = new ArrayDeque<>();
    protected final HashSet<Message> pendingNotifications = new HashSet<>();
    private final XmppConnectionService mXmppConnectionService;
    private OpenPgpApi openPgpApi = null;
    private Message currentMessage;
    private PendingIntent pendingIntent;
    private Intent userInteractionResult;

    public PgpDecryptionService(XmppConnectionService service) {
        this.mXmppConnectionService = service;
    }

    public synchronized boolean decrypt(final Message message, final boolean notify) {
        messages.add(message);
        if (notify && pendingIntent == null) {
            pendingNotifications.add(message);
            continueDecryption();
            return false;
        } else {
            continueDecryption();
            return notify;
        }
    }

    public synchronized void decrypt(final List<Message> list) {
        for (Message message : list) {
            if (message.getEncryption() == Message.ENCRYPTION_PGP) {
                messages.add(message);
            }
        }
        continueDecryption();
    }

    public synchronized void discard(List<Message> discards) {
        this.messages.removeAll(discards);
        this.pendingNotifications.removeAll(discards);
    }

    public synchronized void discard(Message message) {
        this.messages.remove(message);
        this.pendingNotifications.remove(message);
    }

    public void giveUpCurrentDecryption() {
        Message message;
        synchronized (this) {
            if (currentMessage != null) {
                return;
            }
            message = messages.peekFirst();
            if (message == null) {
                return;
            }
            discard(message);
        }
        synchronized (message) {
            if (message.getEncryption() == Message.ENCRYPTION_PGP) {
                message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
            }
        }
        mXmppConnectionService.updateMessage(message, false);
        continueDecryption(true);
    }

    protected synchronized void decryptNext() {
        if (pendingIntent == null
                && getOpenPgpApi() != null
                && (currentMessage = messages.poll()) != null) {
            new Thread(
                            new Runnable() {
                                @Override
                                public void run() {
                                    executeApi(currentMessage);
                                    decryptNext();
                                }
                            })
                    .start();
        }
    }

    public synchronized void continueDecryption(boolean resetPending) {
        if (resetPending) {
            this.pendingIntent = null;
        }
        continueDecryption();
    }

    public synchronized void continueDecryption(Intent userInteractionResult) {
        this.pendingIntent = null;
        this.userInteractionResult = userInteractionResult;
        continueDecryption();
    }

    public synchronized void continueDecryption() {
        if (currentMessage == null) {
            decryptNext();
        }
    }

    private synchronized OpenPgpApi getOpenPgpApi() {
        if (openPgpApi == null) {
            this.openPgpApi = mXmppConnectionService.getOpenPgpApi();
        }
        return this.openPgpApi;
    }

    private void executeApi(final Message message) {
        boolean skipNotificationPush = false;
        synchronized (message) {
            Intent params = userInteractionResult != null ? userInteractionResult : new Intent();
            params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY);
            if (message.getType() == Message.TYPE_TEXT) {
                InputStream is = new ByteArrayInputStream(message.getBody().getBytes());
                final OutputStream os = new ByteArrayOutputStream();
                Intent result = getOpenPgpApi().executeApi(params, is, os);
                switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
                    case OpenPgpApi.RESULT_CODE_SUCCESS:
                        try {
                            os.flush();
                            final String body = os.toString();
                            message.setBody(body);
                            message.setEncryption(Message.ENCRYPTION_DECRYPTED);
                            final HttpConnectionManager manager =
                                    mXmppConnectionService.getHttpConnectionManager();
                            final var autoAcceptFileSize =
                                    new AppSettings(mXmppConnectionService).getAutoAcceptFileSize();
                            // TODO set this as 'oob'
                            if (message.trusted()
                                    && message.treatAsDownloadable()
                                    && autoAcceptFileSize.isPresent()) {
                                manager.createNewDownloadConnection(message);
                            }
                        } catch (final IOException e) {
                            Log.d(Config.LOGTAG, "decryption failed", e);
                            message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
                        }
                        mXmppConnectionService.updateMessage(message);
                        break;
                    case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
                        synchronized (PgpDecryptionService.this) {
                            PendingIntent pendingIntent =
                                    result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
                            messages.addFirst(message);
                            currentMessage = null;
                            storePendingIntent(pendingIntent);
                        }
                        break;
                    case OpenPgpApi.RESULT_CODE_ERROR:
                        Log.d(Config.LOGTAG, "decryption failed (api error)");
                        message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
                        mXmppConnectionService.updateMessage(message);
                        break;
                }
            } else if (message.isFileOrImage()) {
                try {
                    final File inputFile =
                            mXmppConnectionService.getFileBackend().getFile(message, false);
                    final File outputFile =
                            mXmppConnectionService.getFileBackend().getFile(message);
                    if (Objects.requireNonNull(outputFile.getParentFile()).mkdirs()) {
                        Log.d(
                                Config.LOGTAG,
                                "created parent directories for " + outputFile.getAbsolutePath());
                    }
                    outputFile.createNewFile();
                    InputStream is = new FileInputStream(inputFile);
                    OutputStream os = new FileOutputStream(outputFile);
                    Intent result = getOpenPgpApi().executeApi(params, is, os);
                    switch (result.getIntExtra(
                            OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
                        case OpenPgpApi.RESULT_CODE_SUCCESS:
                            OpenPgpMetadata openPgpMetadata =
                                    result.getParcelableExtra(OpenPgpApi.RESULT_METADATA);
                            String originalFilename = openPgpMetadata.getFilename();
                            String originalExtension =
                                    originalFilename == null
                                            ? null
                                            : MimeUtils.extractRelevantExtension(originalFilename);
                            if (originalExtension != null
                                    && MimeUtils.extractRelevantExtension(outputFile.getName())
                                            == null) {
                                Log.d(
                                        Config.LOGTAG,
                                        "detected original filename during pgp decryption");
                                final String mime =
                                        MimeUtils.guessMimeTypeFromExtension(originalExtension);
                                final String filename =
                                        outputFile.getName() + "." + originalExtension;
                                final var fixedStorageLocation =
                                        mXmppConnectionService
                                                .getFileBackend()
                                                .getStorageLocation(filename, mime);
                                // TODO do not dereference fixedFile?!
                                final var fixedFile = fixedStorageLocation.file();
                                if (fixedFile.getParentFile().mkdirs()) {
                                    Log.d(
                                            Config.LOGTAG,
                                            "created parent directories for "
                                                    + fixedFile.getAbsolutePath());
                                }
                                synchronized (mXmppConnectionService.FILENAMES_TO_IGNORE_DELETION) {
                                    mXmppConnectionService.FILENAMES_TO_IGNORE_DELETION.add(
                                            outputFile.getAbsolutePath());
                                }
                                if (outputFile.renameTo(fixedFile)) {
                                    Log.d(
                                            Config.LOGTAG,
                                            "renamed "
                                                    + outputFile.getAbsolutePath()
                                                    + " to "
                                                    + fixedFile.getAbsolutePath());
                                    message.setRelativeFilePath(fixedStorageLocation);
                                }
                            }
                            final String url = message.getFileParams().url;
                            message.setEncryption(Message.ENCRYPTION_DECRYPTED);
                            mXmppConnectionService.getFileBackend().updateFileParams(message, url);
                            mXmppConnectionService.updateMessage(message);
                            if (!inputFile.delete()) {
                                Log.w(
                                        Config.LOGTAG,
                                        "unable to delete pgp encrypted source file "
                                                + inputFile.getAbsolutePath());
                            }
                            skipNotificationPush = true;
                            mXmppConnectionService
                                    .getFileBackend()
                                    .updateMediaScanner(outputFile, () -> notifyIfPending(message));
                            break;
                        case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
                            synchronized (PgpDecryptionService.this) {
                                PendingIntent pendingIntent =
                                        result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
                                messages.addFirst(message);
                                currentMessage = null;
                                storePendingIntent(pendingIntent);
                            }
                            break;
                        case OpenPgpApi.RESULT_CODE_ERROR:
                            message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
                            mXmppConnectionService.updateMessage(message);
                            break;
                    }
                } catch (final IOException e) {
                    message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
                    mXmppConnectionService.updateMessage(message);
                }
            }
        }
        if (!skipNotificationPush) {
            notifyIfPending(message);
        }
    }

    private synchronized void notifyIfPending(Message message) {
        if (pendingNotifications.remove(message)) {
            mXmppConnectionService.getNotificationService().push(message);
        }
    }

    private void storePendingIntent(PendingIntent pendingIntent) {
        this.pendingIntent = pendingIntent;
        mXmppConnectionService.updateConversationUi();
    }

    public synchronized boolean hasPendingIntent(Conversation conversation) {
        if (pendingIntent == null) {
            return false;
        } else {
            for (Message message : messages) {
                if (message.getConversation() == conversation) {
                    return true;
                }
            }
            return false;
        }
    }

    public PendingIntent getPendingIntent() {
        return pendingIntent;
    }

    public boolean isConnected() {
        return getOpenPgpApi() != null;
    }
}
