<?php
declare(strict_types=1);

require_once __DIR__ . '/render.php';

/**
 * ============================================================
 *  パーツ（再利用ブロック）
 * ------------------------------------------------------------
 *  1パーツ = 1フォルダ。配布パーツの構成に合わせている。
 *    parts/<name>/part.json     … メタ＋編集項目定義
 *      { "title", "category", "sort", "fields":[...], "repeater":{...} }
 *    parts/<name>/template.html … 配布スニペット規約のHTML（{{}}差し込み）
 *
 *  この定義から「管理画面の編集フォーム」を自動生成し、同じ定義で
 *  サイト側も描画する（フレームを問わずパーツは共通）。
 * ============================================================
 */

/** 全パーツの定義を取得（key => def） */
function part_defs(): array {
    static $cache = null;
    if ($cache !== null) return $cache;

    $cache = [];
    // コアの parts/ ＋ プラグインが登録したパーツフォルダを併せてスキャン
    $jsonFiles = glob(CMS_PARTS . '/*/part.json') ?: [];
    if (function_exists('cms_plugin_parts_dirs')) {
        foreach (cms_plugin_parts_dirs() as $pdir) {
            foreach (glob($pdir . '/*/part.json') ?: [] as $pf) $jsonFiles[] = $pf;
        }
    }
    foreach ($jsonFiles as $jsonFile) {
        $dir  = dirname($jsonFile);
        $name = basename($dir);
        $def  = json_array((string)file_get_contents($jsonFile));
        // status が public 以外（hidden 等）は読み込まない
        if (isset($def['status']) && $def['status'] !== 'public') continue;
        $tplF = $dir . '/template.html';
        $def['_key']     = $name;
        $def['_tpl']     = is_file($tplF) ? (string)file_get_contents($tplF) : '';
        $def['label']    = $def['title'] ?? $def['label'] ?? $name;
        $def['category'] = $def['category'] ?? 'その他';
        $def['sort']     = (int)($def['sort'] ?? 99);
        $cache[$name] = $def;
    }
    // カテゴリ → sort → 名前 の順で安定ソート
    uasort($cache, function ($a, $b) {
        return [$a['category'], $a['sort'], $a['_key']] <=> [$b['category'], $b['sort'], $b['_key']];
    });
    return $cache;
}

/** カテゴリ → パーツ定義[] のマップ（管理画面の2段階選択用） */
function part_defs_by_category(): array {
    $byCat = [];
    foreach (part_defs() as $name => $def) {
        $byCat[$def['category']][$name] = $def;
    }
    return $byCat;
}

function part_def(string $name): ?array {
    return part_defs()[$name] ?? null;
}

/**
 * パーツのサムネ（管理画面のパーツピッカー用）。['url'=>string, 'video'=>bool] を返す。
 * 置き場所は engine 同梱の `admin/assets/parts-thumb/<key>.{mp4,webp}`（全4フォルダでバイト一致）。
 * カスタムパーツ等で個別に用意したい場合は `parts/<key>/thumb.{mp4,webp,…}` を優先して拾う。
 * mp4 があれば動画サムネ（サイトのカタログ list-parts.html と同じ出し分け）、無ければ webp。
 * どちらも無ければ url='' （UI側で頭文字プレースホルダ表示）。
 */
function cms_part_thumb(string $name): array {
    $none = ['url' => '', 'video' => false];
    $name = (string)$name;
    if ($name === '' || !preg_match('/^[A-Za-z0-9_\-]+$/', $name)) return $none;
    // ① パーツ同梱の個別サムネ（任意・動画優先）
    foreach (['mp4', 'webp', 'png', 'jpg', 'jpeg'] as $ext) {
        if (is_file(CMS_PARTS . '/' . $name . '/thumb.' . $ext)) {
            return ['url' => url('/parts/' . $name . '/thumb.' . $ext), 'video' => ($ext === 'mp4')];
        }
    }
    // ② engine 同梱の中央サムネ（動画があれば動画、無ければ webp）
    $base = CMS_ROOT . '/admin/assets/parts-thumb/';
    if (is_file($base . $name . '.mp4'))  return ['url' => url('/admin/assets/parts-thumb/' . $name . '.mp4'),  'video' => true];
    if (is_file($base . $name . '.webp')) return ['url' => url('/admin/assets/parts-thumb/' . $name . '.webp'), 'video' => false];
    return $none;
}

