const {useEffect,useMemo,useRef,useState}=React;
const {createClient}=window.supabase;

const supabase=createClient(
  'https://kwcrlrvvbsgftlwisksi.supabase.co',
  'sb_publishable_U_8MUpP2vt3_bX0388IPvA_zg_U8jRd'
);

async function compressPhoto(file){
  const bitmap=await createImageBitmap(file);
  const maxSide=900;
  const scale=Math.min(1,maxSide/Math.max(bitmap.width,bitmap.height));
  const width=Math.max(1,Math.round(bitmap.width*scale));
  const height=Math.max(1,Math.round(bitmap.height*scale));
  const canvas=document.createElement('canvas');
  canvas.width=width;canvas.height=height;
  const ctx=canvas.getContext('2d');
  ctx.drawImage(bitmap,0,0,width,height);
  bitmap.close?.();
  const makeBlob=q=>new Promise((resolve,reject)=>canvas.toBlob(b=>b?resolve(b):reject(new Error('画像を圧縮できませんでした')),'image/jpeg',q));
  let quality=.72;
  let blob=await makeBlob(quality);
  while(blob.size>160*1024 && quality>.42){
    quality-=.08;
    blob=await makeBlob(quality);
  }
  if(blob.size>300*1024)throw new Error('compressed image too large');
  return blob;
}

const viewForStatus=(status,hasAnswered,hasVoted,confirming)=>{
  if(status==='lobby') return 'lobby';
  if(status==='answering'){
    if(hasAnswered) return 'wait-answer';
    if(confirming) return 'confirm-answer';
    return 'answer';
  }
  if(status==='voting') return hasVoted?'wait-vote':'voting';
  return status || 'home';
};


const SESSION_KEY='photo_party_session_v1';
const LAST_ROOM_KEY='photo_party_last_room_v1';
const DEVICE_KEY='photo_party_device_id_v1';

function getDeviceId(){
  try{
    let id=localStorage.getItem(DEVICE_KEY);
    if(!id){
      id=crypto.randomUUID();
      localStorage.setItem(DEVICE_KEY,id);
    }
    return id;
  }catch(_e){
    return crypto.randomUUID();
  }
}


function saveSession(roomId,playerId){
  try{
    localStorage.setItem(SESSION_KEY,JSON.stringify({roomId,playerId}));
    rememberRoom(roomId);
  }catch(_e){}
}

function clearSession(){
  try{ localStorage.removeItem(SESSION_KEY); }catch(_e){}
}


function saveSessionData(roomId,playerId,isHost=false){
  try{
    localStorage.setItem(SESSION_KEY,JSON.stringify({roomId,playerId,isHost}));
    rememberRoom(roomId);
  }catch(_e){}
}


function readSavedSession(){
  try{
    const raw=localStorage.getItem(SESSION_KEY);
    if(!raw)return null;
    const parsed=JSON.parse(raw);
    if(!parsed?.roomId||!parsed?.playerId)return null;
    return parsed;
  }catch(_e){
    return null;
  }
}

function rememberRoom(roomId){
  try{
    if(roomId)localStorage.setItem(LAST_ROOM_KEY,roomId);
  }catch(_e){}
}

function clearRememberedRoom(){
  try{localStorage.removeItem(LAST_ROOM_KEY);}catch(_e){}
}

function isLineInAppBrowser(){
  const ua=navigator.userAgent||'';
  return /Line\//i.test(ua);
}

