<?php

declare(strict_types=1);

/**
 * 推薦技師：建檔、列表、點讚／不讚、會員評論（含每週次數限制）。
 * 資料表皆位於 fate 資料庫，採 CREATE TABLE IF NOT EXISTS 自動建立。
 *
 * - fate_store_masseurs    技師主檔（同店名稱不可重複）
 * - fate_masseur_votes     每會員對每技師一次 讚(1)／不讚(-1)
 * - fate_masseur_reviews   會員文字評論（每週限次）
 */

require_once __DIR__ . DIRECTORY_SEPARATOR . 'fate_masseur_image.php';
require_once __DIR__ . DIRECTORY_SEPARATOR . 'fate_masseur_rewards.php';
require_once __DIR__ . DIRECTORY_SEPARATOR . 'fate_masseur_profile_stats.php';

/** @return array<string,string> */
function fate_masseur_gender_labels(): array
{
    return ['female' => '女', 'male' => '男', 'other' => '其他'];
}

function fate_masseur_normalize_gender(string $raw): string
{
    $g = strtolower(trim($raw));
    if ($g === '女' || $g === 'f' || $g === 'female') {
        return 'female';
    }
    if ($g === '男' || $g === 'm' || $g === 'male') {
        return 'male';
    }
    if ($g === 'other' || $g === '其他') {
        return 'other';
    }

    return 'female';
}

function fate_masseur_tables_ddl(): array
{
    return [
        <<<'SQL'
CREATE TABLE IF NOT EXISTS `fate_store_masseurs` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `spa_store_id` INT UNSIGNED NOT NULL,
  `name` VARCHAR(80) NOT NULL,
  `name_norm` VARCHAR(80) NOT NULL,
  `gender` ENUM('female','male','other') NOT NULL DEFAULT 'female',
  `photo_rel` VARCHAR(512) NOT NULL DEFAULT '',
  `created_by_member_id` INT UNSIGNED NOT NULL DEFAULT 0,
  `like_count` INT UNSIGNED NOT NULL DEFAULT 0,
  `dislike_count` INT UNSIGNED NOT NULL DEFAULT 0,
  `is_hidden` TINYINT NOT NULL DEFAULT 0,
  `birth_year` SMALLINT UNSIGNED NULL DEFAULT NULL,
  `measure_bust` SMALLINT UNSIGNED NULL DEFAULT NULL,
  `measure_waist` SMALLINT UNSIGNED NULL DEFAULT NULL,
  `measure_hip` SMALLINT UNSIGNED NULL DEFAULT NULL,
  `cup_size` VARCHAR(8) NULL DEFAULT NULL,
  `profile_stats_hidden` TINYINT NOT NULL DEFAULT 0,
  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uq_store_name` (`spa_store_id`, `name_norm`),
  KEY `idx_store` (`spa_store_id`, `is_hidden`, `id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
SQL,
        <<<'SQL'
CREATE TABLE IF NOT EXISTS `fate_masseur_votes` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `masseur_id` INT UNSIGNED NOT NULL,
  `member_id` INT UNSIGNED NOT NULL,
  `vote` TINYINT NOT NULL DEFAULT 1,
  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uq_vote` (`masseur_id`, `member_id`),
  KEY `idx_masseur` (`masseur_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
SQL,
        <<<'SQL'
CREATE TABLE IF NOT EXISTS `fate_masseur_reviews` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `masseur_id` INT UNSIGNED NOT NULL,
  `member_id` INT UNSIGNED NOT NULL,
  `body` TEXT NOT NULL,
  `is_hidden` TINYINT NOT NULL DEFAULT 0,
  `edited_by_admin` TINYINT NOT NULL DEFAULT 0,
  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_masseur` (`masseur_id`, `is_hidden`, `id`),
  KEY `idx_member_masseur` (`member_id`, `masseur_id`, `created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
SQL,
        <<<'SQL'
CREATE TABLE IF NOT EXISTS `fate_masseur_weekly_recommends` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `masseur_id` INT UNSIGNED NOT NULL,
  `member_id` INT UNSIGNED NOT NULL,
  `week_start` DATE NOT NULL,
  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uq_member_masseur_week` (`member_id`, `masseur_id`, `week_start`),
  KEY `idx_week_masseur` (`week_start`, `masseur_id`),
  KEY `idx_member_week` (`member_id`, `week_start`, `created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
SQL,
    ];
}

