// ===== Mock data + storage helpers =====
const STORAGE_KEY = 'seitai_bookings_v1';
const STORAGE_BLOCKS = 'seitai_blocks_v1';
const STORAGE_CUSTOMERS = 'seitai_customers_v1';
const STORAGE_SETTINGS = 'seitai_settings_v1';
const STORAGE_NOTIFS = 'seitai_notifications_v1';
const STORAGE_NOTIFS_READ = 'seitai_notifications_read_v1';
const STORAGE_AUTH = 'seitai_auth_v1';

// ===== Worker API URL（環境ごとに切替可能） =====
// 全ファイルからこの定数を参照すること（ハードコード禁止）
const API_BASE = (typeof window !== 'undefined' && window.__PHYSIO_API_BASE__) || 'https://physio-admin-api.yuzuman2013.workers.dev';

// ===== APIトークン（localStorageから読む。未設定時はpromptで入力） =====
const API_TOKEN_KEY = 'seitai_api_token_v1';
function getApiToken() {
  try { return localStorage.getItem(API_TOKEN_KEY) || ''; } catch (e) { return ''; }
}
function setApiToken(token) {
  try { localStorage.setItem(API_TOKEN_KEY, token || ''); } catch (e) {}
}
function getAuthHeaders() {
  const t = getApiToken();
  return t ? { 'Authorization': 'Bearer ' + t } : {};
}
// 認証付きfetch ラッパー（既存fetchの代わりに使う）
async function apiFetch(url, options) {
  options = options || {};
  options.headers = { ...(options.headers || {}), ...getAuthHeaders() };
  const r = await fetch(url, options);
  if (r.status === 401) {
    // トークン無効/未設定 → 入力を促す
    const cur = getApiToken();
    const msg = cur
      ? '⚠ APIトークンが無効です。新しいトークンを入力してください：'
      : '🔐 初回起動：APIトークンを入力してください（エンジニアから受け取った文字列）：';
    const newToken = prompt(msg, cur);
    if (newToken && newToken !== cur) {
      setApiToken(newToken.trim());
      // 1度だけリトライ
      const r2 = await fetch(url, { ...options, headers: { ...(options.headers || {}), 'Authorization': 'Bearer ' + newToken.trim() }});
      return r2;
    }
  }
  return r;
}

// ===== アプリ全体の定数（マジック値の集約） =====
const CONSTANTS = {
  // 営業時間（分単位、9:00-21:00 = 540-1260分）
  DAY_START_MIN: 9 * 60,
  DAY_END_MIN: 21 * 60,
  SLOT_MINUTES: 15,
  // 施術後バッファ（片付け・お見送り）
  BOOKING_BUFFER_MIN: 15,
  // リテンション分類（最終来院からの月数）
  RETENTION_ACTIVE_MONTHS: 3,   // ≤3ヶ月：アクティブ
  RETENTION_WARM_MONTHS: 6,     // 3〜6ヶ月：休眠予備
  RETENTION_COOL_MONTHS: 12,    // 6〜12ヶ月：離脱予備（12ヶ月超は離脱）
  // 価格（旧/新）
  TICKET10_OLD_PRICE_PER_VISIT: 7000,  // ¥70,000券
  TICKET10_NEW_PRICE_PER_VISIT: 7200,  // ¥72,000券
  MONTHLY4_PRICE_PER_VISIT: 7000,
};

const WEEK_LABELS = ['日','月','火','水','木','金','土'];

function pad(n) { return String(n).padStart(2,'0'); }
function isoDate(d) { return d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()); }
function parseISO(s) { const [y,m,d]=s.split('-').map(Number); return new Date(y, m-1, d); }
function fmtDateJa(d) {
  if (typeof d === 'string') d = parseISO(d);
  return (d.getMonth()+1) + '月' + d.getDate() + '日（' + WEEK_LABELS[d.getDay()] + '）';
}
function fmtDateLong(d) {
  if (typeof d === 'string') d = parseISO(d);
  return d.getFullYear() + '年' + (d.getMonth()+1) + '月' + d.getDate() + '日（' + WEEK_LABELS[d.getDay()] + '）';
}
// 例: "2024/5/15（水）" - 顧客リスト等の一覧で年を含めつつコンパクトに
function fmtDateShortJa(d) {
  if (typeof d === 'string') d = parseISO(d);
  return d.getFullYear() + '/' + (d.getMonth()+1) + '/' + d.getDate() + '（' + WEEK_LABELS[d.getDay()] + '）';
}
function fmtMonth(d) { return d.getFullYear() + '年' + (d.getMonth()+1) + '月'; }
function timeToMin(t) { const [h,m]=t.split(':').map(Number); return h*60+m; }
function minToTime(m) { return Math.floor(m/60) + ':' + pad(m%60); }
function addMinTime(t, delta) { return minToTime(timeToMin(t)+delta); }

