<?php
declare(strict_types=1);

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

/**
 * ============================================================
 *  DB レイヤ（SQLite / PDO）
 *  ※ 配布先サーバーの SQLite が古い可能性があるため、
 *     UPSERT(ON CONFLICT) など新しい構文は使わない方針。
 * ============================================================
 */

function db(): PDO {
    static $pdo = null;
    if ($pdo instanceof PDO) return $pdo;

    try {
        $pdo = new PDO('sqlite:' . CMS_DB, null, null, [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES   => false,
        ]);
        $pdo->exec('PRAGMA foreign_keys = ON');
        db_migrate($pdo);
    } catch (Throwable $e) {
        // 真っ白にせず、原因（多くは書き込み権限）を日本語で案内
        cms_error_page(
            'データベースを開けませんでした',
            'サイトのデータベースを作成・読み込みできませんでした。多くの場合、<code>data</code> フォルダの'
            . '<b>書き込み権限</b>が原因です。サーバーで <code>data</code> と <code>uploads</code> フォルダの'
            . 'パーミッションを <code>755</code> または <code>775</code> に設定してから、もう一度お試しください。'
        );
    }
    return $pdo;
}

/* 既存DBに不足カラムがあれば追加（SQLite旧バージョン互換） */
function db_add_column_if_missing(PDO $pdo, string $table, string $col, string $def): void {
    $cols = $pdo->query("PRAGMA table_info(" . $table . ")")->fetchAll(PDO::FETCH_ASSOC);
    foreach ($cols as $c) {
        if (($c['name'] ?? '') === $col) return;
    }
    $pdo->exec("ALTER TABLE " . $table . " ADD COLUMN " . $col . " " . $def);
}