function fate_masseur_ensure_tables(PDO $pdo): void
{
    static $done = false;
    if ($done) {
        return;
    }
    try {
        foreach (fate_masseur_tables_ddl() as $ddl) {
            $pdo->exec($ddl);
        }
        fate_masseur_profile_stats_ensure_schema($pdo);
        $done = true;
    } catch (Throwable) {
        /* 無建表權限時略過 */
    }
}

function fate_masseur_name_norm(string $name): string
{
    $n = trim($name);
    if (function_exists('mb_strtolower')) {
        $n = mb_strtolower($n, 'UTF-8');
    } else {
        $n = strtolower($n);
    }

    return preg_replace('/\s+/u', ' ', $n) ?? $n;
}

/**
 * 建立技師。
 *
 * @param array{birth_year:?int,measure_bust:?int,measure_waist:?int,measure_hip:?int,cup_size:?string,profile_stats_hidden:bool}|null $profileStats
 * @return array{ok:bool,error:?string,id:?int}
 */
function fate_masseur_create(PDO $pdo, int $storeId, int $memberId, string $name, string $gender, string $photoDataUrl, ?array $profileStats = null): array
{
    fate_masseur_ensure_tables($pdo);

    if ($storeId <= 0) {
        return ['ok' => false, 'error' => '店家無效。', 'id' => null];
    }
    if ($memberId <= 0) {
        return ['ok' => false, 'error' => '請先登入。', 'id' => null];
    }
    $name = trim($name);
    if (function_exists('mb_substr')) {
        $name = mb_substr($name, 0, 40);
    }
    if ($name === '') {
        return ['ok' => false, 'error' => '請輸入技師名稱。', 'id' => null];
    }
    $g = fate_masseur_normalize_gender($gender);
    $nameNorm = fate_masseur_name_norm($name);

    // 同店重複名稱檢查
    try {
        $chk = $pdo->prepare('SELECT `id` FROM `fate_store_masseurs` WHERE `spa_store_id` = ? AND `name_norm` = ? LIMIT 1');
        $chk->execute([$storeId, $nameNorm]);
        if ($chk->fetchColumn() !== false) {
            return ['ok' => false, 'error' => '這家店已有同名技師，請換個名稱。', 'id' => null];
        }
    } catch (Throwable $e) {
        return ['ok' => false, 'error' => '資料庫錯誤：' . $e->getMessage(), 'id' => null];
    }

    // 相片（可選）
    $photoRel = '';
    $parsed = fate_masseur_parse_photo_data_url($photoDataUrl);
    if ($parsed['error'] !== null) {
        return ['ok' => false, 'error' => $parsed['error'], 'id' => null];
    }
    if ($parsed['ok'] && $parsed['bytes'] !== '') {
        $saved = fate_masseur_save_photo_bytes($parsed['bytes']);
        if (!$saved['ok']) {
            return ['ok' => false, 'error' => $saved['error'] ?? '相片處理失敗。', 'id' => null];
        }
        $photoRel = (string) $saved['rel'];
    }

    try {
        $withStats = fate_masseur_profile_stats_enabled() && $profileStats !== null;
        if ($withStats) {
            $st = $pdo->prepare(
                'INSERT INTO `fate_store_masseurs`
                 (`spa_store_id`, `name`, `name_norm`, `gender`, `photo_rel`, `created_by_member_id`, `birth_year`, `measure_bust`, `measure_waist`, `measure_hip`, `cup_size`, `profile_stats_hidden`)
                 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
            );
            $st->execute([
                $storeId, $name, $nameNorm, $g, $photoRel, $memberId,
                $profileStats['birth_year'],
                $profileStats['measure_bust'],
                $profileStats['measure_waist'],
                $profileStats['measure_hip'],
                $profileStats['cup_size'],
                !empty($profileStats['profile_stats_hidden']) ? 1 : 0,
            ]);
        } else {
            $st = $pdo->prepare(
                'INSERT INTO `fate_store_masseurs` (`spa_store_id`, `name`, `name_norm`, `gender`, `photo_rel`, `created_by_member_id`)
                 VALUES (?, ?, ?, ?, ?, ?)'
            );
            $st->execute([$storeId, $name, $nameNorm, $g, $photoRel, $memberId]);
        }
        $id = (int) $pdo->lastInsertId();
    } catch (Throwable $e) {
        if ($photoRel !== '') {
            fate_masseur_delete_photo_file($photoRel);
        }
        if (str_contains($e->getMessage(), 'uq_store_name') || str_contains($e->getMessage(), 'Duplicate')) {
            return ['ok' => false, 'error' => '這家店已有同名技師，請換個名稱。', 'id' => null];
        }

        return ['ok' => false, 'error' => '建檔失敗：' . $e->getMessage(), 'id' => null];
    }

    fate_masseur_reward_grant($pdo, $memberId, 'masseur_create', $id, '', '建檔技師 #' . $id);

    return ['ok' => true, 'error' => null, 'id' => $id];
}

