// Mail Studio ダッシュボード SPA（依存ゼロ）
const $ = (s) => document.querySelector(s);
const view = $("#view");
let DRY = true;

async function api(path, opts = {}) {
  const res = await fetch(path, {
    headers: { "Content-Type": "application/json" },
    ...opts,
  });
  if (res.status === 401 && path !== "/api/login") {
    showLogin();
    throw new Error("unauthorized");
  }
  const data = await res.json().catch(() => ({}));
  if (!res.ok) throw new Error(data.error || `error ${res.status}`);
  return data;
}
const esc = (s) =>
  String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
function toast(msg) {
  const t = $("#toast");
  t.textContent = msg;
  t.classList.remove("hidden");
  setTimeout(() => t.classList.add("hidden"), 2600);
}

// ---------- 認証 ----------
function showLogin() {
  $("#login").classList.remove("hidden");
  $("#app").classList.add("hidden");
}
function showApp() {
  $("#login").classList.add("hidden");
  $("#app").classList.remove("hidden");
}
$("#login-form").addEventListener("submit", async (e) => {
  e.preventDefault();
  $("#login-error").classList.add("hidden");
  try {
    await api("/api/login", { method: "POST", body: JSON.stringify({ password: $("#password").value }) });
    showApp();
    nav("dashboard");
  } catch {
    $("#login-error").classList.remove("hidden");
  }
});
$("#logout").addEventListener("click", async () => {
  await api("/api/logout", { method: "POST" });
  showLogin();
});
$("#tick-btn").addEventListener("click", async () => {
  const r = await api("/api/scheduler/run", { method: "POST" });
  const s = r.summary;
  toast(`実行: メール一斉${s.broadcastSends} / メールステップ${s.stepSends} / SMS${s.smsSends || 0}通(${s.smsSegments || 0}通分) / skip${s.skipped} / fail${s.failed}`);
  nav(current);
});

// ---------- ルーティング ----------
let current = "dashboard";
const titles = {
  dashboard: "ダッシュボード", subscribers: "購読者", lists: "リスト",
  broadcasts: "メール一斉配信（単発）", smsBroadcasts: "SMS一斉配信（単発）",
  campaigns: "メールステップ配信", smsCampaigns: "SMSステップ配信", hooks: "連携リンク",
  points: "ポイント", lotteries: "抽選会", exchange: "景品交換",
};
function nav(v) {
  current = v;
  $("#view-title").textContent = titles[v];
  document.querySelectorAll(".sidebar nav a").forEach((a) => a.classList.toggle("active", a.dataset.view === v));
  views[v]();
}
document.querySelectorAll(".sidebar nav a").forEach((a) =>
  a.addEventListener("click", () => nav(a.dataset.view)),
);

function empty(msg) { return `<div class="empty">${esc(msg)}</div>`; }
function pill(s) { return `<span class="pill ${esc(s)}">${esc(s)}</span>`; }
/** SMSの送信可否を分かりやすい日本語ラベルで表示する（none=同意待ち） */
function smsPill(status) {
  const map = { active: ["active", "SMS可"], unsubscribed: ["canceled", "SMS停止"], none: ["scheduled", "同意待ち"] };
  const [cls, label] = map[status] || map.none;
  return `<span class="pill ${cls}">${label}</span>`;
}

// ---------- ビュー ----------
const views = {
  async dashboard() {
    const s = await api("/api/stats");
    DRY = s.dryRun;
    if (s.sms) {
      SMS_COST = s.sms.costPerSegment || 10;
      SMS_ENABLED = !!s.sms.live;
      if (s.sms.footerChars) SMS_FOOTER_EST = s.sms.footerChars;
      localStorage.setItem("smsMeta", JSON.stringify({ cost: SMS_COST, footer: SMS_FOOTER_EST }));
    }
    $("#mode-badge").textContent = s.live ? "本番送信" : "DRY_RUN";
    $("#mode-badge").classList.toggle("live", s.live);
    view.innerHTML = `
      <div class="stats">
        <div class="stat"><div class="label">購読者(有効)</div><div class="num">${s.subscribers.active}</div></div>
        ${s.sms ? `<div class="stat"><div class="label">本日のSMS送信</div><div class="num">${s.sms.sentToday}<span class="muted small"> / ${s.sms.maxPerDay}通 (≒${s.sms.sentToday * SMS_COST}円)</span></div></div>
        <div class="stat"><div class="label">SMS受信OKの読者</div><div class="num">${s.sms.subscribers}<span class="muted small"> ${s.sms.live ? "" : "／SMS停止中"}</span></div></div>` : ""}
        <div class="stat"><div class="label">配信(予約中)</div><div class="num">${s.broadcasts.scheduled}</div></div>
        <div class="stat"><div class="label">配信(送信済)</div><div class="num">${s.broadcasts.sent}</div></div>
        <div class="stat"><div class="label">ステップ施策</div><div class="num">${s.campaigns}</div></div>
        <div class="stat"><div class="label">流通ポイント合計</div><div class="num">${s.points ? s.points.totalBalance : 0}<span class="muted small"> pt</span></div></div>
        <div class="stat"><div class="label">開催中の抽選会</div><div class="num">${s.points ? s.points.openLotteries : 0}</div></div>
        <div class="stat"><div class="label">未対応の交換申請</div><div class="num">${s.points ? s.points.pendingRedemptions : 0}</div></div>
      </div>
      <div class="panel">
        <h2>最近の操作ログ</h2>
        ${s.recent.length ? `<table><thead><tr><th>時刻</th><th>操作</th><th>詳細</th></tr></thead><tbody>
          ${s.recent.map((r) => `<tr><td>${new Date(r.ts).toLocaleString("ja-JP")}</td><td>${esc(r.action)}</td><td class="muted">${esc(r.detail)}</td></tr>`).join("")}
        </tbody></table>` : empty("まだ操作ログがありません")}
      </div>
      ${DRY ? `<p class="muted small">⚠ DRY_RUN中：実際のメールは送信されません。本番送信は環境変数 DRY_RUN=false + 配信の承認後に有効化されます。</p>` : ""}`;
  },

  async subscribers() {
    const [subs, lists] = await Promise.all([api("/api/subscribers"), api("/api/lists")]);
    view.innerHTML = `
      <div class="panel">
        <h2>購読者を追加</h2>
        <div class="row">
          <div class="field"><label>メール</label><input id="s-email" type="email" placeholder="user@example.com"></div>
          <div class="field"><label>名前(任意)</label><input id="s-name" type="text"></div>
          <div class="field"><label>リストに追加(任意)</label><select id="s-list"><option value="">—</option>${lists.map((l) => `<option value="${l.id}">${esc(l.name)}</option>`).join("")}</select></div>
          <div class="field"><label>携帯番号(任意・SMS用)</label><input id="s-phone" type="tel" placeholder="090-1234-5678"></div>
          <button id="s-add" style="flex:0 0 auto">追加</button>
        </div>
        <label class="muted small" style="display:block;margin-top:6px">
          <input id="s-consent" type="checkbox"> この読者から <b>SMSの受信同意</b> を得ている（チェックした場合のみSMSが送られます）
        </label>
      </div>
      <div class="panel">
        <h2>購読者一覧（${subs.length}）</h2>
        ${subs.length ? `<table><thead><tr><th>メール</th><th>名前</th><th>状態</th><th>携帯番号 / SMS</th><th>登録元</th><th></th></tr></thead><tbody>
          ${subs.map((s) => `<tr><td>${(s.email || "").endsWith("@sms.invalid")
              ? '<span class="muted">SMS専用（メールなし）</span>'
              : esc(s.email)}</td><td>${esc(s.name || "")}</td><td>${pill(s.status)}</td>
            <td class="small">${s.phone ? `${esc(s.phone)} ${smsPill(s.sms_status)}` : `<button class="ghost sm" data-phone="${s.id}">番号を登録</button>`}</td>
            <td class="muted small">${esc(s.source || "")}</td>
            <td>${s.status === "active" ? `<button class="ghost sm" data-unsub="${s.id}">停止</button>` : `<button class="ghost sm" data-act="${s.id}">有効化</button>`}
              ${s.phone ? (s.sms_status === "active"
                ? `<button class="ghost sm" data-smsoff="${s.id}">SMS停止</button>`
                : `<button class="ghost sm" data-smson="${s.id}">SMS同意あり</button>`) : ""}
              ${s.phone ? `<button class="ghost sm" data-phone="${s.id}">番号変更</button>` : ""}</td></tr>`).join("")}
        </tbody></table>` : empty("まだ購読者がいません。上のフォームか連携リンクから登録できます。")}
      </div>`;
    $("#s-add").addEventListener("click", async () => {
      const email = $("#s-email").value.trim();
      if (!email) return;
      const phone = $("#s-phone").value.trim();
      try {
        await api("/api/subscribers", { method: "POST", body: JSON.stringify({
          email, name: $("#s-name").value.trim() || null,
          listId: Number($("#s-list").value) || undefined,
          phone: phone || undefined,
          sms_consent: phone ? $("#s-consent").checked : undefined,
        }) });
        toast("追加しました"); views.subscribers();
      } catch (e) { toast(e.message); }
    });
    // SMS: 番号の登録・変更 / 同意の付け外し
    view.querySelectorAll("[data-phone]").forEach((b) => b.addEventListener("click", async () => {
      const cur = subs.find((x) => String(x.id) === b.dataset.phone);
      const phone = prompt("携帯番号（空欄にすると削除します）", cur?.phone || "");
      if (phone === null) return;
      const consent = phone.trim() ? confirm("この読者からSMSの受信同意を得ていますか？\n\nOK = 同意あり（SMSを送る対象にする）\nキャンセル = 同意なし（番号だけ保存し、SMSは送らない）") : false;
      try {
        await api(`/api/subscribers/${b.dataset.phone}/phone`, { method: "POST", body: JSON.stringify({ phone: phone.trim() || null, sms_consent: consent }) });
        toast(phone.trim() ? (consent ? "登録しました（SMS送信対象）" : "登録しました（同意待ちのため送信しません）") : "番号を削除しました");
        views.subscribers();
      } catch (e) { toast(e.message); }
    }));
    view.querySelectorAll("[data-smson]").forEach((b) => b.addEventListener("click", async () => {
      if (!confirm("この読者から SMS の受信同意を得ていますか？\n同意なく広告SMSを送ることは法律で禁止されています。")) return;
      try { await api(`/api/subscribers/${b.dataset.smson}/sms-status`, { method: "POST", body: JSON.stringify({ status: "active" }) }); toast("SMS送信対象にしました"); views.subscribers(); }
      catch (e) { toast(e.message); }
    }));
    view.querySelectorAll("[data-smsoff]").forEach((b) => b.addEventListener("click", async () => {
      try { await api(`/api/subscribers/${b.dataset.smsoff}/sms-status`, { method: "POST", body: JSON.stringify({ status: "unsubscribed" }) }); toast("SMSを停止しました"); views.subscribers(); }
      catch (e) { toast(e.message); }
    }));
    view.querySelectorAll("[data-unsub]").forEach((b) => b.addEventListener("click", async () => { await api(`/api/subscribers/${b.dataset.unsub}/status`, { method: "POST", body: JSON.stringify({ status: "unsubscribed" }) }); views.subscribers(); }));
    view.querySelectorAll("[data-act]").forEach((b) => b.addEventListener("click", async () => { await api(`/api/subscribers/${b.dataset.act}/status`, { method: "POST", body: JSON.stringify({ status: "active" }) }); views.subscribers(); }));
  },

  async lists() {
    const lists = await api("/api/lists");
    view.innerHTML = `
      <div class="panel">
        <h2>リストを作成</h2>
        <div class="row"><div class="field"><label>名前</label><input id="l-name" placeholder="例：メルマガ読者"></div><button id="l-add" style="flex:0 0 auto">作成</button></div>
      </div>
      <div class="panel">
        <h2>リスト一覧（${lists.length}）</h2>
        ${lists.length ? `<table><thead><tr><th>ID</th><th>名前</th><th>作成日</th></tr></thead><tbody>
          ${lists.map((l) => `<tr><td>${l.id}</td><td>${esc(l.name)}</td><td class="muted">${new Date(l.created_at).toLocaleDateString("ja-JP")}</td></tr>`).join("")}
        </tbody></table>` : empty("まだリストがありません")}
      </div>`;
    $("#l-add").addEventListener("click", async () => { const name = $("#l-name").value.trim(); if (!name) return; await api("/api/lists", { method: "POST", body: JSON.stringify({ name }) }); toast("作成しました"); views.lists(); });
  },

  async broadcasts() { return renderBroadcasts("email"); },
  async smsBroadcasts() { return renderBroadcasts("sms"); },

  async campaigns() { return renderCampaigns("email"); },
  async smsCampaigns() { return renderCampaigns("sms"); },

  async points() {
    const [rules, balances] = await Promise.all([api("/api/points/rules"), api("/api/points/balances")]);
    view.innerHTML = `
      <div class="panel">
        <h2>🪙 ポイント付与ルール</h2>
        <p class="muted small">読者の行動に応じて自動でポイントが貯まります（同じメール・同じリンクでの重複付与はありません）。変更は次の付与から反映されます。</p>
        <div class="row">
          <div class="field"><label>メール開封で付与</label><input id="pr-open" type="number" min="0" value="${rules.openPoints}"></div>
          <div class="field"><label>リンククリックで付与</label><input id="pr-click" type="number" min="0" value="${rules.clickPoints}"></div>
          <div class="field"><label>新規登録ボーナス</label><input id="pr-signup" type="number" min="0" value="${rules.signupPoints}"></div>
          <button id="pr-save" class="accent" style="flex:0 0 auto">保存</button>
        </div>
      </div>
      <div class="panel">
        <h2>手動でポイントを付与 / 減算</h2>
        <div class="row">
          <div class="field"><label>購読者ID</label><input id="pg-sub" type="number" placeholder="例: 3"></div>
          <div class="field"><label>ポイント（マイナス可）</label><input id="pg-points" type="number" placeholder="例: 100 / -50"></div>
          <div class="field"><label>メモ(任意)</label><input id="pg-note" placeholder="例: キャンペーン特典"></div>
          <button id="pg-grant" style="flex:0 0 auto">実行</button>
        </div>
      </div>
      <div class="panel">
        <h2>ポイント残高ランキング（${balances.length}人）</h2>
        ${balances.length ? `<table><thead><tr><th>ID</th><th>メール</th><th>名前</th><th style="text-align:right">残高</th></tr></thead><tbody>
          ${balances.map((b) => `<tr><td>${b.subscriber_id}</td><td>${esc(b.email)}</td><td>${esc(b.name || "")}</td><td style="text-align:right"><b>${b.balance}</b> pt</td></tr>`).join("")}
        </tbody></table>` : empty("まだポイントを持っている読者がいません。メール配信後、開封・クリックで自動的に貯まります。")}
      </div>`;
    $("#pr-save").addEventListener("click", async () => {
      await api("/api/points/rules", { method: "PUT", body: JSON.stringify({
        openPoints: Number($("#pr-open").value), clickPoints: Number($("#pr-click").value), signupPoints: Number($("#pr-signup").value),
      }) });
      toast("ルールを保存しました");
    });
    $("#pg-grant").addEventListener("click", async () => {
      try {
        const r = await api("/api/points/grant", { method: "POST", body: JSON.stringify({
          subscriberId: Number($("#pg-sub").value), points: Number($("#pg-points").value), note: $("#pg-note").value.trim() || null,
        }) });
        toast(`付与しました（新残高: ${r.balance} pt）`); views.points();
      } catch (e) { toast(e.message); }
    });
  },

  async lotteries() {
    const lotteries = await api("/api/lotteries");
    let detail = "";
    if (lotteries.length) detail = lotteryDetail(await api(`/api/lotteries/${lotteries[0].id}`));
    view.innerHTML = `
      <div class="panel">
        <h2>🎰 抽選会を作成</h2>
        <p class="muted small">読者が貯めたポイントを消費してくじを引けます。景品ごとに当選確率と当選本数上限を設定でき、確率の合計の残りは「はずれ」になります。</p>
        <div class="row">
          <div class="field"><label>名前</label><input id="lo-name" placeholder="例: 夏の大抽選会"></div>
          <div class="field"><label>説明(任意)</label><input id="lo-desc" placeholder="読者に表示されます"></div>
          <div class="field"><label>1回の消費pt</label><input id="lo-cost" type="number" value="100" min="1"></div>
          <button id="lo-add" class="accent" style="flex:0 0 auto">作成</button>
        </div>
      </div>
      <div class="panel"><h2>抽選会一覧（${lotteries.length}）</h2>
        ${lotteries.length ? `<table><thead><tr><th>ID</th><th>名前</th><th>消費pt</th><th>状態</th><th></th></tr></thead><tbody>
          ${lotteries.map((l) => `<tr><td>${l.id}</td><td>${esc(l.name)}</td><td>${l.cost_points}</td><td>${pill(l.status === "open" ? "active" : "canceled")}</td>
            <td><button class="ghost sm" data-lot="${l.id}">詳細</button>
                <button class="ghost sm" data-lot-toggle="${l.id}" data-next="${l.status === "open" ? "closed" : "open"}">${l.status === "open" ? "終了する" : "再開する"}</button></td></tr>`).join("")}
        </tbody></table>` : empty("まだ抽選会がありません")}
      </div>
      <div id="lot-detail">${detail}</div>`;
    $("#lo-add").addEventListener("click", async () => {
      const name = $("#lo-name").value.trim(); if (!name) return toast("名前を入力してください");
      await api("/api/lotteries", { method: "POST", body: JSON.stringify({ name, description: $("#lo-desc").value.trim() || null, cost_points: Number($("#lo-cost").value) || 100 }) });
      toast("作成しました"); views.lotteries();
    });
    bindLottery();
  },

  async exchange() {
    const [items, reds] = await Promise.all([api("/api/exchange/items"), api("/api/exchange/redemptions")]);
    const stLabel = { pending: "手配中", done: "対応済み", canceled: "キャンセル" };
    view.innerHTML = `
      <div class="panel">
        <h2>🎁 交換景品を登録</h2>
        <p class="muted small">読者がポイントと直接交換できる景品です（Amazonギフト券、限定PDF、個別相談など）。交換申請が入ったら下の一覧から「対応済み」にしてください。</p>
        <div class="row">
          <div class="field"><label>景品名</label><input id="ex-name" placeholder="例: Amazonギフト券 500円分"></div>
          <div class="field"><label>説明(任意)</label><input id="ex-desc"></div>
          <div class="field"><label>必要pt</label><input id="ex-cost" type="number" min="1" placeholder="500"></div>
          <div class="field"><label>在庫（空=無制限）</label><input id="ex-stock" type="number" min="0" placeholder=""></div>
          <button id="ex-add" class="accent" style="flex:0 0 auto">登録</button>
        </div>
      </div>
      <div class="panel"><h2>景品一覧（${items.length}）</h2>
        ${items.length ? `<table><thead><tr><th>ID</th><th>景品</th><th>必要pt</th><th>在庫</th><th>交換数</th><th>状態</th><th></th></tr></thead><tbody>
          ${items.map((it) => `<tr><td>${it.id}</td><td>${esc(it.name)}</td><td>${it.cost_points}</td>
            <td>${it.stock < 0 ? "無制限" : it.stock}</td><td>${it.redeemed}</td>
            <td>${pill(it.active ? "active" : "canceled")}</td>
            <td><button class="ghost sm" data-ex-toggle="${it.id}" data-next="${it.active ? "0" : "1"}">${it.active ? "停止" : "再開"}</button></td></tr>`).join("")}
        </tbody></table>` : empty("まだ交換景品がありません")}
      </div>
      <div class="panel"><h2>交換申請（${reds.length}）</h2>
        ${reds.length ? `<table><thead><tr><th>日時</th><th>読者</th><th>景品</th><th>消費pt</th><th>状態</th><th></th></tr></thead><tbody>
          ${reds.map((r) => `<tr><td class="muted small">${new Date(r.created_at).toLocaleString("ja-JP")}</td>
            <td>${esc(r.email)}</td><td>${esc(r.item_name)}</td><td>${r.points_spent}</td>
            <td>${pill(r.status === "done" ? "sent" : r.status === "canceled" ? "canceled" : "scheduled")} <span class="muted small">${stLabel[r.status] || ""}</span></td>
            <td>${r.status === "pending" ? `<button class="ghost sm" data-red-done="${r.id}">対応済みにする</button>
              <button class="ghost sm" data-red-cancel="${r.id}">キャンセル(pt返却)</button>` : ""}</td></tr>`).join("")}
        </tbody></table>` : empty("まだ交換申請はありません")}
      </div>`;
    $("#ex-add").addEventListener("click", async () => {
      const name = $("#ex-name").value.trim(); const cost = Number($("#ex-cost").value);
      if (!name || !cost) return toast("景品名と必要ptを入力してください");
      const stockRaw = $("#ex-stock").value.trim();
      await api("/api/exchange/items", { method: "POST", body: JSON.stringify({
        name, description: $("#ex-desc").value.trim() || null, cost_points: cost,
        stock: stockRaw === "" ? -1 : Number(stockRaw),
      }) });
      toast("登録しました"); views.exchange();
    });
    view.querySelectorAll("[data-ex-toggle]").forEach((b) => b.addEventListener("click", async () => {
      await api(`/api/exchange/items/${b.dataset.exToggle}`, { method: "PUT", body: JSON.stringify({ active: b.dataset.next === "1" }) });
      views.exchange();
    }));
    view.querySelectorAll("[data-red-done]").forEach((b) => b.addEventListener("click", async () => {
      await api(`/api/exchange/redemptions/${b.dataset.redDone}/status`, { method: "POST", body: JSON.stringify({ status: "done" }) });
      toast("対応済みにしました"); views.exchange();
    }));
    view.querySelectorAll("[data-red-cancel]").forEach((b) => b.addEventListener("click", async () => {
      if (!confirm("キャンセルしてポイントを返却しますか？")) return;
      await api(`/api/exchange/redemptions/${b.dataset.redCancel}/status`, { method: "POST", body: JSON.stringify({ status: "canceled" }) });
      toast("キャンセルし、ポイントを返却しました"); views.exchange();
    }));
  },

  async hooks() {
    const [hooks, lists, camps] = await Promise.all([api("/api/hooks"), api("/api/lists"), api("/api/campaigns")]);
    const origin = location.origin;
    view.innerHTML = `
      <div class="panel">
        <h2>連携リンクを発行</h2>
        <p class="muted small">外部ツール(LP・フォーム・決済など)からこのURLを叩く/フォームを埋め込むと、購読者登録＋（指定すれば）ステップ配信が自動で始まります。</p>
        <div class="row">
          <div class="field"><label>名前</label><input id="h-name" placeholder="例：LP登録"></div>
          <div class="field"><label>追加先リスト</label><select id="h-list">${lists.map((l) => `<option value="${l.id}">${esc(l.name)}</option>`).join("")}</select></div>
          <div class="field"><label>開始するステップ施策(任意)</label><select id="h-camp"><option value="">—</option>${camps.map((c) => `<option value="${c.id}">${esc(c.name)}</option>`).join("")}</select></div>
          <button id="h-add" style="flex:0 0 auto">発行</button>
        </div>
      </div>
      <div class="panel"><h2>発行済みリンク（${hooks.length}）</h2>
        ${hooks.length ? hooks.map((h) => `<div style="margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid var(--line)">
          <strong>${esc(h.name || "(無題)")}</strong>
          <div class="muted small" style="margin:8px 0 4px">① 汎用 Webhook 受信URL（POST {email,name?}）</div>
          <div class="codebox">${origin}/api/hooks/${esc(h.token)}</div>
          <div class="muted small" style="margin:8px 0 4px">② 埋め込み登録フォーム（iframe可）</div>
          <div class="codebox">${origin}/f/${esc(h.token)}</div>
          <div class="muted small" style="margin:12px 0 4px">③ LINE Harness 連携（Outgoing Webhook / Automation send_webhook 用）</div>
          <div class="codebox">${origin}/api/integrations/line/${esc(h.token)}</div>
          <div class="muted small" style="margin:4px 0 0;color:var(--muted)">LINE の form_submitted 等で data.email / data.fields.email を含めてPOST</div>
          <div class="muted small" style="margin:8px 0 4px">④ X Harness 連携（Engagement Gate / Step Sequence 完了時用）</div>
          <div class="codebox">${origin}/api/integrations/x/${esc(h.token)}</div>
          <div class="muted small" style="margin:4px 0 0;color:var(--muted)">X Harness から { event, email, xUsername } をPOST</div>
        </div>`).join("") : empty("まだ連携リンクがありません")}
      </div>`;
    $("#h-add").addEventListener("click", async () => { await api("/api/hooks", { method: "POST", body: JSON.stringify({ name: $("#h-name").value.trim() || null, list_id: Number($("#h-list").value), campaign_id: Number($("#h-camp").value) || null }) }); toast("発行しました"); views.hooks(); });
  },
};