function Home(){
  const [entry,setEntry]=useState('home');
  const [name,setName]=useState('');
  const [joinCode,setJoinCode]=useState('');
  const [room,setRoom]=useState(null);
  const [me,setMe]=useState(null);
  const [players,setPlayers]=useState([]);
  const [answers,setAnswers]=useState([]);
  const [votes,setVotes]=useState([]);
  const [text,setText]=useState('');
  const [confirming,setConfirming]=useState(false);
  const [seconds,setSeconds]=useState(60);
  const [voteSeconds,setVoteSeconds]=useState(30);
  const [error,setError]=useState('');
  const [busy,setBusy]=useState(false);
  const [photoFile,setPhotoFile]=useState(null);
  const [photoPreview,setPhotoPreview]=useState('');
  const [replayPhotoFile,setReplayPhotoFile]=useState(null);
  const [replayPhotoPreview,setReplayPhotoPreview]=useState('');
  const [replayUploading,setReplayUploading]=useState(false);
  const [shareMessage,setShareMessage]=useState('');
  const [inviteEntry,setInviteEntry]=useState(false);
  const [inviteAutoJoining,setInviteAutoJoining]=useState(false);
  const [lineFallback,setLineFallback]=useState(false);
  const [editingNickname,setEditingNickname]=useState('');
  const [leavingGame,setLeavingGame]=useState(false);
  const [restoringSession,setRestoringSession]=useState(true);
  const [hostEndedNotice,setHostEndedNotice]=useState(false);
  const activitySent=useRef('');

  const hasAnswered=useMemo(()=>answers.some(a=>a.player_id===me?.id),[answers,me?.id]);
  const hasVoted=useMemo(()=>votes.some(v=>v.voter_player_id===me?.id),[votes,me?.id]);
  const screen=room?viewForStatus(room.status,hasAnswered,hasVoted,confirming):entry;


  useEffect(()=>{
    try{
      const params=new URLSearchParams(window.location.search);
      const inviteCode=(params.get('room')||'').replace(/\D/g,'').slice(0,4);
      if(inviteCode.length===4){
        setJoinCode(inviteCode);
        setInviteEntry(true);
        setEntry('home');
      }
    }catch(_e){}
  },[]);

  useEffect(()=>{
    // v2.53: LINEの外部ブラウザ指定が効かなかった時だけ救済画面を出す。
    try{
      const params=new URLSearchParams(window.location.search);
      const inviteCode=(params.get('room')||'').replace(/\D/g,'').slice(0,4);
      if(inviteCode.length===4 && isLineInAppBrowser()){
        setLineFallback(true);
      }
    }catch(_e){}
  },[]);

  function continueOutsideBrowser(){
    try{
      const url=new URL(window.location.href);
      url.searchParams.set('openExternalBrowser','1');
      window.open(url.toString(),'_blank','noopener,noreferrer');
    }catch(_e){}
  }

  async function copyOutsideLink(){
    try{
      const url=new URL(window.location.href);
      url.searchParams.set('openExternalBrowser','1');
      await navigator.clipboard.writeText(url.toString());
      setShareMessage('リンクをコピーしました');
      setTimeout(()=>setShareMessage(''),2200);
    }catch(_e){
      setShareMessage('コピーできませんでした');
      setTimeout(()=>setShareMessage(''),2200);
    }
  }



  useEffect(()=>{
    let cancelled=false;
    (async()=>{
      const ok=await restoreSavedGame();
      if(cancelled)return;
      if(!ok)setRestoringSession(false);
    })();
    return()=>{cancelled=true};
  },[]);

  useEffect(()=>{
    if(restoringSession)return;
    if(room?.id||me?.id)return;
    if(!inviteEntry||joinCode.length!==4)return;
    autoJoinInviteRoom(joinCode);
  },[restoringSession,inviteEntry,joinCode,room?.id,me?.id]);



  async function cleanupExpiredPhotos(){
    try{
      const cutoff=new Date(Date.now()-24*60*60*1000).toISOString();
      const {data:oldPlayers}=await supabase.from('party_players')
        .select('id,photo_path')
        .lt('joined_at',cutoff)
        .not('photo_path','is',null)
        .limit(100);
      const rows=(oldPlayers||[]).filter(x=>x.photo_path?.startsWith('uploads/'));
      if(!rows.length)return;
      const paths=rows.map(x=>x.photo_path);
      const {error:removeError}=await supabase.storage.from('party-images').remove(paths);
      if(removeError)return;
      await Promise.all(rows.map(x=>supabase.from('party_players').update({photo_url:null,photo_path:null}).eq('id',x.id)));
    }catch(_e){}
  }

  useEffect(()=>{
    cleanupExpiredPhotos();
    const id=setInterval(cleanupExpiredPhotos,10*60*1000);
    return()=>clearInterval(id);
  },[]);

  async function refresh(rid=room?.id){
    if(!rid)return;
    const [{data:r,error:roomError},{data:p},{data:a},{data:v}]=await Promise.all([
      supabase.from('party_rooms').select('*').eq('id',rid).maybeSingle(),
      supabase.from('party_players').select('*').eq('room_id',rid).order('joined_at'),
      supabase.from('party_answers').select('*').eq('room_id',rid),
      supabase.from('party_votes').select('*').eq('room_id',rid)
    ]);
    if(roomError)return;

    // v2.55: 最終結果でホストが部屋を閉じた場合、部屋は即削除したまま、
    // ゲストのブラウザ側だけにゲーム終了案内を残す。
    if(!r){
      const showEndedNotice=room?.status==='finished'&&me?.id&&!me?.is_host;
      clearSession();
      clearRememberedRoom();
      setRoom(null);setMe(null);setPlayers([]);setAnswers([]);setVotes([]);
      setText('');setConfirming(false);setInviteEntry(false);setError('');
      if(showEndedNotice){
        setHostEndedNotice(true);
      }else{
        setEntry('home');
      }
      try{
        const u=new URL(location.href);
        u.searchParams.delete('room');
        history.replaceState({},'',u.pathname+u.search);
      }catch(_e){}
      return;
    }

    const playersNow=p||[];

    // ホストに削除された端末は、名前入力中・写真選択中でもその場でロビーから退出させる。
    // 削除済みの player を端末側だけ保持したまま操作し続けないようにする。
    if(me?.id&&!playersNow.some(x=>x.id===me.id)){
      clearSession();
      clearRememberedRoom();
      setRoom(null);setMe(null);setPlayers([]);setAnswers([]);setVotes([]);
      setText('');setConfirming(false);setInviteEntry(false);setEntry('home');
      setError('ホストによってこの部屋から削除されました');
      try{
        const u=new URL(location.href);
        u.searchParams.delete('room');
        history.replaceState({},'',u.pathname+u.search);
      }catch(_e){}
      return;
    }

    setRoom(r);
    setPlayers(playersNow);
    setAnswers((a||[]).filter(x=>x.round_no===r.round_no));
    setVotes((v||[]).filter(x=>x.round_no===r.round_no));
  }

  useEffect(()=>{
    if(!room?.id)return;
    const ch=supabase.channel('party-'+room.id)
      .on('postgres_changes',{event:'*',schema:'public',table:'party_rooms',filter:`id=eq.${room.id}`},()=>refresh(room.id))
      .on('postgres_changes',{event:'*',schema:'public',table:'party_players',filter:`room_id=eq.${room.id}`},()=>refresh(room.id))
      .on('postgres_changes',{event:'*',schema:'public',table:'party_answers',filter:`room_id=eq.${room.id}`},()=>refresh(room.id))
      .on('postgres_changes',{event:'*',schema:'public',table:'party_votes',filter:`room_id=eq.${room.id}`},()=>refresh(room.id))
      .subscribe();
    return()=>supabase.removeChannel(ch);
  },[room?.id]);

  useEffect(()=>{
    const onPop=()=>{ if(!room) setEntry('home'); };
    window.addEventListener('popstate',onPop);
    return()=>window.removeEventListener('popstate',onPop);
  },[room]);

  // ラウンドが切り替わったら、前ラウンドの入力内容を必ず消す。
  // ホスト以外の端末でも前回の回答がtextareaに残らないようにする。
  useEffect(()=>{
    setText('');
    setConfirming(false);
  },[room?.round_no]);

  useEffect(()=>{
    if(room?.status!=='answering'||!room.answer_deadline)return;
    const tick=()=>setSeconds(Math.max(0,Math.ceil((new Date(room.answer_deadline).getTime()-Date.now())/1000)));
    tick();
    const x=setInterval(tick,400);
    return()=>clearInterval(x);
  },[room?.status,room?.answer_deadline,room?.round_no]);

  useEffect(()=>{
    if(room?.status!=='voting'||!room.vote_deadline)return;
    const tick=()=>setVoteSeconds(Math.max(0,Math.ceil((new Date(room.vote_deadline).getTime()-Date.now())/1000)));
    tick();
    const x=setInterval(tick,400);
    return()=>clearInterval(x);
  },[room?.status,room?.vote_deadline,room?.round_no]);

  async function setActivity(status){
    if(!me?.id||!room?.round_no)return;
    const key=`${room.round_no}:${status}`;
    if(activitySent.current===key)return;
    activitySent.current=key;
    await supabase.from('party_players').update({activity_status:status,activity_round:room.round_no}).eq('id',me.id);
  }

  useEffect(()=>{
    if(!room||!me)return;
    if(room.status==='answering'&&!hasAnswered){
      activitySent.current='';
      setActivity(text.trim()?'typing':'thinking');
    }
    if(room.status==='voting'&&!hasVoted){
      activitySent.current='';
      setActivity('choosing');
    }
  },[room?.status,room?.round_no,me?.id]);

  useEffect(()=>{
    if(!room||room.status!=='answering')return;
    const currentRoundAnswers=answers.filter(a=>a.round_no===room.round_no);
    const everyoneAnswered=players.length>=2 && currentRoundAnswers.length>=players.length;
    const timedOut=!!room.answer_deadline && Date.now()>=new Date(room.answer_deadline).getTime();
    if(everyoneAnswered || timedOut){
      supabase.from('party_rooms')
        .update({status:'voting',vote_deadline:new Date(Date.now()+30000).toISOString()})
        .eq('id',room.id)
        .eq('status','answering')
        .then(()=>refresh(room.id));
    }
  },[room?.status,room?.id,room?.round_no,players.length,answers.length,seconds]);

  useEffect(()=>{
    if(!room||room.status!=='voting')return;

    const currentAnswers=answers.filter(a=>a.round_no===room.round_no);

    // 各プレイヤーについて、自分以外の「実際に回答された回答」が1件でもあるかを判定。
    // 0件なら、そのプレイヤーは操作不要で自動投票完了扱い。
    const autoCompletedIds=players
      .filter(p=>!currentAnswers.some(a=>a.player_id!==p.id))
      .map(p=>p.id);

    const votedIds=new Set(votes.map(v=>v.voter_player_id));
    const completedCount=players.filter(
      p=>votedIds.has(p.id)||autoCompletedIds.includes(p.id)
    ).length;

    const everyoneDone=players.length>0 && completedCount>=players.length;
    const voteTimedOut=!!room.vote_deadline && Date.now()>=new Date(room.vote_deadline).getTime();

    // 全員投票済みなら30秒を待たず即結果へ。
    // 未投票者がいても30秒で締め切り、その人は「投票なし」として進行する。
    if(everyoneDone || voteTimedOut){
      supabase.from('party_rooms')
        .update({status:'result'})
        .eq('id',room.id)
        .eq('status','voting')
        .then(()=>refresh(room.id));
    }
  },[room?.status,room?.id,room?.round_no,players,answers,votes,voteSeconds]);

  async function uploadPlayerPhoto(player,roomId,file){
    const blob=await compressPhoto(file);
    const path=`uploads/${roomId}/${player.id}-${crypto.randomUUID()}.jpg`;
    const {error:upErr}=await supabase.storage.from('party-images').upload(path,blob,{contentType:'image/jpeg',upsert:false});
    if(upErr)throw upErr;
    const {data:urlData}=supabase.storage.from('party-images').getPublicUrl(path);
    const photoUrl=urlData.publicUrl;
    const {data:updatedPlayer,error:updateErr}=await supabase.from('party_players')
      .update({photo_url:photoUrl,photo_path:path})
      .eq('id',player.id)
      .eq('room_id',roomId)
      .select('id')
      .maybeSingle();
    if(updateErr||!updatedPlayer){
      // 写真送信中にホストから削除された場合は、アップロード済み画像を残さない。
      await supabase.storage.from('party-images').remove([path]);
      throw updateErr||new Error('player_removed');
    }
    return {...player,photo_url:photoUrl,photo_path:path};
  }

  function chooseReplayPhoto(file){
    if(!file)return;
    if(!file.type.startsWith('image/')){setError('画像ファイルを選んでください');return;}
    if(file.size>10*1024*1024){setError('写真は10MB以下にしてください');return;}
    setReplayPhotoFile(file);setError('');
    if(replayPhotoPreview)URL.revokeObjectURL(replayPhotoPreview);
    setReplayPhotoPreview(URL.createObjectURL(file));
  }

  async function submitReplayPhoto(){
    if(!me||!room||!replayPhotoFile)return;
    setReplayUploading(true);setError('');
    try{
      const updated=await uploadPlayerPhoto(me,room.id,replayPhotoFile);
      await supabase.from('party_players').update({activity_status:'ready',activity_round:0}).eq('id',me.id);
      setMe({...updated,activity_status:'ready',activity_round:0});
      setReplayPhotoFile(null);
      if(replayPhotoPreview)URL.revokeObjectURL(replayPhotoPreview);
      setReplayPhotoPreview('');
      await refresh(room.id);
    }catch(e){
      setError('写真をアップロードできませんでした。容量の小さい写真でもう一度お試しください');
    }
    setReplayUploading(false);
  }

  function inviteUrl(){
    if(!room?.code)return '';
    const url=new URL(window.location.href);
    url.search='';
    url.hash='';
    url.searchParams.set('room',room.code);
    // v2.52: LINEから招待リンクを開いた場合は、LINE内ブラウザではなく
    // 端末の外部ブラウザへ直接渡す。LINE公式の openExternalBrowser=1 を利用。
    // LINE以外のブラウザでは通常のクエリとして無視され、room参加処理はそのまま動く。
    url.searchParams.set('openExternalBrowser','1');
    return url.toString();
  }

  async function copyInviteLink(){
    const url=inviteUrl();
    if(!url)return;
    try{
      await navigator.clipboard.writeText(url);
      setShareMessage('招待リンクをコピーしました！');
      setTimeout(()=>setShareMessage(''),2200);
    }catch(_e){
      setShareMessage('コピーできませんでした');
      setTimeout(()=>setShareMessage(''),2200);
    }
  }

  async function shareInviteLink(){
    const url=inviteUrl();
    if(!url)return;
    const data={
      title:'写真で一言 PARTY',
      text:`ルームコード ${room.code} で一緒に遊ぼう！`,
      url
    };
    try{
      if(navigator.share){
        await navigator.share(data);
      }else{
        await copyInviteLink();
      }
    }catch(_e){}
  }

  function leaveInviteEntry(){
    clearSession();
    setJoinCode('');
    setInviteEntry(false);
    setError('');
    try{
      const url=new URL(window.location.href);
      url.searchParams.delete('room');
      const clean=url.pathname+(url.searchParams.toString()?`?${url.searchParams.toString()}`:'')+url.hash;
      window.history.replaceState({},'',clean);
    }catch(_e){}
  }


  async function restoreSavedGame({silent=false}={}){
    const saved=readSavedSession();
    if(!saved)return false;

    if(!silent)setRestoringSession(true);

    try{
      const [{data:r,error:re},{data:p,error:pe}]=await Promise.all([
        supabase.from('party_rooms').select('*').eq('id',saved.roomId).maybeSingle(),
        supabase.from('party_players').select('*').eq('id',saved.playerId).eq('room_id',saved.roomId).maybeSingle()
      ]);

      if(re||pe||!r||!p){
        return false;
      }

      // 終了済みのゲームだけは自動復帰しない
      if(r.status==='finished'){
        clearSession();
        clearRememberedRoom();
        return false;
      }

      let restoredPlayer=p;
      if(!p.device_id){
        const {data:patchedPlayer}=await supabase.from('party_players')
          .update({device_id:getDeviceId()})
          .eq('id',p.id)
          .eq('room_id',r.id)
          .select()
          .single();
        if(patchedPlayer)restoredPlayer=patchedPlayer;
      }

      setRoom(r);
      setMe(restoredPlayer);
      setName(restoredPlayer.nickname||'');
      setEditingNickname(p.nickname||'');
      setInviteEntry(false);
      saveSessionData(r.id,restoredPlayer.id,!!restoredPlayer.is_host);
      await refresh(r.id);
      return true;
    }catch(_e){
      // オフライン等では保存情報を消さない。接続復帰時に再試行する。
      return false;
    }finally{
      if(!silent)setRestoringSession(false);
    }
  }


  useEffect(()=>{
    const recover=()=>restoreSavedGame({silent:true});

    const onVisibility=()=>{
      if(document.visibilityState==='visible')recover();
    };
    const onFocus=()=>recover();
    const onOnline=()=>recover();

    document.addEventListener('visibilitychange',onVisibility);
    window.addEventListener('focus',onFocus);
    window.addEventListener('online',onOnline);

    return()=>{
      document.removeEventListener('visibilitychange',onVisibility);
      window.removeEventListener('focus',onFocus);
      window.removeEventListener('online',onOnline);
    };
  },[]);


  useEffect(()=>{
    if(!room?.id||room.status==='finished')return;
    const timer=setInterval(()=>{
      if(document.visibilityState==='visible' && navigator.onLine!==false){
        refresh(room.id);
      }
    },5000);
    return()=>clearInterval(timer);
  },[room?.id,room?.status]);


  useEffect(()=>{
    const onPopState=async()=>{
      const saved=readSavedSession();
      if(!saved)return;
      const ok=await restoreSavedGame({silent:true});
      if(ok){
        try{history.replaceState({photoParty:true},'',location.pathname);}catch(_e){}
      }
    };
    window.addEventListener('popstate',onPopState);
    return()=>window.removeEventListener('popstate',onPopState);
  },[]);


  async function reconcileRoomAfterLeave(roomId){
    const {data:r}=await supabase.from('party_rooms').select('*').eq('id',roomId).maybeSingle();
    if(!r)return;

    const [{data:ps},{data:ans},{data:vs}]=await Promise.all([
      supabase.from('party_players').select('*').eq('room_id',roomId).order('joined_at',{ascending:true}),
      supabase.from('party_answers').select('*').eq('room_id',roomId).eq('round_no',r.round_no),
      supabase.from('party_votes').select('*').eq('room_id',roomId).eq('round_no',r.round_no)
    ]);

    const playersNow=ps||[];
    const answersNow=ans||[];
    const votesNow=vs||[];

    // ゲーム開始前のロビーなら、全員退出しても部屋は残す。
    // 同じコード/招待リンクから再参加でき、最初に戻った人をホストにする。
    if(playersNow.length===0){
      if(r.status==='lobby'){
        await supabase.from('party_rooms')
          .update({status:'lobby',image_url:null,prompt_text:null,prompt_urls:null})
          .eq('id',roomId);
      }else{
        await supabase.from('party_rooms').update({status:'finished'}).eq('id',roomId);
      }
      return;
    }

    // ホスト確定はDB側の排他処理で行う。同時退出/再参加でもホストを1人に保つ。
    await supabase.rpc('party_reconcile_host',{p_room_id:roomId});

    // ロビーでは残り人数に合わせて最大ラウンド数を更新
    if(r.status==='lobby'){
      await supabase.from('party_rooms').update({max_rounds:Math.max(1,playersNow.length)}).eq('id',roomId);
      return;
    }

    // 回答中：残っている全員が回答済みなら投票へ進める
    if(r.status==='answering'){
      const answeredIds=new Set(answersNow.map(a=>a.player_id));
      const everyoneAnswered=playersNow.every(p=>answeredIds.has(p.id));
      if(everyoneAnswered){
        await supabase.from('party_rooms')
          .update({status:'voting',vote_deadline:new Date(Date.now()+30000).toISOString()})
          .eq('id',roomId)
          .eq('status','answering');
      }
      return;
    }

    // 投票中：投票できる回答がない人は自動完了扱い。
    // 残っている全員が投票済み/自動完了なら結果へ。
    if(r.status==='voting'){
      const votedIds=new Set(votesNow.map(v=>v.voter_player_id));
      const autoCompletedIds=playersNow
        .filter(p=>!answersNow.some(a=>a.player_id!==p.id))
        .map(p=>p.id);
      const everyoneDone=playersNow.every(p=>votedIds.has(p.id)||autoCompletedIds.includes(p.id));
      if(everyoneDone){
        await supabase.from('party_rooms')
          .update({status:'result'})
          .eq('id',roomId)
          .eq('status','voting');
      }
    }
  }

  async function removeParticipant(player){
    if(!me?.is_host||!room?.id||!player?.id||player.id===me.id)return;

    const ok=window.confirm(`${player.nickname} をこの部屋から削除しますか？`);
    if(!ok)return;

    setError('');
    setBusy(true);
    try{
      // v2.48: RLSのDELETE結果に依存せず、DB側で「現在のホスト」確認と削除を原子的に行う。
      // 名前入力中・写真選択中でも party_players に存在する参加者なら削除できる。
      const {data:removed,error:removeError}=await supabase.rpc('party_host_remove_player',{
        p_room_id:room.id,
        p_host_player_id:me.id,
        p_host_device_id:getDeviceId(),
        p_target_player_id:player.id
      });

      if(removeError||removed!==true){
        setError('参加者を削除できませんでした。ホスト権限を確認して、もう一度お試しください。');
        await refresh(room.id);
        return;
      }

      // 写真がすでにアップロード済みなら、Storage側もベストエフォートで片付ける。
      if(player.photo_path){
        try{await supabase.storage.from('party-images').remove([player.photo_path]);}catch(_e){}
      }

      await refresh(room.id);
    }catch(_e){
      setError('参加者の削除に失敗しました。もう一度お試しください。');
    }finally{
      setBusy(false);
    }
  }


  async function leaveGame(){
    if(!room?.id||!me?.id){
      // DB側の部屋がすでに消えている場合でも、端末内の状態だけ消して必ずトップへ戻す
      clearSession();
      clearRememberedRoom();
      setRoom(null);
      setMe(null);
      setPlayers([]);
      setAnswers([]);
      setVotes([]);
      setName('');
      setInviteEntry(false);
      setError('');
      setEntry('home');
      try{
        const u=new URL(location.href);
        u.searchParams.delete('room');
        history.replaceState({},'',u.pathname+u.search);
      }catch(_e){}
      return;
    }

    const ok=window.confirm('ゲームから退出しますか？\n退出すると、このゲームには自動復帰できません。');
    if(!ok)return;

    setLeavingGame(true);
    setError('');

    const roomId=room.id;
    const playerId=me.id;

    try{
      const deviceId=getDeviceId();

      // 退出者の削除とホスト譲渡はDB側で同じロック内にまとめて処理する。
      // これにより、ホスト退出直後に古いHOST表示だけ残る競合を防ぐ。
      const {data:leaveResult,error:leaveError}=await supabase.rpc('party_leave_room',{
        p_room_id:roomId,
        p_player_id:playerId,
        p_device_id:deviceId
      });

      if(leaveError||leaveResult?.ok===false){
        // RPCが一時的に使えない場合の後方互換フォールバック。
        const {error:deviceDeleteError}=await supabase
          .from('party_players')
          .delete()
          .eq('room_id',roomId)
          .eq('device_id',deviceId);

        const {error:playerDeleteError}=await supabase
          .from('party_players')
          .delete()
          .eq('id',playerId)
          .eq('room_id',roomId);

        if(deviceDeleteError&&playerDeleteError)throw deviceDeleteError;
        try{ await supabase.rpc('party_reconcile_host',{p_room_id:roomId}); }catch(_e){}
      }

      // 部屋がまだ存在する場合だけ、回答/投票の進行状態も残存参加者に合わせて再調整。
      const {data:roomStillExists}=await supabase
        .from('party_rooms')
        .select('id')
        .eq('id',roomId)
        .maybeSingle();

      if(roomStillExists){
        try{ await reconcileRoomAfterLeave(roomId); }catch(_e){}
      }
    }catch(_e){
      // 通信エラーやDB側削除済みでも、端末側の退出は必ず完了させる。
    }finally{
      clearSession();
      clearRememberedRoom();

      try{
        const u=new URL(location.href);
        u.searchParams.delete('room');
        history.replaceState({},'',u.pathname+u.search);
      }catch(_e){}

      setRoom(null);
      setMe(null);
      setPlayers([]);
      setAnswers([]);
      setVotes([]);
      setName('');
      setInviteEntry(false);
      setError('');
      setEntry('home');
      setLeavingGame(false);
    }
  }

  async function shouldBecomeHost(roomId){
    const {data:hosts}=await supabase.from('party_players')
      .select('id')
      .eq('room_id',roomId)
      .eq('is_host',true)
      .limit(1);
    return !(hosts&&hosts.length);
  }

  async function restoreOriginalHostIfNeeded(roomRow,playerRow){
    if(!roomRow?.id||!playerRow?.id)return playerRow;
    const deviceId=getDeviceId();

    // DB側でロックを取り、元ホスト復帰と通常のホスト確定を1回の処理で行う。
    const {error:hostErr}=await supabase.rpc('party_restore_creator_host',{
      p_room_id:roomRow.id,
      p_device_id:deviceId
    });
    if(hostErr)throw hostErr;

    const {data:updated}=await supabase.from('party_players')
      .select('*')
      .eq('id',playerRow.id)
      .eq('room_id',roomRow.id)
      .maybeSingle();
    return updated||playerRow;
  }

  async function getExistingDevicePlayer(roomId){
    const deviceId=getDeviceId();
    const {data}=await supabase.from('party_players')
      .select('*')
      .eq('room_id',roomId)
      .eq('device_id',deviceId)
      .maybeSingle();
    return data||null;
  }

  async function autoJoinInviteRoom(code){
    if(inviteAutoJoining||room?.id||me?.id)return false;
    if(!/^\d{4}$/.test(code))return false;

    setInviteAutoJoining(true);
    setError('');

    try{
      const {data:r,error:e}=await supabase.from('party_rooms')
        .select('*')
        .eq('code',code)
        .eq('status','lobby')
        .maybeSingle();

      if(e||!r){
        setError('この招待リンクの部屋には現在参加できません');
        return false;
      }

      const existingPlayer=await getExistingDevicePlayer(r.id);
      if(existingPlayer){
        const effectiveExisting=await restoreOriginalHostIfNeeded(r,existingPlayer);
        try{localStorage.setItem('party-token',effectiveExisting.client_token||'');}catch(_e){}
        saveSessionData(r.id,effectiveExisting.id,!!effectiveExisting.is_host);
        setRoom(r);
        setMe(effectiveExisting);
        setName(effectiveExisting.nickname||'');
        setEditingNickname(effectiveExisting.nickname||'');
        setInviteEntry(false);
        try{
          const url=new URL(window.location.href);
          url.searchParams.delete('room');
          const clean=url.pathname+(url.searchParams.toString()?`?${url.searchParams.toString()}`:'')+url.hash;
          window.history.replaceState({},'',clean);
        }catch(_e){}
        await refresh(r.id);
        return true;
      }

      const {count}=await supabase.from('party_players')
        .select('id',{count:'exact',head:true})
        .eq('room_id',r.id);

      if((count||0)>=8){
        setError('この部屋は8人で満員です');
        return false;
      }

      const token=crypto.randomUUID();
      const isHostForJoin=await shouldBecomeHost(r.id);
      const tempName=`ゲスト${String(Math.floor(100+Math.random()*900))}`;
      const deviceId=getDeviceId();

      const {data:p,error:pe}=await supabase.from('party_players')
        .insert({
          room_id:r.id,
          nickname:tempName,
          client_token:token,
          is_host:isHostForJoin,
          activity_status:'naming',
          activity_round:0,
          device_id:deviceId
        })
        .select()
        .single();

      if(pe){
        setError('招待ルームに参加できませんでした');
        return false;
      }

      const effectivePlayer=await restoreOriginalHostIfNeeded(r,p);

      try{localStorage.setItem('party-token',token);}catch(_e){}
      saveSessionData(r.id,effectivePlayer.id,!!effectivePlayer.is_host);

      setRoom(r);
      setMe(effectivePlayer);
      setName('');
      setEditingNickname('');
      setInviteEntry(false);

      try{
        const url=new URL(window.location.href);
        url.searchParams.delete('room');
        const clean=url.pathname+(url.searchParams.toString()?`?${url.searchParams.toString()}`:'')+url.hash;
        window.history.replaceState({},'',clean);
      }catch(_e){}

      await refresh(r.id);
      return true;
    }catch(_e){
      setError('招待ルームへの参加に失敗しました。もう一度お試しください');
      return false;
    }finally{
      setInviteAutoJoining(false);
    }
  }

  async function saveLobbyNickname(){
    const nick=editingNickname.trim();
    if(!me?.id||!room?.id)return;
    if(!nick){
      setError('ニックネームを入力してください');
      return;
    }

    setBusy(true);setError('');
    const {data:updated,error:e}=await supabase.from('party_players')
      .update({nickname:nick,activity_status:me.photo_url?'ready':'preparing',activity_round:0})
      .eq('id',me.id)
      .eq('room_id',room.id)
      .select()
      .single();

    if(e||!updated){
      setError('ニックネームを保存できませんでした');
      setBusy(false);
      return;
    }

    setMe(updated);
    setName(updated.nickname);
    setBusy(false);
    await refresh(room.id);
  }

  async function createRoom(){
    const nick=name.trim();
    if(!nick){setError('ニックネームを入力してください');return;}
    setBusy(true);setError('');

    let created=null;
    for(let i=0;i<8&&!created;i++){
      const code=String(Math.floor(1000+Math.random()*9000));
      const {data,error:e}=await supabase.from('party_rooms')
        .insert({code,status:'lobby',max_rounds:3,creator_device_id:getDeviceId()})
        .select()
        .single();
      if(!e)created=data;
    }

    if(!created){
      setError('部屋を作れませんでした。もう一度お試しください');
      setBusy(false);
      return;
    }

    const token=crypto.randomUUID();
    const {data:p,error:pe}=await supabase.from('party_players')
      .insert({
        room_id:created.id,
        nickname:nick,
        client_token:token,
        is_host:true,
        activity_status:'preparing',
        activity_round:0,
        device_id:getDeviceId()
      })
      .select()
      .single();

    if(pe){
      setError(pe.message);
      setBusy(false);
      return;
    }

    localStorage.setItem('party-token',token);
    setRoom(created);
    setMe(p);
    setEditingNickname(p.nickname||'');
    saveSessionData(created.id,p.id,true);
    setBusy(false);
    await refresh(created.id);
  }

  async function join(){
    const nick=name.trim();
    const code=joinCode.trim();

    if(!/^\d{4}$/.test(code)){
      setError('4桁のルームコードを入力してください');
      return;
    }
    if(!nick){
      setError('ニックネームを入力してください');
      return;
    }

    setBusy(true);setError('');

    const {data:r,error:e}=await supabase.from('party_rooms')
      .select('*')
      .eq('code',code)
      .eq('status','lobby')
      .maybeSingle();

    if(e||!r){
      setError('参加できる部屋が見つかりません');
      setBusy(false);
      return;
    }

    const existingPlayer=await getExistingDevicePlayer(r.id);
    if(existingPlayer){
      const effectiveExisting=await restoreOriginalHostIfNeeded(r,existingPlayer);
      try{localStorage.setItem('party-token',effectiveExisting.client_token||'');}catch(_e){}
      try{
        const url=new URL(window.location.href);
        url.searchParams.delete('room');
        const clean=url.pathname+(url.searchParams.toString()?`?${url.searchParams.toString()}`:'')+url.hash;
        window.history.replaceState({},'',clean);
        setInviteEntry(false);
      }catch(_e){}

      setRoom(r);
      setMe(effectiveExisting);
      setName(effectiveExisting.nickname||nick);
      setEditingNickname(effectiveExisting.nickname||nick);
      saveSessionData(r.id,effectiveExisting.id,!!effectiveExisting.is_host);
      setBusy(false);
      await refresh(r.id);
      return;
    }

    const {count}=await supabase.from('party_players')
      .select('id',{count:'exact',head:true})
      .eq('room_id',r.id);

    if((count||0)>=8){
      setError('この部屋は8人で満員です');
      setBusy(false);
      return;
    }

    const token=crypto.randomUUID();
    const isHostForJoin=await shouldBecomeHost(r.id);
    const deviceId=getDeviceId();
    const {data:p,error:pe}=await supabase.from('party_players')
      .insert({
        room_id:r.id,
        nickname:nick,
        client_token:token,
        is_host:isHostForJoin,
        activity_status:'preparing',
        activity_round:0,
        device_id:deviceId
      })
      .select()
      .single();

    if(pe){
      setError(pe.message);
      setBusy(false);
      return;
    }

    const effectivePlayer=await restoreOriginalHostIfNeeded(r,p);

    localStorage.setItem('party-token',token);

    try{
      const url=new URL(window.location.href);
      url.searchParams.delete('room');
      const clean=url.pathname+(url.searchParams.toString()?`?${url.searchParams.toString()}`:'')+url.hash;
      window.history.replaceState({},'',clean);
      setInviteEntry(false);
    }catch(_e){}

    setRoom(r);
    setMe(effectivePlayer);
    setEditingNickname(effectivePlayer.nickname||'');
    saveSessionData(r.id,effectivePlayer.id,!!effectivePlayer.is_host);
    setBusy(false);
    await refresh(r.id);
  }

  async function start(){
    if(!me?.is_host||!room?.id)return;
    setBusy(true);setError('');

    // 画面上の古い players 状態ではなく、開始ボタンを押した瞬間のDB状態を使う。
    // 名前入力中/写真選択中の人をホストが削除した直後でも、残った全員の写真だけで正確にラウンドを作る。
    const {data:readyPlayers,error:readyError}=await supabase.from('party_players')
      .select('*')
      .eq('room_id',room.id)
      .order('joined_at',{ascending:true});
    const currentPlayers=readyPlayers||[];
    if(readyError||currentPlayers.length<2){
      setError('参加者が2人以上必要です');setBusy(false);return;
    }
    if(currentPlayers.some(p=>!p.photo_url||p.activity_status==='naming')){
      setError('まだ名前や写真を準備している参加者がいます');setBusy(false);return;
    }

    const shuffled=[...currentPlayers].sort(()=>Math.random()-.5);
    await Promise.all(shuffled.map((p,i)=>supabase.from('party_players').update({photo_order:i+1}).eq('id',p.id).eq('room_id',room.id)));
    const promptUrls=shuffled.map(p=>p.photo_url).filter(Boolean);
    const firstUrl=promptUrls[0];
    setText('');setConfirming(false);activitySent.current='';
    const deadline=new Date(Date.now()+60000).toISOString();
    await supabase.from('party_players').update({activity_status:'idle',activity_round:1}).eq('room_id',room.id);
    await supabase.from('party_rooms').update({
      status:'answering',round_no:1,max_rounds:promptUrls.length,answer_deadline:deadline,
      image_url:firstUrl,prompt_text:'この写真に一言！',prompt_urls:promptUrls
    }).eq('id',room.id);
    setBusy(false);await refresh(room.id);
  }

  function onAnswerChange(value){
    setText(value);
    if(value.trim())setActivity('typing');
    else setActivity('thinking');
  }

  async function submitConfirmed(){
    if(!text.trim())return;
    setBusy(true);
    const {error:e}=await supabase.from('party_answers').upsert({room_id:room.id,player_id:me.id,round_no:room.round_no,body:text.trim()},{onConflict:'room_id,player_id,round_no'});
    if(e){setError(e.message);setBusy(false);return;}
    await setActivity('answered');
    setConfirming(false);setBusy(false);
    await refresh(room.id);
  }

  async function vote(a){
    const answerId=typeof a==='object'?a?.id:a;
    const answerPlayerId=typeof a==='object'?a?.player_id:null;
    if(!answerId||String(answerId).startsWith('timeout-'))return;
    if(answerPlayerId===me?.id)return;
    if(busy)return;

    // hasVoted は画面同期の一瞬の遅れで前ラウンドの状態を保持することがあるため、
    // 投票可否はDB上の『現在のround_no』で最終確認する。
    const {data:currentVote}=await supabase
      .from('party_votes')
      .select('id')
      .eq('room_id',room.id)
      .eq('voter_player_id',me.id)
      .eq('round_no',room.round_no)
      .maybeSingle();
    if(currentVote)return;

    setBusy(true);setError('');
    const {error:e}=await supabase.from('party_votes').upsert(
      {room_id:room.id,voter_player_id:me.id,answer_id:answerId,round_no:room.round_no},
      {onConflict:'room_id,voter_player_id,round_no',ignoreDuplicates:false}
    );

    if(e){
      // 端末側で二重タップになっても、実際に投票済みなら正常扱いにする
      const {data:existing}=await supabase
        .from('party_votes')
        .select('id,answer_id')
        .eq('room_id',room.id)
        .eq('voter_player_id',me.id)
        .eq('round_no',room.round_no)
        .maybeSingle();

      if(!existing){
        setError('投票できませんでした。もう一度押してください');
        setBusy(false);
        return;
      }
    }

    await setActivity('voted');
    setBusy(false);
    await refresh(room.id);
  }

  async function next(){
    if(!me?.is_host||!room?.id||busy)return;
    setBusy(true);setError('');
    const expectedRound=room.round_no;
    try{
      // v2.46: ラウンド更新はDB側で1回だけ実行する。
      // 二重タップ・Realtime・複数端末の古い画面から同時実行されても、
      // expectedRound と result 状態が一致する最初の1回だけが次へ進める。
      const {data,error}=await supabase.rpc('party_advance_round',{
        p_room_id:room.id,
        p_player_id:me.id,
        p_expected_round:expectedRound
      });
      if(error){setError('次のラウンドへ進めませんでした。もう一度押してください');return;}
      if(!data?.ok){
        if(data?.reason==='missing_prompt')setError('次のお題写真を読み込めませんでした');
        await refresh(room.id);
        return;
      }
      // 次ラウンド開始時に前ラウンドの端末状態を完全に破棄。
      setText('');setConfirming(false);setAnswers([]);setVotes([]);setSeconds(60);setVoteSeconds(30);activitySent.current='';
      await refresh(room.id);
    }finally{
      setBusy(false);
    }
  }

  async function replay(){
    if(!me?.is_host||!room?.id)return;
    setBusy(true);setError('');

    // 次のゲームへ進む前に、前ゲームで使ったお題写真をStorageから削除する。
    const {data:photoRows,error:photoRowsError}=await supabase
      .from('party_players')
      .select('photo_path')
      .eq('room_id',room.id)
      .not('photo_path','is',null);

    if(photoRowsError){
      setError('前の写真情報を確認できませんでした。もう一度押してください');
      setBusy(false);
      return;
    }

    const oldPhotoPaths=[...new Set((photoRows||[])
      .map(p=>p.photo_path)
      .filter(path=>typeof path==='string'&&path.startsWith(`uploads/${room.id}/`)))];

    if(oldPhotoPaths.length){
      const {error:storageError}=await supabase.storage.from('party-images').remove(oldPhotoPaths);
      if(storageError){
        setError('前の写真を削除できませんでした。もう一度押してください');
        setBusy(false);
        return;
      }
    }

    const {error:votesError}=await supabase.from('party_votes').delete().eq('room_id',room.id);
    const {error:answersError}=await supabase.from('party_answers').delete().eq('room_id',room.id);
    const {error:playersError}=await supabase.from('party_players').update({
      photo_url:null,
      photo_path:null,
      photo_order:null,
      activity_status:'idle',
      activity_round:1
    }).eq('room_id',room.id);
    const {error:roomError}=await supabase.from('party_rooms').update({
      status:'lobby',
      round_no:1,
      answer_deadline:null,
      vote_deadline:null,
      image_url:null,
      prompt_text:null,
      prompt_urls:null
    }).eq('id',room.id);

    if(votesError||answersError||playersError||roomError){
      setError('次のゲームの準備に失敗しました。もう一度押してください');
      setBusy(false);
      await refresh(room.id);
      return;
    }

    setReplayPhotoFile(null);
    if(replayPhotoPreview)URL.revokeObjectURL(replayPhotoPreview);
    setReplayPhotoPreview('');
    setText('');setConfirming(false);activitySent.current='';
    setBusy(false);
    await refresh(room.id);
  }

  async function leaveHome(){
    // 最終結果からホームへ戻る時は、ゲストもDB上の参加者から確実に退出させる。
    // これによりホストが「もう一巡」でロビーへ戻っても、退出済みユーザーが残らない。
    if(room?.status==='finished'&&me?.id){
      if(me.is_host){
        try{
          const {data:closed,error:closeError}=await supabase.rpc('party_finish_room_and_close',{
            p_room_id:room.id,
            p_player_id:me.id,
            p_device_id:getDeviceId()
          });
          if(closeError||closed!==true){
            await supabase.from('party_rooms').delete().eq('id',room.id);
          }
        }catch(_e){
          try{await supabase.from('party_rooms').delete().eq('id',room.id)}catch(_e2){}
        }
      }else{
        try{
          const deviceId=getDeviceId();
          const {data:leaveResult,error:leaveError}=await supabase.rpc('party_leave_room',{
            p_room_id:room.id,
            p_player_id:me.id,
            p_device_id:deviceId
          });
          if(leaveError||leaveResult?.ok===false){
            await supabase.from('party_players').delete().eq('id',me.id).eq('room_id',room.id);
          }
        }catch(_e){
          try{await supabase.from('party_players').delete().eq('id',me.id).eq('room_id',room.id)}catch(_e2){}
        }
      }
    }

    clearSession();
    clearRememberedRoom();
    setInviteEntry(false);setRoom(null);setMe(null);
    setPlayers([]);setAnswers([]);setVotes([]);setText('');setConfirming(false);setEntry('home');setError('');setPhotoFile(null);setPhotoPreview('');setReplayPhotoFile(null);setReplayPhotoPreview('');
    try{
      const u=new URL(location.href);
      u.searchParams.delete('room');
      history.replaceState({},'',u.pathname+u.search);
    }catch(_e){
      history.replaceState({},'',location.pathname);
    }
  }

  function goCreate(){
    setError('');setName('');setPhotoFile(null);setPhotoPreview('');setReplayPhotoFile(null);setReplayPhotoPreview('');setEntry('create');
    history.pushState({screen:'create'},'',location.href);
  }
  function backHome(){setError('');setEntry('home');history.replaceState({},'',location.pathname+location.search);}

  function choosePhoto(file){
    if(!file)return;
    if(!file.type.startsWith('image/')){setError('画像ファイルを選んでください');return;}
    if(file.size>10*1024*1024){setError('写真は10MB以下にしてください');return;}
    setPhotoFile(file);setError('');
    if(photoPreview)URL.revokeObjectURL(photoPreview);
    setPhotoPreview(URL.createObjectURL(file));
  }

  if(hostEndedNotice){
    return <main className="wrap homeWrap endedWrap">
      <div className="card endedCard">
        <div className="endedSpark">✨</div>
        <h1>ゲーム終了！</h1>
        <p className="muted center">ホストがゲームを終了しました</p>
        <div className="beeButtonArea">
          <span className="flyingBee beeOne" aria-hidden="true">🐝</span>
          <span className="flyingBee beeTwo" aria-hidden="true">🐝</span>
          <span className="flyingBee beeThree" aria-hidden="true">🐝</span>
          <button className="primary wide endedHomeBtn" onClick={()=>{
            setHostEndedNotice(false);
            setEntry('home');
            try{history.replaceState({},'',location.pathname)}catch(_e){}
          }}>トップへ戻る</button>
        </div>
        <p className="endedThanks">遊んでくれてありがとう！</p>
      </div>
    </main>;
  }

  if(lineFallback){
    return <main className="wrap homeWrap">
      <div className="card createCard">
        <div className="logo">📸</div>
        <h2>外部ブラウザで開いてください</h2>
        <p className="muted center">LINE内ブラウザで開いています。Safari・Chromeなど、普段お使いのブラウザでプレイしてください。</p>
        <div className="stack">
          <button className="primary wide" onClick={continueOutsideBrowser}>ブラウザで続ける</button>
          <button className="secondary wide" onClick={copyOutsideLink}>リンクをコピー</button>
          {shareMessage&&<div className="shareMessage">{shareMessage}</div>}
        </div>
        <p className="muted center">自動で切り替わらない場合は、LINEのメニューから「ブラウザで開く」を選んでください。</p>
      </div>
    </main>;
  }

  if(screen==='home')return <HomeScreen name={name} setName={setName} joinCode={joinCode} setJoinCode={setJoinCode} join={join} goCreate={goCreate} error={error} busy={busy} inviteEntry={inviteEntry} leaveInviteEntry={leaveInviteEntry}/>;
  if(screen==='create')return <CreateScreen name={name} setName={setName} createRoom={createRoom} backHome={backHome} error={error} busy={busy}/>;

  if(restoringSession){
    return <main className="wrap"><div className="card"><p className="center muted">ゲームに戻っています…</p></div></main>;
  }

  return <main className="wrap"><button className="globalHomeBtn" disabled={leavingGame} onClick={room?.status==='finished'?leaveHome:room?.id?leaveGame:leaveHome}>← トップへ戻る</button>
    <div className="top"><b>📸 写真で一言</b><span className="pill">ROOM {room?.code}</span></div>
    <div className="card">
      {screen==='lobby'&&<Lobby room={room} players={players} me={me} start={start} busy={busy} replayPhotoPreview={replayPhotoPreview} chooseReplayPhoto={chooseReplayPhoto} submitReplayPhoto={submitReplayPhoto} replayUploading={replayUploading} error={error} copyInviteLink={copyInviteLink} shareInviteLink={shareInviteLink} shareMessage={shareMessage} editingNickname={editingNickname} setEditingNickname={setEditingNickname} saveLobbyNickname={saveLobbyNickname} removeParticipant={removeParticipant}/>} 
      {screen==='answer'&&<Answer room={room} seconds={seconds} text={text} onChange={onAnswerChange} onConfirm={()=>text.trim()&&setConfirming(true)}/>} 
      {screen==='confirm-answer'&&<ConfirmAnswer room={room} text={text} back={()=>setConfirming(false)} submit={submitConfirmed} busy={busy}/>} 
      {screen==='wait-answer'&&<WaitScreen title="回答を待っています" subtitle="みんなの回答が揃うと自動で投票へ進みます" room={room} players={players} mode="answer" remainingSeconds={seconds}/>}
      {screen==='voting'&&<Voting room={room} answers={answers} players={players} me={me} vote={vote} busy={busy} error={error} voteSeconds={voteSeconds}/>} 
      {screen==='wait-vote'&&<WaitScreen title="投票を待っています" subtitle="全員の投票が終わると自動で結果発表！" room={room} players={players} mode="vote" remainingSeconds={voteSeconds}/>}
      {screen==='result'&&<><h2>結果</h2><PromptPhoto room={room}/><Results room={room} players={players}/>{me?.is_host?<button className="primary wide mt" disabled={busy} onClick={next}>{busy?'準備中…':room.round_no>=room.max_rounds?'最終結果へ':'次の写真へ'}</button>:<p className="center muted">ホストが次のラウンドを準備しています…</p>}</>}
      {screen==='finished'&&<><h2>ゲーム終了 🏆</h2><Results room={room} players={players} final/><div className="finalActions">{me?.is_host?<><button className="primary wide" disabled={busy} onClick={replay}>{busy?'準備中…':'🔁 新しい写真でもう一巡'}</button><button className="secondary wide" onClick={leaveHome}>⌂ ホームに戻る</button></>:<><p className="center muted">ホストが次のゲームを選びます</p><button className="secondary wide" onClick={leaveHome}>⌂ ホームに戻る</button></>}</div></>}
    </div>
  </main>;
}