/**
 * 取得某店技師列表（含目前會員投票狀態）。
 *
 * @return list<array<string,mixed>>
 */
function fate_masseur_list_for_store(PDO $pdo, int $storeId, int $viewerMemberId = 0): array
{
    fate_masseur_ensure_tables($pdo);
    if ($storeId <= 0) {
        return [];
    }
    try {
        $st = $pdo->prepare(
            'SELECT m.*, (
                SELECT COUNT(*) FROM `fate_masseur_reviews` r
                WHERE r.`masseur_id` = m.`id` AND r.`is_hidden` = 0
             ) AS `review_count`
             FROM `fate_store_masseurs` m
             WHERE m.`spa_store_id` = ? AND m.`is_hidden` = 0
             ORDER BY m.`id` DESC'
        );
        $st->execute([$storeId]);
        $rows = $st->fetchAll(PDO::FETCH_ASSOC);
    } catch (Throwable) {
        return [];
    }
    if (!is_array($rows) || $rows === []) {
        return [];
    }

    $viewerVotes = [];
    if ($viewerMemberId > 0) {
        $ids = array_map(static fn ($r) => (int) $r['id'], $rows);
        $place = implode(',', array_fill(0, count($ids), '?'));
        try {
            $vs = $pdo->prepare("SELECT `masseur_id`, `vote` FROM `fate_masseur_votes` WHERE `member_id` = ? AND `masseur_id` IN ($place)");
            $vs->execute(array_merge([$viewerMemberId], $ids));
            foreach ($vs->fetchAll(PDO::FETCH_ASSOC) as $v) {
                $viewerVotes[(int) $v['masseur_id']] = (int) $v['vote'];
            }
        } catch (Throwable) {
            $viewerVotes = [];
        }
    }

    foreach ($rows as &$r) {
        $r['viewer_vote'] = $viewerVotes[(int) $r['id']] ?? 0;
    }
    unset($r);

    return $rows;
}

/** 前台 Session：目前會員是否為站方管理員（role=admin）。 */
function fate_masseur_member_is_site_admin(): bool
{
    return session_status() === PHP_SESSION_ACTIVE
        && session_name() === 'fate_site'
        && !empty($_SESSION['fate_site_is_admin']);
}

/** 前台 Session：管理員或版主（略過正式會員專區等站方權限）。 */
function fate_masseur_member_is_site_staff(?PDO $pdo = null): bool
{
    if (fate_masseur_member_is_site_admin()) {
        return true;
    }
    if (session_status() !== PHP_SESSION_ACTIVE || session_name() !== 'fate_site') {
        return false;
    }
    $id = isset($_SESSION['fate_site_member_id']) ? (int) $_SESSION['fate_site_member_id'] : 0;
    if ($id <= 0 || !$pdo instanceof PDO) {
        return false;
    }
    require_once __DIR__ . DIRECTORY_SEPARATOR . 'admin_member_auth.php';

    if (fate_member_role_is_mod(fate_member_fetch_role_by_id($pdo, $id))) {
        return true;
    }

    $boardMeta = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'forum' . DIRECTORY_SEPARATOR . 'includes' . DIRECTORY_SEPARATOR . 'forum_board_meta.php';
    if (is_file($boardMeta)) {
        require_once $boardMeta;
        if (function_exists('forum_member_is_any_board_moderator') && forum_member_is_any_board_moderator($pdo, $id)) {
            return true;
        }
    }

    return false;
}

