<?php
/**
 * ============================================================
 *  管理画面 共通ブートストラップ
 *  - 認証ガード / CSRF / アップロード / 画面の枠（ヘッダ・フッタ）
 * ============================================================
 */

// 動作要件チェックを最初に（PHP8構文の本体より前）
require_once dirname(__DIR__) . '/requirements.php';

require_once dirname(__DIR__) . '/config.php';
require_once CMS_CORE . '/frame.php';

// 公開プレビュー版（ルートに siyou.txt）では管理画面を丸ごと無効化する。
// このファイルは login.php を含む全 admin ページが読むので、ここで exit すれば
// ログインも「初回の管理者アカウント作成」も塞がる＝閲覧専用の実サイトを安全に公開できる。
if (cms_preview_mode()) {
    http_response_code(403);
    header('Content-Type: text/html; charset=utf-8');
    echo '<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">'
       . '<title>無料版プレビュー</title>'
       . '<div style="max-width:34rem;margin:14vh auto;padding:0 1.4rem;font-family:sans-serif;text-align:center;line-height:1.85;color:#334155">'
       . '<h1 style="font-size:1.3rem;margin:.2rem 0 1rem">これは無料版のプレビューです</h1>'
       . '<p>公開サイトの見え方を体験していただくためのデモのため、<br>管理画面はご利用いただけません。</p>'
       . '<p style="margin-top:1.2rem"><a href="' . h(url('/')) . '" style="color:#2563eb;font-weight:600">サイトを見る →</a></p>'
       . '<p style="font-size:.85rem;color:#64748b;margin-top:2rem">実際に編集して使うには、公式サイトから無料版をダウンロードしてください。</p>'
       . '</div>';
    exit;
}

/** ログイン必須（未ログインなら login へ）＋無操作タイムアウト */
function require_login(): void {
    if (empty($_SESSION['admin_id'])) {
        header('Location: ' . url('/admin/login.php'));
        exit;
    }
    // セッションは残っているが、その管理者がDBに存在しない場合（DBの再設置・初期化後に
    // 古いセッション／Cookieが残っているケース等）は、中途半端なログイン状態を防ぐため
    // セッションを破棄してログイン（管理者未作成なら初回セットアップ）へ戻す。
    try {
        if (admin_find((int)$_SESSION['admin_id']) === null) {
            $_SESSION = [];
            if (session_status() === PHP_SESSION_ACTIVE) session_destroy();
            header('Location: ' . url('/admin/login.php'));
            exit;
        }
    } catch (Throwable $e) {
        // DB未準備等の例外時は従来どおり通す（ここでの遮断はしない）
    }
    // 無操作が続いたら自動ログアウト（既定3時間）
    $idleMax = 3 * 60 * 60;
    $now = time();
    if (isset($_SESSION['_last']) && ($now - (int)$_SESSION['_last']) > $idleMax) {
        $_SESSION = [];
        if (session_status() === PHP_SESSION_ACTIVE) session_destroy();
        header('Location: ' . url('/admin/login.php?timeout=1'));
        exit;
    }
    $_SESSION['_last'] = $now;
}

function current_admin_name(): string {
    return (string)($_SESSION['admin_name'] ?? '管理者');
}

/** POST + CSRF を検証（NGなら403） */
function require_post_csrf(): void {
    if ($_SERVER['REQUEST_METHOD'] !== 'POST') return;
    if (!csrf_check($_POST['_csrf'] ?? null)) {
        http_response_code(403);
        exit('不正なリクエストです（CSRF）。ブラウザを更新してやり直してください。');
    }
}

/** リダイレクトヘルパー */
function redirect(string $path): void {
    header('Location: ' . url($path));
    exit;
}

/* ============================================================
 *  アップロード処理
 * ============================================================ */
/** アップロードできる形式（拡張子） */
const CMS_UPLOAD_IMAGE_EXT = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'ico'];
const CMS_UPLOAD_VIDEO_EXT = ['mp4', 'webm'];

/** 対応形式の案内文（ヒント・エラー共通） */
function cms_upload_formats_note(): string {
    return '対応形式：画像＝JPG / PNG / GIF / WebP / ICO、動画＝MP4 / WebM。' .
        'iPhoneのHEIC形式は非対応です（JPGでお試しください）。大きな写真は自動で縮小・軽量化されます。';
}