function PhotoPicker({preview,choosePhoto}){
  return <label className="photoPicker"><input className="photoFileInput" type="file" accept="image/*" onChange={e=>choosePhoto(e.target.files?.[0])}/>{preview?<><img src={preview} alt="選択したお題写真"/><span>写真を変更する</span></>:<><div className="uploadIcon">＋</div><b>みんなへのお題写真を1枚選ぶ</b><small>ゲーム開始まで他の人には表示されません</small></>}</label>;
}

function HomeScreen({name,setName,joinCode,setJoinCode,join,goCreate,error,busy,inviteEntry,leaveInviteEntry}){
  if(inviteEntry){
    return <main className="wrap homeWrap">
      <div className="card">
        <div className="logo">📸</div>
        <h2>ルーム {joinCode} に参加しています…</h2>
        <p className="muted center">そのままお待ちください</p>
        {error&&<><p className="error">{error}</p><button className="secondary wide" onClick={leaveInviteEntry}>トップへ戻る</button></>}
      </div>
    </main>;
  }

  return <main className="wrap homeWrap">
    {inviteEntry&&<button className="globalHomeBtn" onClick={leaveInviteEntry}>← トップへ戻る</button>}
    <div className="hero">
      <div className="logo">📸</div>
      <div className="limitBadge">試遊・節約モード</div>
      <h1>写真で一言 PARTY</h1>
      <p className="muted">部屋に入ってみんなへのお題写真を1枚登録し、ゲームを始めよう。</p>
    </div>

    <button className="createHero" onClick={goCreate}>
      <span className="createIcon">＋</span>
      <span><b>部屋を作る</b></span>
      <span>›</span>
    </button>

    <div className="joinZone">
      <div className="zoneTitle">{inviteEntry?`招待されたルーム ${joinCode} に参加`:"コードルームから参加"}</div>
      <div className="stack">
        <input inputMode="numeric" maxLength={4} placeholder="4桁のルームコード" value={joinCode} onChange={e=>setJoinCode(e.target.value.replace(/\D/g,'').slice(0,4))}/>
        <input maxLength={16} placeholder="ニックネーム" value={name} onChange={e=>setName(e.target.value)}/>
        <button className="primary" disabled={busy} onClick={join}>{busy?'参加中…':'この部屋に参加する'}</button>
        <small className="muted center">参加したあと、ロビーでお題写真を選びます</small>
        {error&&<p className="error">{error}</p>}
      </div>
    </div>
  </main>;
}

