// Local player-commerce confirmation. All send callbacks remain native/private.
// SPDX-License-Identifier: GPL-3.0-or-later
function createCommerceGate({readContext,getController,getName,notify}){
  const operations=new Set(['trade-add','trade-lock','trade-final','shop-buy','shop-sell','shop-open-sell','shop-open-buy']);
  let revision=0,busy=false,active=null;
  function canonical(x,seen=new Set()){
    if(x===undefined)return ['undefined'];
    if(typeof x==='bigint')return ['bigint',String(x)];
    if(typeof x==='number')return Number.isFinite(x)?x:['number',String(x)];
    if(x===null||typeof x==='string'||typeof x==='boolean')return x;
    if(typeof x!=='object'||seen.has(x))throw Error('交易資料無法可靠核對。');
    seen.add(x);const v=Array.isArray(x)?Array.from(x,y=>canonical(y,seen)):Object.fromEntries(Object.keys(x).sort().map(k=>[k,canonical(x[k],seen)]));seen.delete(x);return v;
  }
  const key=x=>JSON.stringify(canonical(x));
  const freeze=x=>{if(x&&typeof x==='object'){Object.values(x).forEach(freeze);Object.freeze(x);}return x;};
  function report(text){try{const ui=getController();if(ui?.notice)ui.notice(text);else notify(text);}catch{}}
  function money(value){if(!Number.isSafeInteger(value)||value<0)throw Error('金額無效或超出安全整數範圍。');return value;}
  function favorite(item,source){
    if(typeof item.PlaceETCTab==='boolean')return item.PlaceETCTab;
    if(Number.isInteger(item.PlaceETCTab)&&item.PlaceETCTab>=0&&item.PlaceETCTab<=7)return item.PlaceETCTab!==0;
    if(item.favorite===true||item.favorite===1)return true;
    if(item.favorite===false||item.favorite===0)return false;
    throw Error(source==='cart'?'手推車資料沒有可靠收藏標記，暫停上架；請先移回背包核對。不能把缺少欄位當成未收藏。':'收藏狀態無法確認，本次整筆操作未送出。');
  }
  function context(){
    const c=readContext();if(!c?.entity||!Number.isInteger(Number(c.characterId))||Number(c.characterId)<=0||!c.map||c.packet!==20221005||c.connection?.status!=='connected')throw Error('固定版本、角色、地圖或連線尚未就緒。');
    return {entity:c.entity,key:key({characterId:c.characterId,map:c.map,packet:c.packet,connection:c.connection})};
  }
  function capture(read){
    const state=read();
    if(!state?.session||state.valid!==true||!Array.isArray(state.references))throw Error('原交易／商店視窗已關閉或階段不符。');
    const outgoing=state.outgoing||[],incoming=state.incoming||[];
    if(outgoing.length+incoming.length>200)throw Error('本次物品種類過多，請分批操作。');
    const seen=new Set();
    const rows=[...outgoing.map(x=>({...x,out:true})),...incoming.map(x=>({...x,out:false}))].map(row=>{
      const item=row.item;if(!item||!Number.isInteger(item.ITID)||item.ITID<=0||!Number.isSafeInteger(row.count)||row.count<1)throw Error('交易物品身分或數量無效。');
      const identity=String(row.source)+':'+String(item.index)+':'+row.out;
      if(row.out&&seen.has(identity))throw Error('清單有重複欄位，請重新選擇。');if(row.out)seen.add(identity);
      if(row.out){
        if(!Number.isInteger(item.index)||item.index<0)throw Error('交出物品缺少可核對的原始欄位。');
        const count=item.count===undefined?1:item.count;if(!Number.isSafeInteger(count)||count<row.count)throw Error('可交出物品數量已改變。');
        if(favorite(item,row.source)||(row.additionalFavoriteItem&&favorite(row.additionalFavoriteItem,'inventory')))throw Error('清單含收藏物品，本次整筆操作已阻擋；不會自動取消收藏或略過後送出。');
        if(item.WearState||item.equipSwitch)throw Error('請先卸下或移出切換裝備清單。');
      }
      const identified=item.IsIdentified===1||item.IsIdentified===true;
      const cards=Object.values(item.slot||{}).filter(x=>Number(x)!==0).map(Number);
      const options=(item.Options||[]).filter(x=>x&&(x.index||x.id)).map(x=>({id:x.index??x.id,value:x.value??0,param:x.param??0}));
      return {itemId:identified?item.ITID:null,name:getName(item,identified)||'未辨識物品',count:row.count,
        available:Number.isSafeInteger(item.count)?item.count:row.count,identified,refine:Number(item.RefiningLevel)||0,grade:Number(item.enchantgrade)||0,
        cards:identified?cards:[],options:identified?options:[],rental:!!item.HireExpireDate,binding:Number(item.bindOnEquipType)||0,
        unitPrice:row.price===undefined?null:money(row.price),requiresAcknowledgement:true,
        direction:row.out?'交出':'取得／收購',source:row.source==='cart'?'手推車':row.source==='inventory'?(row.out?'背包':'背包樣本（不扣除）'):row.source==='trade'?'交易清單':'玩家商店'};
    });
    const details={partner:String(state.partner||'未提供對象'),outgoingZeny:money(state.outgoingZeny??0),incomingZeny:money(state.incomingZeny??0),budget:state.budget===undefined?null:money(state.budget),notes:Array.isArray(state.notes)?state.notes.map(String):[]};
    return {session:state.session,references:state.references.slice(),signature:key({data:state.data,outgoing,incoming,details}),items:rows,details};
  }
  return {
    invalidate(reason='交易、商店或物品資料已更新，舊確認已取消。'){revision++;if(busy)try{active?.cancel?.(reason);}catch{}},
    async request(operation,read,send){
      if(!operations.has(operation)||busy){report('請先完成或取消目前的物品確認。');return false;}
      let first,start,ui,rev;
      try{
        start=context();first=capture(read);rev=revision;ui=getController();
        if(ui?.version!==1||ui.commerceVersion!==1||typeof ui.confirm!=='function')throw Error('交易／玩家商店保護介面未就緒，請完整重新載入網頁。');
      }catch(error){report(error.message);return false;}
      busy=true;active=ui;
      try{
        const approved=await ui.confirm(freeze({version:1,kind:'commerce',operation,items:first.items,estimatedTotal:null,commerce:first.details}));
        if(approved!==true)return false;
        const now=context();if(rev!==revision||now.entity!==start.entity||now.key!==start.key||getController()!==ui)throw Error('角色、連線或物品版本已變更。');
        const last=capture(read);
        if(last.session!==first.session||last.signature!==first.signature||last.references.length!==first.references.length||last.references.some((x,i)=>x!==first.references[i]))throw Error('對象、清單、報價、金額或操作階段已變更，請重新確認。');
        send();report('操作要求已送出；實際結果以伺服器回覆為準。');return true;
      }catch(error){report('本次未完成：'+error.message);return false;}
      finally{busy=false;active=null;}
    }
  };
}

