// SupportWave — 응원 물결 섹션 (PRD v1.0 기반 데모 구현)
// localStorage 기반. PRD §3, §8, §9, §10, §12 충실 반영.
//
// 핵심:
//  • approved 메시지 카드 리스트 + 오늘의 응원 (featured)
//  • 응원 메시지 작성 폼 (50자, 익명 기본, 지역 선택, 버튼 동의 + 자세히보기)
//  • 리액션 3종 (heart/like/clap) + sessionId 중복 방지 + count 증가
//  • ReactionFloatLayer — 클릭 시 3~5개, 자동 8~15초 1~2개, 동시 12개 캡
//  • prefers-reduced-motion 시 애니메이션 비활성
//  • 데모용 mini moderation toggle (관리자 모드)
//  • 금칙어/URL/반복문자/중복 IP hash 검증 (클라이언트 데모)

const SW_LS_KEY        = "sw_messages_v1";
const SW_REACTIONS_KEY = "sw_reactions_v1";
const SW_SESSION_KEY   = "sw_session_id";
const SW_ADMIN_RECEIPT_ROLES = ["super_admin", "moderator", "publisher", "auditor"];

// ── Seed 데이터 (자료조사 기반: 22개 시·군 / 5개 광주 자치구) ──────
const SW_REGIONS = [
  "광주 동구","광주 서구","광주 남구","광주 북구","광주 광산",
  "목포","여수","순천","나주","광양",
  "담양","곡성","구례","고흥","보성","화순",
  "장흥","강진","해남","영암","무안","함평","영광","장성","완도","진도","신안",
];

const SW_BAD_WORDS = ["바보","멍청","ㅅㅂ","개새","씨발","좆","fuck","shit"];

const SW_SEED = [
  { id: "s1", nickname: "전남교육지기",  region: "목포",  message: "아이들이 행복한 학교를 기대합니다.",
    heartCount: 128, likeCount: 42, clapCount: 31, isFeatured: true,  featuredLabel: "오늘의 응원",
    createdAt: "2026-04-29T08:00:00Z" },
  { id: "s2", nickname: "곡성학부모",    region: "곡성",  message: "곡성에서 시작된 변화, 통합특별시에서 이어가 주세요.",
    heartCount: 96, likeCount: 38, clapCount: 22, createdAt: "2026-04-28T14:30:00Z" },
  { id: "s3", nickname: "광산구 교사",   region: "광주 광산", message: "현장의 목소리가 정책이 되는 모습, 처음 봅니다.",
    heartCount: 74, likeCount: 51, clapCount: 18, createdAt: "2026-04-28T11:12:00Z" },
  { id: "s4", nickname: "여수 시민",     region: "여수",  message: "근자열 원자래 — 가까운 곳부터 기뻐하게 하소서.",
    heartCount: 62, likeCount: 19, clapCount: 27, createdAt: "2026-04-27T19:45:00Z" },
  { id: "s5", nickname: "고3 자녀 엄마", region: "순천",  message: "AI 진학 상담, 정말 절실했습니다. 응원합니다.",
    heartCount: 53, likeCount: 28, clapCount: 14, createdAt: "2026-04-27T09:20:00Z" },
  { id: "s6", nickname: "5·18을 기억",   region: "광주 동구", message: "민주주의 교육, 광주에서 시작된 약속을 잊지 않습니다.",
    heartCount: 89, likeCount: 24, clapCount: 33, createdAt: "2026-04-26T16:00:00Z" },
];

function swSeedMessages() {
  return SW_SEED.map((m) => ({ ...m, status: "approved", isSeed: true }));
}

// ── helpers ────────────────────────────────────────────────────────
function swSession() {
  try {
    let s = localStorage.getItem(SW_SESSION_KEY);
    if (!s) {
      s = "sw-" + Math.random().toString(36).slice(2, 10) + "-" + Date.now().toString(36);
      localStorage.setItem(SW_SESSION_KEY, s);
    }
    return s;
  } catch { return "sw-anon-" + Date.now(); }
}

function swLoad(key, fallback) {
  try {
    const raw = localStorage.getItem(key);
    if (!raw) return fallback;
    return JSON.parse(raw);
  } catch { return fallback; }
}
function swSave(key, val) { try { localStorage.setItem(key, JSON.stringify(val)); } catch {} }

// 간소화된 폼 검증 — 닉네임은 익명 default라 검증 대상이 아니다.
// 동의는 폼 제출 자체로 갈음한다 (버튼 라벨이 명시적으로 안내).
function swValidateMessage({ region, message }) {
  if (!message || message.trim().length < 5) return "응원 메시지는 5자 이상으로 입력해 주세요.";
  if (message.length > 50) return "응원 메시지는 50자 이내로 입력해 주세요.";
  if (region && !SW_REGIONS.includes(region) && region !== "") return "지역을 다시 선택해 주세요.";
  if (/https?:\/\/|www\.|\.com|\.kr|\.net/i.test(message)) return "URL은 포함할 수 없습니다.";
  if (/(.)\1{4,}/.test(message)) return "같은 글자를 너무 많이 반복할 수 없습니다.";
  const lower = message.toLowerCase();
  if (SW_BAD_WORDS.some((w) => lower.includes(w.toLowerCase()))) return "부적절한 표현이 포함되어 있습니다.";
  return null;
}

function swRelativeTime(iso) {
  const t = new Date(iso).getTime();
  const diff = Date.now() - t;
  if (diff < 60_000) return "방금 전";
  if (diff < 3_600_000) return Math.floor(diff / 60_000) + "분 전";
  if (diff < 86_400_000) return Math.floor(diff / 3_600_000) + "시간 전";
  return Math.floor(diff / 86_400_000) + "일 전";
}

// ── ReactionFloatLayer ─────────────────────────────────────────────
const SW_EMOJI = { heart: "❤️", like: "👍", clap: "👏" };
const SW_MAX_FLOAT = 12;