/** 建檔者或管理員可編輯技師。 */
function fate_masseur_can_edit(array $masseur, int $memberId, bool $isAdmin): bool
{
    if ($memberId <= 0) {
        return false;
    }
    if ($isAdmin) {
        return true;
    }

    return (int) ($masseur['created_by_member_id'] ?? 0) === $memberId;
}

/** 僅管理員可刪除技師（刪除時扣回獎勵）。 */
function fate_masseur_can_delete(bool $isAdmin): bool
{
    return $isAdmin;
}

/** @return array<string,mixed>|null */
function fate_masseur_get(PDO $pdo, int $masseurId): ?array
{
    fate_masseur_ensure_tables($pdo);
    if ($masseurId <= 0) {
        return null;
    }
    try {
        $st = $pdo->prepare('SELECT * FROM `fate_store_masseurs` WHERE `id` = ? LIMIT 1');
        $st->execute([$masseurId]);
        $row = $st->fetch(PDO::FETCH_ASSOC);

        return is_array($row) ? $row : null;
    } catch (Throwable) {
        return null;
    }
}

/** 重算並寫回某技師的讚／不讚計數。 */
function fate_masseur_recount_votes(PDO $pdo, int $masseurId): void
{
    try {
        $st = $pdo->prepare(
            'UPDATE `fate_store_masseurs` m SET
                `like_count` = (SELECT COUNT(*) FROM `fate_masseur_votes` v WHERE v.`masseur_id` = m.`id` AND v.`vote` = 1),
                `dislike_count` = (SELECT COUNT(*) FROM `fate_masseur_votes` v WHERE v.`masseur_id` = m.`id` AND v.`vote` = -1)
             WHERE m.`id` = ?'
        );
        $st->execute([$masseurId]);
    } catch (Throwable) {
        /* 略過 */
    }
}

/**
 * 投票（1＝讚，-1＝不讚）；再次點同一鍵則取消。
 *
 * @return array{ok:bool,error:?string,state:int}
 */
function fate_masseur_vote(PDO $pdo, int $masseurId, int $memberId, int $vote, string $username = ''): array
{
    fate_masseur_ensure_tables($pdo);
    $vote = $vote >= 0 ? 1 : -1;
    if ($masseurId <= 0 || $memberId <= 0) {
        return ['ok' => false, 'error' => '參數錯誤。', 'state' => 0];
    }
    $masseur = fate_masseur_get($pdo, $masseurId);
    if ($masseur === null || (int) $masseur['is_hidden'] === 1) {
        return ['ok' => false, 'error' => '找不到技師。', 'state' => 0];
    }

    $cur = 0;
    try {
        $st = $pdo->prepare('SELECT `vote` FROM `fate_masseur_votes` WHERE `masseur_id` = ? AND `member_id` = ? LIMIT 1');
        $st->execute([$masseurId, $memberId]);
        $r = $st->fetchColumn();
        $cur = $r === false ? 0 : (int) $r;
    } catch (Throwable) {
        $cur = 0;
    }

    $newState = $vote;
    try {
        if ($cur === $vote) {
            // 取消投票
            $pdo->prepare('DELETE FROM `fate_masseur_votes` WHERE `masseur_id` = ? AND `member_id` = ?')
                ->execute([$masseurId, $memberId]);
            $newState = 0;
        } else {
            $pdo->prepare(
                'INSERT INTO `fate_masseur_votes` (`masseur_id`, `member_id`, `vote`) VALUES (?, ?, ?)
                 ON DUPLICATE KEY UPDATE `vote` = VALUES(`vote`)'
            )->execute([$masseurId, $memberId, $vote]);
        }
    } catch (Throwable $e) {
        return ['ok' => false, 'error' => '投票失敗：' . $e->getMessage(), 'state' => $cur];
    }

    fate_masseur_recount_votes($pdo, $masseurId);

    // 給「讚」發放獎勵（每會員對每技師僅首次發放，由帳本去重；取消後再讚不重複給）
    if ($newState === 1) {
        fate_masseur_reward_grant($pdo, $memberId, 'vote_like', $masseurId, $username, '點讚技師 #' . $masseurId);
    }

    return ['ok' => true, 'error' => null, 'state' => $newState];
}

