﻿/* =========================================================
   store.jsx — global app state (wallet / role / bookings / tx)
   ========================================================= */
const { createContext, useContext, useState, useEffect, useCallback, useRef } = React;

const StoreContext = createContext(null);
const useStore = () => useContext(StoreContext);


function ts(ms){ const d=new Date(ms); d.setHours(15,0,0,0); return d.toISOString(); }
function randHash(){
  const h="0123456789abcdef"; let s="0x";
  for(let i=0;i<64;i++) s+=h[Math.floor(Math.random()*16)];
  return s;
}

/* ── MetaMask 헬퍼 ───────────────────────────────────────
   잔액 조회: wei(BigInt) → ETH(소수 4자리 Number)          */
async function fetchBalance(address) {
  const raw = await window.ethereum.request({
    method: "eth_getBalance",
    params: [address, "latest"],
  });
  // raw = "0x..." hex string
  const wei = BigInt(raw);
  const eth = Number(wei) / 1e18;
  return Math.round(eth * 10000) / 10000;
}

/* Sepolia 네트워크로 전환 요청. 없으면 추가까지 시도 */
async function switchToSepolia() {
  const chainHex = "0x" + SEPOLIA_CHAIN_ID.toString(16); // "0xaa36a7"
  try {
    await window.ethereum.request({
      method: "wallet_switchEthereumChain",
      params: [{ chainId: chainHex }],
    });
  } catch (err) {
    // 4902: 체인이 MetaMask에 없으면 추가
    if (err.code === 4902) {
      await window.ethereum.request({
        method: "wallet_addEthereumChain",
        params: [{
          chainId: chainHex,
          chainName: "Sepolia Testnet",
          nativeCurrency: { name: "SepoliaETH", symbol: "ETH", decimals: 18 },
          rpcUrls: ["https://rpc.sepolia.org"],
          blockExplorerUrls: ["https://sepolia.etherscan.io"],
        }],
      });
    } else {
      throw err;
    }
  }
}