function useReducedMotion() {
  const [r, setR] = React.useState(false);
  React.useEffect(() => {
    const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
    const upd = () => setR(mq.matches);
    upd();
    mq.addEventListener?.("change", upd);
    return () => mq.removeEventListener?.("change", upd);
  }, []);
  return r;
}

function ReactionFloatLayer({ floats }) {
  return (
    <div className="sw-float-layer" aria-hidden="true">
      {floats.map((f) => (
        <span
          key={f.id}
          className="sw-float-emoji"
          style={{
            left: `${f.x}%`,
            fontSize: `${f.size}px`,
            animationDuration: `${f.duration}s`,
            animationDelay: `${f.delay}s`,
          }}
        >
          {SW_EMOJI[f.type]}
        </span>
      ))}
    </div>
  );
}

// ── ReactionButton ─────────────────────────────────────────────────
function ReactionButton({ type, count, active, ambient, onClick, label }) {
  return (
    <button
      type="button"
      className={`sw-react-btn ${active ? "is-active" : ""} ${ambient ? "is-ambient" : ""}`}
      data-type={type}
      onClick={onClick}
      aria-label={`${label} 리액션 남기기`}
      aria-pressed={active}
    >
      <span className="sw-react-emoji" aria-hidden="true">{SW_EMOJI[type]}</span>
      <span className="sw-react-label">{label}</span>
      <span className="sw-react-count">{count}</span>
    </button>
  );
}

// ── SupportMessageCard ─────────────────────────────────────────────
function SupportMessageCard({ msg, myReactions, onReact, featured, ambientType }) {
  return (
    <article className={`sw-card ${featured ? "is-featured" : ""} ${ambientType ? "is-ambient" : ""}`}>
      {featured && (
        <div className="sw-card-featured">
          <span className="sw-card-featured-dot" />
          {msg.featuredLabel || "오늘의 응원"}
        </div>
      )}
      <p className="sw-card-msg">"{msg.message}"</p>
      <div className="sw-card-meta">
        <span className="sw-card-nick">{msg.nickname}</span>
        {msg.region && <><span className="sw-card-dot">·</span><span className="sw-card-region">{msg.region}</span></>}
        <span className="sw-card-dot">·</span>
        <span className="sw-card-time">{swRelativeTime(msg.createdAt)}</span>
      </div>
      <div className="sw-card-reactions">
        <ReactionButton type="heart" count={msg.heartCount} active={myReactions.heart} ambient={ambientType === "heart"} onClick={() => onReact(msg.id, "heart")} label="공감" />
        <ReactionButton type="like"  count={msg.likeCount}  active={myReactions.like}  ambient={ambientType === "like"}  onClick={() => onReact(msg.id, "like")}  label="응원" />
        <ReactionButton type="clap"  count={msg.clapCount}  active={myReactions.clap}  ambient={ambientType === "clap"}  onClick={() => onReact(msg.id, "clap")}  label="기대" />
      </div>
    </article>
  );
}

function SupportMiniCard({ msg, myReactions, onReact, ambientType, index }) {
  const total = (msg.heartCount || 0) + (msg.likeCount || 0) + (msg.clapCount || 0);
  return (
    <article className={`sw-mini-card ${ambientType ? "is-ambient" : ""}`}>
      <div className="sw-mini-kicker">
        <span>함께 뜨는 응원</span>
        <em>{String(index + 1).padStart(2, "0")}</em>
      </div>
      <p className="sw-mini-msg">"{msg.message}"</p>
      <div className="sw-mini-meta">
        <span>{msg.nickname}</span>
        {msg.region && <><span className="sw-card-dot">·</span><span>{msg.region}</span></>}
      </div>
      <div className="sw-mini-actions">
        <button
          type="button"
          className={myReactions.heart ? "is-active" : ""}
          onClick={() => onReact(msg.id, "heart")}
          aria-label="공감 리액션 남기기"
        >
          <span aria-hidden="true">❤</span>{msg.heartCount || 0}
        </button>
        <button
          type="button"
          className={myReactions.like ? "is-active" : ""}
          onClick={() => onReact(msg.id, "like")}
          aria-label="응원 리액션 남기기"
        >
          <span aria-hidden="true">👍</span>{msg.likeCount || 0}
        </button>
        <span className="sw-mini-total">{total.toLocaleString()}</span>
      </div>
    </article>
  );
}

// ── SupportTicker (간단 가로 흐름 — pending 승인된 한줄들) ─────────
function SupportTicker({ messages }) {
  if (!messages.length) return null;
  // 두 번 반복해서 무한루프 효과
  const loop = [...messages, ...messages];
  return (
    <div className="sw-ticker" aria-hidden="true">
      <div className="sw-ticker-track">
        {loop.map((m, i) => (
          <span key={i} className="sw-ticker-item">
            <span className="sw-ticker-emoji">❤</span>
            <span className="sw-ticker-msg">{m.message}</span>
            <span className="sw-ticker-meta">— {m.nickname}{m.region ? ` · ${m.region}` : ""}</span>
          </span>
        ))}
      </div>
    </div>
  );
}

// ── SupportWindLayer (배경형 응원 흐름) ───────────────────────────
function SupportWindLayer({ messages }) {
  const source = (messages && messages.length ? messages : SW_SEED).slice(0, 12);
  if (!source.length) return null;
  const loop = [...source, ...source.slice(0, 4)];
  return (
    <div className="sw-wind-layer" aria-hidden="true">
      {loop.map((m, i) => (
        <span
          key={`${m.id || i}-${i}`}
          className="sw-wind-chip"
          style={{
            "--sw-wind-top": `${8 + (i % 6) * 14 + (i % 2) * 3}%`,
            "--sw-wind-delay": `${i * -5.2}s`,
            "--sw-wind-duration": `${48 + (i % 5) * 7}s`,
            "--sw-wind-y": `${((i % 3) - 1) * 18}px`,
            "--sw-wind-end-y": `${((i % 3) - 1) * -11}px`,
          }}
        >
          <span className="sw-wind-mark">응원</span>
          <span className="sw-wind-msg">{m.message}</span>
        </span>
      ))}
    </div>
  );
}