/** 本週起始（週一 00:00，即週日 24:00 重置）之 Y-m-d H:i:s。 */
function fate_masseur_week_start(): string
{
    $now = new DateTimeImmutable('now');
    $dow = (int) $now->format('N'); // 1=Mon ... 7=Sun
    $monday = $now->modify('-' . ($dow - 1) . ' days')->setTime(0, 0, 0);

    return $monday->format('Y-m-d H:i:s');
}

/** 今日 00:00 之 Y-m-d H:i:s（每日重置）。 */
function fate_masseur_day_start(): string
{
    return (new DateTimeImmutable('today'))->format('Y-m-d H:i:s');
}

/**
 * 發表評論（每週限次）。
 *
 * @param array{is_paid?:bool,price?:int,preview_chars?:int} $paidOpts
 * @return array{ok:bool,error:?string,id:?int}
 */
function fate_masseur_review_create(
    PDO $pdo,
    int $masseurId,
    int $memberId,
    string $body,
    string $username = '',
    array $paidOpts = []
): array {
    fate_masseur_ensure_tables($pdo);
    require_once __DIR__ . DIRECTORY_SEPARATOR . 'fate_rich_body.php';
    require_once __DIR__ . DIRECTORY_SEPARATOR . 'fate_masseur_review_paid.php';
    fate_masseur_review_paid_ensure_schema($pdo);

    if ($masseurId <= 0 || $memberId <= 0) {
        return ['ok' => false, 'error' => '參數錯誤。', 'id' => null];
    }
    if (!fate_masseur_reviews_enabled()) {
        return ['ok' => false, 'error' => '本站暫停文字評論功能。', 'id' => null];
    }

    $body = fate_rich_body_prepare_for_storage($body);
    if ($body === '') {
        return ['ok' => false, 'error' => '請輸入評論內容。', 'id' => null];
    }

    $isPaid = !empty($paidOpts['is_paid']);
    $paidPrice = $isPaid ? max(0, (int) ($paidOpts['price'] ?? 0)) : 0;
    $paidPreview = $isPaid ? max(0, (int) ($paidOpts['preview_chars'] ?? 0)) : 0;
    if ($isPaid && !fate_masseur_review_paid_enabled()) {
        return ['ok' => false, 'error' => '付費評論功能目前未開放。', 'id' => null];
    }
    if ($isPaid && $paidPrice < 1) {
        return ['ok' => false, 'error' => '付費評論請設定售價。', 'id' => null];
    }

    $masseur = fate_masseur_get($pdo, $masseurId);
    if ($masseur === null || (int) $masseur['is_hidden'] === 1) {
        return ['ok' => false, 'error' => '找不到技師。', 'id' => null];
    }

    $cfg = fate_masseur_rewards_settings();
    $weekStart = fate_masseur_week_start();
    try {
        $st = $pdo->prepare(
            'SELECT COUNT(*) FROM `fate_masseur_reviews` WHERE `member_id` = ? AND `masseur_id` = ? AND `created_at` >= ?'
        );
        $st->execute([$memberId, $masseurId, $weekStart]);
        $usedThisWeek = (int) $st->fetchColumn();
    } catch (Throwable $e) {
        return ['ok' => false, 'error' => '資料庫錯誤：' . $e->getMessage(), 'id' => null];
    }
    if ($usedThisWeek >= $cfg['review_per_week']) {
        return ['ok' => false, 'error' => '您本週對這位技師的評論次數已用完（每週 ' . $cfg['review_per_week'] . ' 次，週日 24:00 重置）。', 'id' => null];
    }

    try {
        $st = $pdo->prepare(
            'INSERT INTO `fate_masseur_reviews` (`masseur_id`, `member_id`, `body`, `is_paid`, `paid_price`, `paid_preview_chars`) VALUES (?, ?, ?, ?, ?, ?)'
        );
        $st->execute([
            $masseurId,
            $memberId,
            $body,
            $isPaid ? 1 : 0,
            $isPaid ? $paidPrice : 0,
            $isPaid ? $paidPreview : 0,
        ]);
        $id = (int) $pdo->lastInsertId();
    } catch (Throwable $e) {
        return ['ok' => false, 'error' => '評論失敗：' . $e->getMessage(), 'id' => null];
    }

    fate_masseur_reward_grant($pdo, $memberId, 'review', $id, $username, '評論 #' . $id);

    return ['ok' => true, 'error' => null, 'id' => $id];
}