function CreateScreen({name,setName,createRoom,backHome,error,busy}){
  return <main className="wrap homeWrap">
    <button className="backBtn" onClick={backHome}>← トップへ戻る</button>
    <div className="card createCard">
      <div className="logo">🎉</div>
      <h1>部屋を作る</h1>
      <p className="muted">まずは部屋を作り、参加者を招待しましょう。</p>
      <div className="stack">
        <input autoFocus maxLength={16} placeholder="ニックネーム" value={name} onChange={e=>setName(e.target.value)}/>
        <button className="primary" disabled={busy} onClick={createRoom}>{busy?'作成中…':'部屋を作る'}</button>
        {error&&<p className="error">{error}</p>}
      </div>
    </div>
  </main>;
}

function Lobby({room,players,me,start,busy,replayPhotoPreview,chooseReplayPhoto,submitReplayPhoto,replayUploading,error,copyInviteLink,shareInviteLink,shareMessage,editingNickname,setEditingNickname,saveLobbyNickname,removeParticipant}){
  const originalHostDevice=(()=>{try{return localStorage.getItem(DEVICE_KEY);}catch(_e){return null;}})();
  const iAmOriginalHost=!!(room.creator_device_id&&originalHostDevice===room.creator_device_id);

  const allReady=players.length>=2&&players.every(p=>p.photo_url&&p.activity_status!=='naming');
  const myReady=!!players.find(p=>p.id===me?.id)?.photo_url;
  return <>
    <p className="roomCodeLabel">ルームコード</p>
    <div className="code">{room.code}</div>
    {me?.is_host&&!iAmOriginalHost&&room.creator_device_id&&<p className="muted center">現在あなたがホストです。元のホストが戻った場合はホスト権限が自動で戻ります。</p>}

    {me?.is_host&&<div className="inviteBox hostInviteGuide">
      <div className="inviteTitle">友だちを招待</div>
      <div className="joinHowTo">
        <b>参加方法</b>
        <p>① リンクを友だちに送る</p>
        <p>② 友だちはリンクを開くだけで参加</p>
        <p>③ またはトップ画面でルームコードを入力</p>
      </div>
      <div className="inviteActions">
        <button className="inviteBtn" onClick={copyInviteLink}>🔗 リンクをコピー</button>
        <button className="inviteBtn primaryInvite" onClick={shareInviteLink}>📤 共有する</button>
      </div>
      {shareMessage&&<div className="shareMessage">{shareMessage}</div>}
    </div>}

    {me?.activity_status==='naming'&&<div className="photoPrepBox">
      <h2>お題写真を準備しよう</h2>
      <p className="muted center">参加者それぞれが、みんなへのお題写真を1枚登録します</p>
      <div className="replayUploadCard nestedPhotoCard">
      <h3>👋 ニックネームを決めよう</h3>
      <p className="muted center">リンクを開いた時点で、この部屋には参加済みです。</p>
      <input maxLength={16} placeholder="ニックネーム" value={editingNickname} onChange={e=>setEditingNickname(e.target.value)}/>
      <button className="primary wide" disabled={!editingNickname.trim()||busy} onClick={saveLobbyNickname}>
        {busy?'保存中…':'この名前にする'}
      </button>
      </div>
    </div>}

    {me?.activity_status!=='naming'&&!myReady&&<div className="photoPrepBox">
      <h2>お題写真を準備しよう</h2>
      <p className="muted center">参加者それぞれが、みんなへのお題写真を1枚登録します</p>
      <div className="replayUploadCard nestedPhotoCard">
        <h3>📷 あなたのお題写真を選ぶ</h3>
        <PhotoPicker preview={replayPhotoPreview} choosePhoto={chooseReplayPhoto}/>
        <button className="primary wide" disabled={!replayPhotoPreview||replayUploading} onClick={submitReplayPhoto}>
          {replayUploading?'写真を送信中…':'この写真で準備OK'}
        </button>
        {error&&<p className="error">{error}</p>}
      </div>
    </div>}

    {me?.activity_status!=='naming'&&myReady&&<div className="photoPrepBox">
      <h2>お題写真を準備しよう</h2>
      <p className="muted center">参加者それぞれが、みんなへのお題写真を1枚登録します</p>
      <div className="readyNotice">✅ あなたのお題写真は準備OK</div>
    </div>}

    <div className="sectionLabel">参加者 {players.length}人</div>
    <div className="stack">
      {players.map(p=><div className="player" key={p.id}>
        <span className="avatar">{p.nickname.slice(0,1)}</span>
        <b>{p.nickname}</b>
        {p.is_host&&<span className="hostBadge">HOST 👑</span>}
        <span className="photoReady">{p.activity_status==='naming'?'✏️ 名前を入力中…':p.photo_url?'📷 準備OK':'🖼️ 写真を準備中…'}</span>
        {me?.is_host&&p.id!==me?.id&&<button
          className="removePlayerBtn"
          disabled={busy}
          onClick={()=>removeParticipant(p)}
        >削除</button>}
      </div>)}
    </div>

    {me?.is_host
      ? <button className="primary wide mt" disabled={!allReady||busy} onClick={start}>
          {busy?'準備中…':players.length<2?'あと1人参加すると開始できます':!allReady?'写真を準備中の人がいます':'ゲーム開始！'}
        </button>
      : <p className="center muted mt">{allReady?'全員準備OK！ ホストの開始を待っています…':'写真を準備中の人がいます…'}</p>}

    <p className="privacyNote">🔒 写真はゲーム開始まで他の参加者には表示されません。写真を選んでいる間も、あなたは参加者としてロビーに表示されます。</p>
  </>;
}

