/** Broken Link Hunter UA 1.6.1 — bound Google Apps Script, V8. */
var BH_NAMES={donors:'BLH Донори',domains:'BLH Домени',links:'BLH Посилання',queue:'BLH Черга',log:'BLH Журнал',results:'BLH Результати',watch:'BLH Спостереження',deferred:'BLH Відкладені URL',manual:'BLH Ручна перевірка',sitemaps:'BLH Sitemap',processed:'BLH Оброблені'};
var BH_HEADERS={donors:['URL донора (вставляйте з A2)'],domains:['Домен','DNS','RDAP / підсумок','Джерело RDAP','Перевірено','Спроби','Повтор після','Пояснення'],links:['Ключ','Домен','Сторінка донора','Цільовий URL','Анкор','rel','Знайдено'],queue:['URL','Стартовий URL','Глибина','Стан','Спроби','Повтор після','Пояснення'],log:['Час','Подія','Деталі'],results:['Домен-кандидат','Статус — потрібна ручна перевірка','DNS','Перевірено RDAP','RDAP URL','Сторінка донора (приклад)','Цільовий URL (приклад)','Анкор','rel','Знайдених посилань','Унікальних сторінок-донорів','Пояснення']};
BH_HEADERS.watch=BH_HEADERS.results.slice();BH_HEADERS.deferred=['URL','Стартовий URL','Глибина','Причина'];
BH_HEADERS.manual=BH_HEADERS.results.slice();
BH_HEADERS.sitemaps=['URL карти / robots.txt','Стартовий URL','Тип','Стан','Спроби','Повтор після','Пояснення'];
BH_HEADERS.processed=['Ключ завдання','Збережене завершене завдання'];
BH_NAMES.runs='BLH Прогрес донорів';
BH_HEADERS.runs=['Донор','Стан','Завершено','Успішні сторінки','Пропуски / robots','Помилки сторінок','Помилки Sitemap'];
var BH_DEFAULT={mode:'site',depth:4,maxPages:500,maxTotal:5000,tlds:'com,net,org',batch:15,rdapAll:true,fullCrawl:true,sitemaps:true};
function onOpen(){SpreadsheetApp.getUi().createMenu('🔎 Broken Link Hunter').addItem('Відкрити панель','bhSidebar').addToUi();}
function bhSidebar(){SpreadsheetApp.getUi().showSidebar(HtmlService.createHtmlOutputFromFile('Sidebar').setTitle('Broken Link Hunter UA'));}
function bhProps_(){return PropertiesService.getScriptProperties();}
function bhBook_(){var id=bhProps_().getProperty('BH_SHEET');return id?SpreadsheetApp.openById(id):SpreadsheetApp.getActiveSpreadsheet();}
function bhSheet_(k){var s=bhBook_().getSheetByName(BH_NAMES[k]);if(!s)throw Error('Спочатку натисніть «Налаштувати таблицю»');return s;}
function bhOptionalRows_(k){return bhBook_().getSheetByName(BH_NAMES[k])?bhRows_(k):[];}
function bhRows_(k){var s=bhSheet_(k),n=s.getLastRow()-1,out=[];for(var row=2;row<=n+1;row+=5000){var part=s.getRange(row,1,Math.min(5000,n+2-row),BH_HEADERS[k].length).getValues();for(var r of part)out.push(r);}return out;}

function bhSafe_(v){return typeof v==='string'&&/^[=+@-]/.test(v)?"'"+v:v;}
function bhAppend_(k,rows){if(!rows.length)return;try{bhCapacity_({[k]:rows.length});var s=bhSheet_(k);bhGrow_(s,s.getLastRow()+rows.length);s.getRange(s.getLastRow()+1,1,rows.length,BH_HEADERS[k].length).setValues(rows.map(r=>r.map(bhSafe_)));if(k==='processed')BH_ARCHIVE_MEM=null;}catch(e){throw Error('ЗБЕРЕЖЕННЯ: '+e.message);}}

function bhLog_(event,detail){var log=bhSheet_('log');if(log.getLastRow()>501)bhDeleteRowsSafe_(log,2,log.getLastRow()-501);bhAppend_('log',[[new Date().toISOString(),event,String(detail).slice(0,3000)]]);}
function bhLock_(fn){var lock=LockService.getScriptLock();if(!lock.tryLock(1000))throw Error('Зараз завершується пакет. Спробуйте за хвилину.');try{return fn();}finally{lock.releaseLock();}}
function bhSetup(){return bhLock_(function(){var p=bhProps_();if(p.getProperty('BH_STATE')==='RUNNING')throw Error('Спочатку призупиніть пошук');p.setProperty('BH_SHEET',bhBook_().getId());bhStopTriggers_();bhTrim_();bhEnsureSheets_();bhTrim_();bhCompact_(20000,1000);bhSyncResults_();bhInitRuns_();p.setProperty('BH_VERSION','1.6.1');bhSetState_('PAUSED');p.setProperty('BH_MESSAGE','Таблицю налаштовано. Знахідки та чергу збережено. Натисніть Продовжити.');return bhStatus();});}

function bhConfig_(){return Object.assign({},BH_DEFAULT,JSON.parse(bhProps_().getProperty('BH_CONFIG')||'{}'));}
function bhSaveConfig(c){return bhLock_(function(){if(bhProps_().getProperty('BH_STATE')==='RUNNING')throw Error('Спочатку призупиніть пошук');function n(v,min,max){v=Number(v);if(!Number.isInteger(v)||v<min||v>max)throw Error('Невірне числове налаштування');return v;}
 var config={fullCrawl:c.fullCrawl===true,sitemaps:c.sitemaps!==false,mode:c.mode==='page'?'page':'site',depth:n(c.depth,0,10),maxPages:n(c.maxPages,0,5000),maxTotal:n(c.maxTotal,1,20000),rdapAll:c.rdapAll!==false,batch:n(c.batch,1,30),tlds:String(c.tlds||'').toLowerCase().split(/[\s,;]+/).map(x=>x.replace(/^\./,'')).filter(Boolean).join(',')};bhProps_().setProperty('BH_CONFIG',JSON.stringify(config));return bhStatus();});}
function bhStatus(){var p=bhProps_(),b=bhBook_(),counts={};Object.keys(BH_NAMES).forEach(k=>{var s=b&&b.getSheetByName(BH_NAMES[k]);counts[k]=s?Math.max(0,s.getLastRow()-1):0;});var q=b&&b.getSheetByName(BH_NAMES.queue)?bhRows_('queue'):[],d=b&&b.getSheetByName(BH_NAMES.domains)?bhRows_('domains'):[];return {version:'1.6.1',upgraded:p.getProperty('BH_VERSION')==='1.6.1',state:p.getProperty('BH_STATE')||'READY',message:p.getProperty('BH_MESSAGE')||'',config:bhConfig_(),counts:counts,last:p.getProperty('BH_LAST')||'',donors:bhDonorStatus_(),progress:bhProgress_(q,d)};}
function bhStopTriggers_(){ScriptApp.getProjectTriggers().filter(t=>t.getHandlerFunction()==='bhWorker').forEach(t=>ScriptApp.deleteTrigger(t));}
function bhStart(){return bhLock_(function(){var p=bhProps_();bhSheet_('queue');if(p.getProperty('BH_VERSION')!=='1.6.1')throw Error('Спочатку натисніть «Налаштувати таблицю».');bhInitRuns_();if(!bhRows_('runs').length)throw Error('Вставте URL у BLH Донори, починаючи з A2.');bhActivate_();bhStopTriggers_();ScriptApp.newTrigger('bhWorker').timeBased().everyMinutes(5).create();bhSetState_('RUNNING');p.setProperty('BH_MESSAGE','Послідовний обхід запущено. Після завершення донора робочі дані прибираються автоматично.');return bhStatus();});}

