<?php

declare(strict_types=1);

/**
 * 站內簡易 BLOG：分類、文章、公告（管理員前台發文）。
 * 設定：data/fate_blog_settings.json
 */

require_once __DIR__ . DIRECTORY_SEPARATOR . 'fate_rich_body.php';
require_once __DIR__ . DIRECTORY_SEPARATOR . 'fate_public_member_auth.php';

function fate_blog_settings_path(): string
{
    return dirname(__DIR__) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'fate_blog_settings.json';
}

/** @return array{enabled:bool,posts_per_page:int,show_announcements_on_home:bool,home_announcements_limit:int,nav_label:string} */
function fate_blog_settings_default(): array
{
    return [
        'enabled' => true,
        'posts_per_page' => 12,
        'show_announcements_on_home' => true,
        'home_announcements_limit' => 3,
        'nav_label' => '公告／文章',
    ];
}

/** @return array{enabled:bool,posts_per_page:int,show_announcements_on_home:bool,home_announcements_limit:int,nav_label:string} */
function fate_blog_settings(): array
{
    $d = fate_blog_settings_default();
    $p = fate_blog_settings_path();
    if (!is_file($p)) {
        return $d;
    }
    $raw = (string) file_get_contents($p);
    $j = json_decode($raw, true);
    if (!is_array($j)) {
        return $d;
    }
    if (array_key_exists('enabled', $j)) {
        $d['enabled'] = (bool) $j['enabled'];
    }
    if (array_key_exists('posts_per_page', $j)) {
        $d['posts_per_page'] = max(1, min(50, (int) $j['posts_per_page']));
    }
    if (array_key_exists('show_announcements_on_home', $j)) {
        $d['show_announcements_on_home'] = (bool) $j['show_announcements_on_home'];
    }
    if (array_key_exists('home_announcements_limit', $j)) {
        $d['home_announcements_limit'] = max(1, min(10, (int) $j['home_announcements_limit']));
    }
    if (array_key_exists('nav_label', $j) && is_string($j['nav_label'])) {
        $label = trim(preg_replace('/\s+/u', ' ', $j['nav_label']) ?? '');
        if ($label !== '') {
            $d['nav_label'] = mb_substr($label, 0, 24, 'UTF-8');
        }
    }

    return $d;
}