function admin_save_upload(array $file): ?string {
    if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) return null;
    if (!is_uploaded_file($file['tmp_name'])) { flash_set('アップロードに失敗しました。もう一度お試しください。', 'err'); return null; }
    if (($file['size'] ?? 0) <= 0) { flash_set('ファイルが空のためアップロードできませんでした。', 'err'); return null; }
    if ($file['size'] > 60 * 1024 * 1024) { flash_set('ファイルが大きすぎます（60MBまで）。', 'err'); return null; }

    $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
    // SVGはスクリプト内包によるXSSの恐れがあるため除外
    $allow = array_merge(CMS_UPLOAD_IMAGE_EXT, CMS_UPLOAD_VIDEO_EXT);
    if (!in_array($ext, $allow, true)) {
        flash_set('「' . ($ext !== '' ? '.' . $ext : 'この形式') . '」はアップロードできません。' . cms_upload_formats_note(), 'err');
        return null;
    }

    if (!is_dir(CMS_UPLOADS)) @mkdir(CMS_UPLOADS, 0775, true);
    $name = date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
    $dest = CMS_UPLOADS . '/' . $name;
    if (!move_uploaded_file($file['tmp_name'], $dest)) { flash_set('画像の保存に失敗しました（保存先の権限をご確認ください）。', 'err'); return null; }
    @chmod($dest, 0644);

    // 静止画は EXIF回転補正＋長辺リサイズ＋再圧縮（GIF・動画はそのまま）
    if (in_array($ext, ['jpg', 'jpeg', 'png', 'webp'], true)) {
        cms_process_image($dest, $ext);
    }
    return $name;
}

/**
 * 静止画の自動処理：EXIF回転の焼き込み（JPEG）＋長辺の縮小＋再圧縮。
 * GDが無い・読めない時は何もしない（原本のまま）。GIF/動画は呼ばない。
 */
function cms_process_image(string $path, string $ext): void {
    if (!function_exists('imagecreatetruecolor')) return; // GD無し
    $ext = strtolower($ext);
    $loaders = ['jpg' => 'imagecreatefromjpeg', 'jpeg' => 'imagecreatefromjpeg', 'png' => 'imagecreatefrompng', 'webp' => 'imagecreatefromwebp'];
    if (!isset($loaders[$ext]) || !function_exists($loaders[$ext])) return;
    $src = @$loaders[$ext]($path);
    if (!$src) return; // 壊れている等→原本のまま

    $changed = false;

    // EXIF回転（JPEGのみ）。スマホ写真が横倒しになるのを防ぐ＝向きをピクセルに焼き込む。
    if (($ext === 'jpg' || $ext === 'jpeg') && function_exists('exif_read_data')) {
        $exif = @exif_read_data($path);
        $o = is_array($exif) ? (int)($exif['Orientation'] ?? 1) : 1;
        if ($o > 1) {
            if (in_array($o, [2, 4, 5, 7], true) && function_exists('imageflip')) { imageflip($src, IMG_FLIP_HORIZONTAL); $changed = true; }
            $deg = ($o === 3 || $o === 4) ? 180 : (($o === 5 || $o === 6) ? -90 : (($o === 7 || $o === 8) ? 90 : 0));
            if ($deg !== 0) { $r = @imagerotate($src, $deg, 0); if ($r) { imagedestroy($src); $src = $r; $changed = true; } }
        }
    }

    // 長辺の縮小（2000px超のみ）
    $w = imagesx($src); $h = imagesy($src); $max = 2000; $long = max($w, $h);
    if ($long > $max) {
        $sc = $max / $long; $nw = max(1, (int)round($w * $sc)); $nh = max(1, (int)round($h * $sc));
        $dst = imagecreatetruecolor($nw, $nh);
        if ($ext === 'png' || $ext === 'webp') { // 透過を保持
            imagealphablending($dst, false); imagesavealpha($dst, true);
            $t = imagecolorallocatealpha($dst, 0, 0, 0, 127); imagefilledrectangle($dst, 0, 0, $nw, $nh, $t);
        }
        imagecopyresampled($dst, $src, 0, 0, 0, 0, $nw, $nh, $w, $h);
        imagedestroy($src); $src = $dst; $changed = true;
    }

    // 変化があった時だけ保存（小さい原本を無駄に再圧縮しない）
    if ($changed) {
        if ($ext === 'jpg' || $ext === 'jpeg') { @imagejpeg($src, $path, 82); }
        elseif ($ext === 'png') { imagesavealpha($src, true); @imagepng($src, $path, 6); }
        elseif ($ext === 'webp' && function_exists('imagewebp')) { @imagewebp($src, $path, 82); }
    }
    imagedestroy($src);
}

