/* ============================================================================
 * Wildy — 게시물 작성 (PC 네이버 스타일 + 모바일 인스타 스타일)
 * ----------------------------------------------------------------------------
 *   spec 재정의:
 *     - PC (sm+): 큰 모달, 좌측 콘텐츠 (큰 텍스트 영역) / 우측 옵션 패널.
 *                 미니멀 툴바 (Wild / 사진 / YouTube)
 *     - 모바일 (< sm): 풀스크린, 갤러리 우선 → 캡션 작성 2-step
 *
 *   진입점:
 *     - 그리드 첫 칸 "+" 버튼 (인스타 패턴)
 *     - Wild 상세 페이지 "이 Wild에 게시"
 *     - 기존 FAB은 채팅 진입으로 변경됨
 *
 *   사용:
 *     <window.WildyComponents.PostComposer
 *        supabaseClient={...}
 *        user={user}
 *        defaultWildId="lol"
 *        onClose={() => ...}
 *        onCreated={(post) => ...}
 *     />
 *
 *   글로벌 노출:
 *     window.WildyComponents.PostComposer
 *     window.WildyComponents.PostComposerFAB  (호환 stub — 옛 호출자용)
 *     window.WildyComponents.NewPostTile      (그리드 첫 칸 "+" 타일)
 *     window.WildyComponents.ChatFAB           (➕ FAB — 채팅 진입)
 * ========================================================================= */
