<?php

namespace IMATHUZH\Qfq\Core\Imap;

use IMATHUZH\Qfq\Core\Helper\Logger;
use IMATHUZH\Qfq\Core\Helper\OnString;
use IMATHUZH\Qfq\Core\Helper\Path;
use IMATHUZH\Qfq\Core\Store\Store;

use IMATHUZH\Qfq\External\EmailSync;
use Webklex\PHPIMAP\ClientManager;
use Webklex\PHPIMAP\Client;
use Webklex\PHPIMAP\Exceptions\ConnectionFailedException;
use Webklex\PHPIMAP\Exceptions\FolderFetchingException;
use Webklex\PHPIMAP\Exceptions\GetMessagesFailedException;
use Webklex\PHPIMAP\Exceptions\RuntimeException;

/**
 * Class Imap
 * @package qfq
 */
class Imap {

    private $imapLogLevelMin = null;
    private $imapLogAbsolute = null;

    /**
     * @var Client
     */
    private $client = null;

    /**
     * @var ClientManager
     */
    private $cm = null;
    private $imapAccount = '';
    private $connectionSuccess = 0;

    /**
     * @var Store
     */
    private $store = null;

    public function __construct($imapAccount, $db, $dbIndex, $checkAccount = false) {

//        set_error_handler("\\IMATHUZH\\Qfq\\Core\\Exception\\ErrorHandler::exception_error_handler");

        $this->store = Store::getInstance();

        // Set Log Mode
        $this->imapLogLevelMin = Logger::nameToLevel($this->store->getVar(SYSTEM_IMAP_LOG_MODE, STORE_SYSTEM));
        $this->imapLogAbsolute = Path::absoluteImapLogFile();

        // Generate dynamic IMAP configuration from database. Resolve the OAuth2 token only for the
        // targeted account - a report action touches exactly one account, resolving every other
        // account's token (file lock + possible refresh) on each click would be pure waste.
        $credentials = EmailSync::extractImapCredentials($this->store, $db, $dbIndex, $imapAccount);
        $imapConfig = EmailSync::generateImapConfigArray($credentials);

        // Accounts are keyed by sanitized username (dots break the webklex config lookup).
        $accountKey = EmailSync::imapAccountKey($imapAccount);

        if ($checkAccount) {
            $imapConfig['accounts'][$accountKey]['timeout'] = 1;
        }

        $this->cm = new ClientManager($imapConfig);

        $this->client = $this->cm->account($accountKey);

        if (empty($this->client)) {
            throw new \UserReportException("Failed to open account: $imapAccount", ERROR_IMAP);
        }

        try {
            $this->client->connect();

            if ($checkAccount) {
                // A successful AUTH alone is not trustworthy: Microsoft reports success for a
                // mismatched username/token pair and only follow-up commands fail (a documented
                // Microsoft XOAUTH2 quirk). Fire a real command before reporting the account as ok.
                $this->client->getFolders(false);
            }

            $this->connectionSuccess = 1;
        } catch (\Exception $e) {
            // In case of checkAccount don't throw an exception, but return an fake array.
            if (!$checkAccount) {
                throw new \UserReportException("Failed to create connection: " . $e->getMessage(), ERROR_IMAP);
            }
        }

        $this->imapAccount = $imapAccount;
    }

    /**
     * @throws RuntimeException
     * @throws \Webklex\PHPIMAP\Exceptions\ImapBadRequestException
     * @throws \Webklex\PHPIMAP\Exceptions\ImapServerErrorException
     */
    public function __destruct() {
//        if ($this->client->isConnected()) {
//            $this->client->disconnect();
//        }
    }

    /**
     * @return string
     * @throws ConnectionFailedException
     * @throws FolderFetchingException
     * @throws RuntimeException
     * @throws \CodeException
     * @throws \UserFormException
     */
    private function folderList(): string {
        $content = '';

        if (null === ($folders = $this->client->getFolders(false))) {
            throw new \UserReportException("Failed to fetch folder list (" . $this->imapAccount . '): ' . $this->client->getLastError(), ERROR_IMAP);
        }

        foreach ($folders as $folder) {
            $content .= $folder->path . "<br>";
        }
        return $content;
    }