/**
 * 画像フィールド共通：新規アップロードがあればその名、無ければ既存値を返す。
 * @param string $fileKey  $_FILES のキー
 * @param string $current  既存の保存値
 * @param bool   $delete   「この画像を削除する」にチェックが入っていれば true（空に戻す）
 */
function resolve_image_value(string $fileKey, string $current, bool $delete = false): string {
    $f = $_FILES[$fileKey] ?? null;
    if (is_array($f)) {
        $err = $f['error'] ?? UPLOAD_ERR_NO_FILE;
        if ($err === UPLOAD_ERR_OK) {
            $saved = admin_save_upload($f);
            if ($saved !== null) return $saved; // 新規アップロード優先（差し替え）
            // 失敗時のメッセージは admin_save_upload 側でセット済み
        } elseif ($err === UPLOAD_ERR_INI_SIZE || $err === UPLOAD_ERR_FORM_SIZE) {
            // PHPの受信上限（php.ini の upload_max_filesize / post_max_size）を超過
            flash_set('ファイルが大きすぎてサーバーが受け取れませんでした。画像を小さくするか、サーバーのアップロード上限（upload_max_filesize）を上げてください。', 'err');
            return $current;
        } elseif ($err !== UPLOAD_ERR_NO_FILE) {
            flash_set('アップロードに失敗しました。もう一度お試しください。', 'err');
            return $current;
        }
    }
    if ($delete) return ''; // 「この画像を削除する」にチェック＝空に戻す
    return $current;
}

/* ============================================================
 *  画面の枠（共通ヘッダ・フッタ）
 * ============================================================ */
/**
 * 管理ナビ用アイコン（Feather Icons / MITライセンス・商用利用＆再配布可）。
 * 外部読み込みせずSVGを直接埋め込むので、ネット接続も第三者依存も不要。
 */
function adm_nav_icon(string $key): string {
    $paths = [
        'index'    => '<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>',
        'listnews' => '<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/>',
        'listpro'  => '<line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/>',
        'form'     => '<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/>',
        'design'   => '<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>',
        'mainimg'  => '<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/>',
        'menu'     => '<line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>',
        'footer'   => '<rect x="3" y="4" width="18" height="16" rx="2"/><line x1="3" y1="15" x2="21" y2="15"/>',
        'settings' => '<line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/>',
        'account'  => '<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
        'uploads'  => '<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>',
        'code'     => '<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>',
        'license'  => '<circle cx="12" cy="8" r="7"/><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"/>',
    ];
    // 未知のプラグイン項目はファイルのアイコンを既定にする
    $d = $paths[$key] ?? '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>';
    return '<svg class="adm-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" '
        . 'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' . $d . '</svg>';
}