function StoreProvider({ children }){
  const [wallet, setWallet]   = useState(null); // {address,balance}
  const [role, setRole]       = useState(null);   // 'guest' | 'hotel'
  const [network, setNetwork] = useState("sepolia"); // 'sepolia' | 'wrong'
  const [bookings, setBookings] = useState([]);
  const [hotels, setHotels] = useState([]);   // created hotels (browse source)
  const [myHotelId, setMyHotelId] = useState(null);
  const [chainLoading, setChainLoading] = useState(true); // 최초 체인 로드 완료 전

  // keep a plain-JS mirror so getHotel()/hotelName() (outside React) stay current
  window.__HOTELS = hotels;
  window.__WALLET_ADDRESS = wallet?.address || null;
  const hotelProfile = hotels.find(h=>h.id===myHotelId) || null;

  // transaction modal queue
  const [tx, setTx] = useState(null); // {title, subtitle, stage, hash, error, onDone}
  const [toast, setToast] = useState(null);


  const loadFromChain = useCallback(async (walletAddr) => {
    try {
      const provider = new ethers.JsonRpcProvider("https://ethereum-sepolia-rpc.publicnode.com");
      const factory  = new ethers.Contract(FACTORY_ADDRESS, FACTORY_ABI, provider);
      const iface    = new ethers.Interface(HOTEL_ROOM_ABI);

      const hotelCount = Number(await factory.getHotelCount());
      if(hotelCount === 0){ setChainLoading(false); return; }

      // 1) 호텔 기본 정보 + 객실 주소 목록 병렬 조회
      const hotelAddrs = await Promise.all(
        Array.from({length: hotelCount}, (_, i) => factory.hotelList(i))
      );
      const [hotelInfos, roomAddrsByHotel] = await Promise.all([
        Promise.all(hotelAddrs.map(a => factory.getHotelInfo(a))),
        Promise.all(hotelAddrs.map(a => factory.getRoomsByHotel(a))),
      ]);

      // 2) 전체 roomAddr 배열 수집 (호텔→방 매핑 유지)
      const allRoomAddrs = roomAddrsByHotel.flat();

      // 3) 모든 room의 온체인 상태 병렬 조회
      const roomStates = await Promise.all(allRoomAddrs.map(async addr => {
        const c = new ethers.Contract(addr, HOTEL_ROOM_ABI, provider);
        try {
          const [p, d, a, rangeCount] = await Promise.all([c.pricePerNight(), c.depositRatio(), c.isAvailable(), c.getBookedRangesCount()]);
          const count = Number(rangeCount);
          const ranges = count > 0
            ? await Promise.all(Array.from({length: count}, (_, i) => c.bookedRanges(i)))
            : [];
          return {
            pricePerNight: p, depositRatio: d, isAvailable: a,
            bookedRanges: ranges.map(r => ({ checkIn: Number(r.checkIn), checkOut: Number(r.checkOut) })),
          };
        } catch { return null; }
      }));

      // 4) RoomCreated 이벤트 — publicnode는 50000블록 제한이므로 최근 블록만 조회
      const createdTopic = iface.getEvent("RoomCreated").topicHash;
      const latestBlock = await provider.getBlockNumber();
      const fromBlock = Math.max(0, latestBlock - 49000);
      const roomLogResults = await Promise.all(allRoomAddrs.map(roomAddr =>
        provider.getLogs({
          address: roomAddr,
          topics: [createdTopic],
          fromBlock,
          toBlock: "latest",
        }).catch(() => [])
      ));

      const metaMap = {};
      allRoomAddrs.forEach((roomAddr, i) => {
        const log = roomLogResults[i][0];
        if(!log) return;
        try{
          const p = iface.parseLog(log);
          metaMap[roomAddr.toLowerCase()] = {
            roomName:  p.args.roomName,
            roomType:  p.args.roomType,
            beds:      p.args.beds,
            roomSize:  p.args.roomSize,
            maxGuests: Number(p.args.maxGuests),
            imageUrl:  p.args.imageUrl,
            availFrom: Number(p.args.availFrom),
            availTo:   Number(p.args.availTo),
          };
        }catch(_){}
      });

      // 5) 호텔별로 조립
      const chainHotels = [];
      let roomIdx = 0;
      for(let i = 0; i < hotelAddrs.length; i++){
        const hotelAddr = hotelAddrs[i];
        const info = hotelInfos[i];
        if(!info.registered){ roomIdx += roomAddrsByHotel[i].length; continue; }

        const rooms = [];
        for(const roomAddr of roomAddrsByHotel[i]){
          const state = roomStates[roomIdx++];
          if(!state) continue;
          const meta = metaMap[roomAddr.toLowerCase()] || {};
          rooms.push({
            name:      meta.roomName  || "객실",
            type:      meta.roomType  || "-",
            beds:      meta.beds      || "-",
            size:      meta.roomSize  || "-",
            cap:       meta.maxGuests || 0,
            image:     meta.imageUrl  || "",
            availFrom: meta.availFrom > 0 ? new Date(meta.availFrom*1000).toISOString().slice(0,10) : "",
            availTo:   meta.availTo   > 0 ? new Date(meta.availTo  *1000).toISOString().slice(0,10) : "",
            price:        parseFloat(ethers.formatEther(state.pricePerNight)),
            depositRatio: Number(state.depositRatio),
            roomAddress:  roomAddr,
            isAvailable:  state.isAvailable,
            bookedRanges: state.bookedRanges.map(r => ({
              from: new Date(r.checkIn * 1000).toISOString().slice(0,10),
              to:   new Date(r.checkOut * 1000).toISOString().slice(0,10),
            })),
          });
        }

        const maxRatio = rooms.length > 0 ? Math.max(...rooms.map(r => r.depositRatio)) : 100;
        const seed = info.name.toLowerCase().replace(/\s+/g,"-") + "-" + hotelAddr.slice(2,8);
        chainHotels.push({
          id: hotelAddr, hotelAddress: hotelAddr,
          name: info.name, city: info.city,
          tagline: info.description, desc: info.description,
          depositRatio: maxRatio, seed, image: info.imageUrl || "",
          photos: [seed, seed+"-2", seed+"-3", seed+"-4"],
          amenities: ["무료 Wi-Fi", "조식 가능", "예약금 에스크로 보호"],
          reviews: 0, rating: null, rooms,
        });
      }

      setHotels(chainHotels);

      const addr = walletAddr || window.__WALLET_ADDRESS;
      if(addr){
        const mine = chainHotels.find(h => h.hotelAddress.toLowerCase() === addr.toLowerCase());
        if(mine){ setMyHotelId(mine.id); setRole("hotel"); }

        // 소비자 예약 목록 로드
        try {
          const escrowAddrs = await factory.getBookingsByConsumer(addr);
          const myBookingsRaw = await Promise.all(escrowAddrs.map(async (escrowAddr) => {
            const escrow = new ethers.Contract(escrowAddr, ESCROW_ABI, provider);
            try {
            const [checkInTs, checkOutTs, statusRaw, bookingAmount, depositAmount, guestName, guestPhone, guestsRaw, hotelAddr, hotelRoomAddr] =
              await Promise.all([
                escrow.checkInTime(), escrow.checkOutTime(), escrow.status(),
                escrow.bookingAmount(), escrow.depositAmount(),
                escrow.guestName(), escrow.guestPhone(), escrow.guests(),
                escrow.hotel(), escrow.hotelRoom(),
              ]);
            // 배포 블록 타임스탬프로 createdAt 추정
            let createdAt = Date.now();
            try {
              const latestBlk = await provider.getBlockNumber();
              const logs = await provider.getLogs({ address: escrowAddr, fromBlock: Math.max(0, latestBlk - 49000), toBlock: "latest" });
              if(logs.length > 0) {
                const blk = await provider.getBlock(logs[0].blockNumber);
                if(blk) createdAt = blk.timestamp * 1000;
              }
            } catch(_) {}
            const statusMap = ["booked","checkedin","completed","disputed","cancelled","timeout"];
            const hotelObj = chainHotels.find(h => h.hotelAddress.toLowerCase() === hotelAddr.toLowerCase());
            const room = hotelObj?.rooms.find(r => r.roomAddress.toLowerCase() === hotelRoomAddr.toLowerCase());
            const nights = Math.ceil((Number(checkOutTs) - Number(checkInTs)) / 86400);
            const amountEth = parseFloat(ethers.formatEther(bookingAmount));
            const collateralEth = parseFloat(ethers.formatEther(depositAmount));
            return {
              id: escrowAddr,
              escrowAddress: escrowAddr,
              hotelId: hotelAddr,
              hotelName: hotelObj?.name || hotelAddr.slice(0,8),
              roomName: room?.name || "-",
              roomAddress: hotelRoomAddr,
              checkIn:  new Date(Number(checkInTs)  * 1000).toISOString().slice(0,10),
              checkOut: new Date(Number(checkOutTs) * 1000).toISOString().slice(0,10),
              nights,
              guests: Number(guestsRaw),
              createdAt,
              status: statusMap[Number(statusRaw)] || "booked",
              amountEth,
              collateralEth,
              pricePerNight: nights > 0 ? +(amountEth / nights).toFixed(6) : amountEth,
              guestName, guestPhone,
              depositRatio: hotelObj?.depositRatio || 100,
            };
            } catch(e) {
              console.warn("escrow 로드 실패:", escrowAddr, e.message);
              return null;
            }
          }));
          setBookings(myBookingsRaw.filter(Boolean));
        } catch(e) {
          console.warn("예약 목록 로드 실패:", e.message);
        }
      }
    } catch(e) {
      console.warn("체인 데이터 로드 실패:", e.message);
    } finally {
      setChainLoading(false);
    }
  }, []);

  // 앱 시작 시 MetaMask 기존 연결 계정 조용히 복구 후 체인 로드
  useEffect(()=>{
    async function init(){
      let addr = null;
      if(window.ethereum){
        try{
          const accounts = await window.ethereum.request({ method: "eth_accounts" }); // 팝업 없음
          if(accounts.length){
            addr = accounts[0];
            const balance = await fetchBalance(addr);
            setWallet({ address: addr, balance });
            window.__WALLET_ADDRESS = addr;
            const chainId = parseInt(await window.ethereum.request({ method: "eth_chainId" }), 16);
            setNetwork(chainId === SEPOLIA_CHAIN_ID ? "sepolia" : "wrong");
          }
        }catch(_){}
      }
      await loadFromChain(addr);
    }
    init();
  }, []);

  /* MetaMask 이벤트 구독: 계정 변경 / 네트워크 변경 자동 반영 */
  useEffect(()=>{
    if(!window.ethereum) return;

    const onAccountsChanged = async (accounts) => {
      if(!accounts.length){
        setWallet(null);
        setRole(null);
        setNetwork("sepolia");
        return;
      }
      const address = accounts[0];
      const balance = await fetchBalance(address);
      setWallet({ address, balance });
    };

    const onChainChanged = (chainIdHex) => {
      const chainId = parseInt(chainIdHex, 16);
      setNetwork(chainId === SEPOLIA_CHAIN_ID ? "sepolia" : "wrong");
    };

    window.ethereum.on("accountsChanged", onAccountsChanged);
    window.ethereum.on("chainChanged", onChainChanged);
    return ()=>{
      window.ethereum.removeListener("accountsChanged", onAccountsChanged);
      window.ethereum.removeListener("chainChanged", onChainChanged);
    };
  }, []);

  const showToast = useCallback((msg)=>{
    setToast(msg);
    setTimeout(()=> setToast(null), 2600);
  },[]);

  /* ── 실제 MetaMask 지갑 연결 ─────────────────────── */
  const connectWallet = useCallback(async () => {
    if(!window.ethereum){
      alert("MetaMask가 설치되어 있지 않습니다.\nhttps://metamask.io 에서 설치해 주세요.");
      throw new Error("MetaMask not installed");
    }

    setTx({ kind:"connect", title:"MetaMask 연결", stage:"request" });
    try {
      // 1) 계정 요청
      const accounts = await window.ethereum.request({ method: "eth_requestAccounts" });
      const address = accounts[0];

      setTx(t => t && { ...t, stage:"pending" });

      // 2) Sepolia 전환
      await switchToSepolia();

      // 3) 잔액 조회
      const balance = await fetchBalance(address);

      const w = { address, balance };
      setWallet(w);
      window.__WALLET_ADDRESS = address;
      setNetwork("sepolia");
      setTx(t => t && { ...t, stage:"done" });
      setTimeout(() => setTx(null), 900);
      // 체인 로드는 백그라운드로 — 오래 걸려도 모달 블로킹 없음
      loadFromChain(address);
      return w;
    } catch(err) {
      setTx(null);
      // 사용자가 직접 거절한 경우(4001)는 조용히 처리
      if(err.code !== 4001) throw err;
    }
  }, []);

  const disconnect = useCallback(()=>{
    setWallet(null);
    setRole(null);
    showToast("지갑 연결을 해제했습니다");
  }, [showToast]);

  /* ── runTx: 실제 트랜잭션 전송 + 모달 단계 관리 ───────
     opts.fn : async () => ethers.TransactionResponse
     opts.title / subtitle / fnLabel : UI 표시용
     반환: tx hash (string)                              */
  const runTx = useCallback(async (opts) => {
    const { title, subtitle, fnLabel, fn } = opts;
    if(!fn) throw new Error(fnLabel + ": fn is required");
    setTx({ kind:"contract", title, subtitle, fnLabel, hash: null, stage:"request" });
    try {
      const txResp = await fn();
      const hash = txResp.hash;

      setTx(t => t && { ...t, hash, stage:"pending" });
      await txResp.wait();

      setTx(t => t && { ...t, stage:"done" });
      setTimeout(() => setTx(null), 1200);
      return hash;
    } catch(err) {
      setTx(null);
      if(err?.code !== 4001 && err?.code !== "ACTION_REJECTED") {
        showToast("트랜잭션 실패: " + (err?.reason || err?.message || "알 수 없는 오류"));
      }
      throw err;
    }
  }, [showToast]);

  const closeTx = useCallback(()=> setTx(null), []);

  /* ── getSigner: ethers BrowserProvider 서명자 반환 ─── */
  function getSigner() {
    if(!window.ethereum) throw new Error("MetaMask not found");
    const provider = new ethers.BrowserProvider(window.ethereum);
    return provider.getSigner();
  }

  /* ---- hotel registry actions ---- */
  /* registerHotel: factory.registerHotel() 온체인 호출 → 호텔 이름/지역/소개 저장 */
  const registerHotel = useCallback(async ({ hotelMeta }) => {
    const signer = await getSigner();
    const factory = new ethers.Contract(FACTORY_ADDRESS, FACTORY_ABI, signer);
    const { name, city, tagline, image } = hotelMeta;

    await runTx({
      title: "호텔 등록",
      fnLabel: "registerHotel()",
      subtitle: `${name} · ${city}`,
      fn: async () => {
        return await factory.registerHotel(name, city, tagline || "", image || "");
      },
    });

    await loadFromChain();
    return true;
  }, [runTx, loadFromChain]);

  /* addRoom: factory.createRoom() 온체인 호출 → HotelRoom 컨트랙트 배포
     roomMeta         : { name, type, beds, size, cap, image, availFrom, availTo }
     pricePerNightEth : 1박 가격 (ETH)
     depositRatio     : 보증금 비율 (예: 120)
     stakeEth         : 예치금 (최소 pricePerNightEth * depositRatio / 100) */
  const addRoom = useCallback(async ({ hotelId, roomMeta, pricePerNightEth, depositRatio, stakeEth }) => {
    const signer = await getSigner();
    const factory = new ethers.Contract(FACTORY_ADDRESS, FACTORY_ABI, signer);

    const priceWei = ethers.parseEther(String(pricePerNightEth));
    const stakeWei = ethers.parseEther(String(stakeEth));

    await runTx({
      title: "객실 등록",
      fnLabel: "createRoom()",
      subtitle: `${roomMeta.name || "새 객실"} · 보증금 ${depositRatio}% · 예치금 ${stakeEth} ETH`,
      fn: async () => {
        const availFromTs = roomMeta.availFrom ? Math.floor(new Date(roomMeta.availFrom).getTime() / 1000) : 0;
        const availToTs   = roomMeta.availTo   ? Math.floor(new Date(roomMeta.availTo).getTime()   / 1000) : 0;
        return await factory.createRoom(
          priceWei,
          depositRatio,
          roomMeta.name     || "새 객실",
          roomMeta.type     || "스탠다드",
          roomMeta.beds     || "",
          roomMeta.size     || "",
          roomMeta.cap      || 2,
          roomMeta.image    || "",
          availFromTs,
          availToTs,
          { value: stakeWei }
        );
      },
    });

    await new Promise(r => setTimeout(r, 3000));
    await loadFromChain();
    return true;
  }, [runTx, loadFromChain]);
  const updateMyHotel = useCallback(async ({ name, city, description, imageUrl }) => {
    await runTx({
      title: "호텔 정보 수정",
      fnLabel: "updateHotel",
      subtitle: "호텔 이름, 지역, 소개를 온체인에 저장합니다",
      fn: async () => {
        const signer = await getSigner();
        const factory = new ethers.Contract(FACTORY_ADDRESS, FACTORY_ABI, signer);
        return await factory.updateHotel(name, city, description, imageUrl || "");
      },
    });
    await loadFromChain();
    showToast("호텔 정보가 수정되었습니다");
  }, [runTx, loadFromChain, showToast]);
  const clearMyHotel = useCallback(()=>{
    setHotels(prev=> prev.filter(h=> h.id!==myHotelId));
    setMyHotelId(null);
  },[myHotelId]);

  /* removeRoom: withdrawStake 전액 반환 → setAvailable(false) 순서로 호출 */
  const removeRoom = useCallback(async (roomAddress) => {
    const provider = new ethers.JsonRpcProvider("https://ethereum-sepolia-rpc.publicnode.com");
    const roomRead = new ethers.Contract(roomAddress, HOTEL_ROOM_ABI, provider);
    const staked = await roomRead.stakedAmount();

    if(staked > 0n) {
      await runTx({
        title: "보증금 반환",
        fnLabel: "withdrawStake()",
        subtitle: `예치된 보증금 ${parseFloat(ethers.formatEther(staked)).toFixed(6)} ETH를 반환합니다`,
        fn: async () => {
          const signer = await getSigner();
          const roomContract = new ethers.Contract(roomAddress, HOTEL_ROOM_ABI, signer);
          return await roomContract.withdrawStake(staked);
        },
      });
    }

    await runTx({
      title: "객실 비활성화",
      fnLabel: "setAvailable(false)",
      subtitle: "해당 객실을 판매 중지 상태로 변경합니다",
      fn: async () => {
        const signer = await getSigner();
        const roomContract = new ethers.Contract(roomAddress, HOTEL_ROOM_ABI, signer);
        return await roomContract.setAvailable(false);
      },
    });
    await loadFromChain();
    showToast("객실이 비활성화되었습니다");
  }, [runTx, loadFromChain, showToast]);

  /* ---- escrow actions ---- */
  const createBooking = useCallback(async (b)=>{
    // b.roomAddress: 온체인 HotelRoom 주소 (store.registerHotel에서 저장)
    const roomAddress = b.roomAddress;
    let escrowAddress = null;

    // 체크인 15:00 KST, 체크아웃 12:00 KST (UTC 기준)
    const checkInTs  = Math.floor(new Date(b.checkIn.slice(0,10)  + "T06:00:00Z").getTime() / 1000);
    const checkOutTs = Math.floor(new Date(b.checkOut.slice(0,10) + "T03:00:00Z").getTime() / 1000);
    // 컨트랙트와 동일한 nights 올림 계산
    const contractNights = BigInt(Math.floor((checkOutTs - checkInTs + 86399) / 86400));
    // 컨트랙트에서 직접 pricePerNight 읽어서 정확한 wei 계산 (부동소수점 오차 방지)
    const provider = new ethers.JsonRpcProvider("https://ethereum-sepolia-rpc.publicnode.com");
    const roomContract = new ethers.Contract(roomAddress, HOTEL_ROOM_ABI, provider);
    const pricePerNightWei = await roomContract.pricePerNight();
    const bookingWei = pricePerNightWei * contractNights;

    const hash = await runTx({
      title:"예약 결제", fnLabel:"book()",
      subtitle:`예약금 ${fmtEth(b.amountEth)} ETH를 안전하게 맡깁니다`,
      fn: async () => {
        const signer  = await getSigner();
        const factory = new ethers.Contract(FACTORY_ADDRESS, FACTORY_ABI, signer);
        const tx = await factory.book(
          roomAddress,
          checkInTs,
          checkOutTs,
          b.guestName || "Guest",
          b.guestPhone || "000-0000-0000",
          b.guests || 1,
          { value: bookingWei }
        );
        return tx;
      },
    });

    await loadFromChain();
    // loadFromChain 후 bookings state에서 방금 생성된 예약 찾아 반환
    return { id: hash || Date.now().toString() };
  },[runTx, loadFromChain]);

  const checkIn = useCallback(async (id)=>{
    const bk = bookings.find(x=>x.id===id);
    await runTx({
      title:"체크인 확인", fnLabel:"checkIn()", subtitle:"호텔 도착을 온체인에 기록합니다",
      fn: async () => {
        const signer = await getSigner();
        const escrow = new ethers.Contract(bk.escrowAddress, ESCROW_ABI, signer);
        return await escrow.checkIn();
      },
    });
    await loadFromChain();
    showToast("체크인이 기록되었습니다");
  },[runTx,showToast,bookings,loadFromChain]);

  const checkout = useCallback(async (id)=>{
    const bk = bookings.find(x=>x.id===id);
    await runTx({
      title:"체크아웃 완료", fnLabel:"completeCheckout()",
      subtitle:`예약금 + 보증금이 호텔로 정산됩니다`,
      fn: async () => {
        const signer = await getSigner();
        const escrow = new ethers.Contract(bk.escrowAddress, ESCROW_ABI, signer);
        return await escrow.completeCheckout();
      },
    });
    await loadFromChain();
    showToast("정상 완료 — 호텔로 송금되었습니다");
  },[runTx,showToast,bookings,loadFromChain]);

  const reportIssue = useCallback(async (id)=>{
    const bk = bookings.find(x=>x.id===id);
    const refund = bk.amountEth + bk.collateralEth;
    await runTx({
      title:"문제 발생 신고", fnLabel:"dispute()",
      subtitle:`예약금 + 보증금 전액 ${fmtEth(refund)} ETH가 즉시 환급됩니다`,
      fn: async () => {
        const signer = await getSigner();
        const escrow = new ethers.Contract(bk.escrowAddress, ESCROW_ABI, signer);
        return await escrow.dispute();
      },
    });
    await loadFromChain();
    showToast(`보상 집행 완료 · +${fmtEth(refund)} ETH`);
  },[runTx,showToast,bookings,loadFromChain]);

  const cancelByGuest = useCallback(async (id, pct, refundEth)=>{
    const bk = bookings.find(x=>x.id===id);
    await runTx({
      title:"예약 취소", fnLabel:"cancelByConsumer()",
      subtitle:`환불 비율 ${pct}% · ${fmtEth(refundEth)} ETH가 환급됩니다`,
      fn: async () => {
        const signer = await getSigner();
        const escrow = new ethers.Contract(bk.escrowAddress, ESCROW_ABI, signer);
        return await escrow.cancelByConsumer();
      },
    });
    await loadFromChain();
    showToast(`예약이 취소되었습니다 · +${fmtEth(refundEth)} ETH`);
  },[runTx,showToast,bookings,loadFromChain]);

  const value = {
    wallet, role, network, bookings, hotels, hotelProfile, myHotelId, chainLoading,
    setRole, setNetwork,
    registerHotel, addRoom, updateMyHotel, clearMyHotel, removeRoom,
    connectWallet, disconnect,
    tx, closeTx, runTx, toast, showToast,
    createBooking, checkIn, checkout, reportIssue, cancelByGuest,
    setBookings, loadFromChain,
  };
  return <StoreContext.Provider value={value}>{children}</StoreContext.Provider>;
}

function genId(){
  const h="0123456789ABCDEF"; let s="";
  for(let i=0;i<6;i++) s+=h[Math.floor(Math.random()*16)];
  return "BK-"+s;
}

Object.assign(window, { StoreContext, useStore, StoreProvider, randHash });