// ---------- 一斉配信（単発）: メール/SMS 共通の描画 ----------
// ch = "email" | "sms"。同じ画面をチャネル違いで使い回す。
async function renderBroadcasts(ch) {
  const isSms = ch === "sms";
  const [all, lists] = await Promise.all([api("/api/broadcasts"), api("/api/lists")]);
  const bcs = all.filter((b) => (b.channel || "email") === ch);
  const back = isSms ? "smsBroadcasts" : "broadcasts";
  view.innerHTML = `
    <div class="panel">
      <h2>${isSms ? "📱 新しいSMS一斉配信" : "📮 新しいメール一斉配信"}</h2>
      <p class="muted small">${isSms
        ? "登録読者のうち<b>電話番号があり「SMS受信に同意」した人だけ</b>に、1回かぎりのSMSを送ります。同意がない人は自動でスキップされます。"
        : "リストの読者全員に1回かぎりのメールを送ります。配信停止リンクと送信者表示は自動で付きます。"}</p>
      <div class="field"><label>配信名（管理用）</label><input id="b-name" placeholder="管理用の名前"></div>
      <div class="field"><label>${isSms ? "件名（管理用。SMSには入りません）" : "件名"}</label><input id="b-subject" placeholder="${isSms ? "例: 8/10 セミナー前日リマインド" : "メールの件名"}"></div>
      <div class="field"><label>対象リスト</label><select id="b-list">${lists.map((l) => `<option value="${l.id}">${esc(l.name)}</option>`).join("")}</select></div>
      ${isSms
        ? `<div class="field"><label>SMS本文（文字だけ。送信者表示と配信停止URLは自動で付きます）</label>
             <textarea id="b-sms" rows="4" placeholder="例: 明日20時からセミナーです。お待ちしています。"></textarea>
             <p class="muted small" id="b-sms-count"></p></div>`
        : `<div class="field"><label>本文(HTML可)</label><textarea id="b-body" rows="5" placeholder="本文。配信停止リンクと送信者表示は自動で付きます。"></textarea></div>`}
      <div style="margin-top:10px"><button id="b-add" class="accent">下書き作成</button></div>
    </div>
    <div class="panel">
      <h2>${isSms ? "SMS配信一覧" : "メール配信一覧"}（${bcs.length}）</h2>
      ${bcs.length ? `<table><thead><tr><th>名前</th><th>${isSms ? "本文" : "件名"}</th><th>状態</th><th>承認</th><th>予約時刻</th><th></th></tr></thead><tbody>
        ${bcs.map((b) => `<tr>
          <td>${esc(b.name)}</td>
          <td class="muted">${esc(isSms ? (b.body_text || "").slice(0, 40) : b.subject)}${isSms && (b.body_text || "").length > 40 ? "…" : ""}</td>
          <td>${pill(b.status)}</td>
          <td>${b.approved ? "✅" : "—"}</td>
          <td class="muted small">${b.scheduled_at ? new Date(b.scheduled_at).toLocaleString("ja-JP") : "—"}</td>
          <td>${actionBtns(b)}</td></tr>`).join("")}
      </tbody></table>` : empty("まだ配信がありません。上から下書きを作成してください。")}
    </div>`;

  if (isSms) {
    const box = $("#b-sms"), cnt = $("#b-sms-count");
    const sync = () => { const r = smsCounterText(box.value); cnt.textContent = r.text; cnt.style.color = r.over ? "#b91c1c" : ""; };
    box.addEventListener("input", sync); sync();
  }
  $("#b-add").addEventListener("click", async () => {
    const body = {
      name: $("#b-name").value.trim(),
      subject: $("#b-subject").value.trim(),
      list_id: Number($("#b-list").value),
      channel: ch,
    };
    if (!body.name || !body.subject || !body.list_id) return toast("配信名・件名・対象リストを入力してください");
    if (isSms) {
      body.body_text = $("#b-sms").value;
      if (!body.body_text.trim()) return toast("SMSの本文を入力してください");
      if (smsCounterText(body.body_text).over) return toast("SMSが長すぎます。本文を短くしてください");
    } else {
      body.body_html = $("#b-body").value;
      if (!body.body_html) return toast("本文を入力してください");
    }
    try {
      await api("/api/broadcasts", { method: "POST", body: JSON.stringify(body) });
      toast("下書きを作成しました"); views[back]();
    } catch (e) { toast(e.message); }
  });
  view.querySelectorAll("[data-bc]").forEach((b) => b.addEventListener("click", () => bcAction(b.dataset.act2, Number(b.dataset.bc), back)));
}