/**
 * 着せ替え（スキン）＝「同じ中身の別デザイン」。
 * part.json に同じ "family" を宣言したパーツ同士は、入力データ（項目）を共有し、
 * 管理画面のプルダウンで見た目だけを一発で切り替えられる。
 *
 * この関数は、指定パーツと同じ family に属し、かつ**現在このインストールに実在（公開）する**
 * パーツだけを [パーツ名 => 表示ラベル] で返す（part_defs の並び順）。
 *   - 無料版のようにファミリー部材が1つしか入っていない場合は要素1つ＝実質切替なし。
 *   - 購入者が新しいカード系パーツをドロップインすれば、その分だけ自動でスキン候補が増える。
 * family 未宣言／該当なしのパーツは空配列（＝スキン切替UIを出さない）。
 */
function cms_part_family_skins(string $part): array {
    $def = part_def($part);
    if (!$def) return [];
    $fam = (string)($def['family'] ?? '');
    if ($fam === '') return [];
    $out = [];
    foreach (part_defs() as $name => $d) {
        if ((string)($d['family'] ?? '') === $fam) $out[$name] = (string)$d['label'];
    }
    return $out;
}

/**
 * 全パーツ共通の「見出し＋本文」フィールド。
 * どのパーツでも、コンテンツの上に1セットの見出し(h2/h3)とリード文を置ける。
 * （pageタイプCMSと同じ考え方）
 */
function common_head_fields(): array {
    return [
        ['key' => 'bg', 'type' => 'swatch', 'label' => '背景色', 'default' => 'なし', 'options' => [
            ['value' => 'なし',         'label' => 'なし',     'color' => '#ffffff'],
            ['value' => '薄い色',       'label' => '薄い色',   'color' => '#eeeeee', 'color_var' => 'light'],
            ['value' => '標準色',       'label' => '標準色',   'color' => '#333333', 'color_var' => 'text'],
            ['value' => 'メインカラー', 'label' => 'メイン',   'color' => '#2f6fb0', 'color_var' => 'primary'],
            ['value' => 'アクセント',   'label' => 'アクセント', 'color' => '#e07b39', 'color_var' => 'accent'],
        ]],
        ['key' => 'h_text',     'type' => 'text',     'label' => 'セクションの見出し', 'hint' => '空欄のときは、ページに見出しは表示されません。'],
        ['key' => 'h_level',    'type' => 'select',   'label' => '見出しタイプ', 'default' => 'h2', 'options' => [
            ['value' => 'h2', 'label' => '大見出し（h2）'],
            ['value' => 'h3', 'label' => '小見出し（h3）'],
        ]],
        ['key' => 'h_align',    'type' => 'toggle',   'label' => '見出しの寄せ', 'sub' => true, 'default' => '左寄せ', 'options' => [
            ['value' => '左寄せ', 'label' => '左寄せ'], ['value' => '中央寄せ', 'label' => '中央寄せ'],
        ]],
        ['key' => 'h_deco',     'type' => 'text',     'label' => '装飾テキスト（任意）', 'sub' => true],
        ['key' => 'lead',       'type' => 'textarea', 'label' => '本文（見出しの下）'],
        ['key' => 'lead_align', 'type' => 'toggle',   'label' => '本文の寄せ', 'sub' => true, 'default' => '左寄せ', 'options' => [
            ['value' => '左寄せ', 'label' => '左寄せ'], ['value' => '中央寄せ', 'label' => '中央寄せ'],
        ]],
        // ページ内リンク（アンカー）の飛び先名。メニューやボタンから「#この名前」でこのパーツへジャンプできる。任意。
        ['key' => 'anchor', 'type' => 'text', 'label' => 'アンカーID（ページ内リンクの飛び先・任意）',
            'hint' => 'メニューの「ページ内リンク」からこのパーツへジャンプできます。英数字・ハイフンで（例：contact）。'],
    ];
}

/** セクション背景クラスを属性として生成（テンプレの <section{{SECTION_ATTR}}> 用） */
function build_section_attr(array $data): string {
    $map = [
        '薄い色'      => 'bg-parts bg1-light-parts',
        '標準色'      => 'bg-parts bg1-parts',
        'メインカラー' => 'bg-parts bg1-primary-parts',
        'アクセント'   => 'bg-parts bg1-accent-parts',
    ];
    $cls = $map[(string)($data['bg'] ?? 'なし')] ?? '';
    $attr = '';
    // ページ内リンクの飛び先（アンカーID）。英数字・ハイフン・アンダースコアのみに正規化して id 属性に。
    $anchor = preg_replace('/[^A-Za-z0-9_\-]/', '', (string)($data['anchor'] ?? ''));
    if ($anchor !== '') $attr .= ' id="' . $anchor . '"';
    if ($cls !== '') $attr .= ' class="' . $cls . '"';
    return $attr;
}