function admin_header(string $title, string $active = ''): void {
    // 各項目の表示名とリンク（key => [ラベル, URL]）
    $items = [
        'index'    => ['ダッシュボード', url('/admin/index.php')],
        'menu'     => ['メニュー（ナビ）', url('/admin/menu.php')],
        'mainimg'  => ['トップの大きな画像', url('/admin/mainimg.php')],
        'design'   => ['デザイン（枠・色）', url('/admin/design.php')],
        'footer'   => ['フッター', url('/admin/footer.php')],
        'settings' => ['サイト基本設定', url('/admin/settings.php')],
        'account'  => ['アカウント', url('/admin/account.php')],
        'code'     => ['アクセス解析・外部タグ', url('/admin/code.php')],
        'uploads'  => ['未使用ファイルの掃除', url('/admin/uploads.php')],
        'license'  => ['ライセンス', url('/admin/license.php')],
    ];
    if (cms_demo_mode()) unset($items['code']); // デモモードは「外部タグ」画面を隠す

    // プラグインが追加する項目を取り込む（お知らせ・一覧・フォームなど）
    $pluginNav = function_exists('cms_plugin_admin_nav') ? cms_plugin_admin_nav() : [];
    foreach ($pluginNav as $pk => $pv) {
        if (!isset($items[$pk])) $items[$pk] = [$pv['label'], $pv['url']];
    }

    // プラグイン項目の表示順（お知らせ→一覧→フォーム）。未知の項目は後ろに。
    $pluginOrder = ['listnews', 'listpro', 'form'];
    $pluginKeys = array_keys($pluginNav);
    usort($pluginKeys, function ($a, $b) use ($pluginOrder) {
        $ia = array_search($a, $pluginOrder, true); if ($ia === false) $ia = 99;
        $ib = array_search($b, $pluginOrder, true); if ($ib === false) $ib = 99;
        return $ia <=> $ib;
    });

    // グループ（表示順）。プラグイン項目は「コンテンツ」へ自動集約。
    $groups = [
        ['label' => '',                 'keys' => ['index']],
        ['label' => 'コンテンツ（中身）', 'keys' => $pluginKeys],
        ['label' => '見た目（デザイン）', 'keys' => ['design', 'mainimg', 'menu', 'footer']],
        ['label' => '設定',             'keys' => ['settings', 'account', 'code', 'uploads', 'license']],
    ];
    ?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= h($title) ?> | 管理画面</title>
<link rel="stylesheet" href="<?= h(url('/assets/vendor/tabler-icons/tabler-icons.min.css')) ?>">
<link rel="stylesheet" href="<?= h(url('/admin/assets/admin.css')) ?>">
<script src="<?= h(url('/admin/assets/admin.js')) ?>" defer></script>
</head>
<body>
<?php if (cms_demo_mode()): ?>
<div style="background:#fef3c7;color:#92400e;border-bottom:1px solid #fcd34d;padding:.5rem 1rem;font-size:.85rem;text-align:center;">デモモード：外部タグとメール送信は無効です（体験用の安全設定）。</div>
<?php endif; ?>
<?php if (!cms_is_licensed()): ?>
<div class="adm-trial-mark" aria-hidden="true"></div>
<?php endif; ?>
<header class="adm-top">
  <button class="adm-burger" data-adm-menu aria-label="メニュー"><span></span><span></span><span></span></button>
  <span class="adm-brand">Frames CMS<?php if (!cms_is_licensed()): ?> <span class="adm-trial-badge">Free</span><?php endif; ?></span>
  <span class="adm-spacer"></span>
  <a class="adm-view" href="<?= h(url('/')) ?>" target="_blank" rel="noopener">サイトを表示 ↗</a>
  <a class="adm-logout" href="<?= h(url('/admin/logout.php')) ?>">ログアウト</a>
</header>
<div class="adm-body">
  <nav class="adm-side">
    <ul>
      <?php foreach ($groups as $group): ?>
        <?php
          // このグループ内で実際に存在する項目だけを抽出
          $rows = [];
          foreach ($group['keys'] as $key) {
              if (isset($items[$key])) $rows[$key] = $items[$key];
          }
          if (!$rows) continue; // 空グループは見出しごと出さない
        ?>
        <?php if ($group['label'] !== ''): ?>
          <li class="adm-side-head"><?= h($group['label']) ?></li>
        <?php endif; ?>
        <?php foreach ($rows as $key => [$label, $href]): ?>
          <li><a href="<?= h($href) ?>" class="<?= $active === $key ? 'is-active' : '' ?>"><?= adm_nav_icon($key) ?><span class="adm-side-label"><?= h($label) ?></span></a></li>
        <?php endforeach; ?>
      <?php endforeach; ?>
    </ul>
  </nav>
  <main class="adm-main">
<?php
}

function admin_footer(): void {
    ?>
  </main>
</div>
</body>
</html>
<?php
}

/** フラッシュメッセージ */
function flash_set(string $msg, string $type = 'ok'): void {
    // すでにエラーが入っている場合、成功メッセージで上書きしない（アップロード弾きの通知を優先）
    if ($type === 'ok' && !empty($_SESSION['_flash']) && ($_SESSION['_flash']['type'] ?? '') === 'err') {
        return;
    }
    $_SESSION['_flash'] = ['msg' => $msg, 'type' => $type];
}
/**
 * 「非公開でも管理者には見える」の案内（オレンジ）。非公開のものが在るときだけ出す。
 * ※これが無いと「非公開にしたのに自分には見える＝効いていないのでは」と誤解される（依頼者フィードバック）。
 */
function adm_hidden_note(): string {
    return '<p class="adm-hidden-note">※<b>ログイン中のあなたには表示されたままです。</b>'
         . '実際の見え方は、ログアウトするか別のブラウザ（シークレットウィンドウ）で確認してください。</p>';
}

function flash_render(): void {
    if (empty($_SESSION['_flash'])) return;
    $f = $_SESSION['_flash'];
    unset($_SESSION['_flash']);
    echo '<div class="adm-flash ' . h($f['type']) . '">' . h($f['msg']) . '</div>';
}