    /**
     * Creates a folder.
     * If the folder already exist: skip creation / just report.
     * $folderName might be hierachical. E.g.: main/sub/subsub.
     * If hierachical, non existing parent folder will be created too.
     *
     * @param $folderName
     * @return string
     * @throws ConnectionFailedException
     * @throws FolderFetchingException
     * @throws RuntimeException
     * @throws \UserReportException
     * @throws \Webklex\PHPIMAP\Exceptions\AuthFailedException
     * @throws \Webklex\PHPIMAP\Exceptions\EventNotFoundException
     * @throws \Webklex\PHPIMAP\Exceptions\ImapBadRequestException
     * @throws \Webklex\PHPIMAP\Exceptions\ImapServerErrorException
     * @throws \Webklex\PHPIMAP\Exceptions\ResponseException
     */
    private function folderNew($folderName): string {

        if (empty($folderName)) {
            return "Folder new: foldername is empty";
        }

        // Check if folder already exist: If yes, do nothing.
        $existingFolders = $this->client->getFolders(false);
        $folderExists = $existingFolders->contains('path', $folderName);

        if ($folderExists) {
            return "Folder new: '$folderName' already exist - skip.";
        }

        if (!($this->client->createFolder($folderName, false))) {
            throw new \UserReportException("Folder new '$folderName' failed: " . $this->client->getLastError(), ERROR_IMAP);
        }

        return "Folder new '$folderName'.";
    }

    /**
     * @param $folderName
     * @return string
     * @throws ConnectionFailedException
     * @throws FolderFetchingException
     * @throws RuntimeException
     * @throws \UserReportException
     * @throws \Webklex\PHPIMAP\Exceptions\AuthFailedException
     * @throws \Webklex\PHPIMAP\Exceptions\EventNotFoundException
     * @throws \Webklex\PHPIMAP\Exceptions\ImapBadRequestException
     * @throws \Webklex\PHPIMAP\Exceptions\ImapServerErrorException
     * @throws \Webklex\PHPIMAP\Exceptions\ResponseException
     */
    private function folderDelete($folderName): string {
//TODO: das ist hier nur die erste Version. Ziel: der Folder mit allen Unterfoldern und Mails soll nach trash verschoben
// werden. Erst danach wird er geloescht.

        if (empty($folderName)) {
            return "Folder delete: foldername is empty";
        }

        // Check if folder already exist: If yes, do nothing.
        $existingFolders = $this->client->getFolders(false);
        $folderExists = $existingFolders->contains('path', $folderName);

        if (!$folderExists) {
            return "Folder delete: '$folderName' not found - skip.";
        }

        if (!($this->client->deleteFolder($folderName, false))) {
            throw new \UserReportException("Folder new '$folderName' failed: " . $this->client->getLastError(), ERROR_IMAP);
        }

        return "Folder delete '$folderName'.";
    }

    /**
     * @param $folderName
     * @return string
     * @throws ConnectionFailedException
     * @throws FolderFetchingException
     * @throws GetMessagesFailedException
     * @throws RuntimeException
     * @throws \Webklex\PHPIMAP\Exceptions\AuthFailedException
     * @throws \Webklex\PHPIMAP\Exceptions\ImapBadRequestException
     * @throws \Webklex\PHPIMAP\Exceptions\ImapServerErrorException
     * @throws \Webklex\PHPIMAP\Exceptions\ResponseException
     */

    private function mailList($folderName): string {
        if (empty($folderName)) {
            return "Mail list: foldername is empty";
        }

        $folder = $this->client->getFolder($folderName);

        // Header only, newest first, capped: fetching a large folder including bodies in one
        // go exceeds server limits/read timeouts ('SSL: Connection reset by peer') - and this
        // table renders header data only anyway.
        $query = $folder->query()->all()->setFetchBody(false)->setFetchOrderDesc();
        $total = $query->count();
        $messages = $query->limit(IMAP_MAIL_LIST_LIMIT, 1)->get();

        $content = '';
        if ($total > IMAP_MAIL_LIST_LIMIT) {
            $content .= '<p>Showing the newest ' . IMAP_MAIL_LIST_LIMIT . ' of ' . $total . ' mails.</p>';
        }
        $content .= '<table class="table qfq-table-50"><thead><tr><th>subject</th><th>From</th><th>To</th><th>Date</th><th>Header ID</th><th>MessageID</th></tr></thead>';
        foreach ($messages as $message) {
            // Escape every cell: subject/message-id are attacker-controlled (incoming mail) and
            // this HTML is rendered in the admin report - unescaped they are a stored XSS vector.
            // decodeMimeHeader turns RFC 2047 encoded-words into readable UTF-8 first.
            $content .= '<tr><td>' . htmlentities(OnString::decodeMimeHeader((string)$message->getSubject())) . "</td><td>";
            $content .= htmlentities(OnString::decodeMimeHeader((string)$message->getFrom())) . "</td><td>";
            $content .= htmlentities(OnString::decodeMimeHeader((string)$message->getTo())) . "</td><td>";
            $content .= htmlentities((string)$message->getDate()) . "</td><td>";
            $content .= htmlentities((string)$message->header->get('message-id')->first()) . "</td><td>";
            $content .= htmlentities((string)$message->getMessageId()) . "</td></tr>";
        }
        $content .= '</table>';

        return $content;
    }