function promptUrlForRound(room){
  // v2.49: 表示中ラウンドの写真は frozen prompt_urls から決定する。
  // image_url が通信順序や旧クライアントの更新で先の写真になっても、
  // 結果画面などで次ラウンドの写真を誤表示しない。
  const urls=Array.isArray(room?.prompt_urls)?room.prompt_urls:[];
  const index=Math.max(0,(Number(room?.round_no)||1)-1);
  return urls[index]||room?.image_url||'';
}
function PromptPhoto({room}){const src=promptUrlForRound(room);return <><div className="roundTag">ROUND {room.round_no}/{room.max_rounds}</div><img className="photo" src={src} alt="大喜利のお題"/></>}

function Answer({room,seconds,text,onChange,onConfirm}){
  const roundLabel=room.round_no===room.max_rounds?'最終ラウンド':`ラウンド${room.round_no}`;
  return <><PromptPhoto room={room}/><div className="promptLine"><span className="promptRoundBadge">{roundLabel}</span><div className="promptText">この写真に一言！</div></div><div className={`timer ${seconds<=10?'urgent':''}`}>{seconds}<small>秒</small></div><div className="stack"><textarea className="answerInput" maxLength={80} placeholder="思いついた一言を書こう…" value={text} onChange={e=>onChange(e.target.value)}/><div className="charCount">{text.length}/80</div><button className="primary" disabled={!text.trim()} onClick={onConfirm}>回答を確認する</button></div></>;
}