/** 店家頁技師評論列表：每頁筆數（超過則顯示分頁）。 */
function fate_masseur_reviews_per_page(): int
{
    return 10;
}

/**
 * 取得某技師評論（含會員顯示名稱）。
 *
 * @param int|null $limit null＝不限制（後台等）；否則 LIMIT
 * @return list<array<string,mixed>>
 */
function fate_masseur_reviews_for(
    PDO $pdo,
    int $masseurId,
    bool $includeHidden = false,
    ?int $limit = null,
    int $offset = 0
): array {
    fate_masseur_ensure_tables($pdo);
    if ($masseurId <= 0) {
        return [];
    }
    $memberQt = fate_masseur_member_table_qualified();
    $sql = 'SELECT r.*, p.`avatar_rel` AS author_avatar, ';
    $sql .= $memberQt !== null
        ? "COALESCE(NULLIF(TRIM(mb.`display_name`), ''), mb.`username`, CONCAT('會員#', r.`member_id`)) AS author_name "
        : "CONCAT('會員#', r.`member_id`) AS author_name ";
    $sql .= 'FROM `fate_masseur_reviews` r ';
    $sql .= 'LEFT JOIN `fate_site_member_profiles` p ON p.`member_id` = r.`member_id` ';
    if ($memberQt !== null) {
        $sql .= "LEFT JOIN $memberQt mb ON mb.`id` = r.`member_id` ";
    }
    $sql .= 'WHERE r.`masseur_id` = ? ';
    if (!$includeHidden) {
        $sql .= 'AND r.`is_hidden` = 0 ';
    }
    $sql .= 'ORDER BY r.`id` DESC';
    if ($limit !== null) {
        $limit = max(1, min(100, $limit));
        $offset = max(0, $offset);
        $sql .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
    }

    try {
        $st = $pdo->prepare($sql);
        $st->execute([$masseurId]);
        $rows = $st->fetchAll(PDO::FETCH_ASSOC);

        return is_array($rows) ? $rows : [];
    } catch (Throwable) {
        try {
            $fallback = 'SELECT *, CONCAT(\'會員#\', `member_id`) AS author_name FROM `fate_masseur_reviews` WHERE `masseur_id` = ? '
                . ($includeHidden ? '' : 'AND `is_hidden` = 0 ') . 'ORDER BY `id` DESC';
            if ($limit !== null) {
                $fallback .= ' LIMIT ' . max(1, min(100, $limit)) . ' OFFSET ' . max(0, $offset);
            }
            $st = $pdo->prepare($fallback);
            $st->execute([$masseurId]);
            $rows = $st->fetchAll(PDO::FETCH_ASSOC);

            return is_array($rows) ? $rows : [];
        } catch (Throwable) {
            return [];
        }
    }
}

function fate_masseur_member_table_qualified(): ?string
{
    if (!defined('FATE_MEMBER_DATABASE') || !defined('FATE_MEMBER_TABLE')) {
        return null;
    }
    $db = (string) FATE_MEMBER_DATABASE;
    $tbl = (string) FATE_MEMBER_TABLE;
    if (!preg_match('/^[A-Za-z0-9_]+$/', $db) || !preg_match('/^[A-Za-z0-9_]+$/', $tbl)) {
        return null;
    }

    return sprintf('`%s`.`%s`', str_replace('`', '``', $db), str_replace('`', '``', $tbl));
}

/* ───────────────────────── 管理員操作 ───────────────────────── */

/** 刪除技師（連同投票、評論），並依設定扣回相關獎勵。 */
function fate_masseur_admin_delete(PDO $pdo, int $masseurId): array
{
    fate_masseur_ensure_tables($pdo);
    $masseur = fate_masseur_get($pdo, $masseurId);
    if ($masseur === null) {
        return ['ok' => false, 'error' => '找不到技師。'];
    }

    // 扣回：建檔獎勵 + 此技師之所有評論獎勵 + 點讚獎勵
    fate_masseur_reward_revert($pdo, 'masseur_create', $masseurId);
    fate_masseur_reward_revert($pdo, 'vote_like', $masseurId);
    try {
        $rev = $pdo->prepare('SELECT `id` FROM `fate_masseur_reviews` WHERE `masseur_id` = ?');
        $rev->execute([$masseurId]);
        foreach ($rev->fetchAll(PDO::FETCH_COLUMN) as $rid) {
            fate_masseur_reward_revert($pdo, 'review', (int) $rid);
        }
    } catch (Throwable) {
        /* 略過 */
    }

    try {
        require_once __DIR__ . DIRECTORY_SEPARATOR . 'fate_masseur_review_engagement.php';
        fate_masseur_review_engagement_delete_for_masseur($pdo, $masseurId);
        $pdo->prepare('DELETE FROM `fate_masseur_votes` WHERE `masseur_id` = ?')->execute([$masseurId]);
        $pdo->prepare('DELETE FROM `fate_masseur_reviews` WHERE `masseur_id` = ?')->execute([$masseurId]);
        require_once __DIR__ . DIRECTORY_SEPARATOR . 'fate_masseur_tags.php';
        fate_masseur_tag_picks_delete_for_masseur($pdo, $masseurId);
        require_once __DIR__ . DIRECTORY_SEPARATOR . 'fate_masseur_favorites.php';
        fate_masseur_favorites_delete_for_masseur($pdo, $masseurId);
        $pdo->prepare('DELETE FROM `fate_store_masseurs` WHERE `id` = ?')->execute([$masseurId]);
    } catch (Throwable $e) {
        return ['ok' => false, 'error' => '刪除失敗：' . $e->getMessage()];
    }

    $photoRel = isset($masseur['photo_rel']) ? (string) $masseur['photo_rel'] : '';
    if ($photoRel !== '') {
        fate_masseur_delete_photo_file($photoRel);
    }

    return ['ok' => true, 'error' => null];
}

/**
 * 更新技師名稱／性別／相片（建檔者或管理員；後台亦沿用）。
 *
 * @param array{birth_year:?int,measure_bust:?int,measure_waist:?int,measure_hip:?int,cup_size:?string,profile_stats_hidden:bool}|null $profileStats
 * @return array{ok:bool,error:?string}
 */
