'use strict';
  function uniqStrings(arr){
    const seen = new Set(); const out = [];
    for(const v of arr || []){
      const name = String(v ?? '').trim();
      if(!name || seen.has(name)) continue;
      seen.add(name); out.push(name);
    }
    return out;
  }
  function normalizeTags(tags){
    if(!tags) return [];
    if(Array.isArray(tags)) return uniqStrings(tags.map(t => typeof t === 'string' ? t : (t?.name || t?.label || t?.tag || '')));
    if(typeof tags === 'string') return uniqStrings(tags.split(/[,，\s]+/));
    return [];
  }
  function normalizePublicTags(tags){
    if(!tags) return [];
    const list = Array.isArray(tags) ? tags : (typeof tags === 'string' ? tags.split(/[,，\s]+/) : []);
    const out = []; const seen = new Set();
    for(const t of list){
      const name = String(typeof t === 'string' ? t : (t?.name || t?.label || t?.tag || '')).trim();
      if(!name || seen.has(name)) continue;
      seen.add(name);
      const count = typeof t === 'object' && t ? Number(t.count ?? t.total_cont ?? t.total ?? t.num ?? 0) || 0 : 0;
      out.push(count ? {name, count} : {name});
    }
    return out;
  }
  function myTagsOf(c){ return normalizeTags(c?.myTags || c?.my_tags || c?.tags || []); }
  function publicTagsOf(c){ return normalizePublicTags(c?.publicTags || c?.public_tags || c?.subjectTags || c?.raw?.subject?.tags || []); }
  function publicTagNames(c){ return publicTagsOf(c).map(t=>t.name).filter(Boolean); }
  function allTagNames(c){ return uniqStrings([...myTagsOf(c), ...publicTagNames(c)]); }
  function tagKey(scope, name){ return scope + '::' + String(name || ''); }
  function parseTagKey(key){
    if(key === '全部' || key === '无标签') return {scope:'special', name:key};
    const idx = String(key || '').indexOf('::');
    if(idx > 0) return {scope:String(key).slice(0, idx), name:String(key).slice(idx + 2)};
    return {scope:'all', name:String(key || '')};
  }
  function tagLabelFromKey(key){
    const t = parseTagKey(key);
    if(t.scope === 'my') return '我的：' + t.name;
    if(t.scope === 'public') return '公共：' + t.name;
    return t.name || '全部';
  }
  function bgmTagUrl(subjectType, name){
    const path = BGM_TYPE_PATH[Number(subjectType)] || 'subject_search';
    const q = encodeURIComponent(name || '');
    const base = bgmSiteBase();
    return path === 'subject_search' ? `${base}/subject_search/${q}?cat=all` : `${base}/${path}/tag/${q}`;
  }
  function publicTagsForDisplay(c){ return publicTagsOf(c); }
  function shortValue(v, max=80){
    const t = Array.isArray(v) ? v.join(' / ') : String(v ?? '');
    return t.length > max ? t.slice(0, max) + '…' : t;
  }
  function valuesSame(a,b){ return stableStringify(a ?? '') === stableStringify(b ?? ''); }
  function collectionFieldDiff(oldC, newC){
    const fields = [
      ['collection_type','收藏状态', v => STATUS[v] || v || '-'],
      ['rate','我的评分', v => v || '未评分'],
      ['ep_status','章节进度', v => v || '-'],
      ['vol_status','卷进度', v => v || '-'],
      ['comment','吐槽/观后感', v => shortValue(v || '无吐槽', 120)],
      ['private','私密状态', v => v ? '私密' : '公开'],
      ['tags','我的标签', v => shortValue(normalizeTags(v), 120)],
      ['publicTags','公共标签', v => shortValue(normalizePublicTags(v).map(x=>x.count ? `${x.name}(${x.count})` : x.name), 120)],
      ['bangumi_updated_at','API 更新时间', v => formatDate(v) || '-'],
      ['title','标题', v => v || '-'],
      ['date','上映日期', v => v || '-'],
      ['bgm_score','Bangumi 均分', v => formatScoreValue(v) || '-'],
      ['rank','排名', v => v || '-']
    ];
    const changes = [];
    for(const [key, label, fmt] of fields){
      const oldVal = key === 'tags' ? myTagsOf(oldC) : (key === 'publicTags' ? publicTagsOf(oldC) : oldC?.[key]);
      const newVal = key === 'tags' ? myTagsOf(newC) : (key === 'publicTags' ? publicTagsOf(newC) : newC?.[key]);
      if(!valuesSame(oldVal, newVal)) changes.push({field:key, label, from:fmt(oldVal), to:fmt(newVal), old:oldVal ?? '', new:newVal ?? ''});
    }
    return changes;
  }
  function historyChangeHtml(h){
    const changes = Array.isArray(h.changes) ? h.changes : [];
    if(!changes.length) return esc(h.note || '');
    return changes.slice(0,6).map(x => `<div class="hist-change"><span>${esc(x.label || x.field)}</span>：${esc(x.from)} → ${esc(x.to)}</div>`).join('') + (changes.length > 6 ? `<div class="hist-change">还有 ${changes.length - 6} 项变化…</div>` : '');
  }
  function normalizeCollection(item, fallbackType){
    const s = item.subject || {};
    const imgs = s.images || {};
    const st = item.subject_type || s.type || fallbackType?.subject_type || 0;
    const ct = item.type || fallbackType?.collection_type || 0;
    const title = s.name_cn || s.name || `Subject ${item.subject_id || s.id}`;
    const raw = item;
    return {
      subject_id: Number(item.subject_id || s.id), subject_type:Number(st), collection_type:Number(ct),
      title, name:s.name||'', name_cn:s.name_cn||'', date:s.date||'', eps:s.eps ?? '', volumes:s.volumes ?? '',
      bgm_score: s.score ?? s.rating?.score ?? '', rank: s.rank ?? '',
      summary:s.summary||s.short_summary||'', image: imgs.large || imgs.common || imgs.medium || imgs.grid || imgs.small || '', images:imgs,
      rate:item.rate ?? 0, comment:item.comment || '', tags:normalizeTags(item.tags), myTags:normalizeTags(item.tags), publicTags:normalizePublicTags(s.tags || item.publicTags || item.public_tags), ep_status:item.ep_status ?? '', vol_status:item.vol_status ?? '',
      private:!!item.private,
      collection_created_at:item.created_at || item.createdAt || item.collect_created_at || '',
      bangumi_updated_at:item.updated_at || item.updatedAt || '', raw
    };
  }
  function stableStringify(obj){
    if(obj === null || typeof obj !== 'object') return JSON.stringify(obj);
    if(Array.isArray(obj)) return '[' + obj.map(stableStringify).join(',') + ']';
    return '{' + Object.keys(obj).sort().map(k => JSON.stringify(k)+':'+stableStringify(obj[k])).join(',') + '}';
  }
  function hashString(str){ let h=2166136261; for(let i=0;i<str.length;i++){ h ^= str.charCodeAt(i); h = Math.imul(h,16777619); } return (h>>>0).toString(16); }
  function comparable(n){ return {subject_id:n.subject_id, subject_type:n.subject_type, collection_type:n.collection_type, title:n.title, name:n.name, name_cn:n.name_cn, date:n.date, eps:n.eps, volumes:n.volumes, bgm_score:n.bgm_score, rank:n.rank, summary:n.summary, image:n.image, rate:n.rate, comment:n.comment, tags:myTagsOf(n), publicTags:publicTagsOf(n), ep_status:n.ep_status, vol_status:n.vol_status, private:n.private, collection_created_at:n.collection_created_at, bangumi_updated_at:n.bangumi_updated_at}; }
  function makeSnapshot(n){ return {...n, content_hash:hashString(stableStringify(comparable(n)))}; }

  function getAllCollections(){ return Object.values(state.collections || {}); }
  function visibleCollections(){
    const q = filters.q.trim().toLowerCase();
    let arr = getAllCollections().filter(c => c.status !== 'confirmed_deleted');
    // Guests (read-only public mode) never see the safety box, even if the filter is somehow set to it.
    const sf = (!authReady && filters.status === 99) ? 0 : filters.status;
    if(sf === 99) arr = getAllCollections().filter(c => c.status === 'missing_candidate' || c.status === 'protected_local' || c.status === 'confirmed_deleted');
    else if(sf) arr = arr.filter(c => c.collection_type === sf && c.status !== 'missing_candidate');
    else arr = arr.filter(c => c.status !== 'missing_candidate');
    if(filters.type) arr = arr.filter(c => c.subject_type === filters.type);
    if(isPublicMode && !authReady && !publicShowNsfw) arr = arr.filter(c => c.nsfw !== true);
    else if(filters.nsfw === 'only') arr = arr.filter(c => c.nsfw === true);
    else if(filters.nsfw === 'hide') arr = arr.filter(c => c.nsfw !== true);
    if(filters.tag !== '全部') arr = arr.filter(c => {
      const tf = parseTagKey(filters.tag);
      if(tf.name === '无标签') return !myTagsOf(c).length && !publicTagNames(c).length;
      if(tf.scope === 'my') return myTagsOf(c).includes(tf.name);
      if(tf.scope === 'public') return publicTagNames(c).includes(tf.name);
      return allTagNames(c).includes(tf.name);
    });
    if(filters.hasComment) arr = arr.filter(c => !!(c.comment && String(c.comment).trim()));
    if(q){ arr = arr.filter(c => [c.title,c.name,c.name_cn,c.comment,c.summary,c.date].join(' ').toLowerCase().includes(q)); }
    const cmpDate = (x) => Date.parse(x || '') || 0;
    arr.sort((a,b)=>{
      if(filters.sort==='updated_asc') return cmpDate(a.bangumi_updated_at)-cmpDate(b.bangumi_updated_at);
      if(filters.sort==='rate_desc') return (Number(b.rate)||0)-(Number(a.rate)||0) || cmpDate(b.bangumi_updated_at)-cmpDate(a.bangumi_updated_at);
      if(filters.sort==='name_asc') return (a.title||'').localeCompare(b.title||'', 'zh-Hans-CN');
      if(filters.sort==='first_seen_desc') return cmpDate(b.first_seen_at)-cmpDate(a.first_seen_at);
      return cmpDate(b.bangumi_updated_at)-cmpDate(a.bangumi_updated_at);
    });
    return arr;
  }

  async function getCachedImage(id){
    const c=state.collections?.[String(id)];
    // v0.20: Data URL 只在离线导出页优先使用。在线/便携模式优先走真实文件或原始在线图片，
    // 避免超大的 base64 封面残留到运行库后影响在线海报显示。
    if(window.BANGUMI_VAULT_OFFLINE_STATE && c?.cover_data_url) return {dataUrl:c.cover_data_url};
    if(c?.cover_local_url) return {url:c.cover_local_url, file:c.cover_local_file||''};
    try{ const cached = await idbGet('img:'+id); if(cached?.dataUrl || cached?.url) return cached; }catch{}
    if(c?.cover_data_url && !onlineCoverUrl(c)) return {dataUrl:c.cover_data_url};
    return null;
  }
  function onlineCoverUrl(c){ return c.image || c.images?.large || c.images?.common || c.images?.medium || c.images?.grid || c.images?.small || ''; }
  // Rewrite an online image URL through the configured proxy/mirror (empty = no change).
  function applyImageProxy(url, proxy){
    proxy = (proxy || '').trim();
    if(!proxy || !url || !/^https?:\/\//i.test(url)) return url;
    return proxy.includes('{url}') ? proxy.replace('{url}', encodeURIComponent(url)) : proxy + url;
  }
  function coverUrl(c){
    if(window.BANGUMI_VAULT_OFFLINE_STATE && c.cover_data_url) return c.cover_data_url;
    // Local cached covers (/images/...) are served by us — never proxy those; only online bgm images.
    return c.cover_local_url || applyImageProxy(onlineCoverUrl(c), state.settings?.imageProxy) || c.cover_data_url || '';
  }