function bhPause(){bhSetState_('PAUSED');bhStopTriggers_();return bhStatus();}
function bhClear(){BH_ARCHIVE_MEM=null;bhSetState_('PAUSED');return bhLock_(function(){bhStopTriggers_();['domains','links','queue','results','watch','deferred','manual','sitemaps','processed','runs'].forEach(k=>{var s=bhBook_().getSheetByName(BH_NAMES[k]);if(s&&s.getLastRow()>1)s.getRange(2,1,s.getLastRow()-1,s.getLastColumn()).clearContent();});['BH_ACTIVE_SEED','BH_CLEAN_STAGE','BH_TIME','BH_SAMPLES','BH_CURRENT','BH_DEFER_OVERFLOW','BH_RECOVERY','BH_FAIR_SEED','BH_MAP_SEED','BH_PROMOTE_CURSOR','BH_BLOCKED_PENDING','BH_COMPACT_QUEUE','BH_COMPACT_SITEMAPS','BH_STORAGE_FREED','BH_MAPS_HELD'].forEach(k=>bhProps_().deleteProperty(k));bhProps_().setProperty('BH_MESSAGE','Результати очищені. Донори й налаштування збережені.');bhProps_().deleteProperty('BH_LAST');bhLog_('Очищення','Результати, черга та прогрес очищені користувачем');return bhStatus();});}
function bhClearLog(){return bhLock_(function(){var s=bhSheet_('log');if(s.getLastRow()>1)s.getRange(2,1,s.getLastRow()-1,3).clearContent();return bhStatus();});}
function bhRetry(){return bhLock_(function(){var s=bhSheet_('domains'),r=bhRows_('domains');r.forEach((v,i)=>{if(true){v[2]='WAIT';v[5]=0;v[6]=0;s.getRange(i+2,1,1,8).setValues([v]);}});bhSyncResults_();return bhStatus();});}
// Fetches never follow redirects automatically. Each new destination is validated.
function bhFetch_(address,accept){var u=BHCore.url(address);bhPublic_(u);var res=UrlFetchApp.fetch(u.href,{muteHttpExceptions:true,followRedirects:false,validateHttpsCertificates:true,headers:{'User-Agent':'BrokenLinkHunter/1.0','Accept':accept||'text/html'}});var code=res.getResponseCode(),h=res.getAllHeaders();var headers={};Object.keys(h).forEach(k=>headers[k.toLowerCase()]=String(h[k]));return {code:code,headers:headers,text:res.getContentText(),blob:res.getBlob()};}
function bhJson_(address){var r=bhFetch_(address,'application/rdap+json, application/json');try{r.data=JSON.parse(r.text);}catch(e){r.data=null;}return r;}
function bhRobot_(u){var cache=CacheService.getScriptCache(),key='robot:'+u.origin;var saved=cache.get(key),r;
 if(saved)r=JSON.parse(saved);else{var address=u.origin+'/robots.txt';for(var i=0;i<5;i++){r=bhFetch_(address,'text/plain');if(r.code>=300&&r.code<400&&r.headers.location){address=BHCore.url(r.headers.location,address).href;continue;}break;}
 if(r.code===404||r.code===410)r={code:200,text:''};if(r.code!==200)throw Error('robots.txt недоступний: HTTP '+r.code);if(r.text.length>80000)throw Error('robots.txt завеликий для цієї версії');cache.put(key,JSON.stringify({code:r.code,text:r.text}),3600);}
 var rule=BHCore.robot(r.text,u.origin+'/robots.txt',u.href);if(!rule.allowed)throw Error('ROBOTS_DENY');return rule;}
function bhPage_(address){for(var i=0;i<6;i++){var u=BHCore.url(address),rule=bhRobot_(u);if(rule.delay>15)throw Error('Crawl-delay >15 секунд: потрібен окремий сервер');Utilities.sleep(Math.ceil(rule.delay*1000));var r=bhFetch_(u.href);if(r.code>=300&&r.code<400&&r.headers.location){address=BHCore.url(r.headers.location,u.href).href;continue;}r.url=u.href;return r;}throw Error('Забагато редиректів');}
function bhDns_(domain){var r=bhJson_('https://dns.google/resolve?name='+encodeURIComponent(domain)+'&type=NS');if(r.code!==200||!r.data)throw Error('DNS HTTP '+r.code);var d=r.data;if(d.Status===3)return 'NXDOMAIN';if(d.Status!==0)throw Error('DNS RCODE '+d.Status);if((d.Answer||[]).some(x=>x.type===2))return 'NS_OK';return 'NO_NS';}
function bhRdap_(domain){var cache=CacheService.getScriptCache(),raw=cache.get('BH_RDAP');var data;if(raw)data=JSON.parse(raw);else{var boot=bhJson_('https://data.iana.org/rdap/dns.json');if(boot.code!==200||!boot.data||!boot.data.services)throw Error('IANA RDAP недоступний');data=boot.data;try{cache.put('BH_RDAP',JSON.stringify(data),21600);}catch(e){}}
 var tld=domain.split('.').pop(),service=data.services.find(s=>s[0].includes(tld));if(!service)return {status:'НЕПІДТРИМУВАНА ЗОНА',source:'',note:'У IANA немає RDAP сервера. Перевірте в реєстратора.'};var endpoint=service[1].find(x=>x.startsWith('https://'));if(!endpoint)return {status:'НЕПІДТРИМУВАНА ЗОНА',source:'',note:'Немає HTTPS RDAP'};
 var address=endpoint.replace(/\/$/,'')+'/domain/'+encodeURIComponent(domain);for(var i=0;i<4;i++){var r=bhJson_(address);if(r.code>=300&&r.code<400&&r.headers.location){address=BHCore.url(r.headers.location,address).href;continue;}var status=BHCore.rdap(r.code,r.data,domain);if(r.code===404&&status!=='КАНДИДАТ')return {status:'РУЧНА ПЕРЕВІРКА: RDAP 404',source:address,note:'Сервер RDAP повернув HTTP 404 без стандартного errorCode. Це не підтверджує доступність. Перевірте домен у реєстратора.'};if(status==='НЕВІДОМО')throw Error('RDAP HTTP '+r.code+' або неповна відповідь');return bhRdapResult_(status,r.data,address);}throw Error('RDAP: забагато редиректів');}