function startOfWeek(d) {
  const x = new Date(d);
  x.setHours(0,0,0,0);
  x.setDate(x.getDate() - x.getDay()); // Sunday start
  return x;
}
function startOfMonth(d) { return new Date(d.getFullYear(), d.getMonth(), 1); }

// ===== Default settings =====
const DEFAULT_SETTINGS = {
  storeName: 'フィジオサロンキムラ',
  storeNameEn: 'PHYSIO SALON KIMURA',
  phone: '070-8491-5846',
  address: '名古屋市守山区西川原138',
  hours: [
    { day: 0, open: false, from: '9:00', to: '20:00', label: '日曜' },
    { day: 1, open: true,  from: '9:00', to: '20:00', label: '月曜' },
    { day: 2, open: true,  from: '9:00', to: '20:00', label: '火曜' },
    { day: 3, open: true,  from: '9:00', to: '20:00', label: '水曜' },
    { day: 4, open: true,  from: '9:00', to: '20:00', label: '木曜' },
    { day: 5, open: true,  from: '9:00', to: '20:00', label: '金曜' },
    { day: 6, open: true,  from: '9:00', to: '20:00', label: '土曜' }
  ],
  slotMinutes: 15,
  staff: [
    { id: 'st1', name: '木村晋一朗', role: '院長', color: '#5b8259', active: true }
  ],
  notifyNewBooking: true,
  notifyCancel: true,
  notifySound: false,
  // 📋 カウンセリングシート（GoogleフォームURL）
  counselingFormUrl: 'https://docs.google.com/forms/d/e/1FAIpQLSekWYFdlFzbSks1TguYVIDN_SrTA7jY6sVnNmP_kskGeT8TgA/viewform?usp=header',
  // 📧 サロン用メール（送信元）
  salonEmail: 'physiosalon.kimura2022@gmail.com'
};

const DEFAULT_MENUS = [
  { id: 'm1', name: '初回限定コース', duration: 60, price: 5980, color: 'orange', popular: true, active: true },
  { id: 'm2', name: '60分コース', duration: 60, price: 8000, color: 'green', popular: true, active: true },
  { id: 'm3', name: '90分コース', duration: 90, price: 13000, color: 'green', popular: false, active: true },
  { id: 'm4', name: '回数券（10回券）', duration: 60, price: 72000, color: 'amber', popular: false, active: true }
];
const STORAGE_MENUS = 'seitai_menus_v2';

// ===== Mock customer/booking generation =====
const CUSTOMER_NAMES = [
  ['田中 由美','Tanaka Yumi','tanaka@example.com','090-1111-1234','女性',38],
  ['佐藤 健太','Sato Kenta','sato@example.com','080-2222-3456','男性',45],
  ['鈴木 麻衣','Suzuki Mai','suzuki@example.com','070-3333-5678','女性',31],
  ['伊藤 大輔','Ito Daisuke','ito@example.com','090-4444-7890','男性',52],
  ['山本 結衣','Yamamoto Yui','yamamoto@example.com','080-5555-1212','女性',28],
  ['中村 翔太','Nakamura Shota','nakamura@example.com','090-6666-3434','男性',34],
  ['小林 真由美','Kobayashi Mayumi','kobayashi@example.com','070-7777-5656','女性',47],
  ['加藤 隆','Kato Takashi','kato@example.com','090-8888-7878','男性',58],
  ['吉田 沙織','Yoshida Saori','yoshida@example.com','080-9999-9090','女性',35],
  ['山田 美穂','Yamada Miho','yamada@example.com','090-1010-2020','女性',42],
  ['松本 純','Matsumoto Jun','matsumoto@example.com','080-3030-4040','男性',39],
  ['井上 由香','Inoue Yuka','inoue@example.com','070-5050-6060','女性',26]
];