/** @param array<string,mixed> $patch */
function fate_blog_settings_save(array $patch): bool
{
    $cur = fate_blog_settings();
    foreach (['enabled', 'show_announcements_on_home'] as $bk) {
        if (array_key_exists($bk, $patch)) {
            $cur[$bk] = (bool) $patch[$bk];
        }
    }
    foreach (['posts_per_page', 'home_announcements_limit'] as $nk) {
        if (array_key_exists($nk, $patch)) {
            $cur[$nk] = max(1, (int) $patch[$nk]);
        }
    }
    if (array_key_exists('nav_label', $patch) && is_string($patch['nav_label'])) {
        $label = trim(preg_replace('/\s+/u', ' ', $patch['nav_label']) ?? '');
        if ($label !== '') {
            $cur['nav_label'] = mb_substr($label, 0, 24, 'UTF-8');
        }
    }
    $dir = dirname(fate_blog_settings_path());
    if (!is_dir($dir) && !@mkdir($dir, 0755, true) && !is_dir($dir)) {
        return false;
    }
    $json = json_encode($cur, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
    if (!is_string($json)) {
        return false;
    }

    return file_put_contents(fate_blog_settings_path(), $json . "\n") !== false;
}

function fate_blog_enabled(): bool
{
    return (bool) (fate_blog_settings()['enabled'] ?? false);
}

function fate_blog_nav_visible_for_frontend(): bool
{
    return fate_blog_enabled();
}

function fate_blog_nav_label(): string
{
    $label = trim((string) (fate_blog_settings()['nav_label'] ?? ''));

    return $label !== '' ? $label : '公告／文章';
}

/**
 * 側欄所需變數（BLOG 列表／內頁與首頁側欄一致）。
 *
 * @return array{
 *   cityFilter:string,
 *   sort:string,
 *   storeSearchQ:string,
 *   storeSearchMode:string,
 *   buildHomeQuery:Closure,
 *   storeSearchTotal:?int,
 *   blogHomeAnnouncements:list<array<string,mixed>>
 * }
 */
function fate_blog_sidebar_layout_vars(?PDO $pdo = null): array
{
    require_once __DIR__ . DIRECTORY_SEPARATOR . 'fate_store_search.php';
    require_once __DIR__ . DIRECTORY_SEPARATOR . 'fate_public_list_query.php';

    $announcements = [];
    if ($pdo instanceof PDO && fate_blog_enabled()) {
        fate_blog_ensure_tables($pdo);
        $announcements = fate_blog_announcements_for_home($pdo);
    }

    return [
        'cityFilter' => '',
        'sort' => 'rating',
        'storeSearchQ' => '',
        'storeSearchMode' => FATE_STORE_SEARCH_MODE_NONE,
        'buildHomeQuery' => static function (array $overrides): string {
            return fate_build_home_list_query([], $overrides);
        },
        'storeSearchTotal' => null,
        'blogHomeAnnouncements' => $announcements,
    ];
}

function fate_blog_posts_per_page(): int
{
    return max(1, min(50, (int) (fate_blog_settings()['posts_per_page'] ?? 12)));
}

function fate_blog_ensure_tables(PDO $pdo): void
{
    static $done = false;
    if ($done) {
        return;
    }
    try {
        $pdo->exec(
            <<<'SQL'
CREATE TABLE IF NOT EXISTS `fate_blog_categories` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(64) NOT NULL,
  `slug` VARCHAR(64) NOT NULL DEFAULT '',
  `sort_order` INT NOT NULL DEFAULT 0,
  `is_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_slug` (`slug`),
  KEY `idx_sort` (`sort_order`, `id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
SQL
        );
        $pdo->exec(
            <<<'SQL'
CREATE TABLE IF NOT EXISTS `fate_blog_posts` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `category_id` INT UNSIGNED NULL DEFAULT NULL,
  `author_member_id` INT UNSIGNED NOT NULL DEFAULT 0,
  `title` VARCHAR(200) NOT NULL,
  `slug` VARCHAR(120) NOT NULL DEFAULT '',
  `body_html` MEDIUMTEXT NOT NULL,
  `excerpt` VARCHAR(400) NOT NULL DEFAULT '',
  `is_announcement` TINYINT NOT NULL DEFAULT 0,
  `is_published` TINYINT NOT NULL DEFAULT 1,
  `is_hidden` TINYINT NOT NULL DEFAULT 0,
  `published_at` DATETIME NULL DEFAULT NULL,
  `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_list` (`is_hidden`, `is_published`, `published_at`, `id`),
  KEY `idx_category` (`category_id`, `is_hidden`, `is_published`, `published_at`),
  KEY `idx_announcement` (`is_announcement`, `is_hidden`, `is_published`, `published_at`),
  KEY `idx_slug` (`slug`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
SQL
        );
        $cnt = (int) $pdo->query('SELECT COUNT(*) FROM `fate_blog_categories`')->fetchColumn();
        if ($cnt === 0) {
            $pdo->exec(
                "INSERT INTO `fate_blog_categories` (`name`, `slug`, `sort_order`) VALUES
                ('公告', 'announcement', 10),
                ('站務', 'site', 20),
                ('活動', 'event', 30)"
            );
        }
        $done = true;
    } catch (Throwable) {
        /* 無建表權限時略過 */
    }
}

function fate_blog_slugify(string $title): string
{
    $s = mb_strtolower(trim($title), 'UTF-8');
    $s = preg_replace('/[^\p{L}\p{N}]+/u', '-', $s) ?? '';
    $s = trim($s, '-');
    if ($s === '' || !preg_match('/[a-z0-9]/', $s)) {
        return '';
    }

    return mb_substr($s, 0, 80, 'UTF-8');
}

function fate_blog_unique_slug(PDO $pdo, string $base, int $excludeId = 0): string
{
    fate_blog_ensure_tables($pdo);
    $base = fate_blog_slugify($base);
    if ($base === '') {
        return 'post-' . ($excludeId > 0 ? $excludeId : (string) time());
    }
    $candidate = $base;
    $n = 2;
    while (true) {
        $sql = 'SELECT `id` FROM `fate_blog_posts` WHERE `slug` = ?';
        $params = [$candidate];
        if ($excludeId > 0) {
            $sql .= ' AND `id` <> ?';
            $params[] = $excludeId;
        }
        $sql .= ' LIMIT 1';
        $st = $pdo->prepare($sql);
        $st->execute($params);
        if ($st->fetchColumn() === false) {
            return $candidate;
        }
        $candidate = $base . '-' . $n;
        ++$n;
        if ($n > 200) {
            return $base . '-' . uniqid('', false);
        }
    }
}

function fate_blog_excerpt_from_body(string $body, int $maxLen = 160): string
{
    $plain = fate_rich_body_plain_text($body);
    if ($plain === '') {
        return '';
    }
    if (mb_strlen($plain, 'UTF-8') <= $maxLen) {
        return $plain;
    }

    return mb_substr($plain, 0, $maxLen, 'UTF-8') . '…';
}

function fate_blog_viewer_can_manage(PDO $pdo): bool
{
    fate_public_member_ensure_admin_flag($pdo);

    return !empty($_SESSION['fate_site_is_admin']);
}

/** @return list<array<string,mixed>> */
function fate_blog_categories_list(PDO $pdo, bool $includeHidden = false): array
{
    fate_blog_ensure_tables($pdo);
    $sql = 'SELECT * FROM `fate_blog_categories`';
    if (!$includeHidden) {
        $sql .= ' WHERE `is_hidden` = 0';
    }
    $sql .= ' ORDER BY `sort_order` ASC, `id` ASC';
    try {
        return $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC) ?: [];
    } catch (Throwable) {
        return [];
    }
}

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

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

/** @return array{ok:bool,error:?string,id:int} */
function fate_blog_category_save(PDO $pdo, array $data, int $categoryId = 0): array
{
    fate_blog_ensure_tables($pdo);
    $name = trim((string) ($data['name'] ?? ''));
    if ($name === '') {
        return ['ok' => false, 'error' => '請填寫分類名稱。', 'id' => 0];
    }
    if (mb_strlen($name, 'UTF-8') > 64) {
        $name = mb_substr($name, 0, 64, 'UTF-8');
    }
    $slugRaw = trim((string) ($data['slug'] ?? ''));
    $slug = $slugRaw !== '' ? fate_blog_slugify($slugRaw) : fate_blog_slugify($name);
    if ($slug === '') {
        $slug = 'cat-' . ($categoryId > 0 ? $categoryId : time());
    }
    $sortOrder = (int) ($data['sort_order'] ?? 0);
    $isHidden = !empty($data['is_hidden']) ? 1 : 0;

    try {
        if ($categoryId > 0) {
            $st = $pdo->prepare(
                'UPDATE `fate_blog_categories` SET `name` = ?, `slug` = ?, `sort_order` = ?, `is_hidden` = ? WHERE `id` = ?'
            );
            $st->execute([$name, $slug, $sortOrder, $isHidden, $categoryId]);

            return ['ok' => true, 'error' => null, 'id' => $categoryId];
        }
        $st = $pdo->prepare(
            'INSERT INTO `fate_blog_categories` (`name`, `slug`, `sort_order`, `is_hidden`) VALUES (?, ?, ?, ?)'
        );
        $st->execute([$name, $slug, $sortOrder, $isHidden]);
        $newId = (int) $pdo->lastInsertId();
        if ($slug === 'cat-' . time()) {
            $pdo->prepare('UPDATE `fate_blog_categories` SET `slug` = ? WHERE `id` = ?')->execute(['cat-' . $newId, $newId]);
        }

        return ['ok' => true, 'error' => null, 'id' => $newId];
    } catch (Throwable $e) {
        $msg = str_contains($e->getMessage(), 'Duplicate') ? '分類代碼（slug）已存在。' : '儲存失敗。';

        return ['ok' => false, 'error' => $msg, 'id' => 0];
    }
}

/** @return array{ok:bool,error:?string} */
function fate_blog_category_delete(PDO $pdo, int $categoryId): array
{
    fate_blog_ensure_tables($pdo);
    if ($categoryId <= 0) {
        return ['ok' => false, 'error' => '參數錯誤。'];
    }
    try {
        $stCnt = $pdo->prepare('SELECT COUNT(*) FROM `fate_blog_posts` WHERE `category_id` = ?');
        $stCnt->execute([$categoryId]);
        $cnt = (int) $stCnt->fetchColumn();
        if ($cnt > 0) {
            return ['ok' => false, 'error' => '此分類下仍有 ' . $cnt . ' 篇文章，請先移動或刪除。'];
        }
        $pdo->prepare('DELETE FROM `fate_blog_categories` WHERE `id` = ?')->execute([$categoryId]);

        return ['ok' => true, 'error' => null];
    } catch (Throwable) {
        return ['ok' => false, 'error' => '刪除失敗。'];
    }
}

/**
 * @return array{items:list<array<string,mixed>>,total:int}
 */
function fate_blog_posts_list(
    PDO $pdo,
    ?int $categoryId = null,
    ?bool $announcementOnly = null,
    int $page = 1,
    int $perPage = 12,
    bool $includeHidden = false
): array {
    fate_blog_ensure_tables($pdo);
    $page = max(1, $page);
    $perPage = max(1, min(50, $perPage));
    $offset = ($page - 1) * $perPage;

    $where = ['1=1'];
    $params = [];
    if (!$includeHidden) {
        $where[] = 'p.`is_hidden` = 0';
        $where[] = 'p.`is_published` = 1';
    }
    if ($categoryId !== null && $categoryId > 0) {
        $where[] = 'p.`category_id` = ?';
        $params[] = $categoryId;
    }
    if ($announcementOnly === true) {
        $where[] = 'p.`is_announcement` = 1';
    }
    $whereSql = implode(' AND ', $where);

    try {
        $cntSt = $pdo->prepare("SELECT COUNT(*) FROM `fate_blog_posts` p WHERE $whereSql");
        $cntSt->execute($params);
        $total = (int) $cntSt->fetchColumn();

        $sql = "SELECT p.*, c.`name` AS category_name, c.`slug` AS category_slug
                FROM `fate_blog_posts` p
                LEFT JOIN `fate_blog_categories` c ON c.`id` = p.`category_id`
                WHERE $whereSql
                ORDER BY p.`is_announcement` DESC, COALESCE(p.`published_at`, p.`created_at`) DESC, p.`id` DESC
                LIMIT $perPage OFFSET $offset";
        $st = $pdo->prepare($sql);
        $st->execute($params);
        $items = $st->fetchAll(PDO::FETCH_ASSOC) ?: [];

        return ['items' => $items, 'total' => $total];
    } catch (Throwable) {
        return ['items' => [], 'total' => 0];
    }
}

/** @return list<array<string,mixed>> */
function fate_blog_announcements_for_home(PDO $pdo, ?int $limit = null): array
{
    if (!fate_blog_enabled()) {
        return [];
    }
    $cfg = fate_blog_settings();
    if (empty($cfg['show_announcements_on_home'])) {
        return [];
    }
    $lim = $limit ?? (int) ($cfg['home_announcements_limit'] ?? 3);
    $result = fate_blog_posts_list($pdo, null, true, 1, max(1, min(10, $lim)), false);

    return $result['items'];
}

/** @return array<string,mixed>|null */
function fate_blog_post_get(PDO $pdo, int $id, bool $includeHidden = false): ?array
{
    fate_blog_ensure_tables($pdo);
    if ($id <= 0) {
        return null;
    }
    $sql = 'SELECT p.*, c.`name` AS category_name, c.`slug` AS category_slug
            FROM `fate_blog_posts` p
            LEFT JOIN `fate_blog_categories` c ON c.`id` = p.`category_id`
            WHERE p.`id` = ?';
    if (!$includeHidden) {
        $sql .= ' AND p.`is_hidden` = 0 AND p.`is_published` = 1';
    }
    $sql .= ' LIMIT 1';
    try {
        $st = $pdo->prepare($sql);
        $st->execute([$id]);
        $row = $st->fetch(PDO::FETCH_ASSOC);

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

/**
 * @param array<string,mixed> $data
 * @return array{ok:bool,error:?string,id:int}
 */
function fate_blog_post_save(PDO $pdo, array $data, int $authorMemberId, int $postId = 0): array
{
    fate_blog_ensure_tables($pdo);
    if ($authorMemberId <= 0) {
        return ['ok' => false, 'error' => '請先登入。', 'id' => 0];
    }

    $title = trim((string) ($data['title'] ?? ''));
    if ($title === '') {
        return ['ok' => false, 'error' => '請填寫標題。', 'id' => 0];
    }
    if (mb_strlen($title, 'UTF-8') > 200) {
        $title = mb_substr($title, 0, 200, 'UTF-8');
    }

    $bodyRaw = isset($data['body']) && is_string($data['body']) ? $data['body'] : '';
    $body = fate_rich_body_prepare_for_storage($bodyRaw);
    if ($body === '') {
        return ['ok' => false, 'error' => '請填寫內文。', 'id' => 0];
    }

    $categoryId = isset($data['category_id']) ? (int) $data['category_id'] : 0;
    if ($categoryId > 0 && fate_blog_category_get($pdo, $categoryId) === null) {
        $categoryId = 0;
    }
    $categoryId = $categoryId > 0 ? $categoryId : null;

    $excerpt = trim((string) ($data['excerpt'] ?? ''));
    if ($excerpt === '') {
        $excerpt = fate_blog_excerpt_from_body($body);
    }
    if (mb_strlen($excerpt, 'UTF-8') > 400) {
        $excerpt = mb_substr($excerpt, 0, 400, 'UTF-8');
    }

    $isAnnouncement = !empty($data['is_announcement']) ? 1 : 0;
    $isPublished = !empty($data['is_published']) ? 1 : 0;
    $isHidden = !empty($data['is_hidden']) ? 1 : 0;

    $slugInput = trim((string) ($data['slug'] ?? ''));
    $slug = $slugInput !== '' ? fate_blog_slugify($slugInput) : fate_blog_unique_slug($pdo, $title, $postId);
    if ($slug === '') {
        $slug = fate_blog_unique_slug($pdo, 'post-' . ($postId > 0 ? $postId : time()), $postId);
    } else {
        $slug = fate_blog_unique_slug($pdo, $slug, $postId);
    }

    $publishedAt = null;
    if ($isPublished) {
        $publishedAt = trim((string) ($data['published_at'] ?? ''));
        if ($publishedAt === '') {
            $publishedAt = date('Y-m-d H:i:s');
        } elseif (strtotime($publishedAt) === false) {
            $publishedAt = date('Y-m-d H:i:s');
        }
    }

    try {
        if ($postId > 0) {
            $st = $pdo->prepare(
                'UPDATE `fate_blog_posts` SET
                    `category_id` = ?, `title` = ?, `slug` = ?, `body_html` = ?, `excerpt` = ?,
                    `is_announcement` = ?, `is_published` = ?, `is_hidden` = ?,
                    `published_at` = CASE WHEN ? = 1 THEN COALESCE(`published_at`, ?) ELSE NULL END
                 WHERE `id` = ?'
            );
            $st->execute([
                $categoryId,
                $title,
                $slug,
                $body,
                $excerpt,
                $isAnnouncement,
                $isPublished,
                $isHidden,
                $isPublished,
                $publishedAt,
                $postId,
            ]);

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

        $st = $pdo->prepare(
            'INSERT INTO `fate_blog_posts`
                (`category_id`, `author_member_id`, `title`, `slug`, `body_html`, `excerpt`,
                 `is_announcement`, `is_published`, `is_hidden`, `published_at`)
             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
        );
        $st->execute([
            $categoryId,
            $authorMemberId,
            $title,
            $slug,
            $body,
            $excerpt,
            $isAnnouncement,
            $isPublished,
            $isHidden,
            $isPublished ? $publishedAt : null,
        ]);
        $newId = (int) $pdo->lastInsertId();

        return ['ok' => true, 'error' => null, 'id' => $newId];
    } catch (Throwable $e) {
        return ['ok' => false, 'error' => '儲存失敗：' . $e->getMessage(), 'id' => 0];
    }
}

/** @return array{ok:bool,error:?string} */
function fate_blog_post_delete(PDO $pdo, int $postId): array
{
    fate_blog_ensure_tables($pdo);
    if ($postId <= 0) {
        return ['ok' => false, 'error' => '參數錯誤。'];
    }
    try {
        $pdo->prepare('DELETE FROM `fate_blog_posts` WHERE `id` = ?')->execute([$postId]);

        return ['ok' => true, 'error' => null];
    } catch (Throwable) {
        return ['ok' => false, 'error' => '刪除失敗。'];
    }
}

/** @return array{ok:bool,error:?string} */
function fate_blog_post_set_hidden(PDO $pdo, int $postId, bool $hidden): array
{
    fate_blog_ensure_tables($pdo);
    if ($postId <= 0) {
        return ['ok' => false, 'error' => '參數錯誤。'];
    }
    try {
        $pdo->prepare('UPDATE `fate_blog_posts` SET `is_hidden` = ? WHERE `id` = ?')
            ->execute([$hidden ? 1 : 0, $postId]);

        return ['ok' => true, 'error' => null];
    } catch (Throwable) {
        return ['ok' => false, 'error' => '更新失敗。'];
    }
}

function fate_blog_post_url(int $postId): string
{
    return 'blog_post.php?id=' . max(1, $postId);
}

function fate_blog_body_html(string $body): string
{
    return fate_rich_body_html($body);
}

function fate_blog_format_date(?string $dt): string
{
    if ($dt === null || trim($dt) === '') {
        return '';
    }

    return substr(trim($dt), 0, 16);
}