// ---------- ステップ配信: メール/SMS 共通の描画 ----------
// シナリオは主チャネル(channel)で振り分ける。実際に相手チャネルのステップを含む
// 「混在」シナリオは、両方の一覧にバッジ付きで出す（見失わないようにするため）。
async function renderCampaigns(ch) {
  const isSms = ch === "sms";
  const [all, lists, hooks] = await Promise.all([
    api("/api/campaigns"),
    api("/api/lists"),
    api("/api/hooks"),
  ]);
  // シナリオごとの登録フォーム。1つの連携リンクで3種類のフォームを出し分ける
  //   ・メール登録    → /f/:token           （メールアドレスのみ）
  //   ・SMS登録       → /f/:token?sms=only  （電話番号のみ・メールは聞かない）
  //   ・メール＋SMS   → /f/:token?sms=1     （両方。混在シナリオ用）
  // 表示するのは「そのシナリオが実際に使うチャネル」のフォームだけにする。
  const hookOf = (cid) => hooks.find((h) => h.campaign_id === cid) || null;
  const linkRow = (label, url) =>
    `<div><a href="${url}" target="_blank" rel="noopener">${label}</a>
       <button class="ghost sm" data-copy="${url}">コピー</button></div>`;
  const formCell = (c) => {
    const h = hookOf(c.id);
    if (!h)
      return `<button class="ghost sm" data-mkform="${c.id}" data-list="${c.list_id}" data-name="${esc(c.name)}">＋ 登録フォームを作る</button>`;
    const base = `${location.origin}/f/${h.token}`;
    const n = c.channels || { email: 0, sms: 0 };
    const mixed = n.email > 0 && n.sms > 0;
    const rows = [];
    if (mixed) {
      // メールとSMSを両方使うシナリオは、両方を1回で集められるフォームを出す
      rows.push(linkRow("📧📱 メール＋SMS", `${base}?sms=1`));
    } else if (isSms) {
      rows.push(linkRow("📱 SMS登録フォーム", `${base}?sms=only`));
    } else {
      rows.push(linkRow("📧 メール登録フォーム", base));
    }
    return `<div class="form-links">${rows.join("")}</div>`;
  };
  const inView = (c) => {
    const n = c.channels || { email: 0, sms: 0 };
    return (c.channel || "email") === ch || n[ch] > 0;
  };
  const camps = all.filter(inView);
  const back = isSms ? "smsCampaigns" : "campaigns";
  campState.channel = ch;
  if (campState.cid && !camps.find((c) => c.id === campState.cid)) campState.cid = null;
  if (!campState.cid && camps.length) campState.cid = camps[0].id;
  view.innerHTML = `
    <div class="panel">
      <h2>${isSms ? "📲 SMSシナリオを作成" : "🪜 メールシナリオを作成"}</h2>
      <p class="muted small">${isSms
        ? "登録した読者に、SMSを順番に自動送信します。<b>電話番号があり「SMS受信に同意」した人だけ</b>が対象です。1通ごとに料金がかかるので、通数と金額を確認しながら作ってください。"
        : "登録した読者に、メールを順番に自動送信します。"}
        あとから各ステップの配信方法を切り替えれば、1つのシナリオにメールとSMSを混ぜることもできます。</p>
      <div class="row"><div class="field"><label>シナリオ名</label><input id="c-name" placeholder="${isSms ? "例：SMS配信テスト" : "例：登録後7日間講座"}"></div>
      <div class="field"><label>対象リスト</label><select id="c-list">${lists.map((l) => `<option value="${l.id}">${esc(l.name)}</option>`).join("")}</select></div>
      <button id="c-add" class="accent" style="flex:0 0 auto">作成</button></div>
    </div>
    <div class="panel"><h2>シナリオ一覧（${camps.length}）</h2>
      ${camps.length ? `<table><thead><tr><th>名前</th><th>内訳</th><th>状態</th><th></th><th>読者の登録フォーム</th></tr></thead><tbody>
        ${camps.map((c) => {
          const n = c.channels || { email: 0, sms: 0 };
          const mixed = n.email > 0 && n.sms > 0;
          return `<tr class="${c.id === campState.cid ? "row-active" : ""}">
            <td>${esc(c.name)} ${mixed ? '<span class="pill scheduled">混在</span>' : ""}</td>
            <td class="muted small">📧${n.email} / 📱${n.sms}</td>
            <td>${pill(c.status)}</td>
            <td><button class="ghost sm" data-camp="${c.id}">開く</button>
                <button class="ghost sm" data-camp-toggle="${c.id}" data-next="${c.status === "active" ? "paused" : "active"}">${c.status === "active" ? "⏸ 一時停止" : "▶ 再開"}</button></td>
            <td>${formCell(c)}</td></tr>`;
        }).join("")}
      </tbody></table>
      <p class="muted small" style="margin-top:10px">このURLを開いた読者が登録すると、そのシナリオの配信が自動で始まります。${isSms
        ? "<b>SMS登録フォーム</b>では<b>電話番号だけ</b>を聞きます（メールアドレスは不要）。「SMS受信に同意」のチェックが必須なので、同意した人だけが登録されます。"
        : "<b>メール登録フォーム</b>ではメールアドレスを聞きます。"}
        メールとSMSを混ぜたシナリオには、両方を一度に集められるフォームが出ます。</p>`
      : empty(isSms ? "まだSMSシナリオがありません。上から作成してください。" : "まだメールシナリオがありません。上から作成してください。")}
    </div>
    <div id="camp-detail">${empty("読み込み中…")}</div>`;
  $("#c-add").addEventListener("click", async () => {
    const name = $("#c-name").value.trim();
    if (!name) return toast("シナリオ名を入力してください");
    const r = await api("/api/campaigns", { method: "POST", body: JSON.stringify({ name, list_id: Number($("#c-list").value), channel: ch }) });
    campState.cid = r.campaign.id;
    toast("シナリオを作成しました。ステップを追加しましょう"); views[back]();
  });
  view.querySelectorAll("[data-camp]").forEach((b) => b.addEventListener("click", () => { campState.cid = Number(b.dataset.camp); campState.editing = null; views[back](); }));
  view.querySelectorAll("[data-camp-toggle]").forEach((b) => b.addEventListener("click", async () => {
    await api(`/api/campaigns/${b.dataset.campToggle}`, { method: "PUT", body: JSON.stringify({ status: b.dataset.next }) });
    toast(b.dataset.next === "paused" ? "一時停止しました（配信は止まります）" : "再開しました"); views[back]();
  }));
  // 登録フォームがまだ無いシナリオ用: その場で連携リンクを発行する
  view.querySelectorAll("[data-mkform]").forEach((b) => b.addEventListener("click", async () => {
    try {
      await api("/api/hooks", { method: "POST", body: JSON.stringify({
        name: `${b.dataset.name} 登録フォーム`,
        list_id: Number(b.dataset.list),
        campaign_id: Number(b.dataset.mkform),
      }) });
      toast("登録フォームを作りました"); views[back]();
    } catch (e) { toast(e.message); }
  }));
  view.querySelectorAll("[data-copy]").forEach((b) => b.addEventListener("click", async () => {
    try { await navigator.clipboard.writeText(b.dataset.copy); toast("URLをコピーしました"); }
    catch { toast("コピーできませんでした。リンクを右クリックしてコピーしてください"); }
  }));
  if (campState.cid) await renderCampaignDetail(campState.cid);
  else $("#camp-detail").innerHTML = "";
}

