<?php
/**
 * _script functions for the mailMigration report (F24330): OAuth2 device code flow + connection
 * probes, invoked via 'F:...|call:<fn>|arg:mmId=...' (usage: mailMigration.qfqr). Every function
 * enforces row ownership; the token file path is derived from the job id, never typed by users.
 */

use IMATHUZH\Qfq\Core\Database\Database;
use IMATHUZH\Qfq\Core\Helper\EncryptDecrypt;
use IMATHUZH\Qfq\Core\Helper\HelperFile;
use IMATHUZH\Qfq\Core\Imap\OAuth2TokenManager;
use IMATHUZH\Qfq\Core\Store\Store;
use IMATHUZH\Qfq\External\MailMigration;

/**
 * Step 1: request a device code for the Outlook target and show the login instructions.
 *
 * @param array $param - 'mmId' = MailMigration.id
 * @param object $qfq - ScriptFunctions instance (unused)
 * @return void
 */
function mailMigrationOAuth2Start(array $param, $qfq): void {

    try {
        $row = mailMigrationOwnRow((int)($param['mmId'] ?? 0));

        $pathFileNameToken = MailMigration::tokenPathForJob((int)$row['id']);
        HelperFile::mkDirParent($pathFileNameToken);

        // A never-authorized pair must not inherit a stale token file (deleted pair + reused id).
        if ($row['status'] === MM_STATUS_NEW && file_exists($pathFileNameToken)) {
            @unlink($pathFileNameToken);
        }

        $clientId = trim((string)Store::getInstance()->getVar(SYSTEM_IMAP_OAUTH2_CLIENT_ID, STORE_SYSTEM));
        $start = (new OAuth2TokenManager($clientId))->startDeviceAuthorization($pathFileNameToken, $row['dstUser']);

        $uri = htmlspecialchars($start['verification_uri']);
        $minutes = max(1, (int)floor($start['expires_in'] / 60));

        if (!empty($start['was_authorized'])) {
            echo '<div class="alert alert-warning">This account is already authorized - the existing tokens keep '
                . 'working until this new login is completed. Clicked by accident? Simply ignore this page.</div>';
        }

        echo '<div class="alert alert-info">'
            . '<p><b>Authorize ' . htmlspecialchars($row['dstUser']) . ':</b></p>'
            . '<ol>'
            . '<li>Open <a href="' . $uri . '" target="_blank" rel="noopener">' . $uri . '</a></li>'
            . '<li>Enter the code: <b><code>' . htmlspecialchars($start['user_code']) . '</code></b></li>'
            . '<li>Log in with <b>' . htmlspecialchars($row['dstUser']) . '</b> (MFA works), then come back and click <i>Check login</i>.</li>'
            . '</ol>'
            . '<p>The code is valid for ' . $minutes . ' minutes.</p>'
            . '</div>';

    } catch (\Exception $e) {
        echo '<div class="alert alert-danger">' . htmlspecialchars($e->getMessage()) . '</div>';
    }
}

/**
 * Step 2: one token poll per click; on success the pair flips to 'authorized'.
 *
 * @param array $param - 'mmId' = MailMigration.id
 * @param object $qfq - ScriptFunctions instance (unused)
 * @return void
 */
function mailMigrationOAuth2Poll(array $param, $qfq): void {

    try {
        $row = mailMigrationOwnRow((int)($param['mmId'] ?? 0));

        $clientId = trim((string)Store::getInstance()->getVar(SYSTEM_IMAP_OAUTH2_CLIENT_ID, STORE_SYSTEM));
        $result = (new OAuth2TokenManager($clientId))->pollDeviceAuthorization(MailMigration::tokenPathForJob((int)$row['id']));

        if (($result['status'] ?? '') === 'ok') {
            // Only 'new'/'error' become startable - re-auth must not change an active/finished lifecycle.
            mailMigrationDb()->sql("UPDATE `" . MM_TABLENAME . "` SET `status` = ?, `lastMessage` = 'Outlook authorized'"
                . " WHERE `id` = ? AND `status` IN (?,?)",
                ROW_REGULAR, [MM_STATUS_AUTHORIZED, $row['id'], MM_STATUS_NEW, MM_STATUS_ERROR]);
        }

        $cssClass = ['ok' => 'alert-success', 'pending' => 'alert-warning', 'error' => 'alert-danger'][$result['status']] ?? 'alert-info';
        echo '<div class="alert ' . $cssClass . '">' . htmlspecialchars($row['dstUser'] . ': ' . $result['message']) . '</div>';

    } catch (\Exception $e) {
        echo '<div class="alert alert-danger">' . htmlspecialchars($e->getMessage()) . '</div>';
    }
}