function genCustomers() {
  const today = new Date();
  return CUSTOMER_NAMES.map((c, i) => {
    const visits = 1 + ((i*3) % 12);
    const lastVisitDays = (i*5+3) % 90;
    const last = new Date(today); last.setDate(last.getDate() - lastVisitDays);
    const first = new Date(last); first.setDate(first.getDate() - visits*30);
    return {
      id: 'cu' + (i+1),
      name: c[0],
      kana: c[1],
      email: c[2],
      tel: c[3],
      gender: c[4],
      age: c[5],
      visits: visits,
      lastVisit: isoDate(last),
      firstVisit: isoDate(first),
      totalSpent: visits * (i % 3 === 0 ? 5980 : 8000),
      tags: i % 4 === 0 ? ['VIP'] : (i % 4 === 1 ? ['新規'] : []),
      note: i % 3 === 0 ? '右肩のコリが慢性的。腰痛もあり。' : (i % 3 === 1 ? 'デスクワークで首肩がつらい。' : '')
    };
  });
}

function genMockBookings(menus, customers) {
  const out = [];
  const today = new Date(); today.setHours(0,0,0,0);
  const statuses = ['confirmed','confirmed','confirmed','done','cancelled'];
  // generate over a 60-day window centered around today
  for (let offset = -30; offset <= 30; offset++) {
    const d = new Date(today); d.setDate(d.getDate()+offset);
    const dow = d.getDay();
    if (dow === 0) continue; // Sunday closed
    // 1-5 bookings per day
    const seed = (d.getDate() * 37 + d.getMonth() * 113) % 100;
    const count = 1 + (seed % 5);
    for (let i = 0; i < count; i++) {
      const mIdx = (seed + i*7) % menus.length;
      const cIdx = (seed + i*11) % customers.length;
      const menu = menus[mIdx];
      const customer = customers[cIdx];
      const startHour = 9 + ((seed + i*3) % 10);
      const startMin = ((seed + i*5) % 4) * 15;
      if (startHour >= 20) continue;
      const time = startHour + ':' + pad(startMin);
      let status;
      if (offset < 0) status = (seed+i) % 8 === 0 ? 'cancelled' : 'done';
      else if (offset === 0) status = 'confirmed';
      else status = ((seed+i) % 7 === 0) ? 'cancelled' : 'confirmed';
      out.push({
        id: 'mk-' + isoDate(d) + '-' + i,
        createdAt: new Date(d.getTime() - 86400000 * (3+i)).toISOString(),
        source: i % 4 === 0 ? 'lp' : 'admin',
        status: status,
        menuId: menu.id,
        menu: menu.name,
        duration: menu.duration,
        price: menu.price,
        date: isoDate(d),
        time: time,
        customerId: customer.id,
        name: customer.name,
        tel: customer.tel,
        email: customer.email,
        note: '',
        staffId: i % 2 === 0 ? 'st1' : 'st2'
      });
    }
  }
  return out;
}

// ===== Storage init / accessors =====
// Phase 1: Storage モジュール経由（localStorage→将来D1へ）
function loadJSON(key, fallback) {
  const v = (typeof Storage !== 'undefined' && Storage.sync)
    ? Storage.sync.get(key, fallback)
    : (function () { try { const w = localStorage.getItem(key); return w ? JSON.parse(w) : fallback; } catch(e) { return fallback; } })();
  // 🛡️ SETTINGS は admin app が期待する構造（hours配列・staff配列）を持っていない場合 fallback を返す
  // D1の settings は key-value のフラット構造で互換性がないため、settings 翻訳が完成するまでは DEFAULT_SETTINGS を使う
  if (key === STORAGE_SETTINGS) {
    if (!v || !Array.isArray(v.hours) || !Array.isArray(v.staff)) {
      return fallback;
    }
  }
  return v;
}
function saveJSON(key, val) {
  if (typeof Storage !== 'undefined' && Storage.sync) {
    Storage.sync.set(key, val);
  } else {
    try { localStorage.setItem(key, JSON.stringify(val)); } catch(e) {}
  }
}