function fate_masseur_update(
    PDO $pdo,
    int $masseurId,
    string $name,
    string $gender,
    string $photoDataUrl = '',
    bool $removePhoto = false,
    ?array $profileStats = null
): array {
    fate_masseur_ensure_tables($pdo);
    $masseur = fate_masseur_get($pdo, $masseurId);
    if ($masseur === null) {
        return ['ok' => false, 'error' => '找不到技師。'];
    }
    $name = trim($name);
    if (function_exists('mb_substr')) {
        $name = mb_substr($name, 0, 40);
    }
    if ($name === '') {
        return ['ok' => false, 'error' => '名稱不可空白。'];
    }
    $g = fate_masseur_normalize_gender($gender);
    $nameNorm = fate_masseur_name_norm($name);
    $oldPhotoRel = trim((string) ($masseur['photo_rel'] ?? ''));
    $newPhotoRel = $oldPhotoRel;
    $parsed = fate_masseur_parse_photo_data_url($photoDataUrl);
    if ($parsed['error'] !== null) {
        return ['ok' => false, 'error' => $parsed['error']];
    }
    if ($parsed['ok'] && $parsed['bytes'] !== '') {
        $saved = fate_masseur_save_photo_bytes($parsed['bytes']);
        if (!$saved['ok'] || ($saved['rel'] ?? '') === '') {
            return ['ok' => false, 'error' => $saved['error'] ?? '相片處理失敗。'];
        }
        $newPhotoRel = (string) $saved['rel'];
    } elseif ($removePhoto) {
        $newPhotoRel = '';
    }

    try {
        $chk = $pdo->prepare('SELECT `id` FROM `fate_store_masseurs` WHERE `spa_store_id` = ? AND `name_norm` = ? AND `id` <> ? LIMIT 1');
        $chk->execute([(int) $masseur['spa_store_id'], $nameNorm, $masseurId]);
        if ($chk->fetchColumn() !== false) {
            if ($newPhotoRel !== '' && $newPhotoRel !== $oldPhotoRel) {
                fate_masseur_delete_photo_file($newPhotoRel);
            }

            return ['ok' => false, 'error' => '同店已有同名技師。'];
        }
        $withStats = fate_masseur_profile_stats_enabled() && $profileStats !== null;
        if ($withStats) {
            $pdo->prepare(
                'UPDATE `fate_store_masseurs` SET `name` = ?, `name_norm` = ?, `gender` = ?, `photo_rel` = ?,
                 `birth_year` = ?, `measure_bust` = ?, `measure_waist` = ?, `measure_hip` = ?, `cup_size` = ?, `profile_stats_hidden` = ? WHERE `id` = ?'
            )->execute([
                $name, $nameNorm, $g, $newPhotoRel,
                $profileStats['birth_year'],
                $profileStats['measure_bust'],
                $profileStats['measure_waist'],
                $profileStats['measure_hip'],
                $profileStats['cup_size'],
                !empty($profileStats['profile_stats_hidden']) ? 1 : 0,
                $masseurId,
            ]);
        } else {
            $pdo->prepare('UPDATE `fate_store_masseurs` SET `name` = ?, `name_norm` = ?, `gender` = ?, `photo_rel` = ? WHERE `id` = ?')
                ->execute([$name, $nameNorm, $g, $newPhotoRel, $masseurId]);
        }
    } catch (Throwable $e) {
        if ($newPhotoRel !== '' && $newPhotoRel !== $oldPhotoRel) {
            fate_masseur_delete_photo_file($newPhotoRel);
        }

        return ['ok' => false, 'error' => '更新失敗：' . $e->getMessage()];
    }

    if ($newPhotoRel !== $oldPhotoRel && $oldPhotoRel !== '') {
        fate_masseur_delete_photo_file($oldPhotoRel);
    }

    return ['ok' => true, 'error' => null];
}

/** 編輯技師名稱／性別／進階資料（管理員後台；不含相片）。 */
function fate_masseur_admin_update(PDO $pdo, int $masseurId, string $name, string $gender, ?array $profileStats = null): array
{
    return fate_masseur_update($pdo, $masseurId, $name, $gender, '', false, $profileStats);
}

/** 刪除單則評論（扣回該則獎勵）。 */
function fate_masseur_admin_delete_review(PDO $pdo, int $reviewId): array
{
    fate_masseur_ensure_tables($pdo);
    if ($reviewId <= 0) {
        return ['ok' => false, 'error' => '參數錯誤。'];
    }
    fate_masseur_reward_revert($pdo, 'review', $reviewId);
    require_once __DIR__ . DIRECTORY_SEPARATOR . 'fate_masseur_review_engagement.php';
    fate_masseur_review_engagement_delete_for_review($pdo, $reviewId);
    try {
        $pdo->prepare('DELETE FROM `fate_masseur_reviews` WHERE `id` = ?')->execute([$reviewId]);
    } catch (Throwable $e) {
        return ['ok' => false, 'error' => '刪除失敗：' . $e->getMessage()];
    }

    return ['ok' => true, 'error' => null];
}

/** 編輯評論內容（管理員）。 */
function fate_masseur_admin_update_review(PDO $pdo, int $reviewId, string $body): array
{
    fate_masseur_ensure_tables($pdo);
    $body = trim($body);
    if (function_exists('mb_substr')) {
        $body = mb_substr($body, 0, 1000);
    }
    if ($body === '') {
        return ['ok' => false, 'error' => '內容不可空白。'];
    }
    try {
        $pdo->prepare('UPDATE `fate_masseur_reviews` SET `body` = ?, `edited_by_admin` = 1 WHERE `id` = ?')
            ->execute([$body, $reviewId]);
    } catch (Throwable $e) {
        return ['ok' => false, 'error' => '更新失敗：' . $e->getMessage()];
    }

    return ['ok' => true, 'error' => null];
}