function ConfirmAnswer({room,text,back,submit,busy}){
  return <><PromptPhoto room={room}/><div className="confirmBox"><div className="sectionLabel">この回答でいい？</div><div className="bigAnswer">『{text.trim()}』</div></div><div className="row"><button className="secondary flex" onClick={back}>修正する</button><button className="primary flex" disabled={busy} onClick={submit}>{busy?'送信中…':'この回答で決定！'}</button></div></>;
}

function getPlayerState(p,room,mode){
  const current=p.activity_round===room.round_no?p.activity_status:'idle';
  if(mode==='answer'){
    if(current==='answered')return {icon:'✅',label:'回答完了！',done:true};
    if(current==='typing')return {icon:'✏️',label:'書き書き中…',animate:true};
    return {icon:'🤔',label:'考え中…'};
  }
  if(current==='voted')return {icon:'✅',label:'投票完了！',done:true};
  return {icon:'👀',label:'選び中…',animate:true};
}

function WaitScreen({title,subtitle,room,players,mode,remainingSeconds}){
  const label=mode==='answer'?'回答締切まで':'投票締切まで';
  return <><PromptPhoto room={room}/><p className="muted center">{label} 残り{remainingSeconds}秒</p><div className="waitingHead"><div className="pulseDot"/><h2>{title}</h2><p className="muted">{subtitle}</p></div><div className="liveList">{players.map(p=>{const s=getPlayerState(p,room,mode);return <div className={`livePlayer ${s.done?'done':''}`} key={p.id}><div className="playerName"><span className="avatar">{p.nickname.slice(0,1)}</span><b>{p.nickname}</b></div><div className="activity"><span className={s.animate?'wiggle':''}>{s.icon}</span> {s.label}</div></div>})}</div></>;
}