function actionBtns(b) {
  const btns = [];
  if (b.status === "draft" || b.status === "scheduled") {
    if (!b.approved) btns.push(`<button class="ghost sm" data-bc="${b.id}" data-act2="approve">承認</button>`);
    btns.push(`<button class="ghost sm" data-bc="${b.id}" data-act2="schedule">即時予約</button>`);
  }
  if (b.status === "scheduled") btns.push(`<button class="ghost sm" data-bc="${b.id}" data-act2="cancel">中止</button>`);
  return btns.join(" ");
}
async function bcAction(act, id, back = "broadcasts") {
  try {
    if (act === "schedule") await api(`/api/broadcasts/${id}/schedule`, { method: "POST", body: JSON.stringify({}) });
    else await api(`/api/broadcasts/${id}/${act}`, { method: "POST" });
    toast("更新しました"); views[back]();
  } catch (e) { toast(e.message); }
}

function lotteryDetail(d) {
  const misc = Math.max(0, 100 - d.totalProbability);
  return `<div class="panel">
    <h2>「${esc(d.lottery.name)}」の景品設定 <span class="muted small">（1回 ${d.lottery.cost_points} pt / はずれ率 ${misc.toFixed(1)}%）</span></h2>
    <div class="row" style="margin-bottom:14px">
      <div class="field"><label>景品名</label><input id="pz-name" placeholder="例: Amazonギフト券1000円"></div>
      <div class="field"><label>当選確率(%)</label><input id="pz-prob" type="number" min="0.1" step="0.1" placeholder="5"></div>
      <div class="field"><label>当選本数上限（空=無制限）</label><input id="pz-max" type="number" min="1" placeholder=""></div>
      <button id="pz-add" data-lid="${d.lottery.id}" style="flex:0 0 auto">景品を追加</button>
    </div>
    ${d.prizes.length ? `<table><thead><tr><th>景品</th><th>確率</th><th>当選数/上限</th><th>状態</th><th></th></tr></thead><tbody>
      ${d.prizes.map((p) => `<tr><td>${esc(p.name)}</td><td>${p.probability}%</td>
        <td>${p.winners} / ${p.max_winners || "∞"}</td>
        <td>${pill(p.active ? "active" : "canceled")}</td>
        <td><button class="ghost sm" data-pz-toggle="${p.id}" data-lid="${d.lottery.id}" data-next="${p.active ? "0" : "1"}">${p.active ? "無効化" : "有効化"}</button></td></tr>`).join("")}
    </tbody></table>` : empty("景品が未設定です。景品を追加すると読者がくじを引けるようになります。")}
    <h2 style="margin-top:22px">抽選履歴（直近${d.draws.length}件）</h2>
    ${d.draws.length ? `<table><thead><tr><th>日時</th><th>読者ID</th><th>結果</th><th>発送</th><th></th></tr></thead><tbody>
      ${d.draws.map((x) => {
        const prize = d.prizes.find((p) => p.id === x.prize_id);
        return `<tr><td class="muted small">${new Date(x.drawn_at).toLocaleString("ja-JP")}</td><td>${x.subscriber_id}</td>
        <td>${x.prize_id ? `🏆 ${esc(prize ? prize.name : "#" + x.prize_id)}` : '<span class="muted">はずれ</span>'}</td>
        <td>${x.prize_id ? (x.fulfilled ? "✅ 済" : "⏳ 未") : "—"}</td>
        <td>${x.prize_id && !x.fulfilled ? `<button class="ghost sm" data-draw-done="${x.id}" data-lid="${d.lottery.id}">発送済みにする</button>` : ""}</td></tr>`;
      }).join("")}
    </tbody></table>` : empty("まだ抽選されていません")}
  </div>`;
}
function bindLottery() {
  const reload = async (lid) => { $("#lot-detail").innerHTML = lotteryDetail(await api(`/api/lotteries/${lid}`)); bindLottery(); };
  view.querySelectorAll("[data-lot]").forEach((b) => b.addEventListener("click", () => reload(b.dataset.lot)));
  view.querySelectorAll("[data-lot-toggle]").forEach((b) => b.addEventListener("click", async () => {
    await api(`/api/lotteries/${b.dataset.lotToggle}`, { method: "PUT", body: JSON.stringify({ status: b.dataset.next }) });
    toast(b.dataset.next === "closed" ? "抽選会を終了しました" : "抽選会を再開しました"); views.lotteries();
  }));
  const add = document.querySelector("#pz-add");
  if (add) add.addEventListener("click", async () => {
    try {
      const maxRaw = $("#pz-max").value.trim();
      await api(`/api/lotteries/${add.dataset.lid}/prizes`, { method: "POST", body: JSON.stringify({
        name: $("#pz-name").value.trim(), probability: Number($("#pz-prob").value),
        max_winners: maxRaw === "" ? 0 : Number(maxRaw),
      }) });
      toast("景品を追加しました"); reload(add.dataset.lid);
    } catch (e) { toast(e.message); }
  });
  document.querySelectorAll("[data-pz-toggle]").forEach((b) => b.addEventListener("click", async () => {
    await api(`/api/prizes/${b.dataset.pzToggle}`, { method: "PUT", body: JSON.stringify({ active: b.dataset.next === "1" }) });
    reload(b.dataset.lid);
  }));
  document.querySelectorAll("[data-draw-done]").forEach((b) => b.addEventListener("click", async () => {
    await api(`/api/draws/${b.dataset.drawDone}/fulfilled`, { method: "POST", body: JSON.stringify({ fulfilled: true }) });
    toast("発送済みにしました"); reload(b.dataset.lid);
  }));
}