function useSupportAdminSession() {
  const [isAdmin, setIsAdmin] = React.useState(false);

  React.useEffect(() => {
    let alive = true;

    async function check() {
      if (!window.kdjSupabase) {
        if (alive) setIsAdmin(false);
        return;
      }

      try {
        const { data } = await window.kdjSupabase.auth.getSession();
        const user = data?.session?.user;
        if (!user) {
          if (alive) setIsAdmin(false);
          return;
        }

        let role = user.user_metadata?.role || null;
        const { data: roleRow } = await window.kdjSupabase
          .from("users_roles")
          .select("role")
          .eq("user_id", user.id)
          .maybeSingle();
        role = roleRow?.role || role;

        if (alive) setIsAdmin(SW_ADMIN_RECEIPT_ROLES.includes(role));
      } catch (e) {
        console.warn("[support-admin-session]", e);
        if (alive) setIsAdmin(false);
      }
    }

    check();
    const sub = window.kdjSupabase?.auth?.onAuthStateChange?.(() => check());
    return () => {
      alive = false;
      sub?.data?.subscription?.unsubscribe?.();
    };
  }, []);

  return isAdmin;
}

function SupportHeroFlow() {
  const reduced = useReducedMotion();
  const [messages, setMessages] = React.useState(() => swSeedMessages());
  const [floats, setFloats] = React.useState([]);

  React.useEffect(() => {
    let alive = true;
    async function load() {
      if (!swApiMode() || !window.kdjSupportApi) return;
      try {
        const list = await window.kdjSupportApi.listMessages({ sort: "popular", limit: 18 });
        if (alive && list.length) setMessages(list);
      } catch (e) {
        console.warn("[support-hero-flow] list", e);
      }
    }
    load();
    return () => { alive = false; };
  }, []);

  const approved = React.useMemo(
    () => messages.filter((m) => (m.status || "approved") === "approved"),
    [messages]
  );
  const visible = approved.length ? approved.slice(0, 12) : swSeedMessages();
  const loop = [...visible, ...visible];

  const pushFloats = React.useCallback((type, n = 1) => {
    if (reduced || document.hidden) return;
    setFloats((cur) => {
      const next = [...cur];
      for (let i = 0; i < n; i++) {
        if (next.length >= 14) break;
        next.push({
          id: "hf-" + Math.random().toString(36).slice(2),
          type,
          x: 10 + Math.random() * 80,
          size: 18 + Math.random() * 16,
          duration: 3 + Math.random() * 1.4,
          delay: Math.random() * 0.2,
        });
      }
      return next;
    });
  }, [reduced]);

  React.useEffect(() => {
    if (!floats.length) return;
    const t = window.setTimeout(() => {
      setFloats((cur) => cur.slice(Math.max(0, cur.length - 10)));
    }, 4600);
    return () => window.clearTimeout(t);
  }, [floats]);

  React.useEffect(() => {
    if (reduced) return;
    let stop = false;
    let timer;
    const tick = () => {
      if (stop) return;
      const types = ["heart", "like", "clap"];
      pushFloats(types[Math.floor(Math.random() * types.length)], 1 + Math.floor(Math.random() * 3));
      timer = window.setTimeout(tick, 3200 + Math.random() * 2800);
    };
    timer = window.setTimeout(tick, 900);
    return () => {
      stop = true;
      window.clearTimeout(timer);
    };
  }, [reduced, pushFloats]);

  return (
    <section className="sw-hero-flow" aria-label="실시간 응원 메시지">
      <SupportWindLayer messages={visible} />
      <ReactionFloatLayer floats={floats} />
      <div className="sw-hero-flow-inner">
        <a href="/support#write" className="sw-hero-flow-cta">
          <span>응원 남기기</span>
          <strong>함께 쓰는 응원 물결</strong>
        </a>
        <div className="sw-hero-flow-marquee" aria-hidden="true">
          <div className="sw-hero-flow-track">
            {loop.map((m, i) => (
              <span key={`${m.id || i}-${i}`} className="sw-hero-flow-chip">
                <span className="sw-hero-flow-mark">응원</span>
                <span className="sw-hero-flow-msg">"{m.message}"</span>
                <span className="sw-hero-flow-meta">{m.region || m.nickname || "익명"}</span>
              </span>
            ))}
          </div>
        </div>
      </div>
    </section>
  );
}

function SupportPendingReceipts({ receipts }) {
  const isAdmin = useSupportAdminSession();
  if (!receipts.length || !isAdmin) return null;
  return (
    <div className="sw-receipts" role="status" aria-live="polite">
      {receipts.map((r) => (
        <article key={r.id} className="sw-receipt">
          <div className="sw-receipt-kicker">
            <span className="sw-receipt-dot" />
            접수 완료 · 공개 검토 중
          </div>
          <p>"{r.message}"</p>
          <div>{r.region ? `${r.region} · ` : ""}운영자 확인 후 응원 물결에 소개됩니다.</div>
        </article>
      ))}
    </div>
  );
}