function bhWorker(){var lock=LockService.getScriptLock();if(!lock.tryLock(500))return;var p=bhProps_(),start=Date.now(),didRun=false,pageDone=0,domainDone=0;
 try{if(p.getProperty('BH_STATE')!=='RUNNING')return;didRun=true;bhSetState_('RUNNING');if(bhCleaning_()){for(var clean=0;clean<20&&Date.now()-start<45000&&bhCleaning_();clean++){if(p.getProperty('BH_STATE')!=='RUNNING')return;bhCleanupStep_();}return;}if(!bhActivate_()){bhSetState_('DONE');bhStopTriggers_();p.setProperty('BH_MESSAGE','Усіх донорів завершено. Кандидати й ручна перевірка збережені.');return;}bhCompact_(10000,500);bhPromote_();var c=bhConfig_(),q=bhRows_('queue'),domains=bhRows_('domains'),evidence=bhRows_('links');
 var qs=bhSeen_('queue',q),ds=new Set(domains.map(r=>r[0])),ls=new Set(evidence.map(r=>r[0])),pending=bhRows_('deferred').filter(r=>r[0]),pendingSet=new Set(pending.map(r=>r[0])),perSeed={};bhSeedCounts_(q,perSeed);
 var work=bhFair_(q,c.batch,'BH_FAIR_SEED');
 for(var w=0;w<work.length&&Date.now()-start<140000;w++){if(p.getProperty('BH_STATE')!=='RUNNING')return;var i=work[w],row=q[i];p.setProperty('BH_FAIR_SEED',row[1]);p.setProperty('BH_CURRENT','Сторінка: '+row[0]);
 try{var page=bhPage_(row[0]);if(page.code===429||page.code>=500)throw Error('HTTP '+page.code);
 if(page.code!==200){row[3]='SKIP';row[6]='HTTP '+page.code;}else if(!/text\/html|application\/xhtml/i.test(page.headers['content-type']||'')){row[3]='SKIP';row[6]='Не HTML';}else if(page.text.length>2000000){row[3]='SKIP';row[6]='HTML >2 млн символів — потрібна окрема перевірка';}else{
 var base=BHCore.url(page.url),seed=BHCore.url(row[1]);if(base.domain!==seed.domain){row[3]='SKIP';row[6]='Редирект за межі донора: '+page.url;}else{
 var found=BHCore.links(page.text,page.url),newQ=[],newD=[],newL=[],newF=[],localQ=new Set(),localD=new Set(),localL=new Set(),localF=new Set();
 found.forEach(function(link){if(link.domain===seed.domain){if(c.mode==='site'&&!qs.has(link.href)&&!pendingSet.has(link.href)&&!localQ.has(link.href)&&!localF.has(link.href)){
 var exclusion=bhCrawlReason_(link),reason=exclusion?'':bhLimitReason_(Number(row[2])+1,(perSeed[row[1]]||0)+newQ.length,qs.size+newQ.length,c);
 if(reason){localF.add(link.href);newF.push([link.href,row[1],Number(row[2])+1,reason]);}else{localQ.add(link.href);newQ.push([link.href,row[1],Number(row[2])+1,exclusion?'SKIP':'WAIT',0,0,exclusion]);}}return;}
 if(link.private)return;var domain=BHCore.root(link.host);if(!domain)return;var allowed=c.tlds.split(',').filter(Boolean);if(allowed.length&&!allowed.some(t=>domain.endsWith('.'+t)))return;
 var key=Utilities.base64EncodeWebSafe(Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256,JSON.stringify([domain,page.url,link.href,link.anchor,link.rel])));
 if(!ls.has(key)&&!localL.has(key)){localL.add(key);newL.push([key,domain,page.url,link.href,link.anchor,link.rel,new Date().toISOString()]);}if(!ds.has(domain)&&!localD.has(domain)){localD.add(domain);newD.push([domain,'','WAIT','','',0,0,'']);}});
 // Commit discoveries before marking source DONE. A failed/partial commit pauses;
 // next run rebuilds dedup sets from persisted sheets and retries this source.
 bhCapacity_({queue:newQ.length,domains:newD.length,links:newL.length,deferred:newF.length});
 bhAppend_('deferred',newF);bhAppend_('links',newL);bhAppend_('domains',newD);bhAppend_('queue',newQ);
 newF.forEach(r=>pendingSet.add(r[0]));newQ.forEach(r=>qs.add(r[0]));newD.forEach(r=>ds.add(r[0]));newL.forEach(r=>ls.add(r[0]));newQ.forEach(r=>q.push(r));newD.forEach(r=>domains.push(r));perSeed[row[1]]=(perSeed[row[1]]||0)+newQ.length;
 row[3]='DONE';row[6]='Посилань: '+found.length;}}
 }catch(e){if(bhFatal_(e))throw e;row[4]=Number(row[4])+1;row[3]=String(e.message)==='ROBOTS_DENY'?'BLOCKED':row[4]>=3?'ERROR':'WAIT';row[5]=Date.now()+Math.pow(2,row[4])*300000;row[6]=String(e.message);bhLog_('Сторінка',row[0]+' — '+e.message);}
 bhWriteRow_('queue',i,row);if(row[3]!=='WAIT')pageDone++;}
 // Sitemap work has its own small budget and cannot consume the entire run.
 var sitemapHeld=q.filter(r=>r[1]===p.getProperty('BH_ACTIVE_SEED')&&r[3]==='WAIT').length>=50000;p.setProperty('BH_MAPS_HELD',sitemapHeld?'1':'0');if(c.mode==='site'&&c.sitemaps&&!sitemapHeld&&Date.now()-start<160000)bhSitemapBatch_(start,q,qs,perSeed,pendingSet,c);
 var budget=c.batch;for(var j=0;j<domains.length&&budget>0&&Date.now()-start<220000;j++){if(p.getProperty('BH_STATE')!=='RUNNING')return;var d=domains[j];if(d[2]!=='WAIT'||Number(d[6])>Date.now())continue;budget--;p.setProperty('BH_CURRENT','Домен: '+d[0]);try{var dnsError='';try{d[1]=bhDns_(d[0]);}catch(dnsErr){if(bhFatal_(dnsErr))throw dnsErr;dnsError=String(dnsErr.message);d[1]='DNS ПОМИЛКА: '+dnsError;}
 if(d[1]==='NS_OK'&&!c.rdapAll){d[2]='DNS АКТИВНИЙ';d[7]='Швидкий режим: RDAP пропущено.';}else{Utilities.sleep(1000);var result=bhRdap_(d[0]);d[2]=result.status;d[3]=result.source;d[7]=(dnsError?'DNS: '+dnsError+'. ':'')+result.note;}}
 catch(e){if(bhFatal_(e))throw e;d[5]=Number(d[5])+1;d[6]=Date.now()+Math.pow(2,d[5])*300000;d[2]=d[5]>=3?'ПОМИЛКА':'WAIT';d[7]=String(e.message);bhLog_('Домен',d[0]+' — '+e.message);}d[4]=new Date().toISOString();bhWriteRow_('domains',j,d);if(d[2]!=='WAIT')domainDone++;}
 p.setProperty('BH_LAST',new Date().toISOString());if(p.getProperty('BH_STATE')!=='RUNNING')return;
 if(bhCanFinish_()){bhBeginCleanup_();p.setProperty('BH_MESSAGE','Донора перевірено в межах доступного обходу. Знахідки збережені; прибираю робочі дані.');}else p.setProperty('BH_MESSAGE','Перевіряється один донор. Наступний пакет автоматичний.');

 }catch(e){bhSetState_('PAUSED');p.setProperty('BH_MESSAGE',new Date().toISOString()+' — '+String(e.message)+'; прогрес збережено. Усуньте причину та натисніть Продовжити.');try{bhStopTriggers_();bhLog_('Зупинка',p.getProperty('BH_MESSAGE'));}catch(ignore){}}
 finally{try{if(didRun){bhRecordBatch_(start,pageDone,domainDone);p.deleteProperty('BH_CURRENT');if(!bhCleaning_()){bhSyncResults_();if(Date.now()-start<245000)bhCompact_(5000,300);}}}catch(e){p.setProperty('BH_MESSAGE','Не вдалося оновити представлення: '+e.message+'. Основні записи збережені; повторіть Налаштувати таблицю після паузи.');}finally{lock.releaseLock();}}}

function bhQuota_(e){return /quota|too many times|limit exceeded|Service invoked/i.test(String(e));}