// ---------- シナリオビルダー（ステップメール詳細） ----------
// channel: いま開いている画面（"email" | "sms"）。新規ステップの既定チャネルにも使う
const campState = { cid: null, editing: null, channel: "email" };

// ---------- SMS: 文字数・通数・概算コスト（core.ts と同じ換算） ----------
// 70文字までは1通、以降は66文字ごとに1通。改行は2文字として数える。
let SMS_COST = 10; // 1通あたりの円単価（/api/stats の値で上書きされる）
let SMS_ENABLED = false; // 実送信できる状態か（表示用）
// 自動付与される「送信者表示＋配信停止URL」の文字数。
// サーバが実測値（/api/stats の sms.footerChars）を返すので、それで上書きする。
// 停止URLは自ドメイン＋32文字のトークンで、単独でも70文字を超えることがある。
let SMS_FOOTER_EST = 75;
const SMS_MAX = 660;
// ダッシュボードを経由せずシナリオ画面を直接開いた場合に備えて、前回の実測値を復元する
try {
  const m = JSON.parse(localStorage.getItem("smsMeta") || "null");
  if (m) { SMS_COST = m.cost || SMS_COST; SMS_FOOTER_EST = m.footer || SMS_FOOTER_EST; }
} catch { /* 壊れていたら既定値のまま */ }
const smsCharCount = (t) => { let n = 0; for (const ch of t) n += ch === "\n" ? 2 : 1; return n; };
const smsSegments = (len) => (len <= 0 ? 0 : len <= 70 ? 1 : 1 + Math.ceil((len - 70) / 66));
/** 入力中のSMS本文から「◯文字 = ◯通 ≒ ◯円」の案内文を作る */
function smsCounterText(bodyText) {
  const bodyLen = smsCharCount(bodyText);
  const total = bodyLen + SMS_FOOTER_EST;
  const seg = smsSegments(total);
  const over = total > SMS_MAX;
  const warn = total > 330;
  // 次のセグメントに繰り上がるまであと何文字書けるか（1通に収めたい人向けの目安）
  const nextAt = seg <= 1 ? 70 : 70 + (seg - 1) * 66;
  const room = nextAt - total;
  return {
    over,
    text: over
      ? `⚠ 長すぎます（本文${bodyLen}文字＋自動付与${SMS_FOOTER_EST}文字＝${total}文字 / 上限${SMS_MAX}文字）`
      : `本文${bodyLen}文字＋自動付与${SMS_FOOTER_EST}文字（送信者表示・配信停止URL）＝${total}文字 → ${seg}通 ≒ ${seg * SMS_COST}円/人`
        + (seg > 0 ? `　あと${room}文字で${seg + 1}通になります` : "")
        + (warn ? "　⚠ 330文字を超えるとau宛で遅延することがあります" : ""),
  };
}

const JST_OFFSET = 9 * 3600 * 1000;
/** サーバ側 computeStepSendAt と同じ計算（配信予定プレビュー用） */
function simSendAt(step, base) {
  if (step.timing_type === "immediate") return base;
  if (step.timing_type === "day_time" && step.send_time) {
    const [h, m] = step.send_time.split(":").map(Number);
    const jst = new Date(base + JST_OFFSET);
    let at = Date.UTC(jst.getUTCFullYear(), jst.getUTCMonth(), jst.getUTCDate() + (step.delay_days || 0), h, m) - JST_OFFSET;
    if (at <= base) at += 86400000;
    return at;
  }
  return base + (step.delay_minutes || 0) * 60000;
}
function timingLabel(step, isFirst) {
  const baseName = isFirst ? "登録" : "前の配信";
  if (step.timing_type === "immediate") return `${baseName}の直後`;
  if (step.timing_type === "day_time")
    return `${baseName}から${step.delay_days === 0 ? "当日" : `${step.delay_days}日後`}の ${esc(step.send_time || "")}（過ぎていたら翌日）`;
  const mins = step.delay_minutes || 0;
  const d = Math.floor(mins / 1440), h = Math.floor((mins % 1440) / 60), m = mins % 60;
  const parts = [];
  if (d) parts.push(`${d}日`);
  if (h) parts.push(`${h}時間`);
  if (m || !parts.length) parts.push(`${m}分`);
  return `${baseName}から ${parts.join("")}後`;
}
const fmtDt = (ts) => new Date(ts).toLocaleString("ja-JP", { month: "numeric", day: "numeric", weekday: "short", hour: "2-digit", minute: "2-digit" });