function ensureSeed() {
  // 🔥 クラウドDBモード（Cloudflare D1）の場合は初期データ生成をスキップ
  // クラウドには既に初期メニュー等が入っているため、モック生成は不要
  if (typeof Storage !== 'undefined' && Storage.backend === 'cloudflare-d1') {
    console.log('[ensureSeed] cloud-d1 mode, skipping mock data generation');
    return;
  }
  // 旧バージョンのメニューデータを削除し、シード予約も再生成（メニュー名整合性のため）
  if (localStorage.getItem('seitai_menus_v1')) {
    localStorage.removeItem('seitai_menus_v1');
    // 古いシード予約を削除（LP予約は保持）
    const existing = loadJSON(STORAGE_KEY, []);
    const filtered = existing.filter(b => !b.id || !b.id.startsWith('mk-'));
    saveJSON(STORAGE_KEY, filtered);
  }
  // 既存予約のpendingをconfirmedに正規化（凡例から「未確定」を削除した方針に揃える）
  {
    const existing = loadJSON(STORAGE_KEY, []);
    let changed = false;
    const normalized = existing.map(b => {
      if (b.status === 'pending') { changed = true; return { ...b, status: 'confirmed' }; }
      return b;
    });
    if (changed) saveJSON(STORAGE_KEY, normalized);
  }
  // settings
  if (!localStorage.getItem(STORAGE_SETTINGS)) saveJSON(STORAGE_SETTINGS, DEFAULT_SETTINGS);
  // menus
  if (!localStorage.getItem(STORAGE_MENUS)) saveJSON(STORAGE_MENUS, DEFAULT_MENUS);
  // customers
  let customers = loadJSON(STORAGE_CUSTOMERS, null);
  if (!customers) { customers = genCustomers(); saveJSON(STORAGE_CUSTOMERS, customers); }
  // bookings: keep any existing LP bookings, append seed if no admin-seeded ones
  const existing = loadJSON(STORAGE_KEY, []);
  const hasSeed = existing.some(b => b.id && b.id.startsWith('mk-'));
  if (!hasSeed) {
    const menus = loadJSON(STORAGE_MENUS, DEFAULT_MENUS);
    const seed = genMockBookings(menus, customers);
    // merge: keep LP bookings, add seed
    const merged = [...seed, ...existing.filter(b => !b.id || !b.id.startsWith('mk-'))];
    saveJSON(STORAGE_KEY, merged);
  }
  // notifications: derive from bookings
  if (!localStorage.getItem(STORAGE_NOTIFS)) {
    const all = loadJSON(STORAGE_KEY, []);
    const recent = all
      .filter(b => b.source === 'lp' || b.status === 'pending' || b.status === 'cancelled')
      .slice(-12)
      .reverse();
    const notifs = recent.map((b, i) => ({
      id: 'n-' + i + '-' + (b.id || ''),
      type: b.status === 'cancelled' ? 'cancel' : 'new',
      bookingId: b.id,
      title: b.status === 'cancelled' ? '予約がキャンセルされました' : '新しい予約が入りました',
      text: b.name + ' 様 / ' + (b.menu || '') + ' / ' + fmtDateJa(b.date) + ' ' + b.time,
      time: b.createdAt || new Date().toISOString()
    }));
    saveJSON(STORAGE_NOTIFS, notifs);
  }

  // AI相談ログのサンプル生成 + 一部の予約に紐付け
  if (!localStorage.getItem('seitai_ai_logs_v1')) {
    const aiLogs = genMockAiLogs();
    saveJSON('seitai_ai_logs_v1', aiLogs);
    // 「予約到達」フラグのあるログのうち実在する予約と紐付け
    const reachedLogs = aiLogs.filter(l => l.reachedBooking);
    if (reachedLogs.length > 0) {
      const all = loadJSON(STORAGE_KEY, []);
      // 直近のconfirmed予約に AIログをひもづけ（最大3件）
      const targets = all
        .filter(b => b.status === 'confirmed' && !b.aiConsultId)
        .sort((a,b) => (b.createdAt || '').localeCompare(a.createdAt || ''))
        .slice(0, reachedLogs.length);
      const updated = all.map(b => {
        const idx = targets.findIndex(t => t.id === b.id);
        if (idx >= 0) {
          const log = reachedLogs[idx];
          return { ...b, aiConsultId: log.id, aiConsultSummary: log.summary, source: 'ai' };
        }
        return b;
      });
      saveJSON(STORAGE_KEY, updated);
    }
  }
}