    private function mailMove($args): string {

        $content = '';

        if (empty($args[INDEX_IMAP_ARG]) || empty($args[INDEX_IMAP_SRC_FOLDER]) || empty($args[INDEX_IMAP_SRC_ID])) {
            return "Mail move: src-, target-folder or src-id missing";
        }

        try {
            $sourceFolder = $this->client->getFolder($args[INDEX_IMAP_SRC_FOLDER]);
//            $destinationFolder = $this->client->getFolder($args[INDEX_IMAP_ARG]);
//            $message = $sourceFolder->query()->getMessage($args[INDEX_IMAP_SRC_ID]);
            $messages = $sourceFolder->query()->whereMessageId($args[INDEX_IMAP_SRC_ID])->get();
            if ($messages->count() > 0) {
                $message = $messages->first();
            } else {
                throw new \UserReportException("Message with ID '" . $args[INDEX_IMAP_SRC_ID] . "' not found in folder '" . $args[INDEX_IMAP_SRC_FOLDER] . "'", ERROR_IMAP);
            }

            try {
                // Does not work with Zimbra:    $message->move($destinationFolder);
                # For unknown reason, after copying, php-imap tries to fetch the header of the new mail an fails.
                $message->copy($args[INDEX_IMAP_ARG]);
            } catch (\Exception $e) {
                if($e->getMessage() != 'no headers found') {
                    throw new \UserReportException("Error moving message: " . $e->getMessage(), ERROR_IMAP);
                }
            }
            $message->delete();
            $content = 'Moved email successfully';

        } catch (\Exception $e) {
            throw new \UserReportException("Error moving message: " . $e->getMessage(), ERROR_IMAP);
        }

        return $content;
    }

    /**
     * Mark a mail as read or unread.
     *
     * @param $args
     * @param $revert
     * @return string
     * @throws \UserReportException
     */
    private function mailSeen($args, $revert = false): string {
        $content = '';

        if (empty($args[INDEX_IMAP_SRC_FOLDER]) || empty($args[INDEX_IMAP_SRC_ID])) {
            return "Mail read: src-folder or src-id missing";
        }

        try {
            $sourceFolder = $this->client->getFolder($args[INDEX_IMAP_SRC_FOLDER]);
            $messages = $sourceFolder->query()->whereMessageId($args[INDEX_IMAP_SRC_ID])->get();
            if ($messages->count() > 0) {
                $message = $messages->first();
            } else {
                throw new \UserReportException("Message with ID '" . $args[INDEX_IMAP_SRC_ID] . "' not found in folder '" . $args[INDEX_IMAP_SRC_FOLDER] . "'", ERROR_IMAP);
            }

            if ($revert) {
                $message->unsetFlag('Seen');
                $content = 'Message have been marked as unread.';
            } else {
                $message->setFlag('Seen');
                $content = 'Message have been marked as read.';
            }
        } catch (\Exception $e) {
            throw new \UserReportException("Error moving message: " . $e->getMessage(), ERROR_IMAP);
        }

        return $content;

    }

    /**
     * $args: 'J:<action>:[<arg>]:[<srcFolder>]:[<srcId>]:[<account>]' AS _link`
     * const TOKEN_IMAP_ACTON = 1;
     * const TOKEN_IMAP_SRC_ID = 2;
     * const TOKEN_IMAP_SRC_FOLDER = 3;
     * const TOKEN_IMAP_ARG = 4;
     * const TOKEN_IMAP_ACCOUNT = 5;
     *
     * <account>: this is always the current one: each IMAP instance is instantiated per account. The given '<account>' matches already.
     *
     * @return string
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public
    function process(array $args): string {
        $content = '';
        switch ($args[INDEX_IMAP_ACTION] ?? '') {
            case IMAP_ACTION_FOLDER_LIST:
                $content = $this->folderList();
                break;
            case IMAP_ACTION_FOLDER_NEW:
                $content = $this->folderNew($args[INDEX_IMAP_ARG]);
                break;
            case IMAP_ACTION_FOLDER_DELETE:
                $content = $this->folderDelete($args[INDEX_IMAP_ARG]);
                break;
            case IMAP_ACTION_MAIL_LIST:
                $content = $this->mailList($args[INDEX_IMAP_ARG]);
                break;
            case IMAP_ACTION_MAIL_NEW:
                break;
            case IMAP_ACTION_MAIL_MOVE:
                $content = $this->mailMove($args);
                break;
            case IMAP_ACTION_CHECK_ACCOUNT:
                $content = $this->connectionSuccess;
                break;
            case IMAP_ACTION_SEEN:
                $content = $this->mailSeen($args);
                break;
            case IMAP_ACTION_UNSEEN:
                $content = $this->mailSeen($args, true);
                break;
            case IMAP_ACTION_TAG:
                break;
            default:
                throw new \UserReportException("Unknown IMAP command: " . $args[INDEX_IMAP_ACTION] ?? '', ERROR_IMAP);
                break;
        }
        return $content;
    }
}