function stepEditorHtml(steps) {
  const editing = campState.editing ? steps.find((s) => s.id === campState.editing) : null;
  // 新規追加のときは、いま開いている画面のチャネル（メール/SMS）を既定にする
  const s = editing || { timing_type: steps.length ? "relative" : "immediate", delay_minutes: 60, delay_days: 1, send_time: "09:00", subject: "", body_html: "", channel: campState.channel || "email", body_text: "" };
  const isSms = (s.channel || "email") === "sms";
  const mins = s.delay_minutes || 0;
  const dd = Math.floor(mins / 1440), hh = Math.floor((mins % 1440) / 60), mm = mins % 60;
  const idx = editing ? steps.findIndex((x) => x.id === editing.id) : steps.length;
  return `
    <h2 id="st-form-title">${editing ? `✏️ STEP ${idx + 1} を編集` : "➕ ステップを追加"}</h2>
    <div class="field"><label>配信タイミング（${idx === 0 ? "読者の登録" : "前のステップの配信"}が基準）</label>
      <div class="timing-row">
        <select id="st-type">
          <option value="immediate" ${s.timing_type === "immediate" ? "selected" : ""}>直後に送る</option>
          <option value="relative" ${s.timing_type === "relative" ? "selected" : ""}>◯日◯時間◯分後に送る</option>
          <option value="day_time" ${s.timing_type === "day_time" ? "selected" : ""}>◯日後の指定時刻に送る</option>
        </select>
        <span id="st-relative" class="timing-inputs ${s.timing_type === "relative" ? "" : "hidden"}">
          <input id="st-rd" type="number" min="0" value="${dd}"><label>日</label>
          <input id="st-rh" type="number" min="0" max="23" value="${hh}"><label>時間</label>
          <input id="st-rm" type="number" min="0" max="59" value="${mm}"><label>分後</label>
        </span>
        <span id="st-daytime" class="timing-inputs ${s.timing_type === "day_time" ? "" : "hidden"}">
          <input id="st-dd" type="number" min="0" value="${s.delay_days ?? 1}"><label>日後の</label>
          <input id="st-time" type="time" value="${esc(s.send_time || "09:00")}"><label>（日本時間）</label>
        </span>
      </div>
      <p class="muted small">「0日後の指定時刻」がすでに過ぎている場合は翌日の同時刻に送られます（勝手に即時配信はしません）。</p>
    </div>
    <div class="field"><label>配信方法</label>
      <select id="st-channel">
        <option value="email" ${isSms ? "" : "selected"}>📧 メール</option>
        <option value="sms" ${isSms ? "selected" : ""}>📱 SMS（ショートメッセージ）</option>
      </select>
      <p class="muted small">SMSは電話番号を登録し「SMS受信に同意」した読者にだけ送られます（同意がない人は自動でスキップされ、シナリオは次へ進みます）。</p>
    </div>
    <div class="field"><label>${isSms ? "ステップ名（管理画面での表示用。SMSには入りません）" : "件名"}</label><input id="st-subject" value="${esc(s.subject)}" placeholder="${isSms ? "例: 3日目のリマインドSMS" : "メールの件名"}"></div>
    <div class="field ${isSms ? "hidden" : ""}" id="st-body-email"><label>本文（HTML可。配信停止リンクと送信者表示は自動付与）</label><textarea id="st-body" rows="6" placeholder="本文">${esc(s.body_html)}</textarea></div>
    <div class="field ${isSms ? "" : "hidden"}" id="st-body-sms"><label>SMS本文（文字だけ。送信者表示と配信停止URLは自動で付きます）</label>
      <textarea id="st-sms" rows="4" placeholder="例: 明日20時からセミナーです。お待ちしています。">${esc(s.body_text || "")}</textarea>
      <p class="muted small" id="st-sms-count"></p>
    </div>
    <div style="margin-top:10px">
      <button id="st-save" class="accent">${editing ? "保存する" : "この内容でステップを追加"}</button>
      ${editing ? `<button id="st-cancel" class="ghost">キャンセル</button>` : ""}
    </div>`;
}