// Resolve arbitrary fetch hosts through public DNS; reject private/reserved destinations.
function bhPublic_(u){
 if(['dns.google','data.iana.org'].includes(u.host))return;
 var cache=CacheService.getScriptCache(),key='public:'+u.host;if(cache.get(key))return;
 var addresses=[];
 ['A','AAAA'].forEach(function(type){var r=UrlFetchApp.fetch('https://dns.google/resolve?name='+encodeURIComponent(u.host)+'&type='+type,{muteHttpExceptions:true,followRedirects:false});if(r.getResponseCode()!==200)throw Error('Перевірка адреси DNS недоступна');var d=JSON.parse(r.getContentText());if(d.Status!==0)throw Error('DNS адреси: '+d.Status);(d.Answer||[]).forEach(function(a){if(a.type===1||a.type===28)addresses.push(a.data);});});
 if(!addresses.length)throw Error('Немає публічної адреси сервера');
 addresses.forEach(function(a){if(a.includes(':')){if(!/^2[0-9a-f]{3}:/i.test(a)||/^2001:(db8|0):/i.test(a)||/^2002:/i.test(a))throw Error('Непублічна IPv6 адреса');}else{var n=a.split('.').map(Number);if(n.length!==4||n.some(x=>!Number.isInteger(x)||x<0||x>255)||n[0]===0||n[0]===10||n[0]===127||n[0]>=224||(n[0]===169&&n[1]===254)||(n[0]===172&&n[1]>=16&&n[1]<=31)||(n[0]===192&&(n[1]===168||n[1]===0))||(n[0]===100&&n[1]>=64&&n[1]<=127)||(n[0]===198&&(n[1]===18||n[1]===19)))throw Error('Непублічна IPv4 адреса');}});cache.put(key,'1',300);
}
function bhRetryErrors(){return bhLock_(function(){var rows=bhRows_('queue'),s=bhSheet_('queue');rows.forEach(function(r,i){if(r[3]==='ERROR'){r[3]='WAIT';r[4]=0;r[5]=0;r[6]='Повтор за запитом користувача';s.getRange(i+2,1,1,7).setValues([r.map(bhSafe_)]);}});return bhStatus();});}

// Results is a regenerated view; source evidence remains in BLH Посилання.
function bhSyncResults_(){var d=bhRows_('domains'),e=bhRows_('links'),by={};d.forEach(r=>{if(r[2]==='КАНДИДАТ'||String(r[2]).indexOf('СПОСТЕРЕЖЕННЯ')===0||bhManual_(r))by[r[0]]={first:null,n:0,donors:new Set(),quality:{plain:0,nofollow:0,ugc:0,sponsored:0}};});
 e.forEach(function(l){var x=by[l[1]];if(!x)return;var rel=String(l[5]).toLowerCase().split(/\s+/),score=rel.includes('ugc')||rel.includes('sponsored')?0:rel.includes('nofollow')?1:2;if(!x.first||score>x.score){x.first=l;x.score=score;}x.n++;x.donors.add(l[2]);if(!rel.some(t=>['ugc','nofollow','sponsored'].includes(t)))x.quality.plain++;['nofollow','ugc','sponsored'].forEach(t=>{if(rel.includes(t))x.quality[t]++;});});
 ['results','watch','manual'].forEach(function(key){var rows=d.filter(r=>key==='results'?r[2]==='КАНДИДАТ':key==='watch'?String(r[2]).indexOf('СПОСТЕРЕЖЕННЯ')===0:bhManual_(r)).map(function(r){var x=by[r[0]],f=x.first||[],quality='Без nofollow/ugc/sponsored: '+x.quality.plain+'; nofollow: '+x.quality.nofollow+'; ugc: '+x.quality.ugc+'; sponsored: '+x.quality.sponsored+'. Це атрибути, не оцінка якості.';return [r[0],r[2],r[1],r[4],r[3],f[2]||'',f[3]||'',f[4]||'',f[5]||'',x.n,x.donors.size,(r[7]||'')+' '+quality];});bhReplaceView_(key,rows,d);});}

function bhReplaceView_(key,rows,domains){var s=bhSheet_(key),old=bhRows_(key),index=new Map(),incoming=new Map(rows.map(r=>[r[0],r])),source=new Map((domains||[]).map(r=>[r[0],r]));old.forEach((r,i)=>{if(r[0]&&!index.has(r[0]))index.set(r[0],i);});
 // Upsert in place. Never clear, shrink, reorder or remove a protected result row.
 for(var i=0;i<old.length;i++){var r=old[i],fresh=incoming.get(r[0]);if((key==='results'||key==='manual')&&r[0])continue;if(fresh){if(JSON.stringify(r)!==JSON.stringify(fresh))s.getRange(i+2,1,1,12).setValues([fresh.map(bhSafe_)]);}else if(r[0]&&source.has(r[0])){var d=source.get(r[0]);if(r[1]!==d[2]||r[3]!==d[4]){s.getRange(i+2,2,1,4).setValues([[d[2],d[1],d[4],d[3]].map(bhSafe_)]);}}}
 var add=rows.filter(r=>!index.has(r[0]));bhAppend_(key,add);}

function bhRefreshResults(){return bhLock_(function(){bhSyncResults_();return bhStatus();});}
function bhOpenResults(){bhBook_().setActiveSheet(bhSheet_('results'));return null;}

function bhSetState_(state){var p=bhProps_(),now=Date.now(),t=JSON.parse(p.getProperty('BH_TIME')||'null');if(!t&&state==='RUNNING')t={started:now,active:0,resumed:now,ended:0};if(t){if(t.resumed&&state!=='RUNNING'){t.active+=Math.max(0,now-t.resumed);t.resumed=0;}if(state==='RUNNING'&&!t.resumed){t.resumed=now;t.ended=0;}if(state==='DONE')t.ended=now;p.setProperty('BH_TIME',JSON.stringify(t));}p.setProperty('BH_STATE',state);}
function bhRecordBatch_(start,pages,domains){var p=bhProps_(),samples=JSON.parse(p.getProperty('BH_SAMPLES')||'[]');samples.push({ms:Math.max(1,Date.now()-start),p:pages,d:domains});p.setProperty('BH_SAMPLES',JSON.stringify(samples.slice(-8)));}
function bhProgress_(q,d){q=bhWithArchive_('queue',q);var p=bhProps_(),now=Date.now(),state=p.getProperty('BH_STATE')||'READY';function stats(rows,col,time){var wait=rows.filter(r=>r[col]==='WAIT');return {total:rows.length,done:rows.length-wait.length,left:wait.length,delayed:wait.filter(r=>Number(r[time])>now).length,percent:rows.length?Math.floor(100*(rows.length-wait.length)/rows.length):0};}var pages=stats(q,3,5),domains=stats(d,2,6),t=JSON.parse(p.getProperty('BH_TIME')||'null'),samples=JSON.parse(p.getProperty('BH_SAMPLES')||'[]'),eta=null;
 if(state==='DONE')eta=0;else if(state==='RUNNING'&&samples.length>=2){var sp=samples.reduce((n,s)=>n+s.p,0),sd=samples.reduce((n,s)=>n+s.d,0);if((!pages.left||sp)&&(!domains.left||sd)){var batches=Math.max(pages.left?pages.left/(sp/samples.length):0,domains.left?domains.left/(sd/samples.length):0);var cycle=Math.max(300000,samples.reduce((n,s)=>n+s.ms,0)/samples.length);eta=Math.ceil(batches)*cycle;var due=q.filter(r=>r[3]==='WAIT').map(r=>Number(r[5])||0).concat(d.filter(r=>r[2]==='WAIT').map(r=>Number(r[6])||0));if(due.length)eta=Math.max(eta,Math.max(0,due.reduce((a,b)=>Math.min(a,b),Infinity)-now));}}
 var knownPages=new Set(q.map(r=>r[0])),coverage=bhCoverage_(q),deferred=bhOptionalRows_('deferred').filter(r=>r[0]&&!knownPages.has(r[0])).length,maps=bhWithArchive_('sitemaps',bhOptionalRows_('sitemaps')),mapPending=maps.filter(r=>r[3]==='WAIT').length;pages.total+=deferred;pages.left+=deferred;pages.percent=pages.total?Math.floor(100*pages.done/pages.total):0;if(deferred||mapPending)eta=null;return {storage:bhStorage_(),archived:bhArchive_().length,sitemapsHeld:bhProps_().getProperty('BH_MAPS_HELD')==='1',savedCandidates:bhOptionalRows_('results').filter(r=>r[0]).length,savedManual:bhOptionalRows_('manual').filter(r=>r[0]).length,successful:q.filter(r=>r[3]==='DONE').length,manual:d.filter(bhManual_).length,sitemapsPending:mapPending,sitemapsErrors:maps.filter(r=>r[3]==='ERROR'||r[3]==='BLOCKED').length,coverage:coverage,watch:d.filter(r=>String(r[2]).indexOf('СПОСТЕРЕЖЕННЯ')===0).length,pages:pages,domains:domains,candidates:d.filter(r=>r[2]==='КАНДИДАТ').length,pageErrors:q.filter(r=>r[3]==='ERROR').length,skipped:q.filter(r=>r[3]==='SKIP'||r[3]==='BLOCKED').length,domainErrors:d.filter(r=>r[2]==='ПОМИЛКА').length,started:t?t.started:null,elapsedMs:t?Math.max(0,(t.ended||now)-t.started):null,activeMs:t?t.active+(t.resumed?Math.max(0,now-t.resumed):0):null,etaMs:eta,samples:samples.length,current:p.getProperty('BH_CURRENT')||''};}