/** 共通見出し＋リードのHTMLを生成（テンプレの {{{HEAD}}} に差し込む） */
function build_head_html(array $data): string {
    $level = (string)($data['h_level'] ?? 'なし');
    $text  = (string)($data['h_text'] ?? '');
    $deco  = trim((string)($data['h_deco'] ?? ''));
    $lead  = (string)($data['lead'] ?? '');
    $hCenter = (($data['h_align'] ?? '') === '中央寄せ');     // 見出し中央寄せ（.c）
    $lCenter = (($data['lead_align'] ?? '') === '中央寄せ'); // 本文中央寄せ（.c）
    $html = '';
    if ($text !== '' && ($level === 'h2' || $level === 'h3')) {
        $cls = 'inview' . ($hCenter ? ' c' : '');
        // 装飾テキストは h2 のみ（h3 はフレーム側に span のスタイルが無いため出さない）
        $inner = ($deco !== '' && $level === 'h2' ? '<span>' . h($deco) . '</span>' : '') . h($text);
        $html .= '<' . $level . ' class="' . $cls . '">' . $inner . '</' . $level . '>';
    }
    if ($lead !== '') {
        $cls = 'inview' . ($lCenter ? ' c' : '');
        $html .= '<p class="' . $cls . '">' . safe_richtext($lead) . '</p>';
    }
    return $html;
}

/**
 * パーツ説明文（part.json の description）を安全にHTML化する。
 * 基本はエスケープ。マークダウン風リンク [ラベル](リンク先) だけを安全なリンクに変換する。
 *  - 相対パス（plugins/.../admin.php 等）は url() で BASE_URL を付与（サブディレクトリ設置でも正しい）
 *  - http(s)/mailto/tel/#アンカー はそのまま（cms_safe_url で javascript: 等を遮断）
 *  - 外部URLは別タブ。リンク先が危険判定（空）ならラベルだけ残す。
 */
function part_description_html(string $text): string {
    $esc = h($text);
    return preg_replace_callback('/\[([^\]]+)\]\(([^)\s]+)\)/u', function (array $m): string {
        $label = $m[1]; // h() 済み文字列の一部＝既にエスケープ済み
        $href  = html_entity_decode($m[2], ENT_QUOTES, 'UTF-8');
        // まず安全化（javascript: data: 等の危険スキームは '' になる→リンクにしない）
        $safe = cms_safe_url($href);
        if ($safe === '') return $label;
        // 残ったもののうち、スキーム付き（http/mailto/tel）とアンカー(#)以外＝相対パスは
        // BASE_URL を付与（サブディレクトリ設置でも正しく解決）。
        $hasScheme = (bool) preg_match('#^[a-z][a-z0-9+.\-]*:#i', $safe);
        $isAnchor  = ($safe[0] ?? '') === '#';
        if (!$hasScheme && !$isAnchor) $safe = url($safe);
        $ext = preg_match('#^https?:#i', $safe) ? ' target="_blank" rel="noopener"' : '';
        return '<a href="' . h($safe) . '"' . $ext . '>' . $label . '</a>';
    }, $esc);
}

/**
 * リンク欄（type:link）の保存値を最終的な href に変換する。
 *  - "page:<slug>" … サイト内ページ参照。存在すれば現在のURLへ変換。**削除済みページなら '' を返す**（リンクにしない＝リンク切れを防ぐ）。
 *  - それ以外 … 外部URL（https://…）/ #アンカー / 相対パス。cms_safe_url で javascript: 等の危険スキームを遮断してそのまま使う。
 * ※ページ参照（page:slug）で保存しておくと、URLの基点が変わってもリンクが追従し、リンク切れしにくい。
 */
function cms_link_url(string $value): string {
    $value = trim($value);
    if ($value === '') return '';
    if (strncmp($value, 'page:', 5) === 0) {
        $slug = substr($value, 5);
        return page_find($slug) ? page_url($slug) : '';
    }
    return cms_safe_url($value);
}

/** 1ブロックを描画 */
function render_block(string $part, array $data): string {
    $def = part_def($part);
    if (!$def) return '';
    $ctx = part_prepare_data($def, $data);

    // 動的パーツ（プラグインがDBから引くパーツ等）の描画フック。
    //   文字列を返す → そのパーツのHTMLを完全に差し替え
    //   配列を返す   → template.html へ渡すコンテキストに追記
    if (function_exists('cms_part_renderer')) {
        $renderer = cms_part_renderer($part);
        if ($renderer !== null) {
            $res = $renderer($data, $def, $ctx);
            if (is_string($res)) return $res;
            if (is_array($res))  $ctx = array_merge($ctx, $res);
        }
    }

    return render_template($def['_tpl'], $ctx);
}

/**
 * ページ内の全ブロックを描画。
 * status='draft'（非公開）のパーツは一般訪問者に出さない。
 * ログイン中の管理者には表示する＝**ページの非公開とまったく同じ挙動**にしている
 * （挙動が2種類あると「ページは見えるのにパーツは見えない」で混乱するため・依頼者と決定）。
 * どのパーツが非公開かは管理画面のブロック一覧のバッジで分かるので、公開ページ側には印を出さない。
 */
function render_blocks(array $blocks): string {
    $out = '';
    $isAdmin = !empty($_SESSION['admin_id']);
    foreach ($blocks as $b) {
        if (!$isAdmin && (string)($b['status'] ?? 'public') === 'draft') continue;
        $out .= render_block($b['part'], json_array($b['data'] ?? '{}'));
    }
    return $out;
}

/**
 * フォーム定義に従ってデータを描画用に整形
 *  - image   フィールド: <key>_url を付与
 *  - textarea フィールド: 簡易リッチテキスト化（改行→<br>＋一部タグ許可）
 *  - repeater: 各行に同じ整形を適用
 */
function part_prepare_data(array $def, array $data): array {
    $out = $data;

    // 全パーツ共通：背景色（{{SECTION_ATTR}}）＋見出し＋本文（{{{HEAD}}}）
    $out['SECTION_ATTR'] = build_section_attr($data);
    $out['HEAD'] = build_head_html($data);

    foreach (($def['fields'] ?? []) as $f) {
        $key  = $f['key'] ?? '';
        $type = $f['type'] ?? 'text';
        if ($key === '') continue;
        $val = $data[$key] ?? ($f['default'] ?? '');
        if ($type === 'image') {
            $out[$key] = (string)$val;
            $out[$key . '_url'] = $val !== '' ? upload_url((string)$val) : '';
            // 画像項目が動画ファイル(mp4/webm)なら真。テンプレで {{#key_is_video}} 分岐に使う
            $out[$key . '_is_video'] = $val !== '' && (bool)preg_match('/\.(mp4|webm)$/i', (string)$val);
        } elseif ($type === 'textarea') {
            $out[$key] = safe_richtext((string)$val);
        } elseif ($type === 'checklist') {
            $out[$key] = build_checklist_html((string)$val);
        } elseif ($type === 'stars') {
            $out[$key] = build_stars_html((int)$val);
        } elseif ($type === 'embed') {
            $out[$key] = build_embed_html((string)$val);
        } elseif ($type === 'bullets') {
            $out[$key] = build_bullets_html((string)$val);
        } elseif ($type === 'table') {
            // 多列テーブル。見出し（th）にするかは同じパーツの head_row / head_col が「空でない」かで決める
            // （head_row/head_col は swatch：空=見出しにしない／それ以外=見出し〔色は値のクラス〕）
            $headRow = (($data['head_row'] ?? '') !== '');
            $headCol = (($data['head_col'] ?? '') !== '');
            $out[$key] = build_table_html((string)$val, $headRow, $headCol);
        } elseif ($type === 'link') {
            // ページ参照(page:slug)→現URL／外部URL等はそのまま（削除済みページは''＝リンクなし）
            $out[$key] = cms_link_url((string)$val);
        } else {
            $out[$key] = (string)$val;
        }
        // select は {{#key_value}} で分岐できるよう真偽フラグを付与
        if ($type === 'select' && $val !== '') {
            $out[$key . '_' . preg_replace('/[^a-zA-Z0-9_]/', '', (string)$val)] = true;
        }
    }

    // リピーター（gallery など）
    if (isset($def['repeater']) && is_array($def['repeater'])) {
        $rkey = $def['repeater']['key'] ?? 'rows';
        $rows = $data[$rkey] ?? [];
        $rows = is_array($rows) ? $rows : [];
        $newRows = [];
        foreach ($rows as $row) {
            if (!is_array($row)) continue;
            $r = $row;
            foreach (($def['repeater']['fields'] ?? []) as $f) {
                $key = $f['key'] ?? '';
                $type = $f['type'] ?? 'text';
                if ($key === '') continue;
                $v = $row[$key] ?? '';
                if ($type === 'image') {
                    $r[$key] = (string)$v;
                    $r[$key . '_url'] = $v !== '' ? upload_url((string)$v) : '';
                    $r[$key . '_is_video'] = $v !== '' && (bool)preg_match('/\.(mp4|webm)$/i', (string)$v);
                } elseif ($type === 'textarea') {
                    $r[$key] = safe_richtext((string)$v);
                } elseif ($type === 'checklist') {
                    $r[$key] = build_checklist_html((string)$v);
                } elseif ($type === 'stars') {
                    $r[$key] = build_stars_html((int)$v);
                } elseif ($type === 'embed') {
                    $r[$key] = build_embed_html((string)$v);
                } elseif ($type === 'link') {
                    $r[$key] = cms_link_url((string)$v);
                } else {
                    $r[$key] = (string)$v;
                }
                if ($type === 'select' && $v !== '') {
                    $r[$key . '_' . preg_replace('/[^a-zA-Z0-9_]/', '', (string)$v)] = true;
                }
            }
            $newRows[] = $r;
        }
        $out[$rkey] = $newRows;
    }

    return $out;
}

/**
 * 簡易リッチテキスト：基本はエスケープ＋改行→<br>。
 * 安全なインラインタグ（b, strong, i, em, br, a[href]）だけ復活させる。
 */
function safe_richtext(string $s): string {
    $esc = htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');

    // 許可する単純タグ
    $esc = preg_replace('#&lt;(/?)(b|strong|i|em|br)&gt;#i', '<$1$2>', $esc);

    // リンク <a href="https://...">text</a>（http/httpsのみ許可・旧来の生タグ書きにも一応対応）
    $esc = preg_replace_callback(
        '#&lt;a\s+href=&quot;(https?://[^&"\s]+)&quot;&gt;(.*?)&lt;/a&gt;#is',
        fn($m) => '<a href="' . htmlspecialchars($m[1], ENT_QUOTES, 'UTF-8') . '" target="_blank" rel="noopener">' . $m[2] . '</a>',
        $esc
    );

    // かんたん記法（マークダウン風）：初心者がHTMLタグを書かずに装飾できる。管理画面のツールバーが挿入する記法。
    //  [文字](リンク先) → リンク。リンク先は page:slug（内部参照）・外部URL・#アンカーに対応し、cms_link_url で危険スキームを遮断。
    $esc = preg_replace_callback('/\[([^\]\n]+)\]\(([^)\s]+)\)/u', function (array $m): string {
        $label = $m[1]; // htmlspecialchars 済み文字列の一部＝エスケープ済み
        $href  = cms_link_url(html_entity_decode($m[2], ENT_QUOTES, 'UTF-8'));
        if ($href === '') return $label; // リンクにできない（危険/削除済みページ）ならラベルだけ残す
        $ext = preg_match('#^https?:#i', $href) ? ' target="_blank" rel="noopener"' : '';
        return '<a href="' . htmlspecialchars($href, ENT_QUOTES, 'UTF-8') . '"' . $ext . '>' . $label . '</a>';
    }, $esc);
    //  **文字** → 太字
    $esc = preg_replace('/\*\*([^*\n]+)\*\*/u', '<strong>$1</strong>', $esc);

    return nl2br($esc, false);
}

/**
 * チェックリスト（料金表の特典など）：1行＝1項目の <li> 群を生成。
 *  - 行頭が「×」の行は「含まない」= <li class="disabled-parts">（取り消し線）にし、「×」は除去
 *  - 空行はスキップ。全文エスケープ。
 *  - 返すのは <li> の連結（テンプレ側で {{{key}}} として <ul> 内に出す）
 */
function build_checklist_html(string $text): string {
    $lines = preg_split('/\r\n|\r|\n/', $text);
    $out = '';
    foreach ($lines as $line) {
        $line = trim($line);
        if ($line === '') continue;
        $off = false;
        if (mb_strpos($line, '×') === 0) {
            $off = true;
            $line = trim(mb_substr($line, 1));
            if ($line === '') continue;
        }
        $cls = $off ? ' class="disabled-parts"' : '';
        $out .= '<li' . $cls . '>' . htmlspecialchars($line, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</li>';
    }
    return $out;
}

/**
 * 箇条書き：1行＝1項目の <li> 群を生成（ul/ol どちらでもテンプレ側で包む）。
 *  - 空行はスキップ。全文エスケープ。
 */
function build_bullets_html(string $text): string {
    $out = '';
    foreach (preg_split('/\r\n|\r|\n/', $text) as $line) {
        $line = trim($line);
        if ($line === '') continue;
        $out .= '<li>' . htmlspecialchars($line, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</li>';
    }
    return $out;
}

/**
 * 多列テーブルの行HTML（<tr>…</tr>）を生成。
 *  - 1行＝表の1行。セルは縦棒「|」区切り。列数は自由。空行はスキップ。
 *  - $headRow=true で1行目を <th>、$headCol=true で1列目を <th> にする（両方可・左上は <th>）。
 *  - <table> のガワはテンプレ側で付ける。
 */
function build_table_html(string $text, bool $headRow = false, bool $headCol = false): string {
    $out = '';
    $r = 0;
    foreach (preg_split('/\r\n|\r|\n/', $text) as $line) {
        if (trim($line) === '') continue;
        $cells = explode('|', $line);
        $row = '';
        foreach ($cells as $c => $cell) {
            $cell = trim($cell);
            $isTh = ($headRow && $r === 0) || ($headCol && $c === 0);
            $tag  = $isTh ? 'th' : 'td';
            $row .= '<' . $tag . '>' . htmlspecialchars($cell, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</' . $tag . '>';
        }
        $out .= '<tr>' . $row . '</tr>';
        $r++;
    }
    return $out;
}

/**
 * 星評価のHTML（インラインSVG）。n個を塗り星、残りを線星で5つ表示。
 * 色は CSS の color（currentColor）、大きさは font-size に追従。
 */
function build_stars_html(int $n): string {
    $n = max(0, min(5, $n));
    $p = 'M12 .587l3.668 7.431l8.2 1.192l-5.934 5.786l1.402 8.18L12 18.896l-7.336 3.866l1.402 -8.18L.132 9.21l8.2 -1.192z';
    $full  = '<svg class="star-ico" viewBox="0 0 24 24" width="1em" height="1em" fill="currentColor" aria-hidden="true"><path d="' . $p . '"/></svg>';
    $empty = '<svg class="star-ico" viewBox="0 0 24 24" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" aria-hidden="true"><path d="' . $p . '"/></svg>';
    $out = '';
    for ($i = 0; $i < $n; $i++)  $out .= $full;
    for ($i = $n; $i < 5; $i++)  $out .= $empty;
    return $out;
}

/**
 * 埋め込み（YouTube / Googleマップ / 一般iframe）を安全に生成。
 *  - 入力はURL、または貼り付けた <iframe> タグそのものでも可（srcを抽出）
 *  - YouTube の watch / youtu.be / shorts URL は embed 形式に変換
 *  - それ以外は https:// のURLのみ iframe 化（http/javascript/data は不可）
 *  - レスポンシブ枠（.cms-embed）で出力
 */
function build_embed_html(string $url): string {
    $url = trim($url);
    if ($url === '') return '';
    // <iframe ... src="..."> ごと貼られたら src を取り出す
    if (preg_match('#<iframe[^>]*\ssrc=["\\\']([^"\\\']+)["\\\']#i', $url, $m)) {
        $url = trim($m[1]);
    }
    $frame = function (string $src, string $title = '') {
        $t = $title !== '' ? ' title="' . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . '"' : '';
        return '<div class="cms-embed"><iframe src="' . htmlspecialchars($src, ENT_QUOTES, 'UTF-8') . '"'
             . $t . ' frameborder="0" loading="lazy" allowfullscreen'
             . ' allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"'
             . ' referrerpolicy="no-referrer-when-downgrade"></iframe></div>';
    };
    // YouTube
    if (preg_match('#(?:youtube\.com/(?:watch\?(?:.*&)?v=|embed/|shorts/|live/)|youtu\.be/)([A-Za-z0-9_-]{6,})#i', $url, $m)) {
        return $frame('https://www.youtube.com/embed/' . $m[1], 'YouTube');
    }
    // https のURLのみ許可（Googleマップ埋め込み・その他iframe）
    if (preg_match('#^https://#i', $url)) {
        return $frame($url);
    }
    return ''; // http: や javascript: 等は出力しない
}

/* ============================================================
 *  アイコン選択（Tabler Icons・日本語ラベルで業種横断に厳選）
 *  - パーツの field type:"icon" や、フッターSNS等で使う
 *  - 値は Tabler のクラス文字列をそのまま保存（例 "ti ti-phone"）
 *  - すべて Tabler Icons（MIT・商用/再配布OK）。本体は assets/vendor/tabler-icons/ に同梱。
 *  - これは「よく使う厳選セット（カテゴリ表示）」。全6000点は検索で利用可能
 *    （管理画面のアイコン選択で検索ボックスから。一覧は cms_tabler_all_icons()）。
 * ============================================================ */
function cms_icon_set(): array {
    return [
        '連絡・場所' => [
            ['ti ti-phone', '電話'], ['ti ti-device-mobile', '携帯'], ['ti ti-mail', 'メール'],
            ['ti ti-send', '送信'], ['ti ti-map-pin', '地図ピン'], ['ti ti-map', '地図'],
            ['ti ti-map-2', '地図2'], ['ti ti-clock', '時間'], ['ti ti-calendar', 'カレンダー'],
            ['ti ti-calendar-event', '予定'], ['ti ti-printer', '印刷'], ['ti ti-world', 'Web'],
            ['ti ti-navigation', '案内'], ['ti ti-wifi', 'Wi-Fi'],
        ],
        '矢印・ナビ' => [
            ['ti ti-arrow-right', '右矢印'], ['ti ti-arrow-left', '左矢印'], ['ti ti-arrow-up', '上矢印'],
            ['ti ti-arrow-down', '下矢印'], ['ti ti-chevron-right', '山形(右)'], ['ti ti-chevron-down', '山形(下)'],
            ['ti ti-chevron-up', '山形(上)'], ['ti ti-chevrons-up', '上へ'], ['ti ti-external-link', '外部リンク'],
            ['ti ti-circle-arrow-right', '丸矢印'], ['ti ti-home', 'ホーム'], ['ti ti-menu-2', 'メニュー'],
        ],
        '記号・UI' => [
            ['ti ti-check', 'チェック'], ['ti ti-circle-check', '丸チェック'], ['ti ti-x', 'バツ'],
            ['ti ti-circle-x', '丸バツ'], ['ti ti-plus', 'プラス'], ['ti ti-minus', 'マイナス'],
            ['ti ti-search', '検索'], ['ti ti-star', '星'], ['ti ti-heart', 'ハート'],
            ['ti ti-bell', 'ベル'], ['ti ti-settings', '設定'], ['ti ti-tag', 'タグ'],
            ['ti ti-thumb-up', 'いいね'], ['ti ti-info-circle', '情報'], ['ti ti-help-circle', '疑問'],
            ['ti ti-alert-circle', '注意'], ['ti ti-eye', '表示'], ['ti ti-lock', '鍵'],
            ['ti ti-share', '共有'], ['ti ti-filter', '絞り込み'],
        ],
        'ビジネス・店舗' => [
            ['ti ti-building-store', '店舗'], ['ti ti-shopping-bag', '買い物袋'], ['ti ti-shopping-cart', 'カート'],
            ['ti ti-currency-yen', '円'], ['ti ti-credit-card', 'カード'], ['ti ti-briefcase', 'ビジネス'],
            ['ti ti-building', '建物'], ['ti ti-building-community', '街'], ['ti ti-users', '人々'],
            ['ti ti-user', '人'], ['ti ti-gift', 'ギフト'], ['ti ti-truck-delivery', '配送'],
            ['ti ti-clipboard-list', 'リスト'], ['ti ti-certificate', '認定'], ['ti ti-award', '受賞'],
            ['ti ti-chart-bar', 'グラフ'], ['ti ti-coin', 'コイン'], ['ti ti-receipt', 'レシート'],
        ],
        '飲食・カフェ' => [
            ['ti ti-coffee', 'コーヒー'], ['ti ti-cup', 'カップ'], ['ti ti-tools-kitchen-2', '食事'],
            ['ti ti-chef-hat', 'シェフ'], ['ti ti-glass-full', 'ドリンク'], ['ti ti-pizza', 'ピザ'],
            ['ti ti-bread', 'パン'], ['ti ti-cake', 'ケーキ'], ['ti ti-meat', '肉'],
            ['ti ti-salad', 'サラダ'], ['ti ti-beer', 'ビール'], ['ti ti-ice-cream', 'アイス'],
            ['ti ti-bottle', 'ボトル'],
        ],
        '美容・健康' => [
            ['ti ti-scissors', 'はさみ'], ['ti ti-sparkles', 'きらめき'], ['ti ti-mood-smile', '笑顔'],
            ['ti ti-brush', 'ブラシ'], ['ti ti-droplet', 'しずく'], ['ti ti-flower', '花'],
            ['ti ti-massage', 'リラク'], ['ti ti-stethoscope', '医療'], ['ti ti-heartbeat', '健康'],
            ['ti ti-pill', '薬'], ['ti ti-dental', '歯'], ['ti ti-first-aid-kit', '救急'],
            ['ti ti-medical-cross', '医療十字'], ['ti ti-yoga', 'ヨガ'], ['ti ti-barbell', '運動'],
        ],
        '住まい・建築' => [
            ['ti ti-home-2', '住宅'], ['ti ti-building-skyscraper', 'ビル'], ['ti ti-key', '鍵'],
            ['ti ti-ruler-2', '採寸'], ['ti ti-hammer', '工具'], ['ti ti-tools', '道具'],
            ['ti ti-door', 'ドア'], ['ti ti-bed', 'ベッド'], ['ti ti-sofa', 'ソファ'],
            ['ti ti-parking', '駐車'], ['ti ti-building-warehouse', '倉庫'], ['ti ti-paint', '塗装'],
        ],
        '教育・趣味' => [
            ['ti ti-school', '学校'], ['ti ti-book', '本'], ['ti ti-books', '書籍'],
            ['ti ti-pencil', '鉛筆'], ['ti ti-notebook', 'ノート'], ['ti ti-backpack', 'カバン'],
            ['ti ti-bulb', 'ひらめき'], ['ti ti-trophy', 'トロフィー'], ['ti ti-presentation', '発表'],
            ['ti ti-music', '音楽'], ['ti ti-palette', '美術'], ['ti ti-camera', 'カメラ'],
        ],
        '旅行・乗り物' => [
            ['ti ti-car', '車'], ['ti ti-bus', 'バス'], ['ti ti-train', '電車'],
            ['ti ti-plane', '飛行機'], ['ti ti-bike', '自転車'], ['ti ti-walk', '徒歩'],
            ['ti ti-ship', '船'], ['ti ti-luggage', '荷物'], ['ti ti-mountain', '山'],
            ['ti ti-beach', '海'], ['ti ti-ticket', 'チケット'], ['ti ti-map-pins', 'スポット'],
        ],
        '自然・天気' => [
            ['ti ti-sun', '晴れ'], ['ti ti-moon', '月'], ['ti ti-cloud', '雲'],
            ['ti ti-snowflake', '雪'], ['ti ti-flame', '炎'], ['ti ti-wind', '風'],
            ['ti ti-plant', '植物'], ['ti ti-tree', '木'], ['ti ti-leaf', '葉'],
            ['ti ti-paw', '肉球'], ['ti ti-fish', '魚'], ['ti ti-droplet', '水'],
        ],
        'ファイル・メディア' => [
            ['ti ti-file', 'ファイル'], ['ti ti-file-text', '文書'], ['ti ti-photo', '画像'],
            ['ti ti-video', '動画'], ['ti ti-download', 'DL'], ['ti ti-upload', 'アップ'],
            ['ti ti-folder', 'フォルダ'], ['ti ti-link', 'リンク'], ['ti ti-microphone', 'マイク'],
            ['ti ti-headphones', 'ヘッドホン'],
        ],
        'SNS・ブランド' => [
            ['ti ti-brand-x', 'X'], ['ti ti-brand-instagram', 'Instagram'], ['ti ti-brand-facebook', 'Facebook'],
            ['ti ti-brand-line', 'LINE'], ['ti ti-brand-youtube', 'YouTube'], ['ti ti-brand-tiktok', 'TikTok'],
            ['ti ti-brand-threads', 'Threads'], ['ti ti-brand-linkedin', 'LinkedIn'], ['ti ti-brand-pinterest', 'Pinterest'],
            ['ti ti-brand-google', 'Google'], ['ti ti-brand-whatsapp', 'WhatsApp'],
        ],
    ];
}

/**
 * 同梱した Tabler ウェブフォントCSSを解析し、全アイコン名の一覧を返す。
 * 例： ['phone','mail',...]（"ti ti-" を付ける前の名前）。
 * - 管理画面のアイコン「検索」で全6000点から探すために使う。
 * - フォント未設置（ファイル未配置）なら空配列を返す（厳選セットだけで動作）。
 * - 1リクエスト内で再利用するため static でキャッシュ。
 */
function cms_tabler_all_icons(): array {
    static $cache = null;
    if ($cache !== null) return $cache;
    $cache = [];
    $css = CMS_ROOT . '/assets/vendor/tabler-icons/tabler-icons.min.css';
    if (!is_file($css)) { $css = CMS_ROOT . '/assets/vendor/tabler-icons/tabler-icons.css'; }
    if (!is_file($css)) return $cache;
    $txt = (string)@file_get_contents($css);
    if ($txt === '') return $cache;
    // .ti-xxx{...}（同梱のマスク方式）／ .ti-xxx:before（標準ウェブフォント）両対応で抽出
    if (preg_match_all('/\.ti-([a-z0-9-]+)\s*[:{]/i', $txt, $m)) {
        $names = array_values(array_unique($m[1]));
        sort($names);
        $cache = $names;
    }
    return $cache;
}
