(()=>{

  // IGblue DEBUG v70: set IG_DBG_ON=0 or put `return;` at top of dbg() to disable all logs.
  const IG_DBG_ON = 1;
  function dbg(fn, loc, line, value) {
    // return; // DEBUG OFF: uncomment this one line later.
    if (!IG_DBG_ON) return;
    try {
      const clip = (v) => {
        const seen = new WeakSet();
        let s = JSON.stringify(v, (k, val) => {
          if (typeof val === "string") return val.length > 500 ? val.slice(0, 500) + `...<${val.length}>` : val;
          if (val && typeof val === "object") { if (seen.has(val)) return "[Circular]"; seen.add(val); }
          return val;
        });
        if (s && s.length > 2200) s = s.slice(0,2200) + `...<${s.length}>`;
        return s || "";
      };
      const payload = { ts:new Date().toISOString(), file:"background.js", fn, loc, line, value };
      const out = clip(payload);
      const key = `IG_dbg/background.js/${fn}/${loc}/${line}`.replace(/\s+/g,"_");
      const obj = {}; obj[key] = out; obj["IG_dbg/background.js/_last"] = out;
      try { chrome.storage.local.set(obj); } catch(e) {}
      console.log(`[IG_DBG][background][${fn}][${loc}:${line}]`, value);
    } catch (e) {}
  }

  // IGblue local patch: detached popup needs a live Instagram tab target.
  // The popup is not bound to a Chrome sidePanel tab, so the background tracks the currently active Instagram tab.
  let igblueLastInstagramTabId = 0;
  let igblueActiveInstagramTabId = 0;
  let igblueLastShopInfo = null;
  let extpay = null;
  const igblueInfoByTabId = new Map();
  const igblueUrlByTabId = new Map();
  let igblueLastDownloadId = 0;
  const IGBLUE_LOCAL_WS_URL = "ws://127.0.0.1:8771";
  const IGBLUE_LOCAL_WS_TOKEN = "auto_enter_local_SEC_20260623";
  const IGBLUE_WS_OFFLINE_CACHE_MS = 20000;
  let igblueWsHealth = { connected:false, lastOkAt:0, lastFailAt:0, lastCheckedAt:0, error_code:"NOT_CHECKED", message:"RTC 미확인" };
  // v223: OS mouse control, Heart8772, origin calibration and owner-gated automation are retired.

  function igblueWsOfflineResult(act){
    return {
      ok:false,
      connected:false,
      act:act || "",
      error_code:"IGBLUE_RTC_OFFLINE",
      message:"WS 대기중: 로컬 host 미접속, 다음 주기 재시도",
      ws_status:Object.assign({}, igblueWsHealth)
    };
  }



  // v107: WS 미접속은 정상 상태다. 자동 경로는 이미 연결된 적이 없으면 WebSocket을 열지 않는다.
  function igblueWsOptionalAct(act){
    const a = String(act || "");
    return ["ig_media_exists", "ig_media_downloaded", "ig_auto_cycle_state"].includes(a);
  }

  function igblueWsShouldProbe(act, options){
    options = options || {};
    if(options.force_probe === true) return true;
    if(options.force === true && options.allow_refused_probe === true) return true;
    const now = Date.now();
    if(igblueWsHealth.connected) return true;
    if(igblueWsOptionalAct(act)){
      if(igblueWsHealth.lastFailAt && now - igblueWsHealth.lastFailAt < IGBLUE_WS_OFFLINE_CACHE_MS) return false;
      if(igblueWsHealth.lastOkAt && now - igblueWsHealth.lastOkAt < 15000) return true;
      return false;
    }
    if(igblueWsHealth.lastOkAt) return true;
    return false;
  }

  function igblueWsHealthHint(connected, detail){
    const now = Date.now();
    igblueWsHealth.lastCheckedAt = now;
    if(connected){
      igblueWsHealth.connected = true;
      igblueWsHealth.lastOkAt = now;
      igblueWsHealth.error_code = "";
      igblueWsHealth.message = "WS 연결됨";
    }else{
      igblueWsHealth.connected = false;
      igblueWsHealth.lastFailAt = now;
      igblueWsHealth.error_code = String(detail && detail.error_code || "IGBLUE_RTC_OFFLINE");
      igblueWsHealth.message = String(detail && (detail.message || detail.error) || "WS 대기중 / 로컬 host 미접속");
    }
  }

  const IGBLUE_BG_VERSION = (() => { try { const m = chrome.runtime.getManifest(); return String(m.version_name || m.version || "2.3.8.170"); } catch (_) { return "2.3.8.170"; } })();
  const IGBLUE_BG_TAG = (() => { const m = String(IGBLUE_BG_VERSION || "").match(/(\d+(?:a\d*)?)$/i); return m ? ("v" + m[1]) : String(IGBLUE_BG_VERSION || "v170"); })();
  let igblueAuto8700LastStartRequestId = "";

  function igblueWsLooksOffline(res){
    const code = String(res && res.error_code || "");
    return code === "IGBLUE_RTC_OFFLINE" || code.indexOf("IGBLUE_BG_WS_") === 0 || code === "EMPTY_RESPONSE";
  }

  function igblueNoteWsResult(res, act){
    const now = Date.now();
    igblueWsHealth.lastCheckedAt = now;
    if(res && !igblueWsLooksOffline(res)){
      igblueWsHealth.connected = true;
      igblueWsHealth.lastOkAt = now;
      igblueWsHealth.error_code = "";
      igblueWsHealth.message = "WS 연결됨";
    }else{
      igblueWsHealth.connected = false;
      igblueWsHealth.lastFailAt = now;
      igblueWsHealth.error_code = String(res && res.error_code || "IGBLUE_BG_WS_ERROR");
      igblueWsHealth.message = String(res && (res.message || res.error) || "WS 대기중: 로컬 host 미접속");
    }
    return res;
  }

  function igblueWsStatusPayload(extra){
    const now = Date.now();
    return Object.assign({
      ok:!!igblueWsHealth.connected,
      connected:!!igblueWsHealth.connected,
      status:igblueWsHealth.connected ? "connected" : "offline",
      state:igblueWsHealth.connected ? "connected" : "offline",
      message:igblueWsHealth.connected ? "WS 연결됨" : "WS 대기중 / 로컬 host 미접속",
      lastOkAgoMs:igblueWsHealth.lastOkAt ? now - igblueWsHealth.lastOkAt : null,
      lastFailAgoMs:igblueWsHealth.lastFailAt ? now - igblueWsHealth.lastFailAt : null,
      ws_status:Object.assign({}, igblueWsHealth)
    }, extra || {});
  }

  function igblueWsQueueSnapshot(extra){
    let current = null;
    try{
      current = igblueWsCurrentJob ? {
        act:String(igblueWsCurrentJob.act || ''),
        priority:!!igblueWsCurrentJob.priority,
        age_ms:Date.now() - Number(igblueWsCurrentJob.createdAt || Date.now()),
        timeout_ms:Number(igblueWsCurrentJob.timeoutMs || 0)
      } : null;
    }catch(_){ current = null; }
    let queued = [];
    try{
      queued = igblueWsQueue.slice(0, 8).map(function(j){
        return {act:String(j && j.act || ''), priority:!!(j && j.priority), age_ms:Date.now() - Number(j && j.createdAt || Date.now())};
      });
    }catch(_){ queued = []; }
    return Object.assign({
      bg_version:IGBLUE_BG_VERSION,
      bg_tag:IGBLUE_BG_TAG,
      shared_ready:!!igblueWsReady,
      shared_connecting:!!igblueWsConnecting,
      shared_state:igblueWsShared ? Number(igblueWsShared.readyState) : -1,
      backoff_left_ms:Math.max(0, Number(igblueWsBackoffUntil || 0) - Date.now()),
      queue_len:Array.isArray(igblueWsQueue) ? igblueWsQueue.length : 0,
      current_job:current,
      queue_head:queued,
      ws_health:Object.assign({}, igblueWsHealth)
    }, extra || {});
  }

  let igblueWsShared = null;
  let igblueWsReady = false;
  let igblueWsConnecting = false;
  let igblueWsBackoffUntil = 0;
  let igblueWsQueue = [];
  let igblueWsCurrentJob = null;
  let igblueWsConnectWaiters = [];
  const igblueWsLatestOnlyActs = new Set();

  function igblueWsBackoffMs(){
    try{
      const fails = Math.max(1, Number(igblueWsHealth.failCount || 1));
      return Math.min(30000, 1000 * Math.pow(2, Math.min(5, fails - 1)));
    }catch(_){ return 3000; }
  }

  function igblueWsFailAll(error_code, message){
    const jobs = [];
    try{ if(igblueWsCurrentJob) jobs.push(igblueWsCurrentJob); }catch(_){}
    igblueWsCurrentJob = null;
    try{ jobs.push.apply(jobs, igblueWsQueue.splice(0)); }catch(_){}
    jobs.forEach(function(job){
      try{ job.finish({ok:false, connected:false, error_code:error_code || "IGBLUE_BG_WS_ERROR", message:message || "WS 연결 실패", act:job.act}); }catch(_){}
    });
  }

  function igblueWsDisconnect(reason){
    try{ if(igblueWsShared){ igblueWsShared.onopen = igblueWsShared.onmessage = igblueWsShared.onerror = igblueWsShared.onclose = null; igblueWsShared.close(); } }catch(_){}
    igblueWsShared = null;
    igblueWsReady = false;
    igblueWsConnecting = false;
    try{ igblueWsConnectWaiters.splice(0).forEach(function(w){ w.reject(new Error(reason || "ws_disconnected")); }); }catch(_){}
  }

  function igblueEnsureSharedWs(){
    return new Promise(function(resolve, reject){
      const now = Date.now();
      if(igblueWsShared && igblueWsReady && igblueWsShared.readyState === WebSocket.OPEN){ resolve(igblueWsShared); return; }
      if(now < igblueWsBackoffUntil){ reject(new Error("ws_backoff")); return; }
      igblueWsConnectWaiters.push({resolve, reject});
      if(igblueWsConnecting) return;
      igblueWsConnecting = true;
      let ws = null;
      try{
        // 주의: new WebSocket()의 연결 실패는 throw가 아니라 비동기 콘솔 오류로 찍힌다.
        // 그래서 try/catch로 숨길 수 없고, 공유 연결 + backoff로 새 연결 생성을 최소화한다.
        ws = new WebSocket(IGBLUE_LOCAL_WS_URL);
        igblueWsShared = ws;
        ws.onopen = function(){
          igblueWsReady = true;
          igblueWsConnecting = false;
          igblueWsHealth.failCount = 0;
          igblueWsHealthHint(true, {message:"WS 공유 연결됨"});
          const waiters = igblueWsConnectWaiters.splice(0);
          waiters.forEach(function(w){ try{ w.resolve(ws); }catch(_){} });
          setTimeout(igblueWsProcessQueue, 0);
        };
        ws.onmessage = function(ev){
          const job = igblueWsCurrentJob;
          if(!job) return;
          try{ job.finish(JSON.parse(ev.data)); }catch(e){ job.finish({ok:false, error_code:"IGBLUE_BG_BAD_JSON", error:String(e&&e.message||e), act:job.act}); }
          igblueWsCurrentJob = null;
          setTimeout(igblueWsProcessQueue, 0);
        };
        ws.onerror = function(){
          igblueWsReady = false;
          igblueWsConnecting = false;
          igblueWsHealth.failCount = Number(igblueWsHealth.failCount || 0) + 1;
          igblueWsBackoffUntil = Date.now() + igblueWsBackoffMs();
          igblueWsHealthHint(false, {error_code:"IGBLUE_RTC_OFFLINE", message:"WS 미접속 / backoff 재시도 대기"});
          const waiters = igblueWsConnectWaiters.splice(0);
          waiters.forEach(function(w){ try{ w.reject(new Error("ws_error")); }catch(_){} });
          igblueWsFailAll("IGBLUE_RTC_OFFLINE", "WS 미접속 / 재시도 대기");
          try{ if(ws && ws.readyState !== WebSocket.CLOSED) ws.close(); }catch(_){}
        };
        ws.onclose = function(){
          igblueWsReady = false;
          igblueWsConnecting = false;
          if(igblueWsShared === ws) igblueWsShared = null;
          if(igblueWsCurrentJob){
            const job = igblueWsCurrentJob;
            igblueWsCurrentJob = null;
            job.finish({ok:false, connected:false, error_code:"IGBLUE_BG_WS_CLOSED", message:"WS 연결 종료", act:job.act});
          }
          setTimeout(igblueWsProcessQueue, 250);
        };
      }catch(e){
        igblueWsConnecting = false;
        igblueWsBackoffUntil = Date.now() + 10000;
        igblueWsHealth.failCount = Number(igblueWsHealth.failCount || 0) + 1;
        igblueWsHealthHint(false, {error_code:"IGBLUE_BG_WS_CREATE_FAILED", message:String(e&&e.message||e)});
        const waiters = igblueWsConnectWaiters.splice(0);
        waiters.forEach(function(w){ try{ w.reject(e); }catch(_){} });
      }
    });
  }

  function igblueWsProcessQueue(){
    if(igblueWsCurrentJob || !igblueWsQueue.length) return;
    const job = igblueWsQueue.shift();
    const now = Date.now();
    if(now - job.createdAt > Math.max(1000, job.timeoutMs + 600)){
      job.finish({ok:false, connected:false, error_code:"IGBLUE_BG_WS_QUEUE_STALE", message:"WS 요청 대기 만료", act:job.act});
      setTimeout(igblueWsProcessQueue, 0);
      return;
    }
    igblueWsCurrentJob = job;
    igblueEnsureSharedWs().then(function(ws){
      try{
        if(!ws || ws.readyState !== WebSocket.OPEN) throw new Error("ws_not_open");
        ws.send(JSON.stringify({v:1, id:job.id, act:job.act, token:IGBLUE_LOCAL_WS_TOKEN, from:{endpoint:"background", href:(job.data&&job.data.href)||"", host:(job.data&&job.data.host)||""}, data:job.data||{}}));
      }catch(e){
        igblueWsCurrentJob = null;
        try{ igblueWsDisconnect("send_failed"); }catch(_){}
        job.finish({ok:false, connected:false, error_code:"IGBLUE_BG_WS_SEND_ERROR", error:String(e&&e.message||e), act:job.act});
        setTimeout(igblueWsProcessQueue, 250);
      }
    }).catch(function(err){
      igblueWsCurrentJob = null;
      const code = String(err && err.message || "") === "ws_backoff" ? "IGBLUE_BG_WS_BACKOFF" : "IGBLUE_BG_WS_CONNECT_FAILED";
      job.finish({ok:false, connected:false, error_code:code, message:"WS 대기중: 로컬 host 미접속", act:job.act});
      setTimeout(igblueWsProcessQueue, 250);
    });
  }

  function igblueLocalWsRequest(act, data, timeoutMs, options){
    options = options || {};
    const priority = !!options.priority;
    data = Object.assign({}, data || {});
    timeoutMs = Math.max(priority ? 1800 : 800, Number(timeoutMs || 2500));
    return new Promise(function(resolve){
      const now0 = Date.now();
      if(!igblueWsShouldProbe(act, options)){
        const res = igblueWsOfflineResult(act);
        igblueNoteWsResult(res, act);
        resolve(res);
        return;
      }
      if(!priority && !options.force && igblueWsOptionalAct(act) && !igblueWsHealth.connected && (!igblueWsHealth.lastOkAt || (now0 - igblueWsHealth.lastOkAt) >= 15000)){
        const res = igblueWsOfflineResult(act);
        try { res.quiet_skip_probe = true; res.message = "WS 미접속 예상: optional 요청은 연결 생성 없이 생략"; } catch (_) {}
        igblueNoteWsResult(res, act);
        resolve(res);
        return;
      }
      if(!priority && !options.force && igblueWsHealth.lastFailAt && (now0 - igblueWsHealth.lastFailAt) < IGBLUE_WS_OFFLINE_CACHE_MS && now0 < igblueWsBackoffUntil){
        resolve(igblueWsOfflineResult(act));
        return;
      }
      if(igblueWsQueue.length > 24){
        if(priority){
          try{ igblueWsQueue = igblueWsQueue.filter(function(j){ return j && j.priority; }).slice(-12); }catch(_){}
        }else{
          const res = {ok:false, connected:!!igblueWsHealth.connected, error_code:"IGBLUE_BG_WS_QUEUE_FULL", message:"WS 요청이 많아 일시 생략", act, bg_queue_snapshot:igblueWsQueueSnapshot({queue_full:true, dropped_action:act})};
          igblueNoteWsResult(res, act);
          resolve(res);
          return;
        }
      }
      let done = false;
      let timer = 0;
      const job = {
        id:"igblue_bg_" + Date.now() + "_" + Math.floor(Math.random() * 100000),
        act,
        data:data || {},
        timeoutMs,
        priority,
        createdAt:now0,
        finish:function(res){
          if(done) return;
          done = true;
          try{ clearTimeout(timer); }catch(_){}
          res = res || {ok:false, error_code:"EMPTY_RESPONSE", act};
          igblueNoteWsResult(res, act);
          resolve(res);
        }
      };
      timer = setTimeout(function(){
        if(igblueWsCurrentJob === job) igblueWsCurrentJob = null;
        try{ igblueWsDisconnect("request_timeout"); }catch(_){}
        job.finish({ok:false, connected:false, error_code:"IGBLUE_BG_WS_TIMEOUT", message:"WS 응답 timeout", act});
        setTimeout(igblueWsProcessQueue, 250);
      }, timeoutMs);
      if(priority) igblueWsQueue.unshift(job); else igblueWsQueue.push(job);
      igblueWsProcessQueue();
    });
  }

  function igblueSendWsAndRespond(act, data, sendResponse, timeoutMs){
    igblueLocalWsRequest(act, data || {}, timeoutMs).then(function(res){
      try{ sendResponse(res || {ok:false, error_code:"EMPTY_RESPONSE", act}); }catch(e){}
    }).catch(function(err){
      try{ sendResponse({ok:false, error_code:"IGBLUE_BG_WS_EXCEPTION", error:String(err&&err.message||err), act}); }catch(e){}
    });
  }

  function igblueSafePart(value, fallback){
    return String(value || fallback || "unknown")
      .replace(/[\\/:*?"<>|]+/g, "_")
      .replace(/\u0000/g, "")
      .replace(/\s+/g, "_")
      .replace(/_+/g, "_")
      .replace(/^_+|_+$/g, "")
      .slice(0, 90) || String(fallback || "unknown");
  }

  // IGblue local patch v20: allow controlled subfolders such as p/SHORTCODE under the download root.
  function igblueSafePath(value, fallback){
    const raw = String(value || fallback || "instagram").replace(/\\+/g, "/");
    const parts = raw.split("/").map(function(part){ return igblueSafePart(part, ""); }).filter(Boolean);
    return parts.length ? parts.join("/") : igblueSafePart(fallback || "instagram", "instagram");
  }

  function igblueGuessExt(url, prefix, fallback){
    const p = String(prefix || "").split(/[?#]/)[0];
    const m0 = p.match(/\.([a-z0-9]{2,5})$/i);
    if(m0) return m0[1].toLowerCase();
    const u = String(url || "").split(/[?#]/)[0].toLowerCase();
    const m = u.match(/\.([a-z0-9]{2,5})$/i);
    if(m && !["com", "net", "org"].includes(m[1])) return m[1];
    if(u.includes("mp4") || u.includes("/video/") || u.includes("video")) return "mp4";
    return fallback || "jpg";
  }

  function igblueJoinDownloadPath(root, account, base, ext, accountFolderMerge){
    const r = igblueSafePart(root || "IGblue", "IGblue");
    const a = igblueSafePath(account || "instagram", "instagram");
    let b = igblueSafePart(base || "media", "media");
    const e = igblueSafePart(ext || "jpg", "jpg");
    if(!new RegExp("\\." + e + "$", "i").test(b)) b += "." + e;
    return accountFolderMerge ? `${r}/${b}` : `${r}/${a}/${b}`;
  }

  function igblueSendDownloadProgress(payload){
    try{
      chrome.runtime.sendMessage(Object.assign({type:"ndy_down_progress"}, payload || {}), function(){ chrome.runtime.lastError; });
    }catch(e){}
  }

  function igblueMetaShortcode(meta){
    meta = meta && typeof meta === "object" ? meta : {};
    return String(meta.shortcode || meta.code || "").trim();
  }

  function igblueAssetKindFromMeta(meta, url){
    meta = meta && typeof meta === "object" ? meta : {};
    const raw = String(meta.asset_kind || meta.media_kind || meta.download_kind || "").toLowerCase();
    if(raw.includes("video") || raw.includes("mp4")) return "video";
    if(raw.includes("image") || raw.includes("jpg") || raw.includes("jpeg") || raw.includes("png")) return "image";
    const u = String(url || meta.media_url || "").toLowerCase();
    if(u.includes("mp4") || u.includes("video")) return "video";
    return "image";
  }


  function igblueIsPlaceholderAccount(value){
    const v = String(value || "").replace(/^@/, "").trim().toLowerCase();
    return !v || ["instagram", "explore", "explorer", "reel", "reels", "p", "home", "ig", "unknown", "undefined", "null", "none"].includes(v);
  }

  function igblueCleanOwnerAccount(value){
    const safe = igblueSafePart(String(value || "").replace(/^@/, ""), "");
    return igblueIsPlaceholderAccount(safe) ? "" : safe;
  }

  function igblueOwnerFromPrefix(prefix){
    const m = String(prefix || "").match(/^(.*?)_\d{8}_\d{6}(?:_No(?:\d+|없음))?_/);
    return m && m[1] ? igblueCleanOwnerAccount(m[1]) : "";
  }

  function igblueOwnerFromDownloadMsg(msg){
    const meta = msg && msg.meta && typeof msg.meta === "object" ? msg.meta : {};
    const choices = [meta.owner_username, meta.username, meta.ig_account_id, igblueOwnerFromPrefix(msg && msg.prefix)];
    for(const c of choices){
      const owner = igblueCleanOwnerAccount(c);
      if(owner) return owner;
    }
    return "";
  }

  function igblueUploadStampFromMeta(meta){
    meta = meta && typeof meta === "object" ? meta : {};
    const direct = String(meta.upload_stamp || "").trim();
    if(/^\d{8}_\d{6}$/.test(direct) || direct === "upload_unknown") return direct;
    const raw = String(meta.upload_at || meta.created_at || "").trim();
    const m = raw.match(/^(\d{4})[-/]?(\d{2})[-/]?(\d{2})(?:[ T_](\d{2}):?(\d{2}):?(\d{2}))?/);
    if(m) return `${m[1]}${m[2]}${m[3]}_${m[4] || "00"}${m[5] || "00"}${m[6] || "00"}`;
    return "upload_unknown";
  }

  function igblueIsSingleHref(href){
    try{
      const u = new URL(String(href || ""));
      const parts = String(u.pathname || "/").split("/").filter(Boolean);
      return !!(parts.length >= 2 && ["reel", "reels", "p"].includes(parts[0]));
    }catch(e){ return false; }
  }

  function igblueNormalizeDownloadPrefix(msg, ext){
    const meta = msg && msg.meta && typeof msg.meta === "object" ? msg.meta : {};
    const shortcode = igblueSafePart(meta.shortcode || meta.code || "", "");
    if(!shortcode){
      const prefixOwner = igblueOwnerFromPrefix(msg && msg.prefix);
      return prefixOwner ? igblueSafePart(msg && msg.prefix || "media", "media") : "";
    }
    const owner = igblueOwnerFromDownloadMsg(msg);
    if(!owner) return "";
    const upload = igblueUploadStampFromMeta(meta);
    let rankNo = Number(meta.rank_no || 0) || 0;
    if(!rankNo){
      const m = String(meta.rank_label || meta.rank || "").match(/No\s*0*(\d+)/i);
      if(m) rankNo = Number(m[1]) || 0;
    }
    if(!rankNo){
      // v99: final filename may arrive with rank only in the already-built prefix.
      // v170 inherited v169: rank가 없으면 _No없음_으로 정규화한다.
      const m = String(msg && msg.prefix || "").match(/(?:^|_)No\s*0*(\d+)(?:_|$)/i);
      if(m) rankNo = Number(m[1]) || 0;
    }
    const hrefForNo = (msg && (msg.href || meta.href || meta.page_url || meta.current_url || meta.media_url)) || "";
    // v170 inherited v169: rank가 없거나 단일/수동 다운로드이면 _No없음_을 사용한다.
    const noPart = rankNo > 0 ? `_No${String(rankNo).padStart(3, "0")}` : `_No없음`;
    const mediaIndex = Number(meta.media_index || 0) || 0;
    const totalMedia = Number(meta.total_media || 1) || 1;
    const idxPart = totalMedia > 1 ? `_${mediaIndex + 1}` : "";
    return igblueSafePart(`${owner}_${upload}${noPart}_${shortcode}${idxPart}`, "media");
  }

  function igblueLocalFilePathFromExists(existsRes){
    const local = existsRes && existsRes.local && typeof existsRes.local === "object" ? existsRes.local : {};
    if(existsRes && existsRes.file_path) return String(existsRes.file_path);
    if(local.file_path) return String(local.file_path);
    if(Array.isArray(existsRes && existsRes.file_paths) && existsRes.file_paths[0]) return String(existsRes.file_paths[0]);
    if(Array.isArray(local.file_paths) && local.file_paths[0]) return String(local.file_paths[0]);
    return "";
  }

  function igblueDownloadWithApi(msg, sendResponse){
    dbg("igblueDownloadWithApi", "entry", 55, {url:String(msg&&msg.url||"").slice(0,260), prefix:msg&&msg.prefix, root:msg&&msg.root, account:msg&&msg.account, ext:msg&&msg.ext});
    const url = String(msg.url || "");
    if(!url) { sendResponse({status:"error", message:"empty url"}); return; }
    const ext = igblueGuessExt(url, msg.prefix, msg.ext);
    const normalizedPrefix = igblueNormalizeDownloadPrefix(msg, ext);
    if(!normalizedPrefix){
      dbg("igblueDownloadWithApi", "owner_missing_blocked", 58, {prefix:msg&&msg.prefix, account:msg&&msg.account, meta:msg&&msg.meta});
      sendResponse({status:"error", message:"owner_missing: 계정명을 찾지 못해 다운로드를 보류했습니다. explore/reels/p는 계정명으로 사용하지 않습니다.", error_code:"owner_missing"});
      return;
    }
    const accountFolderMerge = !!(msg.account_folder_merge || (msg.meta && msg.meta.account_folder_merge));
    const filename = igblueJoinDownloadPath(msg.root || "IGblue", msg.account || "instagram", normalizedPrefix, ext, accountFolderMerge);
    dbg("igblueDownloadWithApi", "filename", 59, {ext, filename, normalizedPrefix});
    let responded = false;
    const finish = (res) => {
      if(responded) return;
      responded = true;
      try{ sendResponse(res); }catch(e){}
    };
    try{
      chrome.downloads.download({
        url,
        filename,
        saveAs:false,
        conflictAction:"uniquify"
      }, function(downloadId){
        if(chrome.runtime.lastError || !downloadId){
          const message = chrome.runtime.lastError ? chrome.runtime.lastError.message : "download failed";
          dbg("chrome_downloads", "download_callback_error", 73, {filename, message, downloadId});
          igblueSendDownloadProgress({status:"error", file:filename, message});
          finish({status:"error", message});
          return;
        }
        igblueLastDownloadId = downloadId;
        dbg("chrome_downloads", "download_started", 79, {downloadId, filename});
        igblueSendDownloadProgress({status:"start", file:filename, loaded:0, total:0, percent:0, message:"Chrome downloads API"});
        const started = Date.now();
        const timer = setInterval(function(){
          try{
            chrome.downloads.search({id:downloadId}, function(items){
              if(chrome.runtime.lastError) return;
              const item = items && items[0];
              if(!item) return;
              const loaded = Number(item.bytesReceived || 0);
              const total = Number(item.totalBytes || item.fileSize || 0);
              const percent = total > 0 ? Math.floor(Math.max(0, Math.min(100, loaded * 100 / total))) : 0;
              igblueSendDownloadProgress({status:item.state === "complete" ? "done" : item.state === "interrupted" ? "error" : "progress", file:item.filename || filename, file_path:item.filename || filename, shortcode:igblueMetaShortcode(msg && msg.meta), asset_kind:igblueAssetKindFromMeta(msg && msg.meta, url), media_index:(msg && msg.meta && msg.meta.media_index != null ? msg.meta.media_index : 0), loaded, total, percent, message:item.error || ""});
              if(item.state === "complete"){
                dbg("chrome_downloads", "complete", 92, {downloadId, filename:item.filename || filename, loaded, total});
                clearInterval(timer);
                const donePayload = {status:"ok", id:downloadId, filename:item.filename || filename, file_path:item.filename || filename, file_saved:true, meta: msg && msg.meta ? msg.meta : null};
                if (msg && msg.notify_aiwk && msg.meta) {
                  igblueLocalWsRequest("ig_media_downloaded", {
                    root: msg.root || "IGblue",
                    account: accountFolderMerge ? "" : (msg.account || "instagram"),
                    account_folder_merge: accountFolderMerge,
                    prefix: normalizedPrefix || msg.prefix || "",
                    url,
                    href: msg.href || (msg.meta && (msg.meta.href || msg.meta.page_url)) || "",
                    file_path: item.filename || filename,
                    download_id: downloadId,
                    source_mode: String((msg.meta && msg.meta.source_mode) || "MANUAL_SINGLE"),
                    batch_key: String((msg.meta && msg.meta.client_batch_key) || ""),
                    client_job_key: String((msg.meta && msg.meta.client_job_key) || ""),
                    event_uuid: String((msg.meta && msg.meta.client_event_id) || ("chrome-download-" + downloadId)),
                    meta: msg.meta || {}
                  }, 35000).then(function(notifyRes){
                    donePayload.notify_aiwk = notifyRes || null;
                    donePayload.json_saved = !!(notifyRes && notifyRes.json_path);
                    donePayload.report_saved = !!(notifyRes && notifyRes.report_ok && notifyRes.report_path);
                    donePayload.storage_saved = !!(notifyRes && notifyRes.storage_ok);
                    donePayload.media_saved = notifyRes && notifyRes.media_saved ? notifyRes.media_saved : null;
                    if(notifyRes && notifyRes.ok === false){ donePayload.status = "partial"; donePayload.message = notifyRes.error || "MEDIA_SAVED 처리 실패"; }
                    finish(donePayload);
                  }).catch(function(err){ donePayload.notify_aiwk = {ok:false,error:String(err&&err.message||err)}; donePayload.json_saved=false; donePayload.report_saved=false; donePayload.storage_saved=false; donePayload.status="partial"; donePayload.message=String(err&&err.message||err)||"MEDIA_SAVED 알림 실패"; finish(donePayload); });
                } else {
                  finish(donePayload);
                }
              }else if(item.state === "interrupted"){
                dbg("chrome_downloads", "interrupted", 95, {downloadId, filename:item.filename || filename, error:item.error, loaded, total});
                clearInterval(timer);
                finish({status:"error", id:downloadId, message:item.error || "interrupted"});
              }else if(Date.now() - started > 30 * 60 * 1000){
                clearInterval(timer);
                finish({status:"timeout", id:downloadId, filename:item.filename || filename});
              }
            });
          }catch(e){}
        }, 350);
      });
    }catch(e){
      igblueSendDownloadProgress({status:"error", file:filename, message:String(e && e.message || e)});
      finish({status:"error", message:String(e && e.message || e)});
    }
  }

  function igblueIsInstagramUrl(url){
    // IGblue local patch v9: treat any instagram.com / www.instagram.com URL as Instagram.
    // SPA navigation can report a new URL before tab.url is fully refreshed, so callers pass changeInfo.url when available.
    try{
      const u = new URL(String(url || ""));
      const host = u.hostname.replace(/^www\./i, "").toLowerCase();
      return (u.protocol === "https:" || u.protocol === "http:") && host === "instagram.com";
    }catch(e){
      return false;
    }
  }

  function igblueIsExtensionUrl(url){
    return String(url || "").startsWith(chrome.runtime.getURL(""));
  }

  function igblueTabState(tabId){
    // IGblue local patch: when tabId is explicitly 0, report disconnected instead of falling back to an old IG tab.
    const hasExplicitTab = arguments.length > 0;
    const id = Number(hasExplicitTab ? tabId : (igblueActiveInstagramTabId || 0));
    return {
      tabId: id || 0,
      url: id ? (igblueUrlByTabId.get(id) || "") : "",
      info: id ? (igblueInfoByTabId.get(id) || null) : null
    };
  }

  function igblueBroadcastTarget(tabId){
    try{
      const state = igblueTabState(tabId);
      chrome.runtime.sendMessage({
        type: "IGBLUE_TARGET_UPDATED",
        tabId: state.tabId,
        url: state.url,
        info: state.info
      }, function(){ chrome.runtime.lastError; });
    }catch(e){}
  }

  function igblueRememberTab(tab, makeActive, forcedUrl){
    if(!tab || !tab.id) return;
    const url = forcedUrl || tab.url || "";
    if(igblueIsInstagramUrl(url)){
      igblueLastInstagramTabId = tab.id;
      igblueUrlByTabId.set(tab.id, url);
      if(makeActive) igblueActiveInstagramTabId = tab.id;
      igblueBroadcastTarget(tab.id);
    }else if(makeActive && !igblueIsExtensionUrl(url)){
      // IGblue local patch: if the user activates a non-Instagram web tab, the detached popup should show it is not linked.
      igblueActiveInstagramTabId = 0;
      igblueBroadcastTarget(0);
    }
  }

  function igblueRefreshActiveFromFocusedWindow(){
    try{
      chrome.tabs.query({active:true, lastFocusedWindow:true}, function(tabs){
        if(chrome.runtime.lastError) return;
        const tab = tabs && tabs[0];
        igblueRememberTab(tab, true);
      });
    }catch(e){}
  }


  function igblueQueryBestInstagramTab(callback){
    // IGblue local patch v11: resolve an Instagram target by real URL, not only the homepage or a stale active-tab flag.
    // This also recovers when the detached popup window is focused and Chrome reports the extension page as active.
    try{
      chrome.tabs.query({url:["https://www.instagram.com/*", "https://instagram.com/*"]}, function(tabs){
        if(chrome.runtime.lastError) return callback(null);
        const list = (tabs || []).filter(function(tab){ return tab && tab.id && igblueIsInstagramUrl(tab.url || ""); });
        if(!list.length) return callback(null);
        let tab = list.find(function(t){ return t.id === igblueActiveInstagramTabId; })
               || list.find(function(t){ return t.active; })
               || list.find(function(t){ return t.id === igblueLastInstagramTabId; })
               || list[list.length - 1];
        igblueRememberTab(tab, true, tab.url || "");
        callback(tab);
      });
    }catch(e){ callback(null); }
  }

  function igblueGetBestState(callback){
    igblueQueryBestInstagramTab(function(tab){
      if(tab && tab.id) return callback(igblueTabState(tab.id));
      callback(igblueTabState(0));
    });
  }

  function igblueEnsureContentScripts(tabId, callback){
    // IGblue local patch v11: if an Instagram tab was already open when the extension reloaded,
    // declared content scripts may not exist in that tab. Inject both bridge and MAIN script, then retry.
    try{
      tabId = Number(tabId || 0);
      if(!tabId || !chrome.scripting || !chrome.scripting.executeScript) return callback(false);
      chrome.scripting.executeScript({target:{tabId:tabId, allFrames:false}, files:["content_instagram.js"]}, function(){
        if(chrome.runtime.lastError) return callback(false);
        chrome.scripting.executeScript({target:{tabId:tabId, allFrames:true}, files:["js/instagram_inject.js"], world:"MAIN"}, function(){
          if(chrome.runtime.lastError) return callback(false);
          callback(true);
        });
      });
    }catch(e){ callback(false); }
  }

  function igblueRefreshPayState(){
    setTimeout(function(){
      if(!extpay || !extpay.getUser) return;
      extpay.getUser().then(function(user){
        let limit = 3;
        if(user && user.paid) limit = -1;
        chrome.storage.local.set({tg_li: limit}, function(){});
      }).catch(function(){});
    }, 2000);
  }

  chrome.sidePanel.setPanelBehavior({openPanelOnActionClick:true}).catch(function(){});

  chrome.tabs.onActivated.addListener(function(activeInfo){
    try{
      chrome.tabs.get(activeInfo.tabId, function(tab){
        if(chrome.runtime.lastError) return;
        igblueRememberTab(tab, true);
      });
    }catch(e){}
  });

  chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab){
    try{
      if(changeInfo.url || changeInfo.status === "complete"){
        const nextUrl = changeInfo.url || (tab && tab.url) || "";
        if(tab && tab.active) igblueRememberTab(tab, true, nextUrl);
        else if(tab && igblueIsInstagramUrl(nextUrl)){
          igblueUrlByTabId.set(tabId, nextUrl || "");
          if(tabId === igblueActiveInstagramTabId) igblueBroadcastTarget(tabId);
        }
      }
    }catch(e){}
  });

  chrome.tabs.onRemoved.addListener(function(tabId){
    igblueInfoByTabId.delete(tabId);
    igblueUrlByTabId.delete(tabId);
    if(igblueActiveInstagramTabId === tabId) igblueActiveInstagramTabId = 0;
    if(igblueLastInstagramTabId === tabId) igblueLastInstagramTabId = 0;
    igblueBroadcastTarget(0);
  });

  chrome.windows.onFocusChanged.addListener(function(windowId){
    if(windowId === chrome.windows.WINDOW_ID_NONE) return;
    igblueRefreshActiveFromFocusedWindow();
  });

  chrome.runtime.onMessage.addListener(function(msg, sender, sendResponse){
    dbg("runtime_onMessage", "recv", 266, {type:msg&&msg.type, senderTab:sender&&sender.tab&&sender.tab.id, url:msg&&msg.url, prefix:msg&&msg.prefix, root:msg&&msg.root, account:msg&&msg.account, info_count:msg&&Array.isArray(msg.info)?msg.info.length:null});
    if(msg && msg.type === "IGBLUE_CONTENT_READY"){
      const tabId = sender && sender.tab && sender.tab.id ? sender.tab.id : 0;
      if(tabId){
        igblueLastInstagramTabId = tabId;
        if(sender.tab && sender.tab.url) igblueUrlByTabId.set(tabId, sender.tab.url);
        if(sender.tab && sender.tab.active) igblueActiveInstagramTabId = tabId;
        igblueBroadcastTarget(tabId || igblueActiveInstagramTabId || igblueLastInstagramTabId);
      }
      sendResponse({status:"ok", tabId:tabId || 0});
      return true;
    }



    if(msg && msg.type === "IGBLUE_AUTO8700_SCHEDULE_MIRROR"){
      try { chrome.storage.local.set({"IGBLUE_AUTO8700_SCHEDULE": msg.schedule || {}, "IGBLUE_AUTO8700_SCHEDULE_MIRRORED_AT": Date.now()}); } catch(e) {}
      sendResponse({ok:true, status:"mirrored"});
      return true;
    }

    if(msg && msg.type === "IGBLUE_AUTO8700_START_REQUEST"){
      const tabId = sender && sender.tab && sender.tab.id ? sender.tab.id : (igblueActiveInstagramTabId || igblueLastInstagramTabId || 0);
      const req = Object.assign({
        type:"IGBLUE_AUTO8700_START_REQUEST",
        source:"Auto8700",
        bg_received_at:new Date().toISOString(),
        request_id:"auto8700_bg_" + Date.now() + "_" + Math.floor(Math.random()*100000),
        tabId:tabId || 0,
        url:(sender && sender.tab && sender.tab.url) || igblueUrlByTabId.get(tabId) || ""
      }, msg || {});
      if(!req.request_id) req.request_id = "auto8700_bg_" + Date.now() + "_" + Math.floor(Math.random()*100000);
      req.tabId = tabId || Number(req.tabId || 0) || 0;
      const isDuplicateAuto8700Start = !!(req.request_id && req.request_id === igblueAuto8700LastStartRequestId);
      igblueAuto8700LastStartRequestId = String(req.request_id || "");
      try { chrome.storage.local.set({"IGBLUE_AUTO8700_START_REQUEST": req}); } catch(e) {}
      try { chrome.runtime.sendMessage(req, function(){ chrome.runtime.lastError; }); } catch(e) {}
      if(!isDuplicateAuto8700Start){
        try {
          if(req.open_side !== false && chrome.sidePanel && chrome.sidePanel.open && req.tabId){
            chrome.sidePanel.open({tabId:req.tabId}).catch(function(){ chrome.runtime.lastError; });
          }
        } catch(e) {}
        try {
          if(req.open_side !== false){
            chrome.windows.create({url:chrome.runtime.getURL("side.html?popup=1&auto8700_start=1"), type:"popup", width:520, height:860}, function(){ chrome.runtime.lastError; });
          }
        } catch(e) {}
      }
      sendResponse({ok:true, status:isDuplicateAuto8700Start ? "duplicate" : "queued", request_id:req.request_id, tabId:req.tabId});
      return true;
    }

    if(msg && msg.type === "IGBLUE_AUTO8700_SIDE_OPEN_REQUEST"){
      const tabId = sender && sender.tab && sender.tab.id ? sender.tab.id : (igblueActiveInstagramTabId || igblueLastInstagramTabId || 0);
      const req = Object.assign({
        type:"IGBLUE_AUTO8700_SIDE_OPEN_REQUEST",
        source:"Auto8700",
        bg_received_at:new Date().toISOString(),
        request_id:"auto8700_side_bg_" + Date.now() + "_" + Math.floor(Math.random()*100000),
        tabId:tabId || 0,
        url:(sender && sender.tab && sender.tab.url) || igblueUrlByTabId.get(tabId) || ""
      }, msg || {});
      req.tabId = tabId || Number(req.tabId || 0) || 0;
      try { chrome.storage.local.set({"IGBLUE_AUTO8700_SIDE_OPEN_REQUEST": req}); } catch(e) {}
      if(chrome.sidePanel && chrome.sidePanel.open && req.tabId){
        try {
          chrome.sidePanel.open({tabId:req.tabId}).then(function(){
            sendResponse({ok:true, status:"side_open_requested", request_id:req.request_id, tabId:req.tabId});
          }).catch(function(err){
            sendResponse({ok:false, status:"side_open_failed", error:String(err && err.message || err), request_id:req.request_id, tabId:req.tabId});
          });
          return true;
        } catch(e) {
          sendResponse({ok:false, status:"side_open_exception", error:String(e && e.message || e), request_id:req.request_id, tabId:req.tabId});
          return true;
        }
      }
      sendResponse({ok:false, status:"side_panel_api_unavailable", request_id:req.request_id, tabId:req.tabId});
      return true;
    }

    if(msg && msg.type === "ndy_bg_ig"){
      dbg("ndy_bg_ig", "entry", 279, {count:Array.isArray(msg.info)?msg.info.length:null, first:Array.isArray(msg.info)&&msg.info[0]?{code:msg.info[0].code,has_video_url:!!msg.info[0].video_url,has_img_origin:!!msg.info[0].img_origin,play_count:msg.info[0].play_count}:null});
      const tabId = sender && sender.tab && sender.tab.id ? sender.tab.id : 0;
      if(tabId){
        igblueLastInstagramTabId = tabId;
        igblueInfoByTabId.set(tabId, msg.info || null);
        if(sender.tab && sender.tab.url) igblueUrlByTabId.set(tabId, sender.tab.url);
        if(sender.tab && sender.tab.active) igblueActiveInstagramTabId = tabId;
      }
      igblueLastShopInfo = msg.info || null;
      try{
        chrome.runtime.sendMessage({type:"ndy_side_shop", info:msg.info, tabId:tabId || 0, url:(sender && sender.tab && sender.tab.url) || igblueUrlByTabId.get(tabId) || "", source_url:(sender && sender.tab && sender.tab.url) || igblueUrlByTabId.get(tabId) || "", sent_at:new Date().toISOString()}, function(){ chrome.runtime.lastError; });
        igblueBroadcastTarget(tabId || igblueActiveInstagramTabId || igblueLastInstagramTabId);
      }catch(e){}
      sendResponse({status:0});
      return true;
    }

    if(msg && msg.type === "down"){
      chrome.storage.local.get("dcount", function(data){
        if(data && data.dcount && data.dcount > 0){
          const count = data.dcount + 1;
          chrome.storage.local.set({dcount:count}, function(){ sendResponse({status:count}); });
        }else{
          chrome.storage.local.set({dcount:1}, function(){ sendResponse({status:1}); });
        }
      });
      return true;
    }

    if(msg && msg.type === "getc"){
      chrome.storage.local.get("dcount", function(data){
        sendResponse({status:data && data.dcount && data.dcount > 0 ? data.dcount : 0});
      });
      return true;
    }

    if(msg && msg.type === "getl"){
      chrome.storage.local.get("tg_li", function(data){ sendResponse({status:data.tg_li}); });
      return true;
    }

    if(msg && msg.type === "IGBLUE_OPEN_POPUP"){
      const url = chrome.runtime.getURL("side.html?popup=1");
      // IGblue local patch v9: after detaching to a popup, close the original Chrome side panel after about 1 second.
      // chrome.sidePanel.close is Chrome 141+, so keep a safe fallback for older Chrome builds.
      chrome.windows.getLastFocused({windowTypes:["normal"]}, function(sourceWin){
        const sourceWindowId = sourceWin && sourceWin.id ? sourceWin.id : 0;
        chrome.windows.create({url:url, type:"popup", width:760, height:900, focused:true}, function(win){
          const ok = !chrome.runtime.lastError;
          // IGblue local patch v20: popup has been created; close the source side panel immediately.
          try{
            if(chrome.sidePanel && chrome.sidePanel.close && sourceWindowId){
              chrome.sidePanel.close({windowId:sourceWindowId}).catch(function(){});
            }
          }catch(e){}
          sendResponse({status: ok ? "ok" : "error"});
        });
      });
      return true;
    }

    if(msg && msg.type === "IGBLUE_RTC_HEALTH_HINT"){
      igblueWsHealthHint(!!msg.connected, msg || {});
      sendResponse(igblueWsStatusPayload({cached:true, hint:true}));
      return true;
    }

    if(msg && msg.type === "IGBLUE_GET_TARGET_TAB"){
      igblueGetBestState(function(state){ sendResponse({tabId:state.tabId, url:state.url}); });
      return true;
    }

    if(msg && msg.type === "IGBLUE_GET_TARGET_STATE"){
      igblueGetBestState(function(state){ sendResponse(state); });
      return true;
    }

    if(msg && msg.type === "IGBLUE_GET_LAST_INFO"){
      igblueGetBestState(function(state){ sendResponse({info:state.info || igblueLastShopInfo}); });
      return true;
    }

    if(msg && msg.type === "IGBLUE_ENSURE_CONTENT"){
      const tabId = Number(msg.tabId || 0);
      igblueEnsureContentScripts(tabId, function(ok){ sendResponse({status: ok ? "ok" : "error"}); });
      return true;
    }

    if(msg && msg.type === "IGBLUE_RTC_STATUS"){
      const now = Date.now();
      const freshOk = igblueWsHealth.connected && igblueWsHealth.lastOkAt && (now - igblueWsHealth.lastOkAt) < 5000;
      const freshFail = !igblueWsHealth.connected && igblueWsHealth.lastFailAt && (now - igblueWsHealth.lastFailAt) < IGBLUE_WS_OFFLINE_CACHE_MS;
      if(!msg.force && (freshOk || freshFail)){
        sendResponse(igblueWsStatusPayload({cached:true}));
      }else{
        igblueLocalWsRequest("status", {reason:"rtc_status_check", href:msg.href||"", host:msg.host||""}, 1200, {force_probe:!!msg.force_probe}).then(function(res){
          sendResponse(igblueWsStatusPayload({cached:false, raw:res || null}));
        }).catch(function(err){
          igblueNoteWsResult({ok:false,error_code:"IGBLUE_BG_WS_EXCEPTION",error:String(err&&err.message||err)}, "status");
          sendResponse(igblueWsStatusPayload({cached:false}));
        });
      }
      return true;
    }

    if(msg && msg.type === "IGBLUE_PY_SETTING"){
      const data = Object.assign({bg_received_at:new Date().toISOString()}, msg.data || {});
      igblueSendWsAndRespond("ig_rtc_py_setting", data, sendResponse, 1800);
      return true;
    }

    if(msg && msg.type === "IGBLUE_DOWNLOAD_URL"){
      dbg("runtime_onMessage", "IGBLUE_DOWNLOAD_URL", 361, {url:String(msg.url||"").slice(0,260), prefix:msg.prefix, root:msg.root, account:msg.account});
      // v86: status 선검사로 "로컬 중복관리 없음"을 오판하지 않는다.
      // 먼저 실제 중복 체크 ig_media_exists를 요청하고, 그 요청이 실패할 때만 다운로드를 차단한다.
      igblueLocalWsRequest("ig_media_exists", {root:msg.root||"IGblue", account:msg.account||"instagram", account_folder_merge:!!msg.account_folder_merge, prefix:msg.prefix||"", url:msg.url||"", href:msg.href||"", meta:msg.meta||{}}, 8000, {}).then(function(existsRes){
        if(!existsRes || existsRes.ok === false || igblueWsLooksOffline(existsRes)){
          if(msg && msg.auto_direct_download){
            const message = "자동 직접 다운로드: WS 중복/완료 처리가 준비되지 않아 파일 저장을 시작하지 않습니다.";
            igblueSendDownloadProgress({status:"error", file:msg.prefix || "", message, shortcode:igblueMetaShortcode(msg.meta), asset_kind:igblueAssetKindFromMeta(msg.meta, msg.url), media_index:(msg.meta&&msg.meta.media_index!=null?msg.meta.media_index:0), meta:msg.meta||null});
            try{ sendResponse({status:"error", ok:false, error_code:"AUTO_DIRECT_WS_NOT_READY", message, file_saved:false, json_saved:false, report_saved:false, exists:existsRes||null}); }catch(e){}
            return;
          }
          const message = "WS 대기중: 중복체크는 건너뛰고 다운로드는 허용합니다.";
          igblueSendDownloadProgress({status:"warn", file:msg.prefix || "", message, shortcode:igblueMetaShortcode(msg.meta), asset_kind:igblueAssetKindFromMeta(msg.meta, msg.url), media_index:(msg.meta&&msg.meta.media_index!=null?msg.meta.media_index:0), meta:msg.meta||null});
          igblueDownloadWithApi(msg, sendResponse);
          return;
        }
        const local = existsRes && existsRes.local && typeof existsRes.local === "object" ? existsRes.local : {};
        const matchBy = String((existsRes && existsRes.match_by) || local.match_by || "");
        const exists = !!(existsRes && (existsRes.exists === 1 || existsRes.exists === true || local.exists));
        const localFilePathForDup = igblueLocalFilePathFromExists(existsRes);
        // v86: 서버/yellow 중복만으로는 다운로드를 막지 않는다. 로컬 파일/index가 확실할 때만 차단한다.
        const duplicateConfirmed = !(existsRes && existsRes.duplicate_confirmed === false) && !(local && local.duplicate_confirmed === false);
        const strong = duplicateConfirmed && (!!(local && local.exists) || !!localFilePathForDup);
        if(strong){
          const shortcode = String((existsRes && existsRes.shortcode) || igblueMetaShortcode(msg.meta) || "");
          const assetKind = String((existsRes && existsRes.asset_kind) || igblueAssetKindFromMeta(msg.meta, msg.url) || "");
          const mediaIndex = (existsRes && existsRes.media_index != null) ? existsRes.media_index : (msg.meta && msg.meta.media_index != null ? msg.meta.media_index : 0);
          const filePath = localFilePathForDup || igblueLocalFilePathFromExists(existsRes);
          const jsonPath = String((existsRes && existsRes.json_path) || (local && local.json_path) || "");
          const isLocalDup = !!(local && local.exists);
          const message = `${isLocalDup ? "이미 다운로드됨" : "서버 중복 확인됨"}: shortcode=${shortcode || "?"} ${assetKind || "asset"}[${mediaIndex}]${filePath ? " / " + filePath : ""}`;
          const refresh = existsRes && existsRes.metadata_refresh && typeof existsRes.metadata_refresh === "object" ? existsRes.metadata_refresh : {};
          const report = refresh.report && typeof refresh.report === "object" ? refresh.report : {};
          igblueSendDownloadProgress({status:"already_done", file:filePath || msg.prefix || "", file_path:filePath, file_paths:(existsRes&&existsRes.file_paths)||local.file_paths||[], json_path:jsonPath, report_ok:!!report.ok, report_path:String(report.report_path||""), shortcode, asset_kind:assetKind, media_index:mediaIndex, metadata_updated:!!(existsRes&&existsRes.metadata_updated), local, exists:existsRes, message});
          try{ sendResponse({status:"already_done", ok:true, error_code:"IGBLUE_ALREADY_DOWNLOADED", message, shortcode, asset_kind:assetKind, media_index:mediaIndex, file_path:filePath, file_paths:(existsRes&&existsRes.file_paths)||local.file_paths||[], file_saved:!!filePath, json_path:jsonPath, json_saved:!!jsonPath, report_ok:!!report.ok, report_saved:!!(report.ok&&report.report_path), report_path:String(report.report_path||""), report_error:String(report.error||""), metadata_updated:!!(existsRes&&existsRes.metadata_updated), exists:existsRes, meta:msg.meta||null}); }catch(e){}
          return;
        }
        igblueDownloadWithApi(msg, sendResponse);
      }).catch(function(err){
        const detail = String(err&&err.message||err);
        if(msg && msg.auto_direct_download){
          const message = "자동 직접 다운로드 중복 확인 예외: 파일 저장을 시작하지 않습니다. " + detail;
          igblueSendDownloadProgress({status:"error", file:msg.prefix || "", message, shortcode:igblueMetaShortcode(msg.meta), asset_kind:igblueAssetKindFromMeta(msg.meta, msg.url), media_index:(msg.meta&&msg.meta.media_index!=null?msg.meta.media_index:0), meta:msg.meta||null});
          try{ sendResponse({status:"error", ok:false, error_code:"AUTO_DIRECT_PRECHECK_EXCEPTION", message, file_saved:false, json_saved:false, report_saved:false}); }catch(e){}
          return;
        }
        const message = "중복 확인 예외: 다운로드는 허용합니다. " + detail;
        igblueSendDownloadProgress({status:"warn", file:msg.prefix || "", message, shortcode:igblueMetaShortcode(msg.meta), asset_kind:igblueAssetKindFromMeta(msg.meta, msg.url), media_index:(msg.meta&&msg.meta.media_index!=null?msg.meta.media_index:0), meta:msg.meta||null});
        igblueDownloadWithApi(msg, sendResponse);
      });
      return true;
    }

    if(msg && (msg.type === "IGBLUE_OPEN_LOCAL_FILE" || msg.type === "IGBLUE_OPEN_LOCAL_FOLDER")){
      const path = String(msg.path || msg.file_path || "");
      const act = msg.type === "IGBLUE_OPEN_LOCAL_FOLDER" ? "ig_open_folder" : "ig_open_file";
      igblueLocalWsRequest(act, {path}, 5000, {force:true}).then(function(res){
        try{ sendResponse(res || {ok:false,error:"empty_response"}); }catch(e){}
      }).catch(function(err){
        try{ sendResponse({ok:false,error:String(err&&err.message||err)}); }catch(e){}
      });
      return true;
    }

    if(msg && msg.type === "IGBLUE_OPEN_DOWNLOAD_FOLDER"){
      try{
        if(igblueLastDownloadId){
          chrome.downloads.show(igblueLastDownloadId);
          sendResponse({status:"ok", mode:"last"});
        }else if(chrome.downloads.showDefaultFolder){
          chrome.downloads.showDefaultFolder();
          sendResponse({status:"ok", mode:"default"});
        }else sendResponse({status:"error", message:"showDefaultFolder unavailable"});
      }catch(e){
        try{ chrome.downloads.showDefaultFolder(); sendResponse({status:"ok", mode:"default"}); }catch(err){ sendResponse({status:"error", message:String(err && err.message || err)}); }
      }
      return true;
    }

    if(msg && msg.target === "BACKGROUND"){
      if(msg.type === "OPEN PAYMENT PAGE" || msg.type === "OPEN LOGIN PAGE" || msg.type === "OPEN TRIAL PAGE"){
        sendResponse({status:"disabled"});
        return true;
      }
    }

    return false;
  });
 


  // IGblue v172-v178 patch: chrome.storage.local -> Auto8700 8707 /event bridge.
  const IGBLUE_AUTO8700_EVENT_URL = "http://127.0.0.1:8707/event";
  const IGBLUE_AUTO8700_DEVTOOLS_KEYS = new Set(["devtools_network_enabled", "devtools_console", "devtools_file_log", "devtools_capture_body", "devtools_verbose", "devtools_print_media", "devtools_print_json", "devtools_print_graphql", "ig_auto8700_devtools_settings", "ig_manual_click_mode_enabled"]);
  function igblueAuto8700SettingsFromChanges(changes){
    const settings = {};
    try { Object.keys(changes || {}).forEach((key) => { if (!IGBLUE_AUTO8700_DEVTOOLS_KEYS.has(key)) return; const nv = changes[key] && Object.prototype.hasOwnProperty.call(changes[key], "newValue") ? changes[key].newValue : changes[key]; if (key === "ig_auto8700_devtools_settings" && nv && typeof nv === "object") Object.assign(settings, nv); else settings[key] = nv; }); } catch (_) {}
    return settings;
  }
  function igbluePostAuto8700Event(act, settings, extra){
    const payload = Object.assign({ act: act || "ig_devtools_settings_changed", source: "IG_Ext", version: IGBLUE_BG_VERSION, ts: Date.now(), settings: settings || {} }, extra || {});
    try { fetch(IGBLUE_AUTO8700_EVENT_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), cache: "no-store" }).then((res) => { try { dbg("auto8700_8707", "post_ok", 172, {status: res && res.status, settings}); } catch (_) {} }).catch((err) => { try { dbg("auto8700_8707", "post_fail", 172, String(err && err.message || err)); } catch (_) {} }); }
    catch (e) { try { dbg("auto8700_8707", "post_exception", 172, String(e && e.message || e)); } catch (_) {} }
  }
  try { chrome.storage.onChanged.addListener((changes, areaName) => { if (areaName !== "local") return; const settings = igblueAuto8700SettingsFromChanges(changes); if (!settings || !Object.keys(settings).length) return; igbluePostAuto8700Event("ig_devtools_settings_changed", settings, {changes}); }); } catch (_) {}

  importScripts("ExtPay.js");
  extpay = ExtPay("ig-sorter");
  extpay.startBackground();
  extpay.onPaid.addListener(function(){ igblueRefreshPayState(); });
  igblueRefreshPayState();
  igblueRefreshActiveFromFocusedWindow();   

  chrome.runtime.onInstalled.addListener(function(info){
    if(info.reason === "install") chrome.tabs.create({url:"https://www.instagram.com/explore/"});
  });
})();