function bhRdapResult_(status,data,address){var statuses=(data&&Array.isArray(data.status)?data.status:[]).map(String);var normalized=statuses.map(x=>x.toLowerCase().replace(/[^a-z]/g,''));if(status==='ЗАРЕЄСТРОВАНИЙ'){if(normalized.includes('redemptionperiod'))status='СПОСТЕРЕЖЕННЯ: REDEMPTION';else if(normalized.includes('pendingdelete'))status='СПОСТЕРЕЖЕННЯ: PENDING DELETE';else if(normalized.includes('autorenewperiod'))status='СПОСТЕРЕЖЕННЯ: AUTO RENEW';}var note=status==='КАНДИДАТ'?'RDAP не знайшов об’єкт. Перевірте доступність і ціну у реєстратора.':'Домен зареєстрований. Статуси RDAP: '+(statuses.join(', ')||'не вказані')+'.';if(status.indexOf('СПОСТЕРЕЖЕННЯ')===0)note+=' Не вільний; майбутнє видалення і його дата не гарантовані.';return {status:status,source:address,note:note};}
function bhLimitReason_(depth,count,total,c){if(c.fullCrawl)return '';if(depth>c.depth)return 'ГЛИБИНА';if(c.maxPages>0&&count>=c.maxPages)return 'ЛІМІТ ДОНОРА';if(total>=c.maxTotal)return 'ЗАГАЛЬНИЙ ЛІМІТ';return '';}
function bhPromote_(){var s=bhBook_().getSheetByName(BH_NAMES.deferred);if(!s||s.getLastRow()<2)return;var c=bhConfig_(),q=bhRows_('queue'),seen=bhSeen_('queue',q),counts={};bhSeedCounts_(q,counts);var rows=bhRows_('deferred'),add=[],changed=[],p=bhProps_(),cursor=Number(p.getProperty('BH_PROMOTE_CURSOR')||0),end=Math.min(rows.length,cursor+1000),activeSeed=p.getProperty('BH_ACTIVE_SEED');
 for(var i=cursor;i<end;i++){var r=rows[i];if(!r[0]||(activeSeed&&r[1]!==activeSeed))continue;if(seen.has(r[0])){changed.push(i);continue;}var reason=bhLimitReason_(Number(r[2]),counts[r[1]]||0,seen.size,c);if(reason)continue;var u=BHCore.url(r[0]),skip=bhCrawlReason_(u);seen.add(r[0]);counts[r[1]]=(counts[r[1]]||0)+1;add.push([r[0],r[1],Number(r[2]),skip?'SKIP':'WAIT',0,0,skip||'Відновлено з відкладеної черги']);changed.push(i);}
 bhAppend_('queue',add); // Never clear the source before the destination is durable.
 changed.forEach(i=>rows[i]=['','','','']);if(changed.length)s.getRange(cursor+2,1,end-cursor,4).setValues(rows.slice(cursor,end).map(r=>r.map(bhSafe_)));
 if(end===rows.length){var keep=rows.filter(r=>r[0]);if(keep.length)s.getRange(2,1,keep.length,4).setValues(keep.map(r=>r.map(bhSafe_)));if(rows.length>keep.length)s.getRange(keep.length+2,1,rows.length-keep.length,4).clearContent();p.deleteProperty('BH_PROMOTE_CURSOR');}else p.setProperty('BH_PROMOTE_CURSOR',String(end));
}

function bhExpand(){return bhLock_(function(){if(bhProps_().getProperty('BH_STATE')==='RUNNING')throw Error('Спочатку призупиніть пошук');bhRecoverPages_();bhProps_().deleteProperty('BH_SAMPLES');bhProps_().setProperty('BH_MESSAGE','Повторний обхід підготовлено. Знахідки збережені. Натисніть Продовжити.');return bhStatus();});}

function bhOpenWatch(){bhBook_().setActiveSheet(bhSheet_('watch'));return null;}

function bhCoverage_(q){var c=bhConfig_(),by={};q.forEach(function(r){if(!by[r[1]])by[r[1]]={seed:r[1],queued:0,ok:0,errors:0};var x=by[r[1]];x.queued++;if(r[3]==='DONE')x.ok++;if(r[3]==='ERROR'||r[3]==='BLOCKED')x.errors++;});var s=bhBook_().getSheetByName(BH_NAMES.deferred);return {donors:Object.keys(by).map(k=>Object.assign(by[k],{limitReached:!c.fullCrawl&&c.maxPages>0&&by[k].queued>=c.maxPages})),deferred:s?bhRows_('deferred').filter(r=>r[0]).length:0,totalLimit:!c.fullCrawl&&q.length>=c.maxTotal,overflow:bhProps_().getProperty('BH_DEFER_OVERFLOW')==='1'};}

function bhGrow_(s,rows){if(s.getMaxRows&&rows>s.getMaxRows())s.insertRowsAfter(s.getMaxRows(),rows-s.getMaxRows());}

// v1.4 durable queue, migration and sitemap discovery.
function bhEnsureSheets_(){var b=bhBook_();Object.keys(BH_NAMES).forEach(function(k){var s=b.getSheetByName(BH_NAMES[k])||b.insertSheet(BH_NAMES[k]);if(!s.getLastRow()){s.getRange(1,1,1,BH_HEADERS[k].length).setValues([BH_HEADERS[k]]).setBackground('#123b50').setFontColor('#ffffff').setFontWeight('bold');s.setFrozenRows(1);s.setColumnWidths(1,BH_HEADERS[k].length,190);}});}
function bhUpgrade(){return bhSetup();}

function bhRecoverPages_(){if(bhOptionalRows_('processed').length)throw Error('Завершені адреси вже у компактному реєстрі. Для повного повторного обходу потрібен окремий запуск; поточні дані не змінено.');var rows=bhRows_('queue');rows.forEach(function(r){if(['DONE','ERROR','SKIP','BLOCKED'].includes(r[3])){r[3]='WAIT';r[4]=0;r[5]=0;r[6]='Повторний обхід: відновлення охоплення, знахідки збережені';}});if(rows.length)bhSheet_('queue').getRange(2,1,rows.length,7).setValues(rows.map(r=>r.map(bhSafe_)));bhProps_().setProperty('BH_RECOVERY','1');}
function bhManual_(r){return r[2]==='НЕПІДТРИМУВАНА ЗОНА'||r[2]==='ПОМИЛКА'||String(r[2]).indexOf('РУЧНА ПЕРЕВІРКА')===0;}
function bhOpenManual(){bhBook_().setActiveSheet(bhSheet_('manual'));return null;}