async function renderCampaignDetail(cid) {
  const d = await api(`/api/campaigns/${cid}`);
  const steps = d.steps;
  // 「いま登録した場合」の配信予定シミュレーション
  const sims = [];
  let t = Date.now();
  for (let i = 0; i < steps.length; i++) { t = simSendAt(steps[i], t); sims.push(t); }
  const enr = d.enrollments;
  const el = $("#camp-detail");
  el.innerHTML = `
    <div class="panel">
      <div class="camp-head">
        <h2>🪜 ${esc(d.campaign.name)} ${pill(d.campaign.status)}</h2>
        <div>
          <button class="ghost sm" id="camp-rename">名称変更</button>
          <button class="ghost sm" id="camp-toggle" data-next="${d.campaign.status === "active" ? "paused" : "active"}">${d.campaign.status === "active" ? "⏸ 一時停止" : "▶ 再開"}</button>
        </div>
      </div>
      <p class="muted small">登録読者 ${enr.length} 人（配信中 ${enr.filter((e) => e.status === "active").length} / 完了 ${enr.filter((e) => e.status === "completed").length} / 停止 ${enr.filter((e) => e.status === "canceled").length}）</p>
      ${steps.length ? `<div class="timeline">
        ${steps.map((s, i) => `
        <div class="step-card ${campState.editing === s.id ? "editing" : ""}">
          <div class="step-rail"><div class="step-num">${i + 1}</div>${i < steps.length - 1 ? '<div class="step-line"></div>' : ""}</div>
          <div class="step-main">
            <div class="step-timing">🕐 ${timingLabel(s, i === 0)} <span class="muted small">｜ いま登録なら → ${fmtDt(sims[i])}</span></div>
            <div class="step-subject">${(s.channel || "email") === "sms" ? "📱 " : "📧 "}${esc(s.subject)}${(s.channel || "email") === "sms" ? ` <span class="muted small">SMS ${smsSegments(smsCharCount(s.body_text || "") + SMS_FOOTER_EST)}通 ≒ ${smsSegments(smsCharCount(s.body_text || "") + SMS_FOOTER_EST) * SMS_COST}円/人</span>` : ""}</div>
            <div class="muted small">送信記録 ${d.sendCounts[s.id] || 0} 件</div>
            <div class="step-actions">
              <button class="ghost sm" data-move="${s.id}" data-dir="-1" ${i === 0 ? "disabled" : ""}>∧</button>
              <button class="ghost sm" data-move="${s.id}" data-dir="1" ${i === steps.length - 1 ? "disabled" : ""}>∨</button>
              <button class="ghost sm" data-edit="${s.id}">✏️ 編集</button>
              <button class="ghost sm" data-dup="${s.id}">⧉ 複製</button>
              <button class="ghost sm" data-preview="${s.id}">👁 プレビュー</button>
              <button class="ghost sm" data-test="${s.id}">✈ テスト送信</button>
              <button class="ghost sm danger" data-del="${s.id}">🗑 削除</button>
            </div>
            <div class="step-preview hidden" data-preview-box="${s.id}"><iframe sandbox="" data-preview-frame="${s.id}"></iframe></div>
          </div>
        </div>`).join("")}
      </div>` : empty("まだステップがありません。下のフォームから1通目を追加してください。")}
    </div>
    <div class="panel" id="st-form">${stepEditorHtml(steps)}</div>
    <div class="panel">
      <h2>読者をこのシナリオに登録</h2>
      <p class="muted small">登録した瞬間からステップ1のタイミングでカウントが始まります。連携リンク（フォーム/Webhook）からの自動登録も可能です。</p>
      <div class="row">
        <div class="field"><label>メールアドレス</label><input id="en-email" type="email" placeholder="test@example.com"></div>
        <button id="en-add" style="flex:0 0 auto">登録して配信開始</button>
      </div>
    </div>`;

  const reload = () => views[campState.channel === "sms" ? "smsCampaigns" : "campaigns"]();
  const detailReload = () => renderCampaignDetail(cid);

  $("#camp-rename").addEventListener("click", async () => {
    const name = prompt("新しいシナリオ名", d.campaign.name);
    if (!name || !name.trim()) return;
    await api(`/api/campaigns/${cid}`, { method: "PUT", body: JSON.stringify({ name: name.trim() }) });
    toast("名称を変更しました"); reload();
  });
  $("#camp-toggle").addEventListener("click", async () => {
    await api(`/api/campaigns/${cid}`, { method: "PUT", body: JSON.stringify({ status: $("#camp-toggle").dataset.next }) });
    toast("更新しました"); reload();
  });

  // 並べ替え（∧∨）
  el.querySelectorAll("[data-move]").forEach((b) => b.addEventListener("click", async () => {
    const ids = steps.map((s) => s.id);
    const i = ids.indexOf(Number(b.dataset.move));
    const j = i + Number(b.dataset.dir);
    if (j < 0 || j >= ids.length) return;
    [ids[i], ids[j]] = [ids[j], ids[i]];
    await api(`/api/campaigns/${cid}/steps/reorder`, { method: "POST", body: JSON.stringify({ ids }) });
    toast("並べ替えました"); detailReload();
  }));
  el.querySelectorAll("[data-edit]").forEach((b) => b.addEventListener("click", () => {
    campState.editing = Number(b.dataset.edit); detailReload().then(() => $("#st-form").scrollIntoView({ behavior: "smooth" }));
  }));
  el.querySelectorAll("[data-dup]").forEach((b) => b.addEventListener("click", async () => {
    const s = steps.find((x) => x.id === Number(b.dataset.dup));
    await api(`/api/campaigns/${cid}/steps`, { method: "POST", body: JSON.stringify({
      subject: `${s.subject}（コピー）`, body_html: s.body_html,
      timing_type: s.timing_type, delay_minutes: s.delay_minutes, delay_days: s.delay_days, send_time: s.send_time,
      channel: s.channel || "email", body_text: s.body_text,
    }) });
    toast("末尾に複製しました"); detailReload();
  }));
  el.querySelectorAll("[data-del]").forEach((b) => b.addEventListener("click", async () => {
    const s = steps.find((x) => x.id === Number(b.dataset.del));
    if (!confirm(`STEP「${s.subject}」を削除しますか？\n（配信途中の読者がいる場合、以降の順番が繰り上がります）`)) return;
    await api(`/api/campaigns/${cid}/steps/${s.id}`, { method: "DELETE" });
    if (campState.editing === s.id) campState.editing = null;
    toast("削除しました"); detailReload();
  }));
  el.querySelectorAll("[data-preview]").forEach((b) => b.addEventListener("click", () => {
    const sid = b.dataset.preview;
    const box = el.querySelector(`[data-preview-box="${sid}"]`);
    box.classList.toggle("hidden");
    if (!box.classList.contains("hidden")) {
      const s = steps.find((x) => x.id === Number(sid));
      // SMSは本文がプレーンテキストなので、届く見た目に近い形で表示する
      el.querySelector(`[data-preview-frame="${sid}"]`).srcdoc =
        (s.channel || "email") === "sms"
          ? `<div style="font-family:sans-serif;font-size:14px;line-height:1.7;white-space:pre-wrap;background:#e9edf3;padding:12px">
               <div style="background:#fff;border-radius:12px;padding:12px 14px;max-width:420px">${esc(s.body_text || "")}
【送信者表示】
配信停止 ${location.origin}/u/（読者ごとのURL）</div>
               <p style="color:#667;font-size:12px;margin-top:10px">※ 送信者表示と配信停止URLは送信時に自動で付きます</p></div>`
          : s.body_html;
    }
  }));
  el.querySelectorAll("[data-test]").forEach((b) => b.addEventListener("click", async () => {
    const step = steps.find((x) => String(x.id) === b.dataset.test);
    try {
      if (step && (step.channel || "email") === "sms") {
        const phone = prompt("テスト送信先の携帯番号（例 090-1234-5678）", localStorage.getItem("testPhone") || "");
        if (!phone) return;
        localStorage.setItem("testPhone", phone);
        const r = await api(`/api/campaigns/${cid}/steps/${b.dataset.test}/test`, { method: "POST", body: JSON.stringify({ phone }) });
        toast(r.dryRun
          ? `SMS停止中のため送信されていません（${r.segments}通分・約${r.segments * SMS_COST}円相当のプレビュー）`
          : `${phone} にテスト送信しました（${r.segments}通 ≒ ${r.segments * SMS_COST}円）`);
        return;
      }
      const email = prompt("テスト送信先メールアドレス", localStorage.getItem("testEmail") || "");
      if (!email) return;
      localStorage.setItem("testEmail", email);
      const r = await api(`/api/campaigns/${cid}/steps/${b.dataset.test}/test`, { method: "POST", body: JSON.stringify({ email }) });
      toast(r.dryRun ? "DRY_RUN中のため送信記録のみ（実メールなし）" : `${email} にテスト送信しました`);
    } catch (e) { toast(e.message); }
  }));

  // ステップ追加/編集フォーム
  const typeSel = $("#st-type");
  const syncTiming = () => {
    $("#st-relative").classList.toggle("hidden", typeSel.value !== "relative");
    $("#st-daytime").classList.toggle("hidden", typeSel.value !== "day_time");
  };
  typeSel.addEventListener("change", syncTiming);

  // チャネル切替（メール ⇄ SMS）と SMS の文字数カウンタ
  const chSel = $("#st-channel");
  const smsBox = $("#st-sms");
  const smsCount = $("#st-sms-count");
  const syncSmsCount = () => {
    const r = smsCounterText(smsBox.value);
    smsCount.textContent = r.text;
    smsCount.style.color = r.over ? "#b91c1c" : "";
  };
  const syncChannel = () => {
    const sms = chSel.value === "sms";
    $("#st-body-email").classList.toggle("hidden", sms);
    $("#st-body-sms").classList.toggle("hidden", !sms);
    if (sms) syncSmsCount();
  };
  chSel.addEventListener("change", syncChannel);
  smsBox.addEventListener("input", syncSmsCount);
  syncChannel();

  $("#st-save").addEventListener("click", async () => {
    const subject = $("#st-subject").value.trim();
    const channel = chSel.value;
    if (!subject) return toast(channel === "sms" ? "ステップ名を入力してください" : "件名を入力してください");
    const payload = { subject, channel, timing_type: typeSel.value };
    if (channel === "sms") {
      const body_text = smsBox.value;
      if (!body_text.trim()) return toast("SMSの本文を入力してください");
      if (smsCounterText(body_text).over) return toast("SMSが長すぎます。本文を短くしてください");
      payload.body_text = body_text;
    } else {
      const body_html = $("#st-body").value;
      if (!body_html) return toast("本文を入力してください");
      payload.body_html = body_html;
    }
    if (typeSel.value === "relative")
      payload.delay_minutes = Number($("#st-rd").value || 0) * 1440 + Number($("#st-rh").value || 0) * 60 + Number($("#st-rm").value || 0);
    if (typeSel.value === "day_time") {
      payload.delay_days = Number($("#st-dd").value || 0);
      payload.send_time = $("#st-time").value;
    }
    try {
      if (campState.editing) {
        await api(`/api/campaigns/${cid}/steps/${campState.editing}`, { method: "PUT", body: JSON.stringify(payload) });
        campState.editing = null;
        toast("保存しました");
      } else {
        await api(`/api/campaigns/${cid}/steps`, { method: "POST", body: JSON.stringify(payload) });
        toast("ステップを追加しました");
      }
      detailReload();
    } catch (e) { toast(e.message); }
  });
  const cancel = $("#st-cancel");
  if (cancel) cancel.addEventListener("click", () => { campState.editing = null; detailReload(); });

  $("#en-add").addEventListener("click", async () => {
    const email = $("#en-email").value.trim();
    if (!email) return;
    const r = await api(`/api/campaigns/${cid}/enroll`, { method: "POST", body: JSON.stringify({ email }) });
    toast(r.created ? "登録しました。配信が始まります" : "すでに登録済みです");
    detailReload();
  });
}

// ---------- 起動 ----------
(async () => {
  try {
    const me = await api("/api/me");
    if (me.authenticated) { showApp(); nav("dashboard"); }
    else showLogin();
  } catch { showLogin(); }
})();