(function (global) {
  'use strict';

  if (typeof React === 'undefined') {
    console.error('[post-composer.jsx] React not loaded');
    return;
  }
  const { useState, useEffect, useRef, useMemo } = React;

  const MAX_TEXT = 500;
  const MAX_IMAGES = 10;
  const MAX_BYTES_PER_IMAGE = 5 * 1024 * 1024;
  const MAX_UPLOAD_EDGE = 1280;
  const IMAGE_JPEG_QUALITY = 0.74;
  const RETRY_UPLOAD_EDGE = 900;
  const RETRY_IMAGE_JPEG_QUALITY = 0.66;
  const YT_REGEX = /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/shorts\/)([a-zA-Z0-9_-]{11})/;

  function detectYouTube(text) {
    if (!text) return null;
    const m = text.match(YT_REGEX);
    return m ? { id: m[1], url: m[0] } : null;
  }

  function isMobileWidth() {
    if (typeof window === 'undefined') return false;
    return window.innerWidth < 640;  // sm breakpoint
  }

  function fileBaseName(name) {
    return String(name || 'image').replace(/\.[^.]+$/, '') || 'image';
  }

  function loadImageElement(file) {
    return new Promise((resolve, reject) => {
      const url = URL.createObjectURL(file);
      const img = new Image();
      img.onload = () => {
        URL.revokeObjectURL(url);
        resolve(img);
      };
      img.onerror = () => {
        URL.revokeObjectURL(url);
        reject(new Error('image_decode_failed'));
      };
      img.src = url;
    });
  }

  async function optimizeImageFile(file, maxEdge = MAX_UPLOAD_EDGE, quality = IMAGE_JPEG_QUALITY) {
    if (!file || !file.type?.startsWith('image/')) return file;
    if (file.type === 'image/gif' || file.type === 'image/svg+xml') return file;

    const img = await loadImageElement(file);
    const scale = Math.min(1, maxEdge / Math.max(img.naturalWidth || img.width, img.naturalHeight || img.height));
    if (scale >= 1 && file.size <= 900 * 1024) return file;

    const canvas = document.createElement('canvas');
    canvas.width = Math.max(1, Math.round((img.naturalWidth || img.width) * scale));
    canvas.height = Math.max(1, Math.round((img.naturalHeight || img.height) * scale));
    const ctx = canvas.getContext('2d');
    if (!ctx) return file;
    ctx.drawImage(img, 0, 0, canvas.width, canvas.height);

    const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', quality));
    if (!blob || blob.size >= file.size) return file;
    return new File([blob], `${fileBaseName(file.name)}.jpg`, { type: 'image/jpeg', lastModified: Date.now() });
  }

  // ───────────────────────────────────────────────────────────
  // ChatFAB — ➕ 우하단, 채팅 진입 (게시 X)
  // ───────────────────────────────────────────────────────────
  function ChatFAB({ onClick, isActiveUser = true, icon = '💬' }) {
    if (!isActiveUser) return null;
    return (
      <button
        onClick={onClick}
        className="fixed bottom-6 right-6 z-[100] flex h-14 w-14 items-center justify-center rounded-full bg-wildy-ink text-white shadow-ink hover:-translate-y-0.5 transition text-2xl"
        aria-label="채팅"
        style={{ boxShadow: '0 8px 24px rgba(14, 42, 74, 0.3)' }}
      >
        {icon}
      </button>
    );
  }

  // 호환 stub — 옛 PostComposerFAB 호출자가 있어도 안전하게 ChatFAB로 동작
  function PostComposerFAB(props) {
    return <ChatFAB {...props} />;
  }

  // ───────────────────────────────────────────────────────────
  // NewPostTile — 그리드 첫 칸 "+" 타일 (인스타 패턴)
  // ───────────────────────────────────────────────────────────
  function NewPostTile({ onClick }) {
    return (
      <button
        onClick={onClick}
        className="relative aspect-square bg-gradient-to-br from-sky-50 via-white to-amber-50 border-2 border-dashed border-sky-300 hover:border-wildy-ink transition group flex flex-col items-center justify-center gap-1"
        aria-label="새 게시물"
      >
        <span className="text-3xl text-sky-700 group-hover:text-wildy-ink transition">＋</span>
        <span className="text-[10px] font-bold text-sky-700 group-hover:text-wildy-ink">새 게시물</span>
      </button>
    );
  }

  // ───────────────────────────────────────────────────────────
  // Internal: useComposerState — 공유 상태 + submit 로직
  // ───────────────────────────────────────────────────────────
  function useComposerState(supabaseClient, user, defaultWildId, onCreated, onClose, followedGames = [], onToggleFollow) {
    const helpers = (global.WildyLib && global.WildyLib.SupabaseHelpers) || {};
    const withFallbackTimeout = (promise, ms, fallback) => Promise.race([
      promise,
      new Promise(resolve => setTimeout(() => resolve(fallback), ms)),
    ]);
    const formatSubmitError = (e) => {
      const msg = String(e?.message || e || '');
      if (msg.includes('image_upload_timeout')) return '사진 업로드가 오래 걸리고 있어요. 게시물 저장은 시도했지만 이미지는 나중에 다시 올려주세요.';
      if (msg.includes('post_insert_timeout')) return '게시물 저장 응답이 늦어요. 잠시 후 다시 게시해주세요.';
      if (msg.includes('post_tags_timeout') || msg.includes('notify_timeout')) return '게시물은 저장됐지만 태그 알림 처리가 늦어졌어요.';
      if (msg.includes('업로드 실패')) return msg;
      return helpers.translateAuthError ? helpers.translateAuthError(e) : (msg || '게시 실패');
    };
    const [text, setText] = useState('');
    const [wildId, setWildId] = useState(defaultWildId || '');
    const [visibility, setVisibility] = useState('public');
    const [postType, setPostType] = useState('discussion');
    const [images, setImages] = useState([]);
    const [submitting, setSubmitting] = useState(false);
    const [progress, setProgress] = useState('');
    const [error, setError] = useState('');
    const [wilds, setWilds] = useState([]);
    const [followedWildIds, setFollowedWildIds] = useState(followedGames || []);
    const [hashtagText, setHashtagText] = useState('');
    const [mentionQuery, setMentionQuery] = useState('');
    const [mentionResults, setMentionResults] = useState([]);
    const [taggedUsers, setTaggedUsers] = useState([]);

    useEffect(() => {
      if (!supabaseClient) {
        const fallback = (global.WILDY_GAMES || []).map(g => ({
          id: g.id, name: g.name, short_name: g.short, emoji: g.emoji, category: g.category || g.tag,
        }));
        setFollowedWildIds(followedGames || []);
        setWilds([
          ...(followedGames || []).map(id => fallback.find(w => w.id === id)).filter(Boolean),
          ...fallback.filter(w => !(followedGames || []).includes(w.id)),
        ]);
        return;
      }
      let active = true;
      (async () => {
        try {
          const fallback = (global.WILDY_GAMES || []).map(g => ({
            id: g.id, name: g.name, short_name: g.short, emoji: g.emoji, category: g.category || g.tag,
          }));
          let subs = [];
          if (user?.id) {
            const { data, error } = await withFallbackTimeout(
              supabaseClient
                .from('wild_subscriptions')
                .select('wild_id,wild:wilds!wild_id(id,name,short_name,emoji,category)')
                .eq('user_id', user.id),
              5000,
              { data: [], error: null }
            );
            if (!error) subs = data || [];
          }
          const { data: all, error: allErr } = await withFallbackTimeout(
            supabaseClient
              .from('wilds')
              .select('id,name,short_name,emoji,category,rank_in_discover')
              .order('rank_in_discover', { ascending: true })
              .limit(100),
            5000,
            { data: [], error: null }
          );
          const byId = new Map([...(fallback || []), ...(all || []), ...subs.map(s => s.wild).filter(Boolean)].map(w => [w.id, w]));
          const followedIds = Array.from(new Set([...(followedGames || []), ...subs.map(s => s.wild_id).filter(Boolean)]));
          const ordered = [
            ...followedIds.map(id => byId.get(id)).filter(Boolean),
            ...Array.from(byId.values()).filter(w => !followedIds.includes(w.id)),
          ];
          if (active) {
            setFollowedWildIds(followedIds);
            setWilds(ordered);
          }
        } catch (e) {
          console.warn('[Composer] wilds fetch fail', e.message);
          if (active) {
            setWilds((global.WILDY_GAMES || []).map(g => ({
              id: g.id, name: g.name, short_name: g.short, emoji: g.emoji, category: g.category || g.tag,
            })));
          }
        }
      })();
      return () => { active = false; };
      }, [supabaseClient, user?.id, (followedGames || []).join('|')]);

    useEffect(() => {
      const ids = followedGames || [];
      setFollowedWildIds(ids);
      setWilds(prev => {
        if (!prev.length) return prev;
        const followedSet = new Set(ids);
        return [...prev].sort((a, b) => Number(followedSet.has(b.id)) - Number(followedSet.has(a.id)));
      });
    }, [(followedGames || []).join('|')]);

    const ytEmbed = useMemo(() => detectYouTube(text), [text]);

    useEffect(() => {
      if (!supabaseClient) return;
      const q = mentionQuery.trim().replace(/^@/, '').toLowerCase();
      if (q.length < 2) { setMentionResults([]); return; }
      let active = true;
      (async () => {
        try {
          const { data, error } = await supabaseClient
            .from('users')
            .select('id,username,display_name,avatar')
            .ilike('username', `${q}%`)
            .limit(8);
          if (error) throw error;
          if (active) {
            const selected = new Set(taggedUsers.map(u => u.id));
            setMentionResults((data || []).filter(u => !selected.has(u.id) && u.id !== user?.id));
          }
        } catch (e) {
          if (active) setMentionResults([]);
        }
      })();
      return () => { active = false; };
    }, [supabaseClient, mentionQuery, taggedUsers, user?.id]);

    const parseHashtags = () => Array.from(new Set(
      String(hashtagText || '')
        .split(/[\s,]+/)
        .map(t => t.trim().replace(/^#/, '').toLowerCase())
        .filter(t => /^[a-z0-9가-힣_]{1,30}$/.test(t))
    )).slice(0, 10);

    const followWild = async (wildIdToFollow) => {
      if (!wildIdToFollow || followedWildIds.includes(wildIdToFollow) || (followedGames || []).includes(wildIdToFollow)) {
        setWildId(wildIdToFollow || '');
        return;
      }
      const target = wilds.find(w => w.id === wildIdToFollow);
      const ok = global.confirm ? global.confirm(`${target?.name || wildIdToFollow} Wild를 팔로우할까요?`) : true;
      if (!ok) return;
      if (!supabaseClient || !user?.id) { setError('로그인이 필요해요'); return; }
      try {
        setFollowedWildIds(prev => prev.includes(wildIdToFollow) ? prev : [...prev, wildIdToFollow]);
        setWilds(prev => {
          const nextFollowed = new Set([...followedWildIds, wildIdToFollow]);
          return [...prev].sort((a, b) => Number(nextFollowed.has(b.id)) - Number(nextFollowed.has(a.id)));
        });
        setWildId(wildIdToFollow);
        if (typeof onToggleFollow === 'function') {
          onToggleFollow(wildIdToFollow);
          return;
        }
        const { error } = await supabaseClient
          .from('wild_subscriptions')
          .insert({ user_id: user.id, wild_id: wildIdToFollow, source: 'manual' });
        if (error && error.code !== '23505' && !String(error.message || '').includes('duplicate')) throw error;
      } catch (e) {
        console.warn('[Composer] follow wild failed', e?.message || e);
        setError('Wild 팔로우에 실패했어요');
      }
    };

    const addTaggedUser = (u) => {
      if (!u?.id || taggedUsers.some(x => x.id === u.id)) return;
      setTaggedUsers(prev => [...prev, u].slice(0, 10));
      setMentionQuery('');
      setMentionResults([]);
    };

    const addFiles = (fileList) => {
      setError('');
      const files = Array.from(fileList || []);
      const next = [...images];
      for (const f of files) {
        if (next.length >= MAX_IMAGES) { setError(`사진은 최대 ${MAX_IMAGES}장까지`); break; }
        if (!f.type.startsWith('image/')) { setError('이미지 파일만 첨부 가능해요'); continue; }
        if (f.size > MAX_BYTES_PER_IMAGE) { setError(`${f.name}: 5MB 이하만`); continue; }
        next.push({ file: f, preview: URL.createObjectURL(f) });
      }
      setImages(next);
    };
    const removeImage = (idx) => {
      const next = images.slice();
      const removed = next.splice(idx, 1)[0];
      if (removed?.preview) URL.revokeObjectURL(removed.preview);
      setImages(next);
    };

    const sanitize = (s) => s.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 80);

    const createPostId = () => {
      if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
        return crypto.randomUUID();
      }
      return null;
    };

    const uploadOneImage = async (file, path, timeoutMs) => {
      const uploadPromise = supabaseClient.storage
        .from('post-images')
        .upload(path, file, { cacheControl: '3600', upsert: false, contentType: file.type });
      return withFallbackTimeout(uploadPromise, timeoutMs, { error: new Error('image_upload_timeout') });
    };

    const uploadImages = async () => {
      if (!images.length) return [];
      const urls = [];
      const folder = (typeof crypto !== 'undefined' && crypto.randomUUID)
        ? crypto.randomUUID()
        : Date.now() + '-' + Math.random().toString(36).slice(2, 9);
      for (let i = 0; i < images.length; i++) {
        const img = images[i];
        setProgress(`사진 최적화 ${i + 1}/${images.length}...`);
        let uploadFile = img.file;
        try {
          uploadFile = await optimizeImageFile(img.file);
        } catch (e) {
          console.warn('[Composer] image optimize skipped', e?.message || e);
        }
        setProgress(`사진 업로드 ${i + 1}/${images.length}...`);
        const uploadTimeoutMs = Math.max(90000, Math.ceil((uploadFile.size || img.file.size || 0) / (1024 * 1024)) * 45000);
        const path = `${user.id}/${folder}/${Date.now()}_${i}_${sanitize(uploadFile.name || img.file.name)}`;
        let { error: upErr } = await uploadOneImage(uploadFile, path, uploadTimeoutMs);
        if (upErr && String(upErr.message || upErr).includes('image_upload_timeout')) {
          try {
            setProgress(`사진 재시도 ${i + 1}/${images.length}...`);
            const retryFile = await optimizeImageFile(img.file, RETRY_UPLOAD_EDGE, RETRY_IMAGE_JPEG_QUALITY);
            if (retryFile.size < uploadFile.size) {
              uploadFile = retryFile;
              const retryPath = `${user.id}/${folder}/${Date.now()}_${i}_retry_${sanitize(uploadFile.name || img.file.name)}`;
              const retryTimeoutMs = Math.max(120000, Math.ceil((uploadFile.size || 0) / (1024 * 1024)) * 60000);
              const retryRes = await uploadOneImage(uploadFile, retryPath, retryTimeoutMs);
              upErr = retryRes.error;
              if (!upErr) {
                const { data: pub } = supabaseClient.storage.from('post-images').getPublicUrl(retryPath);
                urls.push(pub.publicUrl);
                continue;
              }
            }
          } catch (retryError) {
            console.warn('[Composer] image retry optimize failed', retryError?.message || retryError);
          }
        }
        if (upErr) {
          console.error('[Composer] image upload failed', {
            error: upErr,
            originalSize: img.file.size,
            uploadSize: uploadFile.size,
            uploadTimeoutMs,
          });
          throw new Error(`업로드 실패 (${i + 1}/${images.length}): ${upErr.message}`);
        }
        const { data: pub } = supabaseClient.storage.from('post-images').getPublicUrl(path);
        urls.push(pub.publicUrl);
      }
      setProgress('');
      return urls;
    };

    const submit = async () => {
      setError('');
      const hashtags = parseHashtags();
      const tagLine = hashtags.length ? hashtags.map(t => `#${t}`).join(' ') : '';
      const trimmed = [text.trim(), tagLine].filter(Boolean).join(text.trim() && tagLine ? '\n\n' : '');
      if (!trimmed && images.length === 0) { setError('내용을 입력하거나 사진을 첨부해주세요'); return; }
      if (trimmed.length > MAX_TEXT) { setError(`텍스트는 ${MAX_TEXT}자 이하`); return; }
      if (!supabaseClient || !user?.id) { setError('로그인이 필요해요'); return; }

      setSubmitting(true);
      try {
        const yt = detectYouTube(trimmed);
        const postId = createPostId();
        const insertRow = {
          author_id: user.id,
          type: postType,
          text: trimmed || null,
          image_urls: [],
          video_url: yt ? yt.url : null,
          game_tag: wildId || null,
          language: 'ko',
          visibility,
        };
        if (postId) insertRow.id = postId;
        setProgress('게시물 저장 중...');
        const startedAt = Date.now();
        let data = {
          id: postId,
          text: trimmed || null,
          image_urls: [],
          video_url: yt ? yt.url : null,
          star_count: 0,
          comment_count: 0,
          created_at: new Date().toISOString(),
          game_tag: wildId || null,
          author_id: user.id,
          type: postType,
          visibility,
        };
        const insertQuery = supabaseClient
          .from('posts')
          .insert(insertRow);
        const insertPromise = postId
          ? insertQuery
          : insertQuery.select('id,text,image_urls,video_url,star_count,comment_count,created_at,game_tag,author_id,type,visibility').single();
        const { data: inserted, error: insErr } = await withFallbackTimeout(insertPromise, 15000, { data: null, error: new Error('post_insert_timeout') });
        if (insErr) {
          console.error('post insert failed', {
            message: insErr.message,
            code: insErr.code,
            details: insErr.details,
            hint: insErr.hint,
            durationMs: Date.now() - startedAt,
            insertRow,
            error: insErr,
          });
          throw insErr;
        }
        if (inserted && !postId) data = inserted;
        console.info('[Composer] post insert ok', { postId: data.id, durationMs: Date.now() - startedAt });
        if (taggedUsers.length > 0) {
          const rows = taggedUsers.map(u => ({ post_id: data.id, tagged_user_id: u.id, visibility: 'all', approved: true }));
          const { error: tagErr } = await withFallbackTimeout(
            supabaseClient.from('post_tags').insert(rows),
            5000,
            { error: new Error('post_tags_timeout') }
          );
          if (tagErr) console.warn('[Composer] post_tags insert failed', tagErr);
          await Promise.allSettled(taggedUsers.map(u => withFallbackTimeout(
            supabaseClient.rpc('notify', {
              p_recipient_id: u.id,
              p_type: 'mention',
              p_from_user_id: user.id,
              p_title: `${user.display_name || user.nickname || user.username || 'Wilder'}님이 게시글에 태그했어요`,
              p_preview: trimmed.slice(0, 120),
              p_target_type: 'post',
              p_target_id: data.id,
            }),
            5000,
            { error: new Error('notify_timeout') }
          )));
        }
        if (images.length > 0) {
          setProgress('사진 업로드 중...');
          uploadImages()
            .then(async (imageUrls) => {
              if (!imageUrls.length) return;
              const { error: imageUpdateErr } = await withFallbackTimeout(
                supabaseClient.from('posts').update({ image_urls: imageUrls }).eq('id', data.id),
                10000,
                { error: new Error('post_image_update_timeout') }
              );
              if (imageUpdateErr) {
                console.warn('[Composer] post image update failed', {
                  message: imageUpdateErr.message,
                  code: imageUpdateErr.code,
                  details: imageUpdateErr.details,
                  hint: imageUpdateErr.hint,
                  error: imageUpdateErr,
                });
                return;
              }
              data.image_urls = imageUrls;
            })
            .catch((imageErr) => {
              console.error('[Composer] background image upload failed', {
                message: imageErr?.message,
                code: imageErr?.code,
                details: imageErr?.details,
                hint: imageErr?.hint,
                error: imageErr,
              });
            });
        }
        onCreated && onCreated(data);
        onClose && onClose();
      } catch (e) {
        console.error('[Composer] submit', {
          message: e?.message,
          code: e?.code,
          details: e?.details,
          hint: e?.hint,
          error: e,
        });
        setError(formatSubmitError(e));
        setProgress('');
      } finally { setSubmitting(false); }
    };

    return {
      text, setText, wildId, setWildId, visibility, setVisibility,
      postType, setPostType,
      images, addFiles, removeImage, wilds, followedWildIds, followWild, ytEmbed,
      hashtagText, setHashtagText, mentionQuery, setMentionQuery, mentionResults,
      taggedUsers, addTaggedUser, setTaggedUsers,
      submitting, progress, error, setError, submit,
      isDirty: Boolean(text.trim() || hashtagText.trim() || mentionQuery.trim() || taggedUsers.length || images.length || wildId || visibility !== 'public' || postType !== 'discussion'),
    };
  }

  // ───────────────────────────────────────────────────────────
  // PostComposerMobile — 2-step (gallery → caption) 인스타 스타일
  // ───────────────────────────────────────────────────────────
  function WildPicker({ state }) {
    const followed = state.wilds.filter(w => state.followedWildIds.includes(w.id));
    const others = state.wilds.filter(w => !state.followedWildIds.includes(w.id));
    const Row = ({ w, disabled }) => (
      <button type="button" onClick={() => disabled ? state.followWild(w.id) : state.setWildId(w.id)} className={`flex w-full items-center gap-2 rounded-xl px-3 py-2 text-left text-sm transition ${state.wildId === w.id ? 'bg-wildy-ink text-white' : disabled ? 'bg-white text-slate-400 hover:bg-amber-50' : 'bg-white text-slate-700 hover:bg-sky-50'}`}>
        <span>{w.emoji || '🎮'}</span>
        <span className="min-w-0 flex-1 truncate font-bold">{w.name}</span>
        {disabled && <span className="text-[10px] font-bold">팔로우 필요</span>}
      </button>
    );
    return (
      <div className="rounded-2xl border border-slate-200 bg-slate-50 p-2">
        <button type="button" onClick={() => state.setWildId('')} className={`mb-2 flex w-full items-center gap-2 rounded-xl px-3 py-2 text-left text-sm font-bold ${!state.wildId ? 'bg-wildy-ink text-white' : 'bg-white text-slate-600'}`}>선택 안 함</button>
        {followed.length > 0 && <div className="space-y-1"><div className="px-2 pb-1 text-[10px] font-black uppercase tracking-wide text-sky-600">내가 팔로우한 Wild</div>{followed.map(w => <Row key={w.id} w={w} />)}</div>}
        <div className="my-2 border-t border-slate-200"></div>
        <div className="max-h-44 space-y-1 overflow-y-auto pr-1">
          <div className="px-2 pb-1 text-[10px] font-black uppercase tracking-wide text-slate-400">다른 Wild</div>
          {others.map(w => <Row key={w.id} w={w} disabled />)}
          {state.wilds.length === 0 && <div className="px-3 py-3 text-xs text-slate-500">Wild 목록을 불러오는 중이에요.</div>}
        </div>
      </div>
    );
  }

  function HashtagAndMentionFields({ state }) {
    return (
      <div className="space-y-4">
        <div>
          <label className="mb-1.5 block text-xs font-bold text-slate-700">해시태그</label>
          <input value={state.hashtagText} onChange={(e) => state.setHashtagText(e.target.value)} placeholder="#공략 #듀오모집" className="w-full rounded-2xl border border-slate-200 bg-white px-3 py-2.5 text-sm outline-none focus:border-sky-300" />
        </div>
        <div>
          <label className="mb-1.5 block text-xs font-bold text-slate-700">태그 Wilder</label>
          <input value={state.mentionQuery} onChange={(e) => state.setMentionQuery(e.target.value)} placeholder="@username" className="w-full rounded-2xl border border-slate-200 bg-white px-3 py-2.5 text-sm outline-none focus:border-sky-300" />
          {state.mentionResults.length > 0 && <div className="mt-2 overflow-hidden rounded-2xl border border-sky-100 bg-white">{state.mentionResults.map(u => <button key={u.id} type="button" onClick={() => state.addTaggedUser(u)} className="flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-sky-50"><span className="text-xs font-bold text-wildy-ink">{u.display_name || u.username || 'Wilder'}</span><span className="text-xs text-slate-400">@{u.username}</span></button>)}</div>}
          {state.taggedUsers.length > 0 && <div className="mt-2 flex flex-wrap gap-1.5">{state.taggedUsers.map(u => <button key={u.id} type="button" onClick={() => state.setTaggedUsers(prev => prev.filter(x => x.id !== u.id))} className="rounded-full bg-sky-100 px-2.5 py-1 text-xs font-bold text-sky-700">@{u.username} ×</button>)}</div>}
          <p className="mt-1 text-[11px] text-slate-400">선택한 Wilder에게 앱 내 알림이 전송돼요.</p>
        </div>
      </div>
    );
  }

  function PostTypePicker({ state, user }) {
    const types = [
      { key: 'guide', label: '공략', desc: '팁, 빌드, 운영법' },
      { key: 'discussion', label: '자유', desc: '자유 주제와 잡담' },
    ];
    if (user?.is_admin) types.unshift({ key: 'official', label: '공식', desc: '운영자 공지' });
    return (
      <div>
        <label className="mb-1.5 block text-xs font-bold text-slate-700">게시글 분류</label>
        <div className="grid grid-cols-2 gap-2">
          {types.map(t => (
            <button
              key={t.key}
              type="button"
              onClick={() => state.setPostType(t.key)}
              className={`rounded-2xl border-2 px-3 py-2 text-left transition ${state.postType === t.key ? 'border-wildy-ink bg-wildy-ink text-white' : 'border-sky-100 bg-white text-slate-600 hover:border-sky-200'}`}
            >
              <div className="text-sm font-black">{t.label}</div>
              <div className={`mt-0.5 text-[10px] ${state.postType === t.key ? 'text-white/70' : 'text-slate-400'}`}>{t.desc}</div>
            </button>
          ))}
        </div>
      </div>
    );
  }

  function PostComposerMobile({ state, onClose, user }) {
    const [step, setStep] = useState('gallery');  // 'gallery' | 'caption'
    const fileInputRef = useRef(null);

    // 진입 시 자동으로 파일 선택창 열기 시도 (FAB/CTA 클릭 user gesture 직후라 OK일 가능성)
    useEffect(() => {
      if (step === 'gallery' && state.images.length === 0 && fileInputRef.current) {
        // 일부 브라우저는 user gesture 제한 — 실패해도 큰 버튼으로 대체
        try { fileInputRef.current.click(); } catch {}
      }
    }, []);

    const canNext = state.images.length > 0;

    return (
      <div className="fixed inset-0 z-[150] flex flex-col bg-white" style={{ maxWidth: 480, margin: '0 auto' }}>
        {/* Header */}
        <div className="flex items-center justify-between border-b border-slate-100 px-4 py-3">
          <button onClick={step === 'gallery' ? onClose : () => setStep('gallery')} disabled={state.submitting} className="flex h-9 w-9 items-center justify-center rounded-full hover:bg-slate-100 disabled:opacity-50">
            {step === 'gallery' ? '✕' : '←'}
          </button>
          <h2 className="font-display text-base font-black text-wildy-ink">
            {step === 'gallery' ? '새 게시물' : '캡션 작성'}
          </h2>
          {step === 'gallery' ? (
            <button onClick={() => setStep('caption')} disabled={!canNext} className="rounded-full bg-wildy-ink px-3 py-1.5 text-sm font-bold text-white disabled:bg-slate-300">다음</button>
          ) : (
            <button onClick={state.submit} disabled={state.submitting} className="rounded-full bg-wildy-ink px-3 py-1.5 text-sm font-bold text-white disabled:bg-slate-300">{state.submitting ? '저장' : '공유'}</button>
          )}
        </div>

        {/* Body */}
        <div className="flex-1 overflow-y-auto">
          {step === 'gallery' && (
            <div className="p-4 space-y-3">
              <input
                ref={fileInputRef}
                type="file"
                accept="image/*"
                multiple
                onChange={(e) => state.addFiles(e.target.files)}
                className="hidden"
                id="composer-mobile-file"
              />
              {state.images.length === 0 ? (
                <>
                  <label
                    htmlFor="composer-mobile-file"
                    className="block w-full rounded-3xl border-2 border-dashed border-sky-300 bg-sky-50/50 px-6 py-16 text-center cursor-pointer hover:bg-sky-50"
                  >
                    <div className="text-5xl mb-3">📷</div>
                    <div className="font-display text-lg font-black text-wildy-ink mb-1">사진 선택</div>
                    <div className="text-xs text-slate-500">갤러리에서 최대 10장 선택</div>
                  </label>
                  <button
                    onClick={() => setStep('caption')}
                    className="block w-full text-center text-xs text-slate-500 underline py-2"
                  >
                    사진 없이 글만 쓰기 →
                  </button>
                </>
              ) : (
                <>
                  <div className="grid grid-cols-3 gap-2">
                    {state.images.map((img, i) => (
                      <div key={i} className="relative aspect-square rounded-xl overflow-hidden bg-slate-100">
                        <img src={img.preview} alt="" className="w-full h-full object-cover" />
                        <button onClick={() => state.removeImage(i)} className="absolute top-1 right-1 flex h-6 w-6 items-center justify-center rounded-full bg-black/60 text-white text-xs">×</button>
                      </div>
                    ))}
                    {state.images.length < MAX_IMAGES && (
                      <label htmlFor="composer-mobile-file" className="aspect-square rounded-xl border-2 border-dashed border-sky-200 flex items-center justify-center text-2xl text-sky-400 cursor-pointer hover:border-sky-300">+</label>
                    )}
                  </div>
                  <div className="text-xs text-center text-slate-400">{state.images.length}/{MAX_IMAGES}장</div>
                </>
              )}
              {state.error && <div className="rounded-2xl bg-rose-50 px-4 py-3 text-sm text-rose-600">{state.error}</div>}
            </div>
          )}

          {step === 'caption' && (
            <div className="p-4 space-y-4">
              {state.images.length > 0 && (
                <div className="flex gap-2 overflow-x-auto -mx-4 px-4">
                  {state.images.map((img, i) => (
                    <img key={i} src={img.preview} alt="" className="h-20 w-20 flex-shrink-0 rounded-xl object-cover bg-slate-100" />
                  ))}
                </div>
              )}
              <div>
                <div className="flex items-center justify-between mb-1.5">
                  <span className="text-xs font-bold text-slate-700">캡션</span>
                  <span className={`text-xs ${state.text.length > MAX_TEXT ? 'text-rose-600' : 'text-slate-400'}`}>{state.text.length}/{MAX_TEXT}</span>
                </div>
                <textarea
                  value={state.text}
                  onChange={(e) => state.setText(e.target.value)}
                  placeholder="이 순간을 어떻게 표현할까요?"
                  className="w-full min-h-[140px] rounded-2xl border-2 border-sky-100 px-4 py-3 text-sm outline-none focus:border-sky-300 resize-y"
                  style={{ wordBreak: 'keep-all' }}
                />
                {state.ytEmbed && (
                  <div className="mt-2 rounded-2xl border border-sky-100 bg-sky-50 p-2 text-xs text-sky-700">
                    🎬 YouTube 자동 임베드 — {state.ytEmbed.id}
                  </div>
                )}
              </div>
              <div>
                <label className="block mb-1.5 text-xs font-bold text-slate-700">Wild 태그</label>
                <div className="hidden">
                <select value={state.wildId} onChange={(e) => state.setWildId(e.target.value)} className="w-full rounded-2xl border-2 border-sky-100 px-4 py-3 text-sm outline-none focus:border-sky-300">
                  <option value="">(선택 안 함)</option>
                  {state.wilds.map(w => <option key={w.id} value={w.id}>{w.emoji ? `${w.emoji} ` : ''}{w.name}</option>)}
                </select>
                </div>
                <WildPicker state={state} />
              </div>
              <PostTypePicker state={state} user={user} />
              <HashtagAndMentionFields state={state} />
              <div>
                <label className="block mb-1.5 text-xs font-bold text-slate-700">공개 범위</label>
                <div className="grid grid-cols-2 gap-2">
                  <button type="button" onClick={() => state.setVisibility('public')} className={`rounded-2xl border-2 px-3 py-2.5 text-sm font-bold ${state.visibility === 'public' ? 'border-wildy-ink bg-wildy-ink text-white' : 'border-sky-100 bg-white text-slate-600'}`}>🌐 전체</button>
                  <button type="button" onClick={() => state.setVisibility('wild_only')} disabled={!state.wildId} className={`rounded-2xl border-2 px-3 py-2.5 text-sm font-bold ${state.visibility === 'wild_only' ? 'border-wildy-ink bg-wildy-ink text-white' : 'border-sky-100 bg-white text-slate-600'}`}>🔒 Wild 멤버만</button>
                </div>
              </div>
              {state.progress && <div className="rounded-2xl bg-sky-50 px-4 py-3 text-sm text-sky-700">{state.progress}</div>}
              {state.error && <div className="rounded-2xl bg-rose-50 px-4 py-3 text-sm text-rose-600">{state.error}</div>}
            </div>
          )}
        </div>
      </div>
    );
  }

  // ───────────────────────────────────────────────────────────
  // PostComposerDesktop — PC 네이버 스타일: 큰 텍스트 영역 + 사이드 옵션
  // ───────────────────────────────────────────────────────────
  function PostComposerDesktop({ state, onClose, user }) {
    const fileInputRef = useRef(null);
    return (
      <div className="fixed inset-0 z-[150] flex items-center justify-center bg-black/40 p-4 sm:p-8">
        <div
          onClick={(e) => e.stopPropagation()}
          className="w-full max-w-4xl max-h-[90vh] flex flex-col rounded-3xl bg-white shadow-2xl overflow-hidden"
        >
          {/* Header */}
          <div className="flex items-center justify-between border-b border-slate-100 px-6 py-4">
            <h2 className="font-display text-xl font-black text-wildy-ink">새 게시물 쓰기</h2>
            <div className="flex items-center gap-2">
              <button onClick={onClose} disabled={state.submitting} className="flex h-9 w-9 items-center justify-center rounded-full text-xl font-bold text-slate-500 hover:bg-slate-100 disabled:opacity-50" aria-label="닫기">×</button>
              <button onClick={state.submit} disabled={state.submitting} className="rounded-full bg-wildy-ink px-6 py-2 text-sm font-bold text-white disabled:bg-slate-300 hover:-translate-y-0.5 transition">
                {state.submitting ? '저장 중...' : '게시'}
              </button>
            </div>
          </div>

          {/* Body — 좌(콘텐츠) 우(옵션) */}
          <div className="flex-1 overflow-hidden flex flex-col lg:flex-row">
            {/* 좌: 큰 텍스트 + 미리보기 */}
            <div className="flex-1 overflow-y-auto p-6 lg:p-8 space-y-4">
              <textarea
                value={state.text}
                onChange={(e) => state.setText(e.target.value)}
                placeholder="당신의 이야기를 들려주세요. 공략, 클립 URL, 후기, 자유로운 일기..."
                className="w-full min-h-[260px] text-lg leading-relaxed text-wildy-ink placeholder-slate-300 outline-none resize-y border-0 focus:ring-0"
                style={{ wordBreak: 'keep-all' }}
              />
              <div className="flex justify-end">
                <span className={`text-xs ${state.text.length > MAX_TEXT ? 'text-rose-600' : 'text-slate-400'}`}>{state.text.length}/{MAX_TEXT}</span>
              </div>

              {state.ytEmbed && (
                <div className="rounded-2xl border border-sky-100 bg-sky-50 p-3">
                  <div className="text-xs font-bold text-sky-700 mb-2">🎬 YouTube 자동 임베드</div>
                  <div className="aspect-video w-full max-w-md rounded-xl overflow-hidden bg-black">
                    <img src={`https://img.youtube.com/vi/${state.ytEmbed.id}/hqdefault.jpg`} alt="" className="w-full h-full object-cover" />
                  </div>
                </div>
              )}

              {state.images.length > 0 && (
                <div className="grid grid-cols-3 sm:grid-cols-4 gap-2">
                  {state.images.map((img, i) => (
                    <div key={i} className="relative aspect-square rounded-xl overflow-hidden bg-slate-100">
                      <img src={img.preview} alt="" className="w-full h-full object-cover" />
                      <button onClick={() => state.removeImage(i)} className="absolute top-1 right-1 flex h-6 w-6 items-center justify-center rounded-full bg-black/60 text-white text-xs">×</button>
                    </div>
                  ))}
                </div>
              )}

              {/* 미니멀 툴바 */}
              <div className="flex items-center gap-2 pt-2 border-t border-slate-100">
                <input
                  ref={fileInputRef}
                  type="file"
                  accept="image/*"
                  multiple
                  onChange={(e) => { state.addFiles(e.target.files); if (fileInputRef.current) fileInputRef.current.value = ''; }}
                  className="hidden"
                />
                <button
                  onClick={() => fileInputRef.current && fileInputRef.current.click()}
                  className="flex items-center gap-1.5 rounded-full bg-slate-100 hover:bg-slate-200 px-3 py-2 text-sm font-bold text-slate-700"
                >
                  <span>📷</span><span>사진</span>
                  {state.images.length > 0 && <span className="text-xs text-slate-500">{state.images.length}</span>}
                </button>
                <button
                  onClick={() => state.setText(t => t + (t ? '\n' : '') + 'https://www.youtube.com/watch?v=')}
                  className="flex items-center gap-1.5 rounded-full bg-slate-100 hover:bg-slate-200 px-3 py-2 text-sm font-bold text-slate-700"
                >
                  <span>🎬</span><span>YouTube</span>
                </button>
                <span className="ml-auto text-xs text-slate-400">⌘+Enter로 게시</span>
              </div>
            </div>

            {/* 우: 옵션 사이드바 */}
            <div className="w-full lg:w-72 border-t lg:border-t-0 lg:border-l border-slate-100 bg-slate-50/50 p-6 space-y-5 overflow-y-auto">
              <div>
                <label className="block mb-1.5 text-xs font-bold text-slate-700">Wild 선택</label>
                <div className="hidden">
                <select value={state.wildId} onChange={(e) => state.setWildId(e.target.value)} className="w-full rounded-2xl border border-slate-200 bg-white px-3 py-2.5 text-sm outline-none focus:border-sky-300">
                  <option value="">(선택 안 함)</option>
                  {state.wilds.map(w => <option key={w.id} value={w.id}>{w.emoji ? `${w.emoji} ` : ''}{w.name}</option>)}
                </select>
                </div>
                <WildPicker state={state} />
              </div>
              <PostTypePicker state={state} user={user} />
              <HashtagAndMentionFields state={state} />
              <div>
                <label className="block mb-1.5 text-xs font-bold text-slate-700">공개 범위</label>
                <div className="space-y-1.5">
                  <label className="flex items-center gap-2 cursor-pointer rounded-xl px-2 py-1.5 hover:bg-white">
                    <input type="radio" checked={state.visibility === 'public'} onChange={() => state.setVisibility('public')} className="accent-wildy-ink" />
                    <span className="text-sm text-slate-700">🌐 전체 공개</span>
                  </label>
                  <label className={`flex items-center gap-2 cursor-pointer rounded-xl px-2 py-1.5 hover:bg-white ${!state.wildId ? 'opacity-50' : ''}`}>
                    <input type="radio" disabled={!state.wildId} checked={state.visibility === 'wild_only'} onChange={() => state.setVisibility('wild_only')} className="accent-wildy-ink" />
                    <span className="text-sm text-slate-700">🔒 Wild 멤버만</span>
                  </label>
                </div>
                {!state.wildId && <p className="mt-1 text-[11px] text-amber-600">Wild를 선택해주세요</p>}
              </div>
              {state.progress && <div className="rounded-2xl bg-sky-50 px-3 py-2 text-xs text-sky-700">{state.progress}</div>}
              {state.error && <div className="rounded-2xl bg-rose-50 px-3 py-2 text-xs text-rose-600" style={{ wordBreak: 'keep-all' }}>{state.error}</div>}
            </div>
          </div>
        </div>
      </div>
    );
  }

  // ───────────────────────────────────────────────────────────
  // PostComposer — viewport에 따라 분기
  // ───────────────────────────────────────────────────────────
  function PostComposer(props) {
    const { supabaseClient, user, defaultWildId, onClose, onCreated, followedGames = [], onToggleFollow } = props;
    const state = useComposerState(supabaseClient, user, defaultWildId, onCreated, onClose, followedGames, onToggleFollow);
    const [mobile, setMobile] = useState(isMobileWidth());
    const requestClose = () => {
      if (state.submitting) return;
      if (state.isDirty) {
        const ok = global.confirm ? global.confirm('작성 중인 내용이 사라집니다. 닫을까요?') : false;
        if (!ok) return;
      }
      onClose && onClose();
    };

    useEffect(() => {
      const onResize = () => setMobile(isMobileWidth());
      window.addEventListener('resize', onResize);
      return () => window.removeEventListener('resize', onResize);
    }, []);

    // Ctrl/Cmd + Enter → submit (PC)
    useEffect(() => {
      const onKey = (e) => {
        if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
          e.preventDefault();
          state.submit();
        }
      };
      window.addEventListener('keydown', onKey);
      return () => window.removeEventListener('keydown', onKey);
    }, [state.submit]);

    if (mobile) {
      return <PostComposerMobile state={state} onClose={requestClose} user={user} />;
    }
    return <PostComposerDesktop state={state} onClose={requestClose} user={user} />;
  }

  global.WildyComponents = global.WildyComponents || {};
  global.WildyComponents.PostComposer = PostComposer;
  global.WildyComponents.PostComposerFAB = PostComposerFAB;  // 호환 stub
  global.WildyComponents.ChatFAB = ChatFAB;
  global.WildyComponents.NewPostTile = NewPostTile;
})(typeof window !== 'undefined' ? window : globalThis);