function bhRetryUncertain(){return bhLock_(function(){if(bhProps_().getProperty('BH_STATE')==='RUNNING')throw Error('Спочатку призупиніть пошук');var rows=bhRows_('domains');rows.forEach(function(r){if(bhManual_(r)){r[2]='WAIT';r[5]=0;r[6]=0;}});if(rows.length)bhSheet_('domains').getRange(2,1,rows.length,8).setValues(rows.map(r=>r.map(bhSafe_)));bhSyncResults_();return bhStatus();});}
function bhWriteRow_(key,index,row){try{bhSheet_(key).getRange(index+2,1,1,BH_HEADERS[key].length).setValues([row.map(bhSafe_)]);}catch(e){throw Error('ЗБЕРЕЖЕННЯ: '+e.message);}}
function bhFatal_(e){return bhQuota_(e)||/ЗБЕРЕЖЕННЯ:|МІСТКІСТЬ:|СЛУЖБОВІ ДАНІ:|Service Spreadsheets|timed out|DEADLINE_EXCEEDED/i.test(String(e));}
function bhCapacity_(add){var b=bhBook_();if(!b.getSheets)return;function required(){var n=b.getSheets().reduce((n,s)=>n+s.getMaxRows()*s.getMaxColumns(),0);Object.keys(add).forEach(function(k){var s=bhSheet_(k);n+=Math.max(0,s.getLastRow()+add[k]-s.getMaxRows())*s.getMaxColumns();});return n;}
 if(required()>9000000)bhTrim_();if(required()>9500000)throw Error('МІСТКІСТЬ: порожні клітинки вже прибрано, але дані не вміщаються. Неперевірені адреси та результати збережені. Потрібне окреме сховище; завершені завдання стискаються автоматично.');}

function bhFair_(rows,budget,key){var activeSeed=bhProps_().getProperty('BH_ACTIVE_SEED'),groups={},seeds=[];rows.forEach(function(r,i){if(r[3]!=='WAIT'||Number(r[5])>Date.now()||(activeSeed&&r[1]!==activeSeed))return;if(!groups[r[1]]){groups[r[1]]=[];seeds.push(r[1]);}groups[r[1]].push(i);});var last=bhProps_().getProperty(key),at=seeds.indexOf(last);if(at>=0)seeds=seeds.slice(at+1).concat(seeds.slice(0,at+1));var out=[],cursor={};while(out.length<budget){var found=false;for(var seed of seeds){var n=cursor[seed]||0;if(n<groups[seed].length){out.push(groups[seed][n]);cursor[seed]=n+1;found=true;if(out.length===budget)break;}}if(!found)break;}return out;}
function bhCrawlReason_(link){var path=link.path.split('?')[0];if(/\.(pdf|jpe?g|png|gif|webp|svg|ico|zip|rar|7z|mp[34]|wav|woff2?|ttf|xml|gz|css|js|docx?|xlsx?|pptx?)(?:$)/i.test(path))return 'Фільтр: не HTML (PDF/медіа/файл)';
 if(/\/(?:calendar|events-calendar)(?:\/|$)/i.test(path)&&/(?:\/|[?&])(?:\d{4}(?:[-/]\d{1,2})?|year=|month=)/i.test(link.path))return 'Фільтр: календарна навігація (можлива нескінченна черга)';
 if(/\/(?:wp-admin|wp-login\.php|logout|cart|checkout)(?:\/|$)/i.test(path))return 'Фільтр: службова сторінка';var parts=path.split('/').filter(Boolean);if(parts.some((x,i)=>parts.slice(0,i).filter(y=>y===x).length>=3))return 'Фільтр: повторюваний шлях (можлива пастка)';
 if(!link.href.includes('?'))return '';var query=link.href.split('?')[1];if(!query.split('&').every(function(pair){var a=pair.split('='),k=decodeURIComponent(a[0]||'').toLowerCase(),v=decodeURIComponent(a.slice(1).join('=')||'');return /^(page|paged|pagenum|offset|start|id|p|article|article_id|story|lang|language)$/.test(k)&&/^[\w.-]{1,80}$/.test(v);}))return 'Фільтр: параметри URL (пошук/сортування/відстеження/невідомі)';return '';}