// ===== AI相談ログ サンプルデータ =====
function genMockAiLogs() {
  const now = Date.now();
  const hoursAgo = (h) => new Date(now - h * 3600 * 1000).toISOString();
  const logs = [
    {
      id: 'ai-001',
      startedAt: hoursAgo(2),
      updatedAt: hoursAgo(2),
      rounds: 8,
      summary: '在宅ワークでの首・肩こりが3週間続いており、特に夕方になると頭痛も伴う。整体は初めてで、骨盤矯正コースに興味あり。',
      topics: ['首こり', '肩こり', '頭痛', 'デスクワーク', '初回'],
      triggers: [],
      lineCtaClicked: false,
      reachedBooking: true,
      messages: [
        { role: 'user', content: '在宅ワーク中心になってから首と肩のこりがひどくて、夕方になると頭痛もしてきます。3週間くらい続いています。' },
        { role: 'assistant', content: 'お辛い状況ですね。在宅ワークで姿勢が固定されると、僧帽筋上部や肩甲挙筋に負担がかかりやすく、頭痛にもつながります。デスクの高さや椅子の調整、あるいは肩甲骨を動かすストレッチは試されましたか？' },
        { role: 'user', content: 'ストレッチは時々していますが、その場では楽になってもまた戻ってしまいます。' },
        { role: 'assistant', content: 'なるほど、症状が定着している段階かもしれません。当院では骨盤と背骨のアライメントを整えることで、首肩への負担が根本から軽減するアプローチを行っています。一度状態を見せていただくのが早いと思います。' },
        { role: 'user', content: '整体は初めてなのですが、痛みはありますか？' },
        { role: 'assistant', content: 'バキバキする施術は行わないので、ご安心ください。痛気持ちいい程度の圧で、女性スタッフの対応もできます。初回は60分の検査＋施術で、骨盤の歪みもしっかり見させていただきます。' },
        { role: 'user', content: 'では予約したいです。明日か明後日で空いていますか？' },
        { role: 'assistant', content: 'ありがとうございます。下の予約フォームから空き状況をご確認いただけます。今お伺いした内容も予約と一緒に共有させていただきますね。' }
      ]
    },
    {
      id: 'ai-002',
      startedAt: hoursAgo(6),
      updatedAt: hoursAgo(6),
      rounds: 6,
      summary: 'ぎっくり腰になって2日目。ベッドから起きるのも辛く、すぐに見てほしいと希望。',
      topics: ['ぎっくり腰', '急性痛', '緊急'],
      triggers: ['urgent'],
      lineCtaClicked: true,
      reachedBooking: true,
      messages: [
        { role: 'user', content: '一昨日、重い荷物を持ち上げた時にぎっくり腰になってしまいました。今もベッドから起きるのが辛いです。' },
        { role: 'assistant', content: 'それは大変お辛いですね。急性期のぎっくり腰は無理に動かさず、まずは状態を確認させていただきたいです。発熱や下肢のしびれはありますか？' },
        { role: 'user', content: '熱はないです。しびれも今のところありません。' },
        { role: 'assistant', content: 'よかったです。神経症状がなければ整体でのアプローチが可能です。本日または明日の早いお時間に、無理のない範囲で来ていただける枠をご案内できます。' },
        { role: 'user', content: 'できれば今日中にお願いしたいです。' },
        { role: 'assistant', content: 'かしこまりました。LINEからもご相談いただけますし、下の予約フォームから本日の枠を選んでいただけます。お気をつけてお越しください。' }
      ]
    },
    {
      id: 'ai-003',
      startedAt: hoursAgo(14),
      updatedAt: hoursAgo(14),
      rounds: 5,
      summary: '産後3ヶ月、骨盤の開きや腰回りの違和感が気になる。授乳中でも来院できるか確認したい。',
      topics: ['産後', '骨盤', '授乳中'],
      triggers: [],
      lineCtaClicked: true,
      reachedBooking: true,
      messages: [
        { role: 'user', content: '出産から3ヶ月経ちます。骨盤の開きや腰の違和感が気になっていて、産後ケアを受けたいです。' },
        { role: 'assistant', content: '産後3ヶ月は骨盤を整えるのに良いタイミングです。授乳や抱っこで腰に負担がかかりやすい時期ですね。お子様連れでも対応可能です。' },
        { role: 'user', content: '子連れで大丈夫なんですか？授乳の必要もあって…' },
        { role: 'assistant', content: 'はい、ベビーカーのまま入れますし、授乳スペースもご案内できます。施術中はスタッフが見守り対応をすることも可能です。LINEで詳細をご相談されると、ご来店時間の調整もスムーズです。' },
        { role: 'user', content: 'では一度LINEで相談してみます。' }
      ]
    },
    {
      id: 'ai-004',
      startedAt: hoursAgo(26),
      updatedAt: hoursAgo(26),
      rounds: 7,
      summary: '右膝の痛みが2ヶ月続く。ランニング中に違和感、整形外科では異常なしと言われた。',
      topics: ['膝痛', 'ランニング', 'スポーツ障害'],
      triggers: [],
      lineCtaClicked: false,
      reachedBooking: false,
      messages: [
        { role: 'user', content: '2ヶ月前から右膝の外側が痛みます。ランニングをすると特に。' },
        { role: 'assistant', content: '膝の外側の痛みは腸脛靭帯炎の可能性があります。整形外科は受診されましたか？' },
        { role: 'user', content: '行きました。レントゲンでは異常なしで、湿布をもらっただけです。' },
        { role: 'assistant', content: '画像で異常がない場合、筋膜や関節のアライメントの問題が多いです。当院では股関節〜骨盤からのアプローチで膝への負担を減らす施術が可能です。' },
        { role: 'user', content: '料金はどのくらいですか？' },
        { role: 'assistant', content: '初回60分コースが7,700円、回数券のご用意もあります。ご検討ください。' },
        { role: 'user', content: '少し考えます、ありがとうございました。' }
      ]
    },
    {
      id: 'ai-005',
      startedAt: hoursAgo(40),
      updatedAt: hoursAgo(40),
      rounds: 9,
      summary: '慢性的な頭痛と吐き気。10年以上付き合っている。整体で改善するか相談。',
      topics: ['慢性頭痛', '吐き気', '長期症状'],
      triggers: ['urgent'],
      lineCtaClicked: false,
      reachedBooking: false,
      messages: [
        { role: 'user', content: '10年以上、慢性的な頭痛と吐き気があります。薬でしのいでいますが、整体で改善しますか？' },
        { role: 'assistant', content: '長期間の症状、お辛いと思います。10年以上ということなので、まず神経内科等で精密検査を受けられているか確認させてください。' },
        { role: 'user', content: 'はい、20代の頃にMRIなど受けて、片頭痛と診断されました。' },
        { role: 'assistant', content: '診断済みであれば、頚椎・後頭部周辺の筋緊張からくる二次的な悪化要素を整体で軽減できる可能性があります。ただ、整体は治療ではなく補助的なものとして考えていただくのが正確です。' },
        { role: 'user', content: '実際そういう方は来ていますか？' },
        { role: 'assistant', content: '頭痛持ちの方は多くいらっしゃいます。劇的に治るというより、頻度や強度を抑える目的で続けていただくケースが多いです。' },
        { role: 'user', content: 'なるほど。少し検討してみます。' },
        { role: 'assistant', content: '無理におすすめはしません。気になることがあれば、いつでもLINEでご相談ください。' },
        { role: 'user', content: 'ありがとうございました。' }
      ]
    },
    {
      id: 'ai-006',
      startedAt: hoursAgo(58),
      updatedAt: hoursAgo(58),
      rounds: 5,
      summary: '営業時間と料金の確認のみ。土曜の夕方に来店希望。',
      topics: ['営業時間', '料金'],
      triggers: [],
      lineCtaClicked: false,
      reachedBooking: false,
      messages: [
        { role: 'user', content: '土曜日は何時まで営業していますか？' },
        { role: 'assistant', content: '土曜日は20:00まで営業しております。最終受付は19:00となります。' },
        { role: 'user', content: '料金はいくらですか？' },
        { role: 'assistant', content: '60分コース 7,700円、90分コース 11,000円です。初回は同料金でカウンセリング込みになります。' },
        { role: 'user', content: 'わかりました、検討します。' }
      ]
    }
  ];
  return logs;
}