/**
 * Live probe of the SOURCE account (not via Imap.php - that reads only the Setting table).
 *
 * @param array $param - 'mmId' = MailMigration.id
 * @param object $qfq - ScriptFunctions instance (unused)
 * @return void
 */
function mailMigrationCheckSource(array $param, $qfq): void {

    try {
        $row = mailMigrationOwnRow((int)($param['mmId'] ?? 0));

        $store = Store::getInstance();
        $encryptionKey = $store->getVar(SYSTEM_ENCRYPTION_KEY, STORE_SYSTEM, SANITIZE_ALLOW_ALL);
        $password = EncryptDecrypt::getPlaintext($row['srcPasswordEnc'] ?? '', $encryptionKey);

        $folderCount = mailMigrationProbe($row['srcHost'], $row['srcUser'], $password, null);

        echo '<div class="alert alert-success">Source ' . htmlspecialchars($row['srcUser'] . ' @ ' . $row['srcHost'])
            . ': connection OK (' . $folderCount . ' folders).</div>';

    } catch (\Exception $e) {
        echo '<div class="alert alert-danger">Source connection failed: ' . htmlspecialchars($e->getMessage()) . '</div>';
    }
}

/**
 * Live probe of the Outlook TARGET (real folder listing - XOAUTH2 reports auth success even on
 * user/token mismatch).
 *
 * @param array $param - 'mmId' = MailMigration.id
 * @param object $qfq - ScriptFunctions instance (unused)
 * @return void
 */
function mailMigrationCheckTarget(array $param, $qfq): void {

    try {
        $row = mailMigrationOwnRow((int)($param['mmId'] ?? 0));

        // Never probe with a stale token of a deleted pair (reused id = previous owner's mailbox).
        if ($row['status'] === MM_STATUS_NEW) {
            throw new \UserReportException('Not authorized yet - use the key button first.', ERROR_IMAP);
        }

        $clientId = trim((string)Store::getInstance()->getVar(SYSTEM_IMAP_OAUTH2_CLIENT_ID, STORE_SYSTEM));
        $accessToken = (new OAuth2TokenManager($clientId))->getAccessToken(MailMigration::tokenPathForJob((int)$row['id']));

        $folderCount = mailMigrationProbe($row['dstHost'], $row['dstUser'], $accessToken, IMAP_AUTHENTICATION_OAUTH);

        echo '<div class="alert alert-success">Target ' . htmlspecialchars($row['dstUser'] . ' @ ' . $row['dstHost'])
            . ': connection OK (' . $folderCount . ' folders).</div>';

    } catch (\Exception $e) {
        echo '<div class="alert alert-danger">Target connection failed: ' . htmlspecialchars($e->getMessage())
            . ' - not authorized yet? Use the key button first.</div>';
    }
}

/**
 * Connect + real folder listing, short timeout; shares the worker's buildImapClient() (no drift).
 *
 * @param string $hostPort
 * @param string $username
 * @param string $password
 * @param string|null $authentication
 * @return int - number of folders
 * @throws \Throwable
 */
function mailMigrationProbe(string $hostPort, string $username, string $password, ?string $authentication): int {
    $client = MailMigration::buildImapClient($hostPort, $username, $password, $authentication, 10);
    return count($client->getFolders(false));
}

/**
 * Loads the row and enforces ownership: row.feUser must match the logged-in FE user ({{feUser:UT}}).
 *
 * @param int $mmId
 * @return array
 * @throws \UserReportException
 */
function mailMigrationOwnRow(int $mmId): array {

    if ($mmId <= 0) {
        throw new \UserReportException('Missing or invalid mmId.', ERROR_IMAP);
    }

    $feUser = trim((string)Store::getInstance()->getVar(TYPO3_FE_USER, STORE_USER . STORE_TYPO3, SANITIZE_ALLOW_ALNUMX));
    if ($feUser === '') {
        throw new \UserReportException('Not logged in.', ERROR_IMAP);
    }

    $row = mailMigrationDb()->sql("SELECT * FROM `" . MM_TABLENAME . "` WHERE `id` = ?", ROW_EXPECT_0_1, [$mmId]);
    if (empty($row) || $row['feUser'] !== $feUser) {
        throw new \UserReportException("Migration pair not found (id=$mmId) or not yours.", ERROR_IMAP);
    }

    return $row;
}

/**
 * @return Database
 * @throws \CodeException
 * @throws \UserFormException
 * @throws \UserReportException
 */
function mailMigrationDb(): Database {
    static $db = null;
    if ($db === null) {
        $db = new Database(Store::getInstance()->getVar(SYSTEM_DB_INDEX_DATA, STORE_SYSTEM));
    }
    return $db;
}