// ── SupportConsentModal ────────────────────────────────────────────
// 폼의 [자세히 보기] 클릭 시 띄우는 동의 안내 모달.
// 수집·목적·검토·보관·삭제·정책 링크만 간단히 명시한다.
function SupportConsentModal({ onClose }) {
  React.useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    document.addEventListener("keydown", onKey);
    document.body.style.overflow = "hidden";
    return () => {
      document.removeEventListener("keydown", onKey);
      document.body.style.overflow = "";
    };
  }, [onClose]);

  return (
    <div className="sw-modal-backdrop" onClick={onClose} role="presentation">
      <div className="sw-modal" role="dialog" aria-modal="true" aria-labelledby="sw-modal-title" onClick={(e) => e.stopPropagation()}>
        <button type="button" className="sw-modal-close" onClick={onClose} aria-label="닫기">×</button>
        <h3 id="sw-modal-title" className="sw-modal-title">응원 메시지 — 동의 안내</h3>

        <dl className="sw-modal-dl">
          <dt>수집 항목</dt>
          <dd>지역(선택), 응원 메시지, 접속 기록·세션 식별값(해시 처리)</dd>
          <dt>이용 목적</dt>
          <dd>응원 메시지 접수, 공개 검토, 도배·부정 이용 방지</dd>
          <dt>공개 검토</dt>
          <dd>관리자 검토 후 공개됩니다. 운영정책에 따라 공개되지 않거나 숨김 처리될 수 있습니다.</dd>
          <dt>보관 기간</dt>
          <dd>선거 종료 후 별도 고지된 기간까지. 관계 법령상 의무가 있는 경우 해당 기간을 따릅니다.</dd>
          <dt>삭제 요청</dt>
          <dd>본인이 작성한 메시지의 삭제는 사이트 내 공개 소통 채널로 요청할 수 있습니다.</dd>
        </dl>

        <div className="sw-modal-links">
          <a href="/privacy" target="_blank" rel="noopener noreferrer">개인정보처리방침 →</a>
          <a href="/support-policy" target="_blank" rel="noopener noreferrer">응원 메시지 운영정책 →</a>
        </div>

        <button type="button" className="sw-modal-confirm" onClick={onClose}>확인</button>
      </div>
    </div>
  );
}

// ── SupportMessageForm ─────────────────────────────────────────────
// 부모에 전달할 payload는 항상 다음 형태:
//   { nickname, region, message, displayIdentity, turnstileToken? }
// turnstileToken은 api 모드에서만 채워진다. 부모(SupportWaveSection)가
// kdjSupportApi.submitMessage로 백엔드에 전달한다.
function SupportMessageForm({ onSubmit, onCancel }) {
  // MVP 간소화: 닉네임은 모두 '익명'으로 저장 (서버 nickname 필수 제약 충족).
  // 동의는 [동의하고 응원하기] 버튼 누름 자체로 갈음 + 자세히보기 모달 제공.
  const [region, setRegion] = React.useState("");
  const [message, setMessage] = React.useState("");
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState(null);
  const [done, setDone] = React.useState(false);
  const [showModal, setShowModal] = React.useState(false);
  const [turnstileStatus, setTurnstileStatus] = React.useState("idle");
  const [turnstileErrorCode, setTurnstileErrorCode] = React.useState(null);

  const turnstileRef = React.useRef(null);
  const turnstileStatusRef = React.useRef(turnstileStatus);
  React.useEffect(() => { turnstileStatusRef.current = turnstileStatus; }, [turnstileStatus]);
  const isApiMode = (() => {
    const cfg = (typeof window !== "undefined" && window.__KDJ_CONFIG__) || {};
    return cfg.supportWaveMode === "api";
  })();

  const resetTurnstile = React.useCallback(async () => {
    if (!isApiMode || !window.kdjTurnstile || !turnstileRef.current) return;
    setTurnstileStatus("loading");
    setTurnstileErrorCode(null);
    await window.kdjTurnstile.reset(turnstileRef.current);
  }, [isApiMode]);

  React.useEffect(() => {
    if (!isApiMode || done) return;
    const el = turnstileRef.current;
    if (!el || !window.kdjTurnstile) {
      setTurnstileStatus("missing");
      return;
    }
    let alive = true;
    setTurnstileStatus("loading");
    setTurnstileErrorCode(null);
    window.kdjTurnstile.mount(el, {
      onToken: () => {
        if (!alive) return;
        setTurnstileStatus("verified");
        setTurnstileErrorCode(null);
      },
      onError: (code) => {
        if (!alive) return;
        setTurnstileStatus("error");
        setTurnstileErrorCode(code || null);
      },
      onExpired: () => {
        if (!alive) return;
        setTurnstileStatus("expired");
        setTurnstileErrorCode(null);
      },
      onMissing: () => {
        if (!alive) return;
        setTurnstileStatus("missing");
      },
      onStatus: (status, code) => {
        if (!alive) return;
        setTurnstileStatus(status);
        setTurnstileErrorCode(code || null);
      },
    });
    return () => {
      alive = false;
      window.kdjTurnstile?.unmount(el);
    };
  }, [isApiMode, done]);

  const submit = async (e) => {
    e.preventDefault();
    setError(null);
    const err = swValidateMessage({ region, message });
    if (err) { setError(err); return; }
    setBusy(true);

    let turnstileToken = null;
    if (isApiMode) {
      if (!window.kdjTurnstile || !turnstileRef.current) {
        setBusy(false);
        setError("보안 확인을 불러오지 못했습니다. 새로고침 후 다시 시도해 주세요.");
        return;
      }
      turnstileToken = await window.kdjTurnstile.getToken(turnstileRef.current, { waitMs: 1200 });
      if (!turnstileToken) {
        setBusy(false);
        const currentTurnstileStatus = turnstileStatusRef.current;
        const needsRetry = currentTurnstileStatus === "error" || currentTurnstileStatus === "expired";
        setError(needsRetry
          ? "보안 확인에 실패했습니다. 아래의 '다시 확인'을 누른 뒤 다시 응원해 주세요."
          : "보안 확인이 아직 완료되지 않았습니다. 잠시 후 다시 시도해 주세요.");
        return;
      }
    }

    try {
      await onSubmit({
        nickname: "익명",
        region: region || null,
        message: message.trim(),
        // 닉네임은 항상 '익명'. 지역을 고르면 지역만 함께 공개한다.
        displayIdentity: !!region,
        turnstileToken,
      });
      setBusy(false);
      setDone(true);
    } catch (err2) {
      setBusy(false);
      if ((err2 && err2.message || "").includes("보안 확인")) {
        resetTurnstile();
      }
      setError((err2 && err2.message) || "전송에 실패했습니다. 잠시 후 다시 시도해 주세요.");
    }
  };

  if (done) {
    return (
      <div className="sw-form-done">
        <div className="sw-form-done-stamp" aria-hidden="true">접수</div>
        <h4>응원 메시지가 접수되었습니다.</h4>
        <p>운영자 확인 후 홈페이지에 소개될 수 있습니다.<br/>따뜻한 마음, 감사합니다.</p>
        <button type="button" className="sw-form-done-btn" onClick={() => { setDone(false); setRegion(""); setMessage(""); }}>
          한 번 더 작성하기
        </button>
      </div>
    );
  }

  const remain = 50 - message.length;
  const submitDisabled = busy || (isApiMode && turnstileStatus !== "verified");

  return (
    <>
    <form className="sw-form" onSubmit={submit} noValidate>
      <label className="sw-form-field">
        <span className="sw-form-label">지역 <em className="sw-opt">(선택)</em></span>
        <select value={region} onChange={(e) => setRegion(e.target.value)} className="sw-form-input">
          <option value="">지역 선택 안함 (익명)</option>
          <optgroup label="광주">
            {SW_REGIONS.slice(0, 5).map((r) => <option key={r} value={r}>{r}</option>)}
          </optgroup>
          <optgroup label="전남">
            {SW_REGIONS.slice(5).map((r) => <option key={r} value={r}>{r}</option>)}
          </optgroup>
        </select>
      </label>

      <label className="sw-form-field">
        <span className="sw-form-label">
          응원 메시지 <em>*</em>
          <span className={`sw-form-counter ${remain < 0 ? "is-over" : ""}`}>{message.length}/50</span>
        </span>
        <textarea
          value={message} onChange={(e) => setMessage(e.target.value)}
          placeholder="전남광주 교육에 바라는 한 마디를 남겨주세요. (5~50자)"
          maxLength={60}
          rows={3}
          className="sw-form-input sw-form-textarea"
        />
      </label>

      {isApiMode && (
        <div className={`sw-turnstile-block sw-turnstile-${turnstileStatus}`}>
          <div className="sw-form-turnstile" ref={turnstileRef} aria-label="봇 방지 검증" />
          {turnstileStatus !== "verified" && (
            <div className="sw-turnstile-status">
              {turnstileStatus === "idle" && "보안 확인을 준비하는 중입니다."}
              {turnstileStatus === "loading" && "보안 확인을 불러오는 중입니다."}
              {turnstileStatus === "rendered" && "보안 확인이 완료되면 응원할 수 있습니다."}
              {turnstileStatus === "expired" && "보안 확인 시간이 만료되었습니다."}
              {turnstileStatus === "missing" && "보안 확인 설정을 불러오지 못했습니다."}
              {turnstileStatus === "error" && `보안 확인에 실패했습니다${turnstileErrorCode ? ` (${turnstileErrorCode})` : ""}.`}
              {(turnstileStatus === "error" || turnstileStatus === "expired") && (
                <button type="button" className="sw-turnstile-retry" onClick={resetTurnstile}>
                  다시 확인
                </button>
              )}
            </div>
          )}
        </div>
      )}

      {error && <div className="sw-form-error" role="alert">{error}</div>}

      <div className="sw-form-actions">
        {onCancel && <button type="button" className="sw-form-cancel" onClick={onCancel}>닫기</button>}
        <button type="submit" className="sw-form-submit" disabled={submitDisabled}>
          {busy ? "전송 중…" : isApiMode && turnstileStatus !== "verified" ? "보안 확인 중" : "동의하고 응원하기"}
          {!busy && <IconArrow size={14} />}
        </button>
      </div>

      <div className="sw-form-fineprint">
        응원하기를 누르면 <strong>개인정보 수집·이용</strong> 및 <strong>응원 메시지 운영정책</strong>에 동의한 것으로 봅니다.
        <button type="button" className="sw-form-fineprint-more" onClick={() => setShowModal(true)}>자세히 보기</button>
      </div>
    </form>

    {showModal && (
      <SupportConsentModal onClose={() => setShowModal(false)} />
    )}
    </>
  );
}