// =====================================================
// 日本の祝日（2026年〜2030年）
// =====================================================
// 国民の祝日。振替休日も含む。年1回（年初）にメンテナンス推奨。
const JP_HOLIDAYS = {
  '2026': {
    '01-01': '元日', '01-12': '成人の日', '02-11': '建国記念の日', '02-23': '天皇誕生日',
    '03-20': '春分の日', '04-29': '昭和の日', '05-03': '憲法記念日', '05-04': 'みどりの日',
    '05-05': 'こどもの日', '05-06': '振替休日', '07-20': '海の日', '08-11': '山の日',
    '09-21': '敬老の日', '09-22': '国民の休日', '09-23': '秋分の日', '10-12': 'スポーツの日',
    '11-03': '文化の日', '11-23': '勤労感謝の日',
  },
  '2027': {
    '01-01': '元日', '01-11': '成人の日', '02-11': '建国記念の日', '02-23': '天皇誕生日',
    '03-21': '春分の日', '03-22': '振替休日', '04-29': '昭和の日', '05-03': '憲法記念日',
    '05-04': 'みどりの日', '05-05': 'こどもの日', '07-19': '海の日', '08-11': '山の日',
    '09-20': '敬老の日', '09-23': '秋分の日', '10-11': 'スポーツの日', '11-03': '文化の日',
    '11-23': '勤労感謝の日',
  },
  '2028': {
    '01-01': '元日', '01-10': '成人の日', '02-11': '建国記念の日', '02-23': '天皇誕生日',
    '03-20': '春分の日', '04-29': '昭和の日', '05-03': '憲法記念日', '05-04': 'みどりの日',
    '05-05': 'こどもの日', '07-17': '海の日', '08-11': '山の日', '09-18': '敬老の日',
    '09-22': '秋分の日', '10-09': 'スポーツの日', '11-03': '文化の日', '11-23': '勤労感謝の日',
  },
  '2029': {
    '01-01': '元日', '01-08': '成人の日', '02-11': '建国記念の日', '02-12': '振替休日',
    '02-23': '天皇誕生日', '03-20': '春分の日', '04-29': '昭和の日', '04-30': '振替休日',
    '05-03': '憲法記念日', '05-04': 'みどりの日', '05-05': 'こどもの日', '07-16': '海の日',
    '08-11': '山の日', '09-17': '敬老の日', '09-23': '秋分の日', '09-24': '振替休日',
    '10-08': 'スポーツの日', '11-03': '文化の日', '11-23': '勤労感謝の日',
  },
  '2030': {
    '01-01': '元日', '01-14': '成人の日', '02-11': '建国記念の日', '02-23': '天皇誕生日',
    '03-20': '春分の日', '04-29': '昭和の日', '05-03': '憲法記念日', '05-04': 'みどりの日',
    '05-05': 'こどもの日', '05-06': '振替休日', '07-15': '海の日', '08-11': '山の日',
    '09-16': '敬老の日', '09-23': '秋分の日', '10-14': 'スポーツの日', '11-03': '文化の日',
    '11-04': '振替休日', '11-23': '勤労感謝の日',
  },
};
function getHolidayName(dateISO) {
  if (!dateISO) return null;
  const y = dateISO.slice(0, 4);
  const md = dateISO.slice(5);
  return (JP_HOLIDAYS[y] && JP_HOLIDAYS[y][md]) || null;
}
function isHoliday(dateISO) { return !!getHolidayName(dateISO); }

window.AdminData = {
  STORAGE_KEY, STORAGE_BLOCKS, STORAGE_CUSTOMERS, STORAGE_SETTINGS,
  STORAGE_NOTIFS, STORAGE_NOTIFS_READ, STORAGE_AUTH, STORAGE_MENUS,
  WEEK_LABELS, DEFAULT_SETTINGS, DEFAULT_MENUS,
  API_BASE, CONSTANTS, API_TOKEN_KEY,
  getApiToken, setApiToken, getAuthHeaders, apiFetch,
  pad, isoDate, parseISO, fmtDateJa, fmtDateLong, fmtDateShortJa, fmtMonth,
  timeToMin, minToTime, addMinTime,
  startOfWeek, startOfMonth,
  loadJSON, saveJSON, ensureSeed,
  JP_HOLIDAYS, getHolidayName, isHoliday,
};
