<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 05.01.2024
 * Time: 8:49 AM
 */

namespace IMATHUZH\Qfq\External;


use IMATHUZH\Qfq\Core\Database\Database;
use IMATHUZH\Qfq\Core\Evaluate;
use IMATHUZH\Qfq\Core\Helper\EncryptDecrypt;
use IMATHUZH\Qfq\Core\Helper\Logger;
use IMATHUZH\Qfq\Core\Helper\Path;
use IMATHUZH\Qfq\Core\Store\Store;

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;

//TODO Logging
//TODO Fehlermeldungen: user/pw wrong, server unknown, protocol - werden die sauber geloggt?
//TODO Falls PW falsch ist scheint PHP bei 'disconnect' mit 100% CPU zu craschen: Ganz schlecht.
//TODO Check das folder selektiert werden kann.
//TODO Wenn der Prozess via Browser aufgerufen wird: Alles ausgeben.
//TODO Mail als gelesen/ ungelesen markieren
//TODO Mails mit Tags versehen (Farbe a'la Thunderbird)


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

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

    /**
     * @var Evaluate
     */
    protected $evaluate = null;

    /**
     * @var Database[] - Array of Database instantiated class
     */
    private $dbArray = array();

    private $dbIndexQfq = '';
    private $dbIndexData = '';

    /**
     * @var string|null MODE_LIST_ALL_FOLDER | MODE_SYNC | MODE_ACTION
     */
    private $mode = null;
    /**
     * @var Client[]
     */
    private $client = array();
    private $emailSyncColumns = '';

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

    /**
     * AutoCron constructor.
     *
     * @param string $mode
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public function __construct($mode = null) {

        $this->mode = $mode;

        mb_internal_encoding("UTF-8");

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

        $this->store = Store::getInstance();
        $this->store->FillStoreSystemBySql(); // Do this after the DB-update

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

        $this->dbIndexQfq = $this->store->getVar(SYSTEM_DB_INDEX_QFQ, STORE_SYSTEM);
        $this->dbIndexData = $this->store->getVar(SYSTEM_DB_INDEX_DATA, STORE_SYSTEM);

        $this->dbArray[$this->dbIndexData] = new Database($this->dbIndexData);

        $this->evaluate = new Evaluate($this->store, $this->dbArray[$this->dbIndexData]);
        $this->emailSyncColumns = "`" . ES_COL_ACCOUNT . "`,`" . ES_COL_MESSAGE_ID . "`,`" . ES_COL_FOLDER_PATH . "`,`" .
            ES_COL_SENDER . "`,`" . ES_COL_RECEIVER . "`,`" . ES_COL_CC . "`,`" . ES_COL_SUBJECT . "`,`" .
            ES_COL_BODYTEXT . "`,`" . ES_COL_BODYHTML . "`,`" . ES_COL_ATTACHMENT . "`,`" . ES_COL_HEADER . "`,`" .
            ES_COL_FLAGS . "`";

    }

    /**
     * @param array $accountList
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \Webklex\PHPIMAP\Exceptions\ConnectionFailedException
     * @throws \Webklex\PHPIMAP\Exceptions\FolderFetchingException
     * @throws \Webklex\PHPIMAP\Exceptions\RuntimeException
     */
    private function showAllAccountAllFolder(array $accountList): string {

        $content = '';
        foreach ($accountList as $accountName) {
            if (null === ($folders = $this->client[$accountName]->getFolders(false))) {
                Logger::logMessage("Failed to fetch folder list: $accountName", $this->imapLogAbsolute, FILE_MODE_APPEND, LOG_LEVEL_ERROR, $this->imapLogLevelMin);
                return '';
            }

            foreach ($folders as $folder) {
                $content .= "[$accountName]: path=" . $folder->path . "\n";
                if ($this->mode == ES_MODE_LIST_ALL_FOLDER_ALL_MAIL) {
                    $messages = $folder->messages()->all()->get();
                    foreach ($messages as $message) {
                        $content .= "..." . $message->getSubject() . "\n";
                    }
                    $content .= "\n";
                }
            }
        }
        return $content;
    }

    /**
     * @param string $accountName
     * @param string $folderName
     * @param \Webklex\PHPIMAP\Message $msg
     * @return void
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     */
    private function syncMessage2Db(string $accountName, string $folderName, \Webklex\PHPIMAP\Message $msg): void {

        $arr = $this->message2array($accountName, $folderName, $msg);
        $sql = "SELECT id FROM " . ES_TABLENAME . " WHERE messageId=?";
        if (empty($ds = $this->dbArray[$this->dbIndexData]->sql($sql, ROW_EXPECT_0_1, [$arr[ES_COL_MESSAGE_ID]]))) {
            $sql = "INSERT INTO `" . ES_TABLENAME . "` (" . $this->emailSyncColumns . ") VALUES (?,?,?,?,?,?,?,?,?,?,?,?)";
            $this->dbArray[$this->dbIndexData]->sql($sql, ROW_REGULAR, $arr);
        }
    }

    /**
     * @param Message $message
     * @return array
     */
    private function message2array(string $accountName, string $folderName, \Webklex\PHPIMAP\Message $msg): array {
//
//        @method Attribute getPriority()
//        * @method Attribute getSubject()
//        * @method Attribute getMessageId()
//        * @method Attribute getMessageNo()
//        * @method Attribute getReferences()
//        * @method Attribute getDate()
//        * @method Attribute getFrom()
//        * @method Attribute getTo()
//        * @method Attribute getCc()
//        * @method Attribute getBcc()
//        * @method Attribute getReplyTo()
//        * @method Attribute getInReplyTo()
//        * @method Attribute getSender()

        $arr[ES_COL_ACCOUNT] = $accountName;
        $arr[ES_COL_MESSAGE_ID] = (string)$msg->getMessageId() ?? '';
        $arr[ES_COL_FOLDER_PATH] = $folderName;
        $arr[ES_COL_SENDER] = (string)$msg->getFrom() ?? '';
        $arr[ES_COL_RECEIVER] = (string)$msg->getTo() ?? '';
        $arr[ES_COL_CC] = (string)$msg->getCc() ?? '';
        $arr[ES_COL_SUBJECT] = (string)$msg->getSubject() ?? '';
        $arr[ES_COL_BODYTEXT] = $msg->getTextBody() ?? '';
        $arr[ES_COL_BODYHTML] = $msg->getHTMLBody() ?? '';

        $arr[ES_COL_ATTACHMENT] = '';
//        $arr[ES_COL_ATTACHMENT]=$msg->getAttachments();

        $ii = (array)$msg->getHeader();
        $arr[ES_COL_HEADER] = $ii['raw'];

//        $ii=(array)$msg->getFlags();
//        $jj=$ii->items;
        $arr[ES_COL_FLAGS] = '';
//        $arr[ES_COL_FLAGS] = implode(',', $ii);

//        foreach($arr as $value){
//            $new[]=$value;
//        }

        return $arr;
    }

    /**
     * @param $accountList
     * @param $mode   ES_MODE_SYNC|ES_MODE_SYNC_IDLE
     * @return void
     * @throws \CodeException
     * @throws \DbException
     * @throws \UserFormException
     * @throws ConnectionFailedException
     * @throws FolderFetchingException
     * @throws GetMessagesFailedException
     * @throws RuntimeException
     */
    private function syncEmailToDb($accountList, $mode = ES_MODE_SYNC): void {
        $errorAlreadyLoggedUnknownFolder = false;

        while (true) {
            // Iterate over all accounts and folders
            foreach ($accountList as $accountName => $value) {

                # "account1=INBOX,Sent|account2=INBOX"
                $folderNameList = explode(',', $value);
                foreach ($folderNameList as $folderName) {
                    if (null === ($folder = $this->client[$accountName]->getFolderByPath($folderName))) {
                        if (!$errorAlreadyLoggedUnknownFolder) {
                            Logger::logMessage("Folder not found: $value:$folderName", $this->imapLogAbsolute, FILE_MODE_APPEND, LOG_LEVEL_ERROR, $this->imapLogLevelMin);
                            Logger::logMessage("Available folder:\n" . $this->showAllAccountAllFolder([$accountName]), $this->imapLogAbsolute, FILE_MODE_APPEND, LOG_LEVEL_DEBUG, $this->imapLogLevelMin);
                            # do not log over and over again
                            $errorAlreadyLoggedUnknownFolder = true;
                        }
                        continue;
                    }


                    // Get all Messages of the current Mailbox $folder
                    /** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */
                    $messages = $folder->messages()->all()->get();

                    /** @var \Webklex\PHPIMAP\Message $message */
                    foreach ($messages as $message) {
                        $this->syncMessage2Db($accountName, $folderName, $message);
                        echo $message->getSubject() . '<br>';
                        //Move the current Message to 'INBOX.read'
//                if ($message->move('INBOX.read') == true) {
//                    echo 'Message has ben moved';
//                } else {
//                    echo 'Message could not be moved';
//                }
                    }
                }
            }

            switch ($mode) {
                case ES_MODE_SYNC:
                    return;
                case ES_MODE_SYNC_POLL:
                default:
                    sleep(10);
                    break;
            }
        }
    }

    /**
     * @param $mode    ES_MODE_LIST_ALL_FOLDER | ES_MODE_LIST_ALL_FOLDER_ALL_MAIL | ES_MODE_SYNC | ES_MODE_SYNC_POLL
     * @param $accountListConfig
     * @param $accountListQfq
     * @return array
     * @throws \UserFormException
     */
    private function getAccountListToProcess($mode, $accountsConfig, $credentials): array {
        $accountList = array();

        // Create a lookup map for folders by username
        $folderMap = [];
        foreach ($credentials as $credential) {
            $accountName = !empty($credential['username']) ? $credential['username'] : "account_" . count($folderMap);
            $folderMap[$accountName] = $credential['folder'] ?? 'INBOX';
        }

        switch ($mode) {
            case ES_MODE_LIST_ALL_FOLDER:
            case ES_MODE_LIST_ALL_FOLDER_ALL_MAIL:
                // For listing, just use account names with flag
                $accountList = array_fill_keys(array_keys($accountsConfig), 1);
                break;
            case ES_MODE_SYNC:
            case ES_MODE_SYNC_POLL:
                // For syncing, use account names with their folders
                foreach (array_keys($accountsConfig) as $accountName) {
                    $accountList[$accountName] = $folderMap[$accountName] ?? 'INBOX';
                }
                break;
            case ES_MODE_ACTION:
                throw new \UserFormException("TBD");
                break;
            default:
                break;
        }

        return $accountList;
    }

    /**
     * @param ClientManager $cm
     * @param $accountList
     * @return void
     * @throws \Webklex\PHPIMAP\Exceptions\ConnectionFailedException
     * @throws \Webklex\PHPIMAP\Exceptions\MaskNotFoundException
     */
    private function connectAll(ClientManager &$cm, $accountList): void {

        // Open connections
        foreach ($accountList as $accountName => $value) {
            $this->client[$accountName] = $cm->account($accountName);
            $this->client[$accountName]->connect();
        }
    }

    /**
     * Iterates over all AutoCron jobs and fire pending.
     *
     * @throws \CodeException
     * @throws \DbException
     * @throws \DownloadException
     * @throws \ShellException
     * @throws \UserFormException
     * @throws \UserReportException
     * @throws \PhpOffice\PhpSpreadsheet\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
     */
    public function process() {
        // Generate dynamic IMAP configuration from database
        $credentials = $this->extractImapCredentials($this->store, $this->dbArray, $this->dbIndexQfq);

//        $this->mode = MODE_LIST_ALL_FOLDER_ALL_MAIL;

        // Nothing to do?
        if (($this->mode == ES_MODE_SYNC || $this->mode == ES_MODE_SYNC_POLL) && empty($credentials)) {
            # Sync without QFQ configured accounts/folders: do nothing
            return;
        }

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

        $accountList = $this->getAccountListToProcess($this->mode, $imapConfig['accounts'], $credentials);
        $this->connectAll($cm, $accountList);

        // Process
        switch ($this->mode) {
            case ES_MODE_LIST_ALL_FOLDER:
            case ES_MODE_LIST_ALL_FOLDER_ALL_MAIL:
                // Dump all configured accounts
                echo str_replace($this->showAllAccountAllFolder($accountList), '\n', '<br>');
                break;
            case ES_MODE_SYNC:
            case ES_MODE_SYNC_POLL:
                $this->syncEmailToDb($accountList, $this->mode);
                break;
            case ES_MODE_ACTION:
                break;
            default:
        }
    }

    /**
     * Extract imap credentials from Setting table
     *
     */
    public static function extractImapCredentials($store, $db, $dbIndex): array {
        $encryptionKey = $store->getVar(SYSTEM_ENCRYPTION_KEY, STORE_SYSTEM, SANITIZE_ALLOW_ALL);

        // Constants for IMAP connection
        $imapPort = 993;
        $imapEncryption = 'ssl';
        $imapValidateCert = true;

        $imapCredentials = [];

        $sql = "SELECT `feUser`, `view`, `name`, `name2` FROM `Setting` WHERE `type` = ?";
        $result = $db[$dbIndex]->sql($sql, ROW_REGULAR, [SETTING_TABLE_COLUMN_TYPE_CREDENTIALS], "No IMAP credentials found in Setting table.");

        // Check if we got results
        if ($result && is_array($result)) {
            // If multiple rows are returned, loop through them
            foreach ($result as $row) {
                $imapCredentials[] = [
                    'host' => $row['name'] ?? '',
                    'username' => $row['feUser'] ?? '',
                    'password' => EncryptDecrypt::getPlaintext($row['view'] ?? '', $encryptionKey),
                    'folder' => $row['name2'] ?? '',
                    'port' => $imapPort,
                    'encryption' => $imapEncryption,
                    'validate_cert' => $imapValidateCert
                ];
            }
        } elseif ($result) {
            // If single row is returned
            $imapCredentials[] = [
                'host' => $result['name'] ?? '',
                'username' => $result['feUser'] ?? '',
                'password' => EncryptDecrypt::getPlaintext($row['view'] ?? '', $encryptionKey),
                'folder' => $result['name2'] ?? '',
                'port' => $imapPort,
                'encryption' => $imapEncryption,
                'validate_cert' => $imapValidateCert
            ];
        }

        return $imapCredentials;
    }

    /**
     * Generate the IMAP configuration array for Webklex\PHPIMAP\ClientManager
     *
     * @param array $credentials
     * @return array
     */
    public static function generateImapConfigArray(array $credentials): array {
        $accounts = [];
        $defaultAccount = '';

        foreach ($credentials as $index => $credential) {
            $accountName = !empty($credential['username']) ? $credential['username'] : "account_{$index}";

            // Set first account as default
            if ($index === 0) {
                $defaultAccount = $accountName;
            }

            $accounts[$accountName] = [
                'host' => $credential['host'],
                'port' => $credential['port'],
                'protocol' => 'imap',
                'encryption' => $credential['encryption'],
                'validate_cert' => $credential['validate_cert'],
                'username' => $credential['username'],
                'password' => $credential['password'],
                'authentication' => null,
                'rfc' => 'RFC822',
                'proxy' => [
                    'socket' => null,
                    'request_fulluri' => false,
                    'username' => null,
                    'password' => null,
                ],
                'timeout' => 30,
                'extensions' => []
            ];
        }

        return [
            'date_format' => 'd-M-Y',
            'default' => $defaultAccount,

            'security' => [
                'detect_spoofing' => true,
                'detect_spoofing_exception' => false,
                'sanitize_filenames' => true,
            ],

            'accounts' => $accounts,

            'options' => [
                'delimiter' => '/',
                'fetch' => \Webklex\PHPIMAP\IMAP::FT_PEEK,
                'sequence' => \Webklex\PHPIMAP\IMAP::ST_UID,
                'fetch_body' => true,
                'fetch_flags' => true,
                'soft_fail' => false,
                'rfc822' => true,
                'debug' => false,
                'unescaped_search_dates' => false,
                'uid_cache' => true,
                'boundary' => '/boundary=(.*?(?=;)|(.*))/i',
                'message_key' => 'list',
                'fetch_order' => 'asc',
                'dispositions' => ['attachment', 'inline'],
                'common_folders' => [
                    'root' => 'INBOX',
                    'junk' => 'INBOX/Junk',
                    'draft' => 'INBOX/Drafts',
                    'sent' => 'INBOX/Sent',
                    'trash' => 'INBOX/Trash',
                ],
                'open' => []
            ],

            'decoding' => [
                'options' => [
                    'header' => 'utf-8',
                    'message' => 'utf-8',
                    'attachment' => 'utf-8'
                ],
                'decoder' => [
                    'header' => \Webklex\PHPIMAP\Decoder\HeaderDecoder::class,
                    'message' => \Webklex\PHPIMAP\Decoder\MessageDecoder::class,
                    'attachment' => \Webklex\PHPIMAP\Decoder\AttachmentDecoder::class
                ]
            ],

            'flags' => ['recent', 'flagged', 'answered', 'deleted', 'seen', 'draft'],

            'events' => [
                'message' => [
                    'new' => \Webklex\PHPIMAP\Events\MessageNewEvent::class,
                    'moved' => \Webklex\PHPIMAP\Events\MessageMovedEvent::class,
                    'copied' => \Webklex\PHPIMAP\Events\MessageCopiedEvent::class,
                    'deleted' => \Webklex\PHPIMAP\Events\MessageDeletedEvent::class,
                    'restored' => \Webklex\PHPIMAP\Events\MessageRestoredEvent::class,
                ],
                'folder' => [
                    'new' => \Webklex\PHPIMAP\Events\FolderNewEvent::class,
                    'moved' => \Webklex\PHPIMAP\Events\FolderMovedEvent::class,
                    'deleted' => \Webklex\PHPIMAP\Events\FolderDeletedEvent::class,
                ],
                'flag' => [
                    'new' => \Webklex\PHPIMAP\Events\FlagNewEvent::class,
                    'deleted' => \Webklex\PHPIMAP\Events\FlagDeletedEvent::class,
                ],
            ],

            'masks' => [
                'message' => \Webklex\PHPIMAP\Support\Masks\MessageMask::class,
                'attachment' => \Webklex\PHPIMAP\Support\Masks\AttachmentMask::class
            ]
        ];
    }

}