function Voting({room,answers,players,me,vote,busy,error,voteSeconds}){
  const [selectedVote,setSelectedVote]=useState(null);
  const currentAnswers=answers.filter(a=>a.round_no===room.round_no);
  const answerByPlayer=new Map(currentAnswers.map(a=>[a.player_id,a]));

  useEffect(()=>{setSelectedVote(null)},[room.round_no]);

  const choices=players.map(p=>{
    const answer=answerByPlayer.get(p.id);
    if(answer)return {...answer,is_self:p.id===me?.id};
    return {
      id:`timeout-${p.id}-${room.round_no}`,
      player_id:p.id,
      body:'時間切れ',
      is_timeout:true,
      is_self:p.id===me?.id
    };
  });

  // v2.50: 投票画面の回答順から入室順を推測できないよう、ラウンドごとに固定シャッフルする。
  // Math.random() だと再描画のたびに順番が動くため、room + round + answer id から安定した乱数順を作る。
  const voteOrderKey=(a)=>{
    const seed=`${room.id}:${room.round_no}:${a.id}`;
    let h=2166136261;
    for(let i=0;i<seed.length;i++){h^=seed.charCodeAt(i);h=Math.imul(h,16777619);}
    return h>>>0;
  };
  choices.sort((a,b)=>voteOrderKey(a)-voteOrderKey(b));

  const votableChoices=choices.filter(a=>!a.is_timeout&&!a.is_self);
  const autoCompleted=votableChoices.length===0;

  if(autoCompleted){
    return <>
      <h2>投票</h2>
      <PromptPhoto room={room}/>
      <div className={`timer ${voteSeconds<=10?'urgent':''}`}>{voteSeconds}<small>秒</small></div>
      <div className="autoVoteNotice">
        <b>自動的に投票完了になりました</b>
        <span>投票できる回答がないため、操作は必要ありません。</span>
      </div>
      <p className="muted center">みんなの投票が終わると自動で結果発表に進みます。</p>
      <div className="stack voteList">
        {choices.map(a=><div className={`voteCard lockedVoteCard ${a.is_timeout?'timeoutCard':''} ${a.is_self?'selfVoteCard':''}`} key={a.id}>
          <span>『{a.body}』</span>
          <b>{a.is_timeout?'時間切れ':a.is_self?'自分の回答':'投票対象外'}</b>
        </div>)}
      </div>
      {error&&<p className="error">{error}</p>}
    </>;
  }

  if(selectedVote){
    return <>
      <h2>投票を確認</h2>
      <PromptPhoto room={room}/>
      <div className={`timer ${voteSeconds<=10?'urgent':''}`}>{voteSeconds}<small>秒</small></div>
      <div className="confirmBox">
        <div className="sectionLabel">この回答に投票する？</div>
        <div className="bigAnswer">『{selectedVote.body}』</div>
      </div>
      <div className="row">
        <button className="secondary flex" disabled={busy} onClick={()=>setSelectedVote(null)}>選び直す</button>
        <button className="primary flex" disabled={busy||voteSeconds<=0} onClick={()=>vote(selectedVote)}>{busy?'投票中…':'この回答に投票！'}</button>
      </div>
      <p className="muted center">投票すると変更できません。</p>
      {error&&<p className="error">{error}</p>}
    </>;
  }

  return <>
    <h2>いちばん面白い回答に投票！</h2>
    <PromptPhoto room={room}/>
    <p className="votePrompt">一番おもしろい回答を選ぼう！</p>
    <p className="muted center">回答者の名前は結果発表まで秘密です。</p>
    <p className="muted center">投票の残り時間：{voteSeconds}秒</p>
    <div className="stack voteList">
      {choices.map(a=>{
        const locked=a.is_timeout||a.is_self;
        return <button
          className={`voteCard ${locked?'lockedVoteCard':''} ${a.is_timeout?'timeoutCard':''} ${a.is_self?'selfVoteCard':''}`}
          disabled={busy||locked}
          aria-disabled={busy||locked}
          key={a.id}
          onClick={()=>!locked&&setSelectedVote(a)}
        >
          <span>『{a.body}』</span>
          <b>{a.is_timeout?'時間切れ':a.is_self?'自分の回答':'これを選ぶ →'}</b>
        </button>
      })}
    </div>
    {error&&<p className="error">{error}</p>}
  </>;
}