function bhSeedSitemaps_(){var c=bhConfig_();if(c.mode!=='site'||!c.sitemaps)return;var rows=bhRows_('sitemaps'),seen=bhSeen_('sitemaps',rows),add=[];(bhProps_().getProperty('BH_ACTIVE_SEED')?[[bhProps_().getProperty('BH_ACTIVE_SEED')]]:bhRows_('donors')).forEach(function(r){if(!r[0])return;try{var raw=String(r[0]).trim(),u=BHCore.url(/^https?:\/\//i.test(raw)?raw:'https://'+raw);[[u.origin+'/robots.txt','ROBOTS'],[u.origin+'/sitemap.xml','XML'],[u.origin+'/sitemap_index.xml','XML'],[u.origin+'/wp-sitemap.xml','XML']].forEach(function(pair){if(!seen.has(pair[0])){seen.add(pair[0]);add.push([pair[0],u.href,pair[1],'WAIT',0,0,'']);}});}catch(e){bhLog_('Sitemap: пропущено донора',String(r[0])+' — '+e.message);}});bhAppend_('sitemaps',add);}
function bhSitemapFetch_(address,seed,robots){for(var i=0;i<6;i++){var u=BHCore.url(address);if(u.domain!==BHCore.url(seed).domain)throw Error('Sitemap: редирект за межі донора');if(!robots){var rule=bhRobot_(u);if(rule.delay>15)throw Error('Crawl-delay >15 секунд');Utilities.sleep(Math.ceil(rule.delay*1000));}var r=bhFetch_(u.href,'application/xml,text/xml,text/plain,*/*');if(r.code>=300&&r.code<400&&r.headers.location){address=BHCore.url(r.headers.location,u.href).href;continue;}r.url=u.href;if(r.code===200&&(/\.gz(?:\?|$)/i.test(u.href)||/application\/(?:x-)?gzip/i.test(r.headers['content-type']||''))){r.text=Utilities.ungzip(r.blob).getDataAsString('UTF-8');}return r;}throw Error('Sitemap: забагато редиректів');}
function bhSitemapBatch_(start,q,qs,perSeed,pendingSet,c){var maps=bhRows_('sitemaps'),seen=bhSeen_('sitemaps',maps),todo=bhFair_(maps,2,'BH_MAP_SEED');for(var index of todo){if(Date.now()-start>180000||bhProps_().getProperty('BH_STATE')!=='RUNNING')return;var row=maps[index];bhProps_().setProperty('BH_MAP_SEED',row[1]);bhProps_().setProperty('BH_CURRENT','Sitemap: '+row[0]);try{var r=bhSitemapFetch_(row[0],row[1],row[2]==='ROBOTS');if(r.code===404||r.code===410){row[3]='SKIP';row[6]='HTTP '+r.code+' — карта/robots відсутні';}else{if(r.code!==200)throw Error('Sitemap HTTP '+r.code);if(r.text.length>8000000)throw Error('Sitemap >8 млн символів: потрібна окрема обробка; джерело збережено');
 if(row[2]!=='ROBOTS'){if(/<!DOCTYPE|<!ENTITY/i.test(r.text))throw Error('Sitemap: DTD/ENTITY не підтримуються');XmlService.parse(r.text);}
 var parsed=row[2]==='ROBOTS'?{index:true,urls:BHCore.sitemapHints(r.text)}:BHCore.sitemap(r.text),addMaps=[],addQ=[],addF=[],local=new Set(),outside=0;
 parsed.urls.forEach(function(address){var u;try{u=BHCore.url(address,r.url);}catch(e){outside++;return;}if(u.domain!==BHCore.url(row[1]).domain){outside++;return;}if(local.has(u.href))return;local.add(u.href);if(parsed.index){if(!seen.has(u.href))addMaps.push([u.href,row[1],'XML','WAIT',0,0,'З карти/robots '+row[0]]);}else if(!qs.has(u.href)&&!pendingSet.has(u.href)){var skip=bhCrawlReason_(u),reason=skip?'':bhLimitReason_(0,(perSeed[row[1]]||0)+addQ.length,qs.size+addQ.length,c);if(reason)addF.push([u.href,row[1],0,reason]);else addQ.push([u.href,row[1],0,skip?'SKIP':'WAIT',0,0,skip||'З Sitemap: '+row[0]]);}});
 bhCapacity_({sitemaps:addMaps.length,queue:addQ.length,deferred:addF.length});bhAppend_('sitemaps',addMaps);bhAppend_('deferred',addF);bhAppend_('queue',addQ);addMaps.forEach(r=>seen.add(r[0]));addF.forEach(r=>pendingSet.add(r[0]));addQ.forEach(r=>qs.add(r[0]));addQ.forEach(r=>q.push(r));perSeed[row[1]]=(perSeed[row[1]]||0)+addQ.length;row[3]='DONE';row[6]='URL: '+parsed.urls.length+'; додано сторінок: '+addQ.length+'; карт: '+addMaps.length+'; поза межами/невалідні: '+outside;}
 }catch(e){if(bhFatal_(e))throw e;row[4]=Number(row[4])+1;row[3]=String(e.message)==='ROBOTS_DENY'?'BLOCKED':row[4]>=3?'ERROR':'WAIT';row[5]=Date.now()+Math.pow(2,row[4])*300000;row[6]=e.message;bhLog_('Sitemap',row[0]+' — '+e.message);}bhWriteRow_('sitemaps',index,row);}}
function bhRetrySitemaps(){return bhLock_(function(){if(bhProps_().getProperty('BH_STATE')==='RUNNING')throw Error('Спочатку призупиніть пошук');var rows=bhRows_('sitemaps');rows.forEach(r=>{if(['ERROR','BLOCKED'].includes(r[3])){r[3]='WAIT';r[4]=0;r[5]=0;}});if(rows.length)bhSheet_('sitemaps').getRange(2,1,rows.length,7).setValues(rows.map(r=>r.map(bhSafe_)));return bhStatus();});}

function bhCrawlable_(link){return !bhCrawlReason_(link);}

// Compact history uses exact task URLs, not a probabilistic filter.
// History is written before source rows are physically deleted.
var BH_ARCHIVE_MEM=null;
function bhArchive_(){var s=bhBook_().getSheetByName(BH_NAMES.processed);if(!s)return [];var n=s.getLastRow();if(BH_ARCHIVE_MEM&&BH_ARCHIVE_MEM.n===n)return BH_ARCHIVE_MEM.rows;var out=[];bhRows_('processed').forEach(function(r){if(!r[0])return;var split=String(r[0]).indexOf('|'),kind=String(r[0]).slice(0,split),row;try{row=JSON.parse(r[1]);}catch(e){throw Error('СЛУЖБОВІ ДАНІ: пошкоджено запис завершеного завдання. Очищення зупинено.');}if(!['queue','sitemaps'].includes(kind)||!Array.isArray(row)||row.length!==7||String(r[0])!==kind+'|'+row[0]||!['DONE','SKIP'].includes(row[3]))throw Error('СЛУЖБОВІ ДАНІ: некоректний запис завершеного завдання');out.push({key:String(r[0]),kind:kind,row:row});});BH_ARCHIVE_MEM={n:n,rows:out};return out;}
function bhSeen_(kind,rows){var seen=new Set(rows.map(r=>r[0]));bhArchive_().forEach(a=>{if(a.kind===kind)seen.add(a.row[0]);});return seen;}
function bhWithArchive_(kind,rows){var seen=new Set(rows.map(r=>r[0])),all=rows.slice();bhArchive_().forEach(a=>{if(a.kind===kind&&!seen.has(a.row[0])){all.push(a.row);seen.add(a.row[0]);}});return all;}
function bhSeedCounts_(rows,counts){bhWithArchive_('queue',rows).forEach(r=>counts[r[1]]=(counts[r[1]]||0)+1);}
function bhStorage_(){var b=bhBook_();return {cells:b.getSheets?b.getSheets().reduce((n,s)=>n+s.getMaxRows()*s.getMaxColumns(),0):0,limit:9500000,freed:Number(bhProps_().getProperty('BH_STORAGE_FREED')||0)};}
function bhTrim_(){var b=bhBook_(),freed=0;Object.keys(BH_NAMES).forEach(function(k){var s=b.getSheetByName(BH_NAMES[k]);if(!s||!s.deleteColumns||!s.deleteRows)return;var before=s.getMaxRows()*s.getMaxColumns();
 // getLastColumn/getLastRow include values and formulas, including user columns.
 // Only unused tails of app-owned sheets are removed; foreign sheets are untouched.
 var cols=Math.max(BH_HEADERS[k].length,s.getLastColumn(),(s.getFrozenColumns?s.getFrozenColumns():0)+1),rows=Math.max(2,s.getLastRow(),(s.getFrozenRows?s.getFrozenRows():1)+1);if(s.getMaxColumns()>cols)s.deleteColumns(cols+1,s.getMaxColumns()-cols);if(s.getMaxRows()>rows)s.deleteRows(rows+1,s.getMaxRows()-rows);freed+=before-s.getMaxRows()*s.getMaxColumns();});if(freed)bhProps_().setProperty('BH_STORAGE_FREED',String(Number(bhProps_().getProperty('BH_STORAGE_FREED')||0)+freed));return freed;}
function bhCompact_(scanSize,maxMove){bhTrim_();var b=bhBook_();if(!b.getSheetByName(BH_NAMES.processed))return;var history=new Set(bhArchive_().map(a=>a.key));['queue','sitemaps'].forEach(function(kind){var s=b.getSheetByName(BH_NAMES[kind]);if(!s||!s.deleteRows||s.getLastRow()<2||s.getLastColumn()>BH_HEADERS[kind].length)return;var p=bhProps_(),cursorKey=kind==='queue'?'BH_COMPACT_QUEUE':'BH_COMPACT_SITEMAPS',n=s.getLastRow()-1,cursor=Math.max(0,Number(p.getProperty(cursorKey)||0));if(cursor>=n)cursor=0;var len=Math.min(scanSize,n-cursor),rows=s.getRange(cursor+2,1,len,7).getValues(),groups=[],add=[],moved=0,examined=0;
 for(var i=0;i<rows.length;i++){var r=rows[i];examined=i+1;if(!r[0]||!['DONE','SKIP'].includes(r[3]))continue;var payload=JSON.stringify(r);if(payload.length>45000)continue;var physical=cursor+i+2,last=groups[groups.length-1];if((!last||last.start+last.count!==physical)&&groups.length>=16){examined=i;break;}if(last&&last.start+last.count===physical)last.count++;else groups.push({start:physical,count:1});var key=kind+'|'+r[0];if(!history.has(key)){history.add(key);add.push([key,payload]);}moved++;if(moved>=maxMove)break;}
 if(add.length)bhAppend_('processed',add);BH_ARCHIVE_MEM=null;
 // Delete from bottom to top only after the whole selected history is durable.
 // If a deletion fails, abort; a later invocation scans fresh physical positions.
 for(var g=groups.length-1;g>=0;g--)bhDeleteRowsSafe_(s,groups[g].start,groups[g].count);
 var next=cursor+examined-moved;p.setProperty(cursorKey,String(cursor+examined>=n?0:Math.max(0,next)));
 });bhTrim_();}

// v1.6: one active donor, durable completion marker before bounded cleanup.
function bhInitRuns_(){var existing=bhRows_('runs'),seen=new Set(existing.map(r=>r[0])),seeds=[];
 bhRows_('donors').forEach(function(r){if(!r[0])return;try{var raw=String(r[0]).trim();seeds.push(BHCore.url(/^https?:\/\//i.test(raw)?raw:'https://'+raw).href);}catch(e){throw Error('Некоректний донор: '+r[0]);}});
 ['queue','sitemaps','deferred'].forEach(k=>bhRows_(k).forEach(r=>{if(r[0]&&r[1])seeds.push(r[1]);}));bhArchive_().forEach(a=>seeds.push(a.row[1]));
 var add=[];seeds.forEach(seed=>{if(!seen.has(seed)){seen.add(seed);add.push([seed,'WAIT','',0,0,0,0]);}});bhAppend_('runs',add);
}
function bhCleaning_(){return bhOptionalRows_('runs').some(r=>r[1]==='CLEANING');}
function bhActivate_(){var p=bhProps_(),runs=bhRows_('runs'),i=runs.findIndex(r=>r[1]==='CLEANING'||r[1]==='ACTIVE');if(i<0)i=runs.findIndex(r=>r[1]==='WAIT');if(i<0){p.deleteProperty('BH_ACTIVE_SEED');return false;}var row=runs[i];p.setProperty('BH_ACTIVE_SEED',row[0]);if(row[1]==='CLEANING')return true;
 if(row[1]==='WAIT'){row[1]='ACTIVE';bhWriteRow_('runs',i,row);p.deleteProperty('BH_SAMPLES');}
 var q=bhRows_('queue');if(!bhSeen_('queue',q).has(row[0]))bhAppend_('queue',[[row[0],row[0],0,'WAIT',0,0,'Стартова сторінка']]);bhSeedSitemaps_();return true;
}
function bhCanFinish_(){var seed=bhProps_().getProperty('BH_ACTIVE_SEED');if(!seed)return false;
 var c=bhConfig_();if((c.mode!=='site'||!c.sitemaps)&&bhRows_('sitemaps').some(r=>r[1]===seed&&r[3]==='WAIT'))throw Error('Для завершення донора увімкніть режим Сайт та Sitemap: залишилися неперевірені карти.');if(!c.fullCrawl&&bhRows_('deferred').some(r=>r[0]&&r[1]===seed))throw Error('Для завершення донора увімкніть Повний обхід: залишилися відкладені URL.');
 if(['queue','sitemaps'].some(k=>bhRows_(k).some(r=>r[1]===seed&&r[3]==='WAIT')))return false;
 if(bhRows_('deferred').some(r=>r[0]&&r[1]===seed))return false;
 // Drain legacy domain work too before deleting any evidence from mixed old runs.
 return !bhRows_('domains').some(r=>r[2]==='WAIT');
}
function bhBeginCleanup_(){if(!bhCanFinish_())return false;var seed=bhProps_().getProperty('BH_ACTIVE_SEED'),runs=bhRows_('runs'),i=runs.findIndex(r=>r[0]===seed);if(i<0)throw Error('СЛУЖБОВІ ДАНІ: немає активного донора');
 bhSyncResults_(); // Fail closed: no deletion unless both protected views were written.
 var q=bhWithArchive_('queue',bhRows_('queue')).filter(r=>r[1]===seed),m=bhWithArchive_('sitemaps',bhRows_('sitemaps')).filter(r=>r[1]===seed);
 var row=[seed,'CLEANING',new Date().toISOString(),q.filter(r=>r[3]==='DONE').length,q.filter(r=>r[3]==='SKIP'||r[3]==='BLOCKED').length,q.filter(r=>r[3]==='ERROR').length,m.filter(r=>r[3]==='ERROR'||r[3]==='BLOCKED').length];
 bhProps_().setProperty('BH_CLEAN_STAGE','0');bhWriteRow_('runs',i,row);return true;
}
// Deletes only matching physical rows, bottom-up. Retry always reads fresh positions.
function bhDeleteMatching_(key,predicate){var s=bhSheet_(key),rows=bhRows_(key),groups=[];for(var i=rows.length-1;i>=0;i--){if(!predicate(rows[i]))continue;var row=i+2,g=groups[groups.length-1];if(g&&row+1===g.start&&g.count<5000){g.start=row;g.count++;}else {if(groups.length>=20)break;groups.push({start:row,count:1});}}
 for(var g of groups){bhDeleteRowsSafe_(s,g.start,g.count);}if(key==='processed')BH_ARCHIVE_MEM=null;return groups.length===0;
}
function bhCleanupStep_(){var p=bhProps_(),runs=bhRows_('runs'),i=runs.findIndex(r=>r[1]==='CLEANING');if(i<0)return;var seed=runs[i][0];p.setProperty('BH_ACTIVE_SEED',seed);var stage=Number(p.getProperty('BH_CLEAN_STAGE')||0),keys=['queue','deferred','sitemaps','processed','links','domains','watch','log'];
 if(stage<keys.length){var key=keys[stage],predicate;
 if(['queue','deferred','sitemaps'].includes(key))predicate=r=>r[1]===seed;
 else if(key==='processed')predicate=r=>{if(!r[0])return false;return JSON.parse(r[1])[1]===seed;};
 else if(key==='links'){var host=BHCore.url(seed).domain,shared=runs.some(r=>r[0]!==seed&&r[1]!=='DONE'&&BHCore.url(r[0]).domain===host);predicate=r=>{if(shared)return false;try{return BHCore.url(r[2]).domain===host;}catch(e){return false;}};}
 else if(key==='domains'){var referenced=new Set(bhRows_('links').map(r=>r[1]));predicate=r=>r[2]!=='WAIT'&&!referenced.has(r[0]);}
 else if(key==='watch'){var retained=new Set(bhRows_('domains').map(r=>r[0]));predicate=r=>!retained.has(r[0]);}
 else predicate=r=>true;
 if(bhDeleteMatching_(key,predicate))p.setProperty('BH_CLEAN_STAGE',String(stage+1));bhTrim_();return;
 }
 // DONE is written last. A crash during cleanup cannot start the next donor early.
 runs[i][1]='DONE';bhWriteRow_('runs',i,runs[i]);p.deleteProperty('BH_CLEAN_STAGE');p.deleteProperty('BH_ACTIVE_SEED');['BH_PROMOTE_CURSOR','BH_COMPACT_QUEUE','BH_COMPACT_SITEMAPS','BH_SAMPLES'].forEach(k=>p.deleteProperty(k));BH_ARCHIVE_MEM=null;
 p.setProperty('BH_MESSAGE','Робочі дані донора прибрано. Кандидати й ручна перевірка збережені. Наступний донор почнеться автоматично.');
}
function bhDonorStatus_(){var rows=bhOptionalRows_('runs'),active=rows.find(r=>r[1]==='ACTIVE'||r[1]==='CLEANING');return {total:rows.length,done:rows.filter(r=>r[1]==='DONE').length,left:rows.filter(r=>r[1]!=='DONE').length,active:active?active[0]:'',cleaning:!!active&&active[1]==='CLEANING',withIssues:rows.filter(r=>['DONE','CLEANING'].includes(r[1])&&(Number(r[4])||Number(r[5])||Number(r[6]))).length};}

// Google Sheets must retain at least one unfrozen row. Insert a blank tail
// BEFORE deletion; do not clear/rewrite source data or unfreeze headers.
// If the deletion fails, retry reads fresh positions; the blank row is harmless.
function bhDeleteRowsSafe_(sheet,start,count){
 if(!Number.isInteger(start)||!Number.isInteger(count)||start<2||count<1||start+count-1>sheet.getMaxRows())throw Error('СЛУЖБОВІ ДАНІ: некоректний діапазон очищення');
 var frozen=sheet.getFrozenRows?sheet.getFrozenRows():1;
 var reserve=Math.max(2,frozen+1),missing=reserve-(sheet.getMaxRows()-count);
 if(missing>0)sheet.insertRowsAfter(sheet.getMaxRows(),missing);
 sheet.deleteRows(start,count);
}