function db_migrate(PDO $pdo): void {
    $pdo->exec("
        CREATE TABLE IF NOT EXISTS settings (
            key   TEXT PRIMARY KEY,
            value TEXT NOT NULL DEFAULT ''
        );
        CREATE TABLE IF NOT EXISTS admins (
            id            INTEGER PRIMARY KEY AUTOINCREMENT,
            email         TEXT NOT NULL UNIQUE,
            password_hash TEXT NOT NULL,
            name          TEXT NOT NULL DEFAULT 'Admin',
            created_at    TEXT NOT NULL DEFAULT (datetime('now'))
        );
        CREATE TABLE IF NOT EXISTS pages (
            id               INTEGER PRIMARY KEY AUTOINCREMENT,
            slug             TEXT NOT NULL UNIQUE,
            title            TEXT NOT NULL DEFAULT '',
            meta_description TEXT NOT NULL DEFAULT '',
            parent           TEXT NOT NULL DEFAULT '',
            in_nav           INTEGER NOT NULL DEFAULT 1,
            sort             INTEGER NOT NULL DEFAULT 0,
            created_at       TEXT NOT NULL DEFAULT (datetime('now')),
            updated_at       TEXT NOT NULL DEFAULT (datetime('now'))
        );
        CREATE TABLE IF NOT EXISTS blocks (
            id      INTEGER PRIMARY KEY AUTOINCREMENT,
            page_id INTEGER NOT NULL,
            part    TEXT NOT NULL,
            data    TEXT NOT NULL DEFAULT '{}',
            sort    INTEGER NOT NULL DEFAULT 0,
            FOREIGN KEY (page_id) REFERENCES pages(id) ON DELETE CASCADE
        );
        CREATE INDEX IF NOT EXISTS idx_blocks_page ON blocks(page_id, sort);
        CREATE TABLE IF NOT EXISTS menu_items (
            id          INTEGER PRIMARY KEY AUTOINCREMENT,
            label       TEXT NOT NULL DEFAULT '',
            link_type   TEXT NOT NULL DEFAULT 'page',  -- page | url | anchor | heading
            link        TEXT NOT NULL DEFAULT '',
            parent_id   INTEGER,                        -- NULL=トップ階層
            show_pc     INTEGER NOT NULL DEFAULT 1,
            show_mobile INTEGER NOT NULL DEFAULT 1,
            is_button   INTEGER NOT NULL DEFAULT 0,  -- 1=ボタン風(li class=\"btn\")
            sort        INTEGER NOT NULL DEFAULT 0
        );
        CREATE INDEX IF NOT EXISTS idx_menu_sort ON menu_items(parent_id, sort);
        CREATE TABLE IF NOT EXISTS login_attempts (
            id         INTEGER PRIMARY KEY AUTOINCREMENT,
            ip         TEXT NOT NULL DEFAULT '',
            created_at TEXT NOT NULL DEFAULT (datetime('now'))
        );
        CREATE INDEX IF NOT EXISTS idx_login_ip ON login_attempts(ip, created_at);
    ");

    // 既存DB向け：不足カラムを安全に追加
    db_add_column_if_missing($pdo, 'menu_items', 'is_button', "INTEGER NOT NULL DEFAULT 0");
    db_add_column_if_missing($pdo, 'pages', 'status', "TEXT NOT NULL DEFAULT 'public'"); // public / draft（＝非公開）
    // パーツ（ブロック）単位の公開/非公開。ページと同じ値・同じ挙動（管理者ログイン中は見える）。
    // 求人バナー・キャンペーン等「消したくないが今は出したくない」を、削除せずに引っ込めるため。
    db_add_column_if_missing($pdo, 'blocks', 'status', "TEXT NOT NULL DEFAULT 'public'"); // public / draft（＝非公開）

    // 初期データ投入は「初回インストール時のみ」。settingsのdb_seededフラグで一度きりにする。
    // これを付けないと、メニューやページを全部消した直後に再投入（復活）されてしまう。
    // 既存DB保護のため、フラグ未設定時も各テーブルが空のときだけseedする二重ガードを残す。
    $alreadySeeded = (int)$pdo->query("SELECT COUNT(*) FROM settings WHERE key = 'db_seeded'")->fetchColumn() > 0;
    if (!$alreadySeeded) {
        if ((int)$pdo->query("SELECT COUNT(*) FROM pages")->fetchColumn() === 0) {
            db_seed($pdo);
        }
        if ((int)$pdo->query("SELECT COUNT(*) FROM menu_items")->fetchColumn() === 0) {
            db_seed_menu($pdo);
        }
        $pdo->exec("INSERT OR IGNORE INTO settings(key, value) VALUES('db_seeded', '1')");
    }

    // プラグインのテーブル作成・初期化（ドロップインで増える）。
    // plugins.php が読み込まれていれば実行（フロント/管理の通常リクエストでは
    // core/frame.php 経由で読込済み）。各プラグインのmigrationは冪等に書く。
    if (function_exists('cms_run_plugin_migrations')) {
        cms_run_plugin_migrations($pdo);
    }
}

/* ---------- 設定 ---------- */
function setting_get(string $key, ?string $default = null): ?string {
    $st = db()->prepare("SELECT value FROM settings WHERE key = ?");
    $st->execute([$key]);
    $v = $st->fetchColumn();
    return $v === false ? $default : (string)$v;
}
function setting_get_json(string $key): array {
    return json_array(setting_get($key, ''));
}
function setting_set(string $key, string $value): void {
    // 旧SQLite互換（UPSERTを使わない）
    db()->prepare("INSERT OR REPLACE INTO settings(key, value) VALUES(?, ?)")
        ->execute([$key, $value]);
}

/* ---------- ライセンス（無料版 / 製品版の切替） ----------
 *  シリアル形式：YYYYMMDD-XXXXX-XXXXX（日付8桁 ＋ 英数字5文字×2ブロック）
 *  例）20260614-K7esG-Es2Fb
 *  ※ 英字の大文字小文字はどちらでも可（手入力ミスで弾かれないように）。
 *  ※ 判定基準を変える時はこの cms_license_valid() の正規表現だけ直せばよい。 */
function cms_license_code(): string {
    return trim((string)(setting_get('license_code', '') ?? ''));
}
function cms_license_valid(string $code): bool {
    return (bool) preg_match('/^\d{8}-[A-Za-z0-9]{5}-[A-Za-z0-9]{5}$/', trim($code));
}
/** 有効なシリアルが登録されていれば製品版（解除済み） */
function cms_is_licensed(): bool {
    return cms_license_valid(cms_license_code());
}

/* ---------- 管理者 ---------- */
function admin_exists(): bool {
    return (int)db()->query("SELECT COUNT(*) FROM admins")->fetchColumn() > 0;
}
function admin_create(string $email, string $password, string $name = '管理者'): void {
    db()->prepare("INSERT INTO admins(email, password_hash, name) VALUES(?,?,?)")
        ->execute([$email, password_hash($password, PASSWORD_DEFAULT), $name]);
}
function admin_verify(string $email, string $password): ?array {
    $st = db()->prepare("SELECT * FROM admins WHERE email = ?");
    $st->execute([$email]);
    $row = $st->fetch();
    return ($row && password_verify($password, $row['password_hash'])) ? $row : null;
}
function admin_find(int $id): ?array {
    $st = db()->prepare("SELECT * FROM admins WHERE id = ?");
    $st->execute([$id]);
    $row = $st->fetch();
    return $row ?: null;
}
function admin_update_password(int $id, string $newPassword): void {
    db()->prepare("UPDATE admins SET password_hash = ? WHERE id = ?")
        ->execute([password_hash($newPassword, PASSWORD_DEFAULT), $id]);
}
/** 指定メールが自分以外の管理者に使われているか（重複チェック） */
function admin_email_taken(string $email, int $exceptId): bool {
    $st = db()->prepare("SELECT COUNT(*) FROM admins WHERE email = ? AND id != ?");
    $st->execute([$email, $exceptId]);
    return (int)$st->fetchColumn() > 0;
}
function admin_update_email(int $id, string $newEmail): void {
    db()->prepare("UPDATE admins SET email = ? WHERE id = ?")
        ->execute([$newEmail, $id]);
}

/* ---------- ログイン試行（総当たり対策） ---------- */
/** 直近 $minutes 分の、このIPからの失敗回数 */
function login_attempts_count(string $ip, int $minutes): int {
    $st = db()->prepare("SELECT COUNT(*) FROM login_attempts WHERE ip = ? AND created_at >= datetime('now', ?)");
    $st->execute([$ip, '-' . max(1, $minutes) . ' minutes']);
    return (int)$st->fetchColumn();
}
/** 失敗を1件記録（ついでに古い記録を掃除） */
function login_attempt_record(string $ip): void {
    db()->prepare("INSERT INTO login_attempts(ip) VALUES(?)")->execute([$ip]);
    db()->exec("DELETE FROM login_attempts WHERE created_at < datetime('now', '-1 day')");
}
/** ログイン成功時にこのIPの失敗記録を消す */
function login_attempts_clear(string $ip): void {
    db()->prepare("DELETE FROM login_attempts WHERE ip = ?")->execute([$ip]);
}

/* ---------- ページ / ブロック ---------- */
function page_find(string $slug): ?array {
    $st = db()->prepare("SELECT * FROM pages WHERE slug = ?");
    $st->execute([$slug]);
    return $st->fetch() ?: null;
}
function page_all(): array {
    return db()->query("SELECT * FROM pages ORDER BY sort, id")->fetchAll();
}
function page_nav(): array {
    return db()->query("SELECT * FROM pages WHERE in_nav = 1 ORDER BY sort, id")->fetchAll();
}
function blocks_of(int $pageId): array {
    $st = db()->prepare("SELECT * FROM blocks WHERE page_id = ? ORDER BY sort, id");
    $st->execute([$pageId]);
    return $st->fetchAll();
}

/* ---------- メニュー ---------- */
function menu_all(): array {
    return db()->query("SELECT * FROM menu_items ORDER BY sort, id")->fetchAll();
}
function menu_find(int $id): ?array {
    $st = db()->prepare("SELECT * FROM menu_items WHERE id = ?");
    $st->execute([$id]);
    return $st->fetch() ?: null;
}
/** トップ階層の項目のみ（親選択用） */
function menu_tops(): array {
    return db()->query("SELECT * FROM menu_items WHERE parent_id IS NULL ORDER BY sort, id")->fetchAll();
}

/* ============================================================
 *  デモデータ
 * ============================================================ */
function db_seed(PDO $pdo): void {
    // デモ内容（settings/ページ/ブロック）は demo-pack.php に分離（業種別に差し替え可）。
    $pack = CMS_ROOT . '/demo-pack.php';
    if (is_file($pack)) { require_once $pack; if (function_exists('cms_demo_pack_seed')) cms_demo_pack_seed($pdo); }
}

/** メニューのデモ（親子・ドロップダウンの例つき） */
function db_seed_menu(PDO $pdo): void {
    // メニューの初期投入も demo-pack.php へ分離。
    $pack = CMS_ROOT . '/demo-pack.php';
    if (is_file($pack)) { require_once $pack; if (function_exists('cms_demo_pack_menu')) cms_demo_pack_menu($pdo); }
}