const praiseLines=[
  n=>`${n}、あんたおもろいやん。`,
  n=>`${n}、今年一番わろたわ。`,
  n=>`${n}、キレッキレやな。`,
  n=>`${n}、これは強いわ。`,
  n=>`${n}、腹よじれたわ。`,
  n=>`${n}、その発想どこから来たんや。`,
  n=>`${n}、今日いち持ってったな。`,
  n=>`${n}、参りましたわ。`
];
function praiseFor(name,seed=1){
  let h=seed;for(const c of name)h=(h*31+c.charCodeAt(0))>>>0;
  return praiseLines[h%praiseLines.length](name);
}
function UnclePraise({name,seed}){
  if(!name)return null;
  return <div className="unclePraise"><div className="uncle" aria-hidden="true">👨🏻‍🦳<span className="clap">👏</span></div><div><div className="uncleLabel">笑いのご意見番</div><div className="praiseText">「{praiseFor(name,seed)}」</div></div></div>;
}

function Results({room,players,final=false}){
  const [rows,setRows]=useState([]);
  useEffect(()=>{(async()=>{
    if(final){
      const [{data:a},{data:v}]=await Promise.all([
        supabase.from('party_answers').select('id,player_id').eq('room_id',room.id),
        supabase.from('party_votes').select('answer_id').eq('room_id',room.id)
      ]);
      const owner={};(a||[]).forEach(x=>owner[x.id]=x.player_id);
      const scores={};players.forEach(p=>scores[p.id]=0);
      (v||[]).forEach(x=>{const pid=owner[x.answer_id];if(pid)scores[pid]=(scores[pid]||0)+1;});
      setRows(players.map(p=>({...p,total:scores[p.id]||0})).sort((a,b)=>b.total-a.total||a.nickname.localeCompare(b.nickname)));
      return;
    }
    const [{data:a},{data:v}]=await Promise.all([
      supabase.from('party_answers').select('id,body,player_id').eq('room_id',room.id).eq('round_no',room.round_no),
      supabase.from('party_votes').select('answer_id').eq('room_id',room.id).eq('round_no',room.round_no)
    ]);
    const counts={};(v||[]).forEach(x=>counts[x.answer_id]=(counts[x.answer_id]||0)+1);
    setRows((a||[]).map(x=>({...x,votes:counts[x.id]||0,name:players.find(p=>p.id===x.player_id)?.nickname||'不明'})).sort((x,y)=>y.votes-x.votes||x.name.localeCompare(y.name)));
  })()},[room.id,room.round_no,players.length,final]);

  if(final){
    const winner=rows[0];
    return <><UnclePraise name={winner?.nickname} seed={99}/><div className="stack leaderboard">{rows.map((r,i)=><div className={`answer resultRow ${i===0?'winner':''}`} key={r.id}><span className="rank">{i===0?'🥇':i===1?'🥈':i===2?'🥉':`${i+1}位`}</span><b>{r.nickname}</b><strong>{r.total} pt</strong></div>)}</div></>;
  }
  const topVotes=rows[0]?.votes;
  const winner=rows.find(r=>r.votes===topVotes);
  return <><div className="stack">{rows.map((r,i)=><div className={`answer resultAnswer ${i===0?'winner':''}`} key={r.id}><div><span className="rank">{i===0?'🥇':i===1?'🥈':i===2?'🥉':''}</span><b className="resultQuote">『{r.body}』</b><div className="muted author">— {r.name}</div></div><strong>{r.votes}票</strong></div>)}</div></>;
}


ReactDOM.createRoot(document.getElementById('root')).render(<Home/>);