// ── 운영 모드 헬퍼 ────────────────────────────────────────────────
// supportApi가 아직 로드 안 됐을 수도 있으니 호출 시점에 evaluate.
function swApiMode() {
  const cfg = (typeof window !== "undefined" && window.__KDJ_CONFIG__) || {};
  return cfg.supportWaveMode === "api" && !!window.kdjSupportApi;
}

// ── SupportWaveSection (메인) ──────────────────────────────────────
function SupportWaveSection() {
  const reduced = useReducedMotion();
  const sessionId = React.useMemo(() => swSession(), []);
  const sectionRef = React.useRef(null);

  // demo 모드의 초기 데이터: stored or SEED. api 모드도 첫 페인트는 SEED로 시작했다가
  // useEffect에서 listMessages로 교체된다. approved가 0건이면 MVP fallback으로 SEED 유지.
  const [messages, setMessages] = React.useState(() => {
    if (swApiMode()) return swSeedMessages();
    const stored = swLoad(SW_LS_KEY, null);
    return stored && Array.isArray(stored) ? stored : swSeedMessages();
  });
  const [myReactions, setMyReactions] = React.useState(() => swLoad(SW_REACTIONS_KEY, {})); // { msgId: {heart:bool, like:bool, clap:bool} }
  const [floats, setFloats] = React.useState([]);
  const [localReceipts, setLocalReceipts] = React.useState([]);
  const [adminMode, setAdminMode] = React.useState(false);
  const [sort, setSort] = React.useState("recent"); // "recent" | "popular"
  const [ambientReact, setAmbientReact] = React.useState(null);

  // api 모드: 마운트 시 백엔드에서 approved 메시지 목록 fetch.
  // 이후 액션(handleSubmit/handleReact/adminAction)에서 필요 시 재조회.
  const refresh = React.useCallback(async () => {
    if (!swApiMode() || !window.kdjSupportApi) return;
    try {
      const list = await window.kdjSupportApi.listMessages({ sort, limit: 50 });
      setMessages(list.length ? list : swSeedMessages());
    } catch (e) { console.warn('[supportwave] list', e); }
  }, [sort]);

  React.useEffect(() => { refresh(); }, [refresh]);

  // persist — demo 모드에서만. api 모드는 백엔드가 진실의 원천.
  React.useEffect(() => { if (swApiMode()) return; swSave(SW_LS_KEY, messages); }, [messages]);
  React.useEffect(() => swSave(SW_REACTIONS_KEY, myReactions), [myReactions]);

  // approved + sorted
  const approved = React.useMemo(() => {
    const list = messages.filter((m) => (m.status || "approved") === "approved");
    if (sort === "popular") {
      return [...list].sort((a, b) => (b.heartCount + b.likeCount + b.clapCount) - (a.heartCount + a.likeCount + a.clapCount));
    }
    return [...list].sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
  }, [messages, sort]);

  const featured = React.useMemo(() => approved.find((m) => m.isFeatured) || null, [approved]);
  const others = React.useMemo(() => approved.filter((m) => !m.isFeatured), [approved]);
  const pendingCount = messages.filter((m) => m.status === "pending").length;

  // ── Floats ───────────────────────────────────────────────────────
  const pushFloats = React.useCallback((type, n = 1) => {
    if (reduced) return;
    if (document.hidden) return;
    setFloats((cur) => {
      const next = [...cur];
      for (let i = 0; i < n; i++) {
        if (next.length >= SW_MAX_FLOAT) break;
        next.push({
          id: "f-" + Math.random().toString(36).slice(2),
          type,
          x: 8 + Math.random() * 84,           // %
          size: 22 + Math.random() * 18,       // px
          duration: 3.4 + Math.random() * 1.8, // s
          delay: Math.random() * 0.25,
        });
      }
      return next;
    });
  }, [reduced]);

  // garbage-collect floats by age — each lives ~5s
  React.useEffect(() => {
    if (!floats.length) return;
    const t = setTimeout(() => {
      setFloats((cur) => cur.slice(Math.max(0, cur.length - SW_MAX_FLOAT + 2)));
    }, 5400);
    return () => clearTimeout(t);
  }, [floats]);

  // auto floating reactions — 첫 진입부터 화면이 살아 있도록 1.2초 후 시작.
  // prefers-reduced-motion이면 자동 효과 자체가 비활성.
  React.useEffect(() => {
    if (reduced) return;
    let stop = false;
    const tick = () => {
      if (stop) return;
      if (!document.hidden) {
        const types = ["heart","like","clap"];
        const t1 = types[Math.floor(Math.random() * types.length)];
        pushFloats(t1, 1 + Math.floor(Math.random() * 2));
      }
      const next = 7000 + Math.random() * 6000;
      setTimeout(tick, next);
    };
    const initial = setTimeout(tick, 1200);
    return () => { stop = true; clearTimeout(initial); };
  }, [reduced, pushFloats]);

  // 자동 리액션 하이라이트. 실제 DB 카운트는 올리지 않고, 카드/버튼/이모지만
  // 가볍게 살아 움직이게 해서 모바일에서도 클릭 전부터 흐름이 보이게 한다.
  React.useEffect(() => {
    if (reduced || !approved.length) return;
    let stop = false;
    let timer;
    let clearTimer;
    const tick = () => {
      if (stop) return;
      if (!document.hidden) {
        const visible = [featured, ...others.slice(0, 6)].filter(Boolean);
        const msg = visible[Math.floor(Math.random() * visible.length)];
        const types = ["heart", "like", "clap"];
        const type = types[Math.floor(Math.random() * types.length)];
        const nonce = Date.now();
        if (msg) {
          setAmbientReact({ id: msg.id, type, nonce });
          pushFloats(type, 2);
          window.clearTimeout(clearTimer);
          clearTimer = window.setTimeout(() => {
            setAmbientReact((cur) => cur && cur.nonce === nonce ? null : cur);
          }, 1500);
        }
      }
      timer = window.setTimeout(tick, 3600 + Math.random() * 2600);
    };
    timer = window.setTimeout(tick, 700);
    return () => {
      stop = true;
      window.clearTimeout(timer);
      window.clearTimeout(clearTimer);
    };
  }, [reduced, approved, featured, others, pushFloats]);

  // ── Reactions ────────────────────────────────────────────────────
  // demo: 로컬 상태에서 즉시 카운트 +1
  // api : 백엔드에 reactMessage POST → counts 응답으로 갱신
  // 둘 다 이모티콘 상승 효과는 즉시 발생.
  const handleReact = async (msgId, type) => {
    const mine = myReactions[msgId] || {};
    if (mine[type]) {
      pushFloats(type, 1);  // 중복 — 작게만 안내
      return;
    }
    // 즉시 시각 효과
    pushFloats(type, 3 + Math.floor(Math.random() * 3));

    if (swApiMode()) {
      try {
        const res = await window.kdjSupportApi.reactMessage({
          messageId: msgId, reactionType: type, sessionId,
        });
        if (res && res.ok && res.counts) {
          setMessages((cur) => cur.map((m) => m.id === msgId ? {
            ...m,
            heartCount: res.counts.heart,
            likeCount:  res.counts.like,
            clapCount:  res.counts.clap,
          } : m));
          setMyReactions((cur) => ({ ...cur, [msgId]: { ...mine, [type]: true } }));
        }
      } catch (e) { console.warn('[react]', e); }
      return;
    }

    // demo — optimistic update
    setMessages((cur) => cur.map((m) => {
      if (m.id !== msgId) return m;
      const key = type === "heart" ? "heartCount" : type === "like" ? "likeCount" : "clapCount";
      return { ...m, [key]: (m[key] || 0) + 1 };
    }));
    setMyReactions((cur) => ({ ...cur, [msgId]: { ...mine, [type]: true } }));
  };

  // ── 메시지 제출 ──────────────────────────────────────────────────
  // demo: 로컬 상태 push (pending). 데모 자동승인 옵션이 있으면 별도 처리.
  // api : 백엔드에 submitMessage POST. 결과는 관리자 승인 후에야 노출되므로
  //       클라이언트 목록에는 추가하지 않는다 (실제 운영 동작 그대로).
  // displayIdentity: 폼에서 지역을 선택하면 true(지역 공개), 아니면 false(완전 익명)
  const handleSubmit = async ({ nickname, region, message, displayIdentity, turnstileToken }) => {
    if (swApiMode()) {
      const res = await window.kdjSupportApi.submitMessage({
        nickname, region, message,
        displayIdentity,
        turnstileToken,
        sessionId,
      });
      if (!res || !res.ok) {
        const msg = (res && res.message) ||
          (res && res.code === 'rate_limited' ? '잠시 후 다시 시도해 주세요. (요청이 너무 많습니다.)' :
           res && res.code === 'bot_check_failed' ? '보안 확인에 실패했습니다. 다시 확인 후 응원해 주세요.' :
           '전송에 실패했습니다.');
        throw new Error(msg);
      }
      setLocalReceipts((cur) => [{
        id: "receipt-" + Date.now().toString(36),
        nickname: "익명",
        region: displayIdentity ? region : null,
        message,
        createdAt: new Date().toISOString(),
      }, ...cur].slice(0, 3));
      pushFloats("heart", 3);
      // 새 메시지는 pending이라 즉시 노출되지 않지만, 카운트/featured 변경 등을 위해
      // 가벼운 refresh. 실패해도 무시.
      refresh();
      return;
    }

    // demo 모드 — 로컬에서 즉시 pending으로 적재 (시연용)
    const finalNickname = displayIdentity ? nickname : "익명";
    const finalRegion = displayIdentity ? region : null;
    const newMsg = {
      id: "u-" + Math.random().toString(36).slice(2, 10),
      nickname: finalNickname, region: finalRegion, message,
      status: "pending",
      heartCount: 0, likeCount: 0, clapCount: 0,
      isFeatured: false,
      createdAt: new Date().toISOString(),
      sessionId,
    };
    setMessages((cur) => [newMsg, ...cur]);
    setLocalReceipts((cur) => [newMsg, ...cur].slice(0, 3));
    pushFloats("heart", 3);
  };

  // ── 관리자 액션 ──────────────────────────────────────────────────
  // demo: 로컬 상태 즉시 변경
  // api : 백엔드 admin-action 호출. 인증된 관리자만 성공.
  //       실제 관리자 콘솔은 /admin에서 사용 (Step 7).
  //       메인 페이지의 "관리자 모드 토글"은 데모 시연용으로만 유지된다.
  const adminAction = async (id, action) => {
    if (swApiMode()) {
      try {
        const res = await window.kdjSupportApi.adminAction({ messageId: id, action });
        if (!res || !res.ok) {
          console.warn('[admin]', res);
          return;
        }
        await refresh();
        return;
      } catch (e) { console.warn('[admin]', e); return; }
    }
    setMessages((cur) => cur.map((m) => {
      if (m.id !== id) return m;
      if (action === "approve") return { ...m, status: "approved", approvedAt: new Date().toISOString() };
      if (action === "hide")    return { ...m, status: "hidden" };
      if (action === "feature") return { ...m, isFeatured: true, featuredLabel: "오늘의 응원" };
      if (action === "unfeature") return { ...m, isFeatured: false, featuredLabel: null };
      return m;
    }).map((m) => {
      if (action === "feature" && m.id !== id && m.isFeatured) return { ...m, isFeatured: false };
      return m;
    }));
  };
  const adminDelete = async (id) => {
    if (swApiMode()) {
      try {
        await window.kdjSupportApi.adminAction({ messageId: id, action: 'delete' });
        await refresh();
        return;
      } catch (e) { console.warn('[admin]', e); return; }
    }
    setMessages((cur) => cur.filter((m) => m.id !== id));
  };

  const resetDemo = () => {
    setMessages(swSeedMessages());
    setMyReactions({});
  };

  // ── Stats ────────────────────────────────────────────────────────
  const totalMsgs = messages.filter((m) => m.status !== "deleted" && m.status !== "hidden").length;
  const totalReacts = messages.reduce((s, m) => s + (m.heartCount||0) + (m.likeCount||0) + (m.clapCount||0), 0);
  const regionsCount = new Set(messages.filter((m) => m.region).map((m) => m.region)).size;
  const spotlight = featured ? others.slice(0, 4) : [];
  const gridMessages = others.slice(spotlight.length, spotlight.length + 6);

  return (
    <section className="sw-section" id="support" ref={sectionRef}>
      <SupportWindLayer messages={approved.slice(0, 12)} />
      <ReactionFloatLayer floats={floats} />

      <div className="container">
        <div className="sw-hd">
          <div className="sec-eyebrow">
            <span className="sec-eyebrow-line" />
            <span>SUPPORT WAVE · 응원 물결</span>
          </div>
          <h2 className="sec-title">
            전남광주 교육을 향한<br/>
            <span className="title-em">응원 한마디.</span>
          </h2>
          <p className="sec-sub">작은 응원이 모여 더 나은 교육의 길이 됩니다. 이름을 남기고, 다른 분의 응원에 마음을 더해 주세요.</p>
        </div>

        {/* Stats strip */}
        <div className="sw-stats">
          <div className="sw-stat">
            <div className="sw-stat-num">{totalMsgs.toLocaleString()}</div>
            <div className="sw-stat-lbl">응원 메시지</div>
          </div>
          <div className="sw-stat">
            <div className="sw-stat-num">{totalReacts.toLocaleString()}</div>
            <div className="sw-stat-lbl">리액션</div>
          </div>
          <div className="sw-stat">
            <div className="sw-stat-num">{regionsCount}</div>
            <div className="sw-stat-lbl">참여 지역</div>
          </div>
          <div className="sw-stat sw-stat-cta">
            <a href="#sw-form" className="sw-stat-cta-btn">
              ✏️ 응원 메시지 남기기
            </a>
          </div>
        </div>

        {/* Featured + 폼 (인라인 — 항상 보임) */}
        <div className="sw-top">
          {featured && (
            <div className="sw-feat">
              <SupportMessageCard
                msg={featured}
                myReactions={myReactions[featured.id] || {}}
                onReact={handleReact}
                ambientType={ambientReact?.id === featured.id ? ambientReact.type : null}
                featured
              />
              {spotlight.length > 0 && (
                <div className="sw-feat-more" aria-label="오늘 함께 뜨는 응원">
                  {spotlight.map((m, i) => (
                    <SupportMiniCard
                      key={m.id}
                      msg={m}
                      index={i}
                      myReactions={myReactions[m.id] || {}}
                      onReact={handleReact}
                      ambientType={ambientReact?.id === m.id ? ambientReact.type : null}
                    />
                  ))}
                </div>
              )}
            </div>
          )}
          <div className="sw-form-wrap" id="sw-form">
            <div className="sw-form-hd">
              <span className="sw-form-eyebrow">WRITE · 응원 메시지 작성</span>
              <h3>한 마디 남겨주세요.</h3>
              <p className="sw-form-note">
                · 지역(선택)과 메시지는 공개됩니다. · 부적절한 표현·URL·연락처는 자동 차단됩니다.
              </p>
            </div>
            <SupportMessageForm onSubmit={handleSubmit} />
          </div>
        </div>

        <SupportPendingReceipts receipts={localReceipts} />

        {/* Sort + Admin toggle */}
        <div className="sw-toolbar">
          <div className="sw-toolbar-sort">
            <button className={`sw-sort ${sort==="recent" ? "is-active" : ""}`} onClick={() => setSort("recent")}>최신순</button>
            <button className={`sw-sort ${sort==="popular" ? "is-active" : ""}`} onClick={() => setSort("popular")}>인기순</button>
          </div>
          <div className="sw-toolbar-right">
            <a href="/support" className="sw-toolbar-link">전체 응원 보기 <IconArrow size={12} /></a>
            {/* 데모용 관리자 토글 — 운영(prodMode)에서는 노출 금지.
                실제 모더레이션은 /admin에서 인증 후 사용. */}
            {!((window.__KDJ_CONFIG__ || {}).prodMode) && (
              <button className={`sw-admin-toggle ${adminMode ? "is-on" : ""}`} onClick={() => setAdminMode(v => !v)} aria-pressed={adminMode}>
                🛡 관리자 모드 {adminMode ? "ON" : ""}
                {pendingCount > 0 && <span className="sw-admin-badge">{pendingCount}</span>}
              </button>
            )}
          </div>
        </div>

        {/* Card grid */}
        <div className="sw-grid">
          {gridMessages.map((m) => (
            <SupportMessageCard
              key={m.id}
              msg={m}
              myReactions={myReactions[m.id] || {}}
              onReact={handleReact}
              ambientType={ambientReact?.id === m.id ? ambientReact.type : null}
            />
          ))}
        </div>

        {/* Ticker — approved 메시지 한 줄 흐름 */}
        <SupportTicker messages={others.slice(0, 12)} />

        {/* 관리자 모드 패널 */}
        {adminMode && (
          <div className="sw-admin">
            <div className="sw-admin-hd">
              <div>
                <div className="sw-admin-eyebrow">ADMIN · 관리자 패널 (데모)</div>
                <h4>접수·노출 관리</h4>
              </div>
              <button className="sw-admin-reset" onClick={resetDemo}>데모 초기화</button>
            </div>
            <div className="sw-admin-table">
              <div className="sw-admin-row sw-admin-th">
                <div>상태</div>
                <div>표시명 · 지역</div>
                <div>메시지</div>
                <div>리액션</div>
                <div>액션</div>
              </div>
              {messages.map((m) => (
                <div key={m.id} className="sw-admin-row">
                  <div>
                    <span className={`sw-admin-status sw-admin-status-${m.status || "approved"}`}>
                      {m.status === "pending" ? "대기" :
                       m.status === "hidden"  ? "숨김" :
                       m.status === "deleted" ? "삭제" : "공개"}
                    </span>
                    {m.isFeatured && <span className="sw-admin-feat">⭐ 오늘의 응원</span>}
                  </div>
                  <div className="sw-admin-meta">
                    <strong>{m.nickname}</strong>
                    {m.region && <span> · {m.region}</span>}
                    <div className="sw-admin-time">{swRelativeTime(m.createdAt)}</div>
                  </div>
                  <div className="sw-admin-msg">{m.message}</div>
                  <div className="sw-admin-counts">
                    ❤ {m.heartCount} · 👍 {m.likeCount} · 👏 {m.clapCount}
                  </div>
                  <div className="sw-admin-actions">
                    {m.status === "pending" && (
                      <button onClick={() => adminAction(m.id, "approve")} className="sw-admin-btn sw-admin-btn-go">승인</button>
                    )}
                    {m.status !== "hidden" && m.status !== "pending" && (
                      <button onClick={() => adminAction(m.id, "hide")} className="sw-admin-btn">숨김</button>
                    )}
                    {!m.isFeatured && m.status === "approved" && (
                      <button onClick={() => adminAction(m.id, "feature")} className="sw-admin-btn">⭐ 오늘</button>
                    )}
                    {m.isFeatured && (
                      <button onClick={() => adminAction(m.id, "unfeature")} className="sw-admin-btn">⭐ 해제</button>
                    )}
                    <button onClick={() => adminDelete(m.id)} className="sw-admin-btn sw-admin-btn-danger">삭제</button>
                  </div>
                </div>
              ))}
            </div>
          </div>
        )}

      </div>
    </section>
  );
}

Object.assign(window, {
  SupportWaveSection, SupportHeroFlow, SupportMessageCard, SupportMessageForm,
  ReactionFloatLayer, ReactionButton, SupportTicker, SupportWindLayer, SupportPendingReceipts,
  SW_SEED, SW_REGIONS, SW_LS_KEY, SW_REACTIONS_KEY, swValidateMessage, swRelativeTime,
  swSeedMessages,
});
