<?php

declare(strict_types=1);

require_once __DIR__ . DIRECTORY_SEPARATOR . 'admin_member_auth.php';
require_once __DIR__ . DIRECTORY_SEPARATOR . 'fate_member_password.php';
require_once __DIR__ . DIRECTORY_SEPARATOR . 'fate_mail.php';

function fate_member_email_verify_token_table_sql(): string
{
    return <<<'SQL'
CREATE TABLE IF NOT EXISTS `fate_member_email_verify_tokens` (
  `member_id` INT UNSIGNED NOT NULL,
  `token_hash` CHAR(64) NOT NULL COMMENT 'SHA-256 hex',
  `code_hash` CHAR(64) NULL DEFAULT NULL COMMENT 'SHA-256 hex of 6-digit verify code',
  `expires_at` DATETIME NOT NULL,
  `sent_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`member_id`),
  UNIQUE KEY `uq_fevt_hash` (`token_hash`),
  KEY `idx_fevt_exp` (`expires_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL;
}

/** 6 位數驗證碼（100000–999999）。 */
function fate_member_generate_verify_code(): string
{
    return (string) random_int(100000, 999999);
}

function fate_member_normalize_verify_code(string $raw): ?string
{
    $digits = preg_replace('/\D+/', '', trim($raw));
    if (!is_string($digits) || strlen($digits) !== 6) {
        return null;
    }

    return $digits;
}

function fate_member_hash_verify_code(string $code): string
{
    return hash('sha256', $code);
}

function fate_member_verify_code_attempt_ok(): array
{
    fate_public_session_start_safe();
    $lockUntil = isset($_SESSION['fate_verify_code_lock_until']) ? (int) $_SESSION['fate_verify_code_lock_until'] : 0;
    if ($lockUntil > time()) {
        return ['ok' => false, 'secs' => $lockUntil - time()];
    }

    return ['ok' => true, 'secs' => 0];
}

function fate_member_verify_code_attempt_fail(): void
{
    fate_public_session_start_safe();
    $fails = isset($_SESSION['fate_verify_code_fails']) ? (int) $_SESSION['fate_verify_code_fails'] : 0;
    $fails++;
    $_SESSION['fate_verify_code_fails'] = $fails;
    if ($fails >= 8) {
        $_SESSION['fate_verify_code_lock_until'] = time() + 600;
        $_SESSION['fate_verify_code_fails'] = 0;
    }
}

function fate_member_verify_code_attempt_reset(): void
{
    fate_public_session_start_safe();
    unset($_SESSION['fate_verify_code_fails'], $_SESSION['fate_verify_code_lock_until']);
}

/**
 * 安全網：權威 schema 定義於 sql/install.sql。此處僅「缺了才補」——
 * 先檢查欄位／資料表是否存在，僅在缺漏時才建立，不再每次盲目 ALTER。
 */
function fate_member_email_bootstrap_schema(PDO $pdo): void
{
    static $done = false;
    if ($done) {
        return;
    }
    if (!fate_admin_member_auth_ready()) {
        return;
    }
    $done = true;

    $db = FATE_MEMBER_DATABASE;
    $tbl = FATE_MEMBER_TABLE;
    $dbEsc = str_replace('`', '``', $db);
    $tblEsc = str_replace('`', '``', $tbl);

    // 讀取目前欄位（單次 SHOW COLUMNS）；讀不到會員表即放手交由上層處理。
    $cols = [];
    try {
        $st = $pdo->query(sprintf('SHOW COLUMNS FROM `%s`.`%s`', $dbEsc, $tblEsc));
        if ($st === false) {
            return;
        }
        while (($row = $st->fetch(PDO::FETCH_ASSOC)) !== false) {
            $f = isset($row['Field']) ? strtolower((string) $row['Field']) : '';
            if ($f !== '') {
                $cols[$f] = true;
            }
        }
    } catch (Throwable) {
        return;
    }

    $changed = false;
    if (!isset($cols['email'])) {
        try {
            $pdo->exec(sprintf('ALTER TABLE `%s`.`%s` ADD COLUMN `email` VARCHAR(254) NULL DEFAULT NULL AFTER `display_name`', $dbEsc, $tblEsc));
            $changed = true;
        } catch (Throwable) {
        }
    }
    if (!isset($cols['email_verified_at'])) {
        try {
            $pdo->exec(sprintf('ALTER TABLE `%s`.`%s` ADD COLUMN `email_verified_at` DATETIME NULL DEFAULT NULL AFTER `email`', $dbEsc, $tblEsc));
            $changed = true;
            $cols['email_verified_at'] = true;
        } catch (Throwable) {
        }
    }
    if (!isset($cols['approved_at'])) {
        try {
            $after = isset($cols['email_verified_at']) ? ' AFTER `email_verified_at`' : '';
            $pdo->exec(sprintf(
                'ALTER TABLE `%s`.`%s` ADD COLUMN `approved_at` DATETIME NULL DEFAULT NULL COMMENT \'管理員人工審核通過時間\'%s',
                $dbEsc,
                $tblEsc,
                $after
            ));
            $changed = true;
            $cols['approved_at'] = true;
            if (isset($cols['created_at'])) {
                $pdo->exec(sprintf(
                    'UPDATE `%s`.`%s` SET `approved_at` = COALESCE(`created_at`, NOW()) WHERE `approved_at` IS NULL',
                    $dbEsc,
                    $tblEsc
                ));
            }
        } catch (Throwable) {
        }
    }
    if (!isset($cols['approved_by'])) {
        try {
            $pdo->exec(sprintf(
                'ALTER TABLE `%s`.`%s` ADD COLUMN `approved_by` INT UNSIGNED NULL DEFAULT NULL COMMENT \'審核管理員 id\' AFTER `approved_at`',
                $dbEsc,
                $tblEsc
            ));
            $changed = true;
        } catch (Throwable) {
        }
    }

    // 驗證 token 表：不存在才建（建立用的 SQL 已含 code_hash 欄位）。
    $hasTok = false;
    try {
        $tk = $pdo->query("SHOW TABLES LIKE 'fate_member_email_verify_tokens'");
        $hasTok = $tk !== false && $tk->fetchColumn() !== false;
    } catch (Throwable) {
    }
    if (!$hasTok) {
        try {
            $pdo->exec(fate_member_email_verify_token_table_sql());
        } catch (Throwable) {
        }
    }

    if ($changed) {
        unset($GLOBALS['__fate_member_cols_snapshot']);
        $GLOBALS['fate_member_table_columns_reload'] = true;
    }

    require_once __DIR__ . DIRECTORY_SEPARATOR . 'fate_member_tier.php';
    fate_member_tier_bootstrap_schema($pdo);
}

function fate_members_table_column_map(PDO $pdo): array
{
    fate_member_email_bootstrap_schema($pdo);

    if (isset($GLOBALS['__fate_member_cols_snapshot']) && is_array($GLOBALS['__fate_member_cols_snapshot'])) {
        return $GLOBALS['__fate_member_cols_snapshot'];
    }

    $colsMap = [];

    if (!fate_admin_member_auth_ready()) {
        return $colsMap;
    }

    try {
        $db = FATE_MEMBER_DATABASE;
        $tbl = FATE_MEMBER_TABLE;
        $stmt = $pdo->query(sprintf(
            'SHOW COLUMNS FROM `%s`.`%s`',
            str_replace('`', '``', $db),
            str_replace('`', '``', $tbl)
        ));
        if ($stmt !== false) {
            while (($row = $stmt->fetch(PDO::FETCH_ASSOC)) !== false) {
                $f = isset($row['Field']) ? strtoupper((string) $row['Field']) : '';
                if ($f !== '') {
                    $colsMap[$f] = true;
                }
            }
        }
    } catch (Throwable) {
        $colsMap = [];
    }

    $GLOBALS['__fate_member_cols_snapshot'] = $colsMap;

    return $colsMap;
}

function fate_members_table_has_column(PDO $pdo, string $columnUppercase): bool
{
    $m = fate_members_table_column_map($pdo);

    return isset($m[strtoupper($columnUppercase)]);
}

function fate_member_email_verify_feature_enabled(PDO $pdo): bool
{
    return fate_members_table_has_column($pdo, 'EMAIL')
        && fate_members_table_has_column($pdo, 'EMAIL_VERIFIED_AT');
}

/**
 * 依 Email 查會員（註冊／修改 Email 時檢查重複）。
 *
 * @return array{found:bool,id:int,username:string,verified:bool}
 */
function fate_member_lookup_by_email(PDO $pdo, string $emailNorm): array
{
    $empty = ['found' => false, 'id' => 0, 'username' => '', 'verified' => false];
    $mail = strtolower(trim($emailNorm));
    if ($mail === '' || !filter_var($mail, FILTER_VALIDATE_EMAIL)) {
        return $empty;
    }

    if (!fate_admin_member_auth_ready()) {
        return $empty;
    }

    fate_member_email_bootstrap_schema($pdo);
    if (!fate_members_table_has_column($pdo, 'EMAIL')) {
        return $empty;
    }

    $db = FATE_MEMBER_DATABASE;
    $tbl = FATE_MEMBER_TABLE;
    $hasEv = fate_members_table_has_column($pdo, 'EMAIL_VERIFIED_AT');
    $sel = $hasEv
        ? '`id`, `username`, `email_verified_at`'
        : '`id`, `username`';

    try {
        $st = $pdo->prepare(sprintf(
            'SELECT %s FROM `%s`.`%s` WHERE LOWER(TRIM(`email`)) = ? LIMIT 1',
            $sel,
            str_replace('`', '``', $db),
            str_replace('`', '``', $tbl)
        ));
        $st->execute([$mail]);
        $row = $st->fetch(PDO::FETCH_ASSOC);
        if (!is_array($row)) {
            return $empty;
        }
        $verified = false;
        if ($hasEv) {
            $ev = $row['email_verified_at'] ?? null;
            $verified = $ev !== null && is_string($ev) && trim($ev) !== '';
        }

        return [
            'found' => true,
            'id' => (int) ($row['id'] ?? 0),
            'username' => (string) ($row['username'] ?? ''),
            'verified' => $verified,
        ];
    } catch (Throwable) {
        return $empty;
    }
}

/** @param array<string,mixed> $row */
function fate_member_requires_email_verify_for_row(PDO $pdo, array $row): bool
{
    if (!fate_member_email_verify_feature_enabled($pdo)) {
        return false;
    }
    $role = isset($row['role']) ? strtolower(trim((string) $row['role'])) : '';
    if (fate_member_role_is_admin($role)) {
        return false;
    }
    $emRaw = isset($row['email']) ? trim((string) $row['email']) : '';
    if ($emRaw === '' || !filter_var($emRaw, FILTER_VALIDATE_EMAIL)) {
        return false;
    }

    $ev = $row['email_verified_at'] ?? null;

    return $ev === null || (is_string($ev) && trim($ev) === '');
}

/**
 * @return array{found:bool,row:?array<string,mixed>}
 */
function fate_public_member_login_row_fetch(PDO $pdo, string $username): array
{
    $db = FATE_MEMBER_DATABASE;
    $tbl = FATE_MEMBER_TABLE;
    $ac = FATE_MEMBER_ACTIVE_COLUMN;
    $u = trim($username);

    $withEmail = fate_member_email_verify_feature_enabled($pdo);
    $withApproval = fate_member_manual_approval_enabled() && fate_members_table_has_column($pdo, 'APPROVED_AT');
    $bases = $withEmail ? ['we', 'ne'] : ['ne'];

    foreach ($bases as $variant) {
        $selectCore = '`id`, `username`, `password`, `display_name`';
        if ($variant === 'we') {
            $selectCore .= ', `email`, `email_verified_at`';
        }
        if ($withApproval) {
            $selectCore .= ', `approved_at`';
        }
        $selectCore .= sprintf(', `%s` AS `_mem_active`', str_replace('`', '``', $ac));

        foreach (['with_role', 'no_role'] as $rr) {
            $sel = $selectCore;
            if ($rr === 'with_role') {
                $sel .= ', `role`';
            }

            $sql = sprintf(
                'SELECT %s FROM `%s`.`%s` WHERE `username` = ? LIMIT 1',
                $sel,
                str_replace('`', '``', $db),
                str_replace('`', '``', $tbl)
            );

            try {
                $st = $pdo->prepare($sql);
                $st->execute([$u]);
                $r = $st->fetch(PDO::FETCH_ASSOC);
                if (is_array($r)) {
                    if ($rr === 'no_role' || !array_key_exists('role', $r)) {
                        $r['role'] = '';
                    }

                    return ['found' => true, 'row' => $r];
                }
            } catch (PDOException $e) {
                $msg = $e->getMessage();
                if (str_contains($msg, 'Unknown column') && $variant === 'we') {
                    continue 2;
                }
                if (str_contains($msg, 'Unknown column') && str_contains($msg, 'role')) {
                    continue;
                }
                throw $e;
            }
        }
    }

    return ['found' => false, 'row' => null];
}

/**
 * @return array{label:string,origin:string}
 */
function fate_public_site_absolute_base(): array
{
    $env = trim((string) (getenv('FATE_PUBLIC_SITE_ORIGIN') ?: ''));
    if ($env !== '' && preg_match('#^https?://[^\s]+$#', $env) === 1) {
        return [
            'label' => (string) (getenv('FATE_SITE_SHORT_NAME') ?: ''),
            'origin' => rtrim($env, '/'),
        ];
    }

    $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
    $host = isset($_SERVER['HTTP_HOST']) ? trim((string) $_SERVER['HTTP_HOST']) : '';
    $script = $_SERVER['SCRIPT_NAME'] ?? '';
    $dir = is_string($script) && $script !== '' ? str_replace('\\', '/', dirname($script)) : '';

    $origin = $host !== '' ? $scheme . '://' . $host : '';

    if ($dir === '/' || $dir === '\\' || $dir === '.') {
        $suffix = '';
    } else {
        $suffix = $dir;
    }

    $full = $origin . $suffix;

    return [
        'label' => ($host !== '' ? $host : '本站'),
        'origin' => rtrim($full, '/'),
    ];
}

function fate_absolute_url(string $pathFromFateRoot): string
{
    $o = fate_public_site_absolute_base()['origin'] ?? '';

    return $o !== '' ? $o . '/' . ltrim(str_replace('\\', '/', $pathFromFateRoot), '/') : '/' . ltrim($pathFromFateRoot, '/');
}

/**
 * @return array{ok:bool,detail:string}
 */
function fate_member_send_verify_email_token(PDO $pdo, int $memberId, string $recipientEmail, string $displayLabel): array
{
    if ($memberId <= 0 || $recipientEmail === '' || !filter_var($recipientEmail, FILTER_VALIDATE_EMAIL)) {
        return ['ok' => false, 'detail' => '收件信箱無效'];
    }

    fate_member_email_bootstrap_schema($pdo);

    $rawToken = bin2hex(random_bytes(24));
    $hash = hash('sha256', $rawToken);
    $rawCode = fate_member_generate_verify_code();
    $codeHash = fate_member_hash_verify_code($rawCode);
    $expires = (new DateTimeImmutable('now'))->add(new DateInterval('PT48H'))->format('Y-m-d H:i:s');

    try {
        $up = $pdo->prepare(
            'INSERT INTO `fate_member_email_verify_tokens` (`member_id`, `token_hash`, `code_hash`, `expires_at`) VALUES (?, ?, ?, ?)
             ON DUPLICATE KEY UPDATE `token_hash` = VALUES(`token_hash`), `code_hash` = VALUES(`code_hash`), `expires_at` = VALUES(`expires_at`), `sent_at` = CURRENT_TIMESTAMP'
        );
        $up->execute([$memberId, $hash, $codeHash, $expires]);
    } catch (Throwable $e) {
        return ['ok' => false, 'detail' => '無法寫入驗證權杖：' . $e->getMessage()];
    }

    $verifyUrl = fate_absolute_url('verify_email.php?token=' . rawurlencode($rawToken));
    $base = fate_public_site_absolute_base();
    if (!function_exists('fate_public_site_branding')) {
        require_once __DIR__ . DIRECTORY_SEPARATOR . 'fate_ui_settings.php';
    }
    if (!function_exists('fate_mail_render_verify_email')) {
        require_once __DIR__ . DIRECTORY_SEPARATOR . 'fate_mail_settings.php';
    }

    $brand = fate_public_site_branding();
    $siteLabel = $brand['title_main'] !== '' ? $brand['title_main'] : ($base['label'] !== '' ? $base['label'] : 'FATE');
    $rendered = fate_mail_render_verify_email(null, [
        'display_name' => $displayLabel,
        'verify_url' => $verifyUrl,
        'verify_code' => $rawCode,
        'site_label' => $siteLabel,
        'hours' => '48',
    ]);

    $payload = [
        'subject' => $rendered['subject'],
        'to' => $recipientEmail,
        'plaintext' => $rendered['plaintext'],
    ];
    if ($rendered['html'] !== '') {
        $payload['html'] = $rendered['html'];
    }

    $sent = fate_mail_send_transactional($payload);

    return $sent['ok'] === true ? ['ok' => true, 'detail' => $sent['detail']] : ['ok' => false, 'detail' => $sent['detail'] ?? '送信失敗'];
}

/**
 * @return array{ok:bool,member:?array{id:int,username:string,display_name:string,role:string},detail:string}
 */
function fate_member_mark_email_verified(PDO $pdo, int $memberId): array
{
    if ($memberId <= 0) {
        return ['ok' => false, 'member' => null, 'detail' => '資料異常'];
    }

    $db = FATE_MEMBER_DATABASE;
    $tbl = FATE_MEMBER_TABLE;

    try {
        $pdo->prepare(sprintf(
            'UPDATE `%s`.`%s` SET `email_verified_at` = CURRENT_TIMESTAMP WHERE `id` = ? LIMIT 1',
            str_replace('`', '``', $db),
            str_replace('`', '``', $tbl)
        ))->execute([$memberId]);
    } catch (Throwable) {
        return ['ok' => false, 'member' => null, 'detail' => '無法更新驗證狀態（請確認資料表有 email_verified_at 欄位）'];
    }

    require_once __DIR__ . DIRECTORY_SEPARATOR . 'fate_member_tier.php';
    fate_member_tier_upgrade_formal_if_eligible($pdo, $memberId);

    try {
        $pdo->prepare('DELETE FROM `fate_member_email_verify_tokens` WHERE `member_id` = ?')->execute([$memberId]);
    } catch (Throwable) {
    }

    unset($GLOBALS['__fate_member_cols_snapshot']);
    fate_member_verify_code_attempt_reset();

    try {
        $sql = sprintf(
            'SELECT `id`,`username`,`display_name`,`role`,`%s` AS `_mem_active` FROM `%s`.`%s` WHERE `id` = ? LIMIT 1',
            str_replace('`', '``', FATE_MEMBER_ACTIVE_COLUMN),
            str_replace('`', '``', $db),
            str_replace('`', '``', $tbl)
        );
        $st2 = $pdo->prepare($sql);
        $st2->execute([$memberId]);
    } catch (Throwable $e2) {
        return ['ok' => false, 'member' => null, 'detail' => $e2->getMessage()];
    }

    $mem = $st2->fetch(PDO::FETCH_ASSOC);
    if (!is_array($mem)) {
        return [
            'ok' => true,
            'member' => ['id' => $memberId, 'username' => '', 'display_name' => '', 'role' => ''],
            'detail' => '',
        ];
    }

    return [
        'ok' => true,
        'member' => [
            'id' => (int) $mem['id'],
            'username' => (string) ($mem['username'] ?? ''),
            'display_name' => (string) ($mem['display_name'] ?? ''),
            'role' => isset($mem['role']) ? trim((string) $mem['role']) : '',
        ],
        'detail' => '',
    ];
}

/**
 * @return array{ok:bool,member:?array{id:int,username:string,display_name:string,role:string},detail:string}
 */
function fate_member_verify_email_with_token(PDO $pdo, string $rawToken): array
{
    $t = trim($rawToken);
    if ($t === '') {
        return ['ok' => false, 'member' => null, 'detail' => '無效的驗證連結'];
    }
    if (!preg_match('/^[a-f0-9]{48,128}$/i', $t)) {
        return ['ok' => false, 'member' => null, 'detail' => '無效的驗證連結'];
    }

    fate_member_email_bootstrap_schema($pdo);
    $hash = hash('sha256', strtolower($t));

    try {
        $st = $pdo->prepare('SELECT `member_id`, `expires_at` FROM `fate_member_email_verify_tokens` WHERE `token_hash` = ? LIMIT 1');
        $st->execute([$hash]);
        $tok = $st->fetch(PDO::FETCH_ASSOC);
        if (!is_array($tok)) {
            return ['ok' => false, 'member' => null, 'detail' => '找不到或連結已失效；可改以驗證碼完成，或重新寄送驗證信。'];
        }
        $exp = strtotime((string) ($tok['expires_at'] ?? ''));
        if ($exp !== false && $exp < time()) {
            return ['ok' => false, 'member' => null, 'detail' => '驗證已逾期，請重新寄送驗證信。'];
        }

        $mid = (int) ($tok['member_id'] ?? 0);

        return fate_member_mark_email_verified($pdo, $mid);
    } catch (Throwable $e) {
        return ['ok' => false, 'member' => null, 'detail' => $e->getMessage()];
    }
}

/**
 * 已登入會員以 6 位驗證碼完成驗證（與連結擇一即可）。
 *
 * @return array{ok:bool,member:?array{id:int,username:string,display_name:string,role:string},detail:string}
 */
function fate_member_verify_email_with_code(PDO $pdo, int $memberId, string $rawCode): array
{
    if ($memberId <= 0) {
        return ['ok' => false, 'member' => null, 'detail' => '請先登入。'];
    }

    $attempt = fate_member_verify_code_attempt_ok();
    if (!$attempt['ok']) {
        return ['ok' => false, 'member' => null, 'detail' => '驗證碼嘗試次數過多，請 ' . (string) $attempt['secs'] . ' 秒後再試。'];
    }

    $code = fate_member_normalize_verify_code($rawCode);
    if ($code === null) {
        fate_member_verify_code_attempt_fail();

        return ['ok' => false, 'member' => null, 'detail' => '請輸入 6 位數驗證碼。'];
    }

    fate_member_email_bootstrap_schema($pdo);

    if (!function_exists('fate_public_member_login_row_fetch')) {
        require_once __DIR__ . DIRECTORY_SEPARATOR . 'fate_public_member_auth.php';
    }
    fate_public_session_start_safe();
    $sessionId = isset($_SESSION['fate_site_member_id']) ? (int) $_SESSION['fate_site_member_id'] : 0;
    $sessionUser = isset($_SESSION['fate_site_username']) ? trim((string) $_SESSION['fate_site_username']) : '';
    if ($sessionId !== $memberId || $sessionUser === '') {
        return ['ok' => false, 'member' => null, 'detail' => '請使用目前登入的帳號操作。'];
    }
    $fr = fate_public_member_login_row_fetch($pdo, $sessionUser);
    if (!$fr['found'] || !is_array($fr['row']) || (int) ($fr['row']['id'] ?? 0) !== $memberId) {
        return ['ok' => false, 'member' => null, 'detail' => '找不到會員資料。'];
    }
    if (!fate_member_requires_email_verify_for_row($pdo, $fr['row'])) {
        return ['ok' => false, 'member' => null, 'detail' => '您的信箱已驗證，無須再輸入驗證碼。'];
    }

    $codeHash = fate_member_hash_verify_code($code);

    try {
        $st = $pdo->prepare(
            'SELECT `expires_at`, `code_hash` FROM `fate_member_email_verify_tokens` WHERE `member_id` = ? LIMIT 1'
        );
        $st->execute([$memberId]);
        $tok = $st->fetch(PDO::FETCH_ASSOC);
        if (!is_array($tok)) {
            fate_member_verify_code_attempt_fail();

            return ['ok' => false, 'member' => null, 'detail' => '尚無有效驗證資料，請先重寄驗證信。'];
        }
        $storedCodeHash = isset($tok['code_hash']) ? trim((string) $tok['code_hash']) : '';
        if ($storedCodeHash === '') {
            fate_member_verify_code_attempt_fail();

            return ['ok' => false, 'member' => null, 'detail' => '此驗證信僅支援連結；請重寄驗證信以取得驗證碼。'];
        }
        if (!hash_equals($storedCodeHash, $codeHash)) {
            fate_member_verify_code_attempt_fail();

            return ['ok' => false, 'member' => null, 'detail' => '驗證碼不正確。'];
        }
        $exp = strtotime((string) ($tok['expires_at'] ?? ''));
        if ($exp !== false && $exp < time()) {
            return ['ok' => false, 'member' => null, 'detail' => '驗證碼已逾期，請重新寄送驗證信。'];
        }

        return fate_member_mark_email_verified($pdo, $memberId);
    } catch (Throwable $e) {
        return ['ok' => false, 'member' => null, 'detail' => $e->getMessage()];
    }
}

/**
 * @return array{ok:bool,detail:string}
 */
function fate_member_retry_send_verify(PDO $pdo, string $username, string $password): array
{
    $fr = fate_public_member_login_row_fetch($pdo, $username);
    if (!$fr['found'] || !is_array($fr['row'])) {
        return ['ok' => false, 'detail' => '帳號或密碼錯誤。'];
    }

    /** @var array<string,mixed> $row */
    $row = $fr['row'];
    $hash = (string) ($row['password'] ?? '');
    if ($hash === '' || !function_exists('fate_member_verify_password') || !fate_member_verify_password($password, $hash)) {
        return ['ok' => false, 'detail' => '帳號或密碼錯誤。'];
    }

    if ((int) ($row['_mem_active'] ?? 0) !== 1) {
        return ['ok' => false, 'detail' => '帳號未啟用。'];
    }

    if (!fate_member_requires_email_verify_for_row($pdo, $row)) {
        return ['ok' => false, 'detail' => '您的信箱已無需驗證，請直接登入。'];
    }

    $cool = fate_member_resend_cooldown_ok();
    if (!$cool['ok']) {
        return ['ok' => false, 'detail' => '請於 ' . (string) $cool['secs'] . ' 秒後再試。'];
    }

    $id = (int) ($row['id'] ?? 0);
    $mail = isset($row['email']) ? trim((string) $row['email']) : '';
    if (!function_exists('fate_public_member_display_label')) {
        require_once __DIR__ . DIRECTORY_SEPARATOR . 'fate_public_member_auth.php';
    }
    $label = fate_public_member_display_label([
        'id' => $id,
        'username' => (string) ($row['username'] ?? ''),
        'display_name' => trim((string) ($row['display_name'] ?? '')),
        'avatar_rel' => '',
    ]) ?: trim((string) ($row['username'] ?? ''));

    $snd = fate_member_send_verify_email_token($pdo, $id, $mail, $label);
    if (!$snd['ok']) {
        return ['ok' => false, 'detail' => $snd['detail'] ?? '送信失敗'];
    }
    fate_member_resend_mark();

    return ['ok' => true, 'detail' => ''];
}

function fate_member_resend_cooldown_ok(): array
{
    fate_public_session_start_safe();
    $now = time();
    $last = isset($_SESSION['fate_mail_resend_unix']) ? (int) $_SESSION['fate_mail_resend_unix'] : 0;
    $cool = max(35, min(600, (int) (getenv('FATE_EMAIL_RESEND_COOLDOWN_SEC') ?: '65')));
    $delta = $now - $last;

    return $delta >= $cool ? ['ok' => true, 'secs' => 0] : ['ok' => false, 'secs' => $cool - $delta];
}

function fate_member_resend_mark(): void
{
    fate_public_session_start_safe();
    $_SESSION['fate_mail_resend_unix'] = time();
}

function fate_public_session_start_safe(): void
{
    if (!function_exists('fate_public_session_start')) {
        require_once __DIR__ . DIRECTORY_SEPARATOR . 'fate_public_session.php';
    }
    fate_public_session_start();
}