// Private adapter in the pinned GPL web bundle. No packet construction exposed.
var roLocalCommerceGate;
function roLocalCommerceContext(){const s=ExtensionRuntime_default.snapshot();return {entity:SessionStorage_default.Entity,characterId:Number(SessionStorage_default.GID),packet:PacketVerManager_default.value,map:typeof s.map==='string'?s.map:s.map?.name,connection:s.connection};}
function roLocalGetCommerceGate(){return roLocalCommerceGate||(roLocalCommerceGate=createCommerceGate({
  readContext:roLocalCommerceContext,getController:()=>globalThis.ROLocalItemOperations,
  getName:(item,identified)=>{const info=DB.getItemInfo(item.ITID);return String(identified?info?.identifiedDisplayName||'未辨識物品':info?.unidentifiedDisplayName||'未鑑定物品').replace(/\^[0-9a-f]{6}/gi,'');},
  notify:text=>{try{UIManager.showErrorBox(text);}catch{console.warn(text);}}
}));}
function roLocalMoney(value){if(!Number.isSafeInteger(value)||value<0)throw Error('金額或數量無效。');return value;}
function roLocalDigits(value){const s=String(value??'');if(!/^\d+$/.test(s))throw Error('金額須為非負整數。');return roLocalMoney(Number(s));}
function roLocalRows(rows){
  if(!Array.isArray(rows)||!rows.length||rows.length>200)throw Error('請選擇1～200種物品。');
  const seen=new Set();return rows.map(x=>{if(!Number.isInteger(x.index)||x.index<0||seen.has(x.index)||!Number.isSafeInteger(x.count)||x.count<1)throw Error('欄位或數量無效。');seen.add(x.index);return x;});
}
function roLocalShopQuote(rows){let total=0;for(const x of rows){total+=roLocalMoney(x.price)*x.count;roLocalMoney(total);}return total;}
function roLocalReadTrade(operation,index,count){
  const s=Trade_default.roLocalCommerceState();
  if(!s.open)throw Error('交易視窗已關閉。');
  const send=s.send.map(item=>{const remaining=InventoryController.getItemByIndex(item.index);return {item,count:item.count,source:'trade',additionalFavoriteItem:remaining?.ITID===item.ITID?remaining:undefined};}),incoming=s.recv.filter(x=>x.ITID!==0).map(item=>({item,count:item.count,source:'trade'}));
  if(operation!=='trade-add'){
    for(const id of Object.keys(s.pending))if(Number(id)!==0&&!s.send.some(x=>x.index===Number(id)))throw Error('仍有物品等待伺服器加入交易，請稍候再確認。');
  }
  let zeny;
  if(operation==='trade-lock'){if(s.sendLocked||Object.hasOwn(s.pending,0))throw Error('己方交易已鎖定或仍有金額等待回覆；請等待，必要時取消交易重開。');zeny=roLocalDigits(s.zenyInput);if(zeny>SessionStorage_default.zeny)throw Error('交易金額超出持有金額。');}
  else zeny=roLocalMoney(Number(s.pending[0]||0));
  if(operation==='trade-final'&&(!s.sendLocked||!s.recvLocked))throw Error('雙方尚未鎖定交易內容。');
  const refs=[...s.send,...s.recv,...send.map(x=>x.additionalFavoriteItem).filter(Boolean)];
  if(operation==='trade-add'){
    if(s.sendLocked||Object.hasOwn(s.pending,0)||s.pending[index]!==count)throw Error('待加入欄位已變更或正在鎖定交易。');
    const item=InventoryController.getItemByIndex(index);if(!item)throw Error('待交易物品已移動。');
    send.length=0;send.push({item,count,source:'inventory'});refs.push(item);
  }
  return {session:s.epoch,references:refs,valid:true,data:{send:s.send,recv:s.recv,pending:s.pending,zenyInput:s.zenyInput,zenyRecv:s.zenyRecv,sendLocked:s.sendLocked,recvLocked:s.recvLocked,title:s.title,balance:SessionStorage_default.zeny},outgoing:send,incoming,partner:s.title,
    outgoingZeny:zeny,incomingZeny:roLocalDigits(String(s.zenyRecv||'0').replace(/,/g,'')),notes:[operation==='trade-add'?'這一步只加入交易清單，尚未完成交易。':operation==='trade-lock'?'這一步鎖定己方內容；最後交易仍需再次確認。':'最後確認會交付目前雙方已鎖定的物品與金額。']};
}
function roLocalConfirmTrade(operation,send){return roLocalGetCommerceGate().request(operation,()=>roLocalReadTrade(operation),send);}
function roLocalConfirmTradeAdd(index,count,send){
  if(index===0){send();return;}
  const epoch=Trade_default.roLocalCommerceState().epoch;
  return roLocalGetCommerceGate().request('trade-add',()=>roLocalReadTrade('trade-add',index,count),send).then(ok=>{if(ok!==true)Trade_default.roLocalRejectPending(index,count,epoch);return ok;});
}
function roLocalShopState(store,callback,type){
  if(!store.__active||!store.getRoot()?.isConnected||store.onSubmit!==callback||store.getCurrentType()!==type)throw Error('商店已關閉、更換或重新開啟。');
  return store.roLocalCommerceSelection();
}
function roLocalConfirmStore(operation,packet,callback,type,send){
  return roLocalGetCommerceGate().request(operation,()=>{
    const state=roLocalShopState(NpcStore_default,callback,type),rows=roLocalRows(state.rows),refs=[],outgoing=[],incoming=[];
    for(const row of rows){
      const quote=packet.itemList.find(x=>operation==='shop-buy'?x.index===row.index:x.ITID===row.ITID);
      if(!quote||quote.ITID!==row.ITID||quote.price!==row.price)throw Error('玩家商店報價或物品已變更。');
      if(Number.isFinite(quote.count)&&quote.count<row.count)throw Error('商店剩餘數量不足。');
      if(operation==='shop-sell'){
        const item=InventoryController.getItemByIndex(row.index);if(!item||item.ITID!==row.ITID)throw Error('出售清單與目前背包不符。');refs.push(item);outgoing.push({item,count:row.count,price:row.price,source:'inventory'});
      }else{refs.push(quote);incoming.push({item:quote,count:row.count,price:row.price,source:'shop'});}
    }
    const total=roLocalShopQuote(rows);
    if(operation==='shop-buy'&&total>SessionStorage_default.zeny)throw Error('購買金額超過持有金額。');
    if(operation==='shop-sell'&&total>roLocalMoney(packet.limitZeny))throw Error('售出金額超過收購商店剩餘預算。');
    return {session:callback,references:refs,valid:true,data:{state,aid:packet.AID,uniqueID:packet.UniqueID,budget:packet.limitZeny,balance:SessionStorage_default.zeny,quote:packet.itemList},outgoing,incoming,
      partner:state.seller||('商店角色編號 '+packet.AID),outgoingZeny:operation==='shop-buy'?total:0,incomingZeny:operation==='shop-sell'?total:0,notes:['取消會保留原選擇；送出前再核對商店與報價，實際結果以伺服器回覆為準。']};
  },send);
}
function roLocalConfirmOpenShop(store,send){
  const initial=store.roLocalCommerceState(),operation=initial.type===store.Type.VENDING_STORE?'shop-open-sell':'shop-open-buy';
  return roLocalGetCommerceGate().request(operation,()=>{
    const s=store.roLocalCommerceState();if(!s.open||!s.visible||s.type!==initial.type||s.epoch!==initial.epoch||!s.shopName.trim())throw Error('擺攤視窗、名稱或種類已變更。');
    const rows=roLocalRows(s.rows);if(rows.length>s.slots)throw Error('超過目前可使用攤位格數。');
    const refs=[],outgoing=[],incoming=[];
    for(const row of rows){
      const item=operation==='shop-open-sell'?CartItems_default.getItemByIndex(row.index):InventoryController.getItemByIndex(row.index);
      if(!item||item.ITID!==row.ITID)throw Error('商店清單與目前物品不符。');
      roLocalMoney(row.price);if(row.price<=0)throw Error('商店單價須大於零。');refs.push(item);
      if(operation==='shop-open-sell')outgoing.push({item,count:row.count,price:row.price,source:'cart'});else incoming.push({item,count:row.count,price:row.price,source:'inventory'});
    }
    const total=roLocalShopQuote(rows),budget=operation==='shop-open-buy'?roLocalDigits(s.limitText):undefined;
    if(budget!==undefined&&(budget===0||budget>SessionStorage_default.zeny))throw Error('收購預算須大於零且不超過持有金額。');
    return {session:s.epoch,references:refs,valid:true,data:{type:s.type,rows:s.rows,shopName:s.shopName,limitText:s.limitText,slots:s.slots,balance:SessionStorage_default.zeny},outgoing,incoming,partner:s.shopName,
      outgoingZeny:budget===undefined?0:Math.min(budget,total),incomingZeny:budget===undefined?total:0,budget,
      notes:[operation==='shop-open-sell'?'上架後其他玩家可能購買；顯示金額是全部售出的估算，不保證成交。':'顯示物品是預計收購，不會扣掉背包中的參考樣本；預算是支出上限，不保證收齊。']};
  },send);
}

function roLocalGetItemGate(){