// Global State
let isRunning = false;
let abortController = null;
let scrapedAdvertisers = [];
let activeDomainScanId = null;
let isDomainScanPaused = false;
let sheetExportRecords = [];
let authenticatedUser = null;
let userExclusions = { enabled: false, domains: [], advertisers: [] };
let exclusionSessionStats = { domains: new Set(), advertisers: new Set() };
function exclusionDomainSet() { return new Set((userExclusions.domains || []).map(normalizeProjectDomain).filter(Boolean)); }
function exclusionAdvertiserSet() { return new Set((userExclusions.advertisers || []).map(item => String(item.id || '').trim()).filter(Boolean)); }
function renderExclusionFilterNotice() {
    const notice = document.getElementById('exclusionFilterNotice');
    if (!notice) return;
    notice.hidden = !userExclusions.enabled;
    const configured = document.getElementById('exclusionConfiguredSummary');
    if (configured) configured.textContent = `${(userExclusions.domains || []).length.toLocaleString('vi-VN')} domain · ${(userExclusions.advertisers || []).length.toLocaleString('vi-VN')} nhà quảng cáo đã cấu hình`;
    const domains = document.getElementById('excludedDomainCount');
    const advertisers = document.getElementById('excludedAdvertiserCount');
    if (domains) domains.textContent = exclusionSessionStats.domains.size.toLocaleString('vi-VN');
    if (advertisers) advertisers.textContent = exclusionSessionStats.advertisers.size.toLocaleString('vi-VN');
    notice.classList.toggle('has-filtered-results', exclusionSessionStats.domains.size + exclusionSessionStats.advertisers.size > 0);
}
function resetExclusionSessionStats() {
    exclusionSessionStats = { domains: new Set(), advertisers: new Set() };
    renderExclusionFilterNotice();
}
function isExcludedDomain(value) {
    const normalized = normalizeProjectDomain(value);
    const excluded = userExclusions.enabled && Boolean(normalized) && exclusionDomainSet().has(normalized);
    if (excluded && !exclusionSessionStats.domains.has(normalized)) { exclusionSessionStats.domains.add(normalized); renderExclusionFilterNotice(); }
    return excluded;
}
function isExcludedAdvertiser(value) {
    const normalized = String(value || '').trim();
    const excluded = userExclusions.enabled && Boolean(normalized) && exclusionAdvertiserSet().has(normalized);
    if (excluded && !exclusionSessionStats.advertisers.has(normalized)) { exclusionSessionStats.advertisers.add(normalized); renderExclusionFilterNotice(); }
    return excluded;
}
let presenceTimer = null;
let workspaceDbPromise = null, workspaceSaveTimer = null, workspaceAutosaveTimer = null, workspaceRestored = false;

async function sendPresenceHeartbeat() {
    if (!authenticatedUser || document.visibilityState === 'hidden') return;
    try {
        await fetch('/api/presence', {
            method: 'POST',
            headers: { 'content-type': 'application/json' },
            body: JSON.stringify(currentPresencePayload()),
            keepalive: true
        });
    } catch (error) {}
}

function currentPresencePayload() {
    const activeTab = document.querySelector('.nav-tab.active, .tab-btn.active');
    return {
        page: location.pathname,
        section: activeTab?.textContent?.replace(/\s+/g, ' ').trim() || document.title || 'Ứng dụng'
    };
}

function startPresenceHeartbeat() {
    clearInterval(presenceTimer);
    sendPresenceHeartbeat();
    presenceTimer = setInterval(sendPresenceHeartbeat, 30000);
}

document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'visible') sendPresenceHeartbeat();
});

window.addEventListener('pagehide', () => {
    workspaceWrite().catch(() => {});
    if (!authenticatedUser || !navigator.sendBeacon) return;
    navigator.sendBeacon('/api/presence', new Blob([JSON.stringify(currentPresencePayload())], { type: 'application/json' }));
});

// Multi-level domain trace state
const projectTrafficProviderSelect = document.getElementById('projectTrafficProvider');
if (projectTrafficProviderSelect && !projectTrafficProviderSelect.querySelector('option[value="sitetraffic_api"]')) {
    projectTrafficProviderSelect.insertAdjacentHTML('beforeend', '<option value="sitetraffic_api">SiteTraffic API (GoAnyAPI)</option>');
}
if (projectTrafficProviderSelect && !projectTrafficProviderSelect.querySelector('option[value="workfast_sitetraffic"]')) {
    projectTrafficProviderSelect.insertAdjacentHTML('beforeend', '<option value="workfast_sitetraffic">Workfast trước → SiteTraffic khi hết lượt</option>');
}
const bulkTrafficProviderSelect = document.getElementById('bulkTrafficProvider');
if (bulkTrafficProviderSelect) bulkTrafficProviderSelect.value = '';

const traceEls = {
    root: document.getElementById('traceRootDomain'), level: document.getElementById('traceMaxLevel'), unlimited: document.getElementById('traceUnlimited'), domainLimit: document.getElementById('traceDomainLimit'),
    advertiserLimit: document.getElementById('traceAdvertiserLimit'), ownerThreshold: document.getElementById('traceOwnerThreshold'), filterGoogleSheet: document.getElementById('traceFilterGoogleSheet'), googleSheetStatus: document.getElementById('traceGoogleSheetStatus'), start: document.getElementById('btnTraceStart'),
    pause: document.getElementById('btnTracePause'), stop: document.getElementById('btnTraceStop'), retry: document.getElementById('btnTraceRetry'), reset: document.getElementById('btnTraceReset'), traffic: document.getElementById('btnTraceTraffic'), copy: document.getElementById('btnTraceCopy'), export: document.getElementById('btnTraceExport'),
    status: document.getElementById('traceStatus'), running: document.getElementById('traceRunningCount'), domains: document.getElementById('traceDomainCount'), advertisers: document.getElementById('traceAdvertiserCount'), advertiserScanned: document.getElementById('traceAdvertiserScannedCount'), advertiserRunning: document.getElementById('traceAdvertiserRunningCount'),
    errors: document.getElementById('traceErrorCount'), progressText: document.getElementById('traceProgressText'), progressPercent: document.getElementById('traceProgressPercent'), progressBar: document.getElementById('traceProgressBar'),
    levels: document.getElementById('traceLevelSummary'), tbody: document.getElementById('traceTableBody'), summary: document.getElementById('traceResultSummary'), ownerSkipped: document.getElementById('traceOwnerSkipped'), ownerSkippedCount: document.getElementById('traceOwnerSkippedCount'), ownerSkippedList: document.getElementById('traceOwnerSkippedList'), registeredDuplicates: document.getElementById('traceRegisteredDuplicates'), registeredDuplicateCount: document.getElementById('traceRegisteredDuplicateCount'), registeredDuplicateList: document.getElementById('traceRegisteredDuplicateList'), activityState: document.getElementById('traceActivityState'), activityLog: document.getElementById('traceActivityLog'), errorDetails: document.getElementById('traceErrorDetails'), errorList: document.getElementById('traceErrorList'), pageSize: document.getElementById('tracePageSize'), pageSummary: document.getElementById('tracePageSummary'), pageIndicator: document.getElementById('tracePageIndicator'), pagePrev: document.getElementById('tracePagePrev'), pageNext: document.getElementById('tracePageNext'), deploymentFilter: document.getElementById('traceDeploymentFilter'), runningDaysFilter: document.getElementById('traceRunningDaysFilter'), minVolumeFilter: document.getElementById('traceMinVolumeFilter'), minTrafficFilter: document.getElementById('traceMinTrafficFilter'), nicheFilter: document.getElementById('traceNicheFilter'), clearFilters: document.getElementById('btnClearTraceFilters')
};
let traceState = { running: false, paused: false, stopped: false, results: new Map(), advertisers: new Set(), scannedAdvertisers: new Set(), activeAdvertisers: new Set(), seenAdvertisers: new Set(), visited: new Set(), googleSheetDomains: new Set(), googleSheetRegistrations: new Map(), registeredDuplicates: new Map(), skippedOwners: [], activities: [], errors: [], failedJobs: [], activeCount: 0, controller: null, maxLevel: 1, currentLevel: 0 };
let tracePage = 1, traceRenderTimer = null, traceActivityRenderTimer = null, traceResultsVersion = 0, traceRowsCacheVersion = -1, traceRowsCache = [];
const traceInsightCache = new Map();

const traceEscape = value => String(value ?? '').replace(/[&<>"']/g, char => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[char]));

function showAppConfirm(message, options = {}) {
    return new Promise(resolve => {
        const overlay = document.createElement('div');
        overlay.className = 'app-confirm-overlay';
        overlay.innerHTML = `<section class="app-confirm-dialog" role="alertdialog" aria-modal="true" aria-labelledby="appConfirmTitle"><div class="app-confirm-icon"><i class="fa-solid fa-circle-exclamation"></i></div><div><h3 id="appConfirmTitle">${traceEscape(options.title || 'Xác nhận thao tác')}</h3><p>${traceEscape(message).replace(/\n/g, '<br>')}</p></div><div class="app-confirm-actions"><button type="button" class="btn btn-secondary" data-confirm-cancel>Hủy</button><button type="button" class="btn ${options.danger ? 'btn-danger' : 'btn-primary'}" data-confirm-ok>${traceEscape(options.confirmText || 'Xác nhận')}</button></div></section>`;
        const finish = value => { document.removeEventListener('keydown', onKey); overlay.remove(); resolve(value); };
        const onKey = event => { if (event.key === 'Escape') finish(false); };
        overlay.addEventListener('click', event => { if (event.target === overlay || event.target.closest('[data-confirm-cancel]')) finish(false); else if (event.target.closest('[data-confirm-ok]')) finish(true); });
        document.addEventListener('keydown', onKey);
        document.body.appendChild(overlay);
        requestAnimationFrame(() => { overlay.classList.add('visible'); overlay.querySelector('[data-confirm-ok]')?.focus(); });
    });
}

async function traceReadNdjson(url, body, signal, onChunk) {
    const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal });
    if (!response.ok) throw new Error((await response.text().catch(() => '')) || `HTTP ${response.status}`);
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';
    while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        buffer += decoder.decode(value, { stream: true });
        const lines = buffer.split('\n'); buffer = lines.pop() || '';
        for (const line of lines) {
            if (!line.trim()) continue;
            const chunk = JSON.parse(line);
            if (chunk.type === 'error') throw new Error(chunk.error || 'API trả lỗi không xác định.');
            await onChunk(chunk);
        }
    }
    if (buffer.trim()) {
        const chunk = JSON.parse(buffer);
        if (chunk.type === 'error') throw new Error(chunk.error || 'API trả lỗi không xác định.');
        await onChunk(chunk);
    }
}

async function tracePool(items, concurrency, worker) {
    let cursor = 0;
    const runners = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
        while (cursor < items.length && !traceState.stopped) {
            await traceWaitWhilePaused();
            if (traceState.stopped) break;
            const index = cursor++;
            await worker(items[index], index);
        }
    });
    await Promise.all(runners);
}

async function traceWaitWhilePaused() {
    while (traceState.paused && !traceState.stopped) await new Promise(resolve => setTimeout(resolve, 200));
}

function traceLog(message, type = '') {
    traceState.activities.push({ time: new Date().toISOString(), message, type });
    if (traceState.activities.length > 200) traceState.activities.splice(0, traceState.activities.length - 200);
    if (!traceActivityRenderTimer) traceActivityRenderTimer = setTimeout(() => { traceActivityRenderTimer = null; traceRenderActivity(); }, 250);
}

function traceRenderActivity() {
    if (!traceEls.activityLog) return;
    const rows = traceState.activities.slice(-100);
    traceEls.activityLog.innerHTML = rows.length ? rows.map(item => `<div class="trace-activity-${traceEscape(item.type)}">[${new Date(item.time).toLocaleTimeString('vi-VN')}] ${traceEscape(item.message)}</div>`).join('') : '<div>Chưa có hoạt động.</div>';
    traceEls.activityLog.scrollTop = traceEls.activityLog.scrollHeight;
    if (traceEls.activityState) traceEls.activityState.textContent = traceState.paused ? 'Đang tạm dừng' : traceState.running ? `${traceState.activeCount} tác vụ đang chạy` : traceState.stopped ? 'Đã dừng' : 'Đang chờ';
}

function traceAddResult(domain, level, parent, advertiserId, source, sampleUrl, ads = []) {
    const clean = normalizeProjectDomain(domain);
    if (!clean || isExcludedDomain(clean) || isExcludedAdvertiser(advertiserId) || clean === normalizeProjectDomain(traceEls.root?.value)) return false;
    let item = traceState.results.get(clean);
    const isNew = !item;
    if (!item) item = { domain: clean, level, parents: new Set(), advertisers: new Set(), sources: new Set(), ads: [], sampleUrl: '', status: 'success' };
    item.level = Math.min(Number(item.level || level), level);
    if (parent) item.parents.add(parent);
    if (advertiserId) item.advertisers.add(advertiserId);
    if (source) item.sources.add(source);
    if (!item.sampleUrl && sampleUrl) item.sampleUrl = sampleUrl;
    if (ads.length) item.ads = [...new Map([...(item.ads || []), ...ads].map(ad => [ad.creativeId || `${ad.firstSeen || ''}:${ad.lastSeen || ''}:${ad.imageUrl || ''}`, ad])).values()];
    traceState.results.set(clean, item);
    traceResultsVersion++;
    return isNew;
}

function traceSerializable() {
    return {
        root: traceEls.root?.value || '', maxLevel: traceState.maxLevel, currentLevel: traceState.currentLevel, startedAt: traceState.startedAt || '', sessionId: traceState.sessionId || '',
        advertisers: [...traceState.advertisers], scannedAdvertisers: [...traceState.scannedAdvertisers], seenAdvertisers: [...traceState.seenAdvertisers], visited: [...traceState.visited], skippedOwners: traceState.skippedOwners.slice(-500), activities: traceState.activities.slice(-200), errors: traceState.errors.slice(-100), failedJobs: traceState.failedJobs,
        registeredDuplicates: [...traceState.registeredDuplicates.values()],
        results: [...traceState.results.values()].map(item => ({ ...item, parents: [...item.parents], advertisers: [...item.advertisers], sources: [...item.sources] }))
    };
}

const TRACE_DB_NAME = 'check_gg_ads_trace_v1', TRACE_DB_STORE = 'checkpoints';
let traceDbPromise = null, traceSaveTimer = null;
function traceOpenDb() {
    if (traceDbPromise) return traceDbPromise;
    traceDbPromise = new Promise((resolve, reject) => { const request = indexedDB.open(TRACE_DB_NAME, 1); request.onupgradeneeded = () => { if (!request.result.objectStoreNames.contains(TRACE_DB_STORE)) request.result.createObjectStore(TRACE_DB_STORE, { keyPath: 'id' }); }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); });
    return traceDbPromise;
}
function traceCheckpoint() { return { root: traceEls.root?.value || '', maxLevel: traceState.maxLevel, currentLevel: traceState.currentLevel, startedAt: traceState.startedAt || '', sessionId: traceState.sessionId || '', advertisers: traceState.advertisers, scannedAdvertisers: traceState.scannedAdvertisers, seenAdvertisers: traceState.seenAdvertisers, visited: traceState.visited, skippedOwners: traceState.skippedOwners.slice(-500), registeredDuplicates: traceState.registeredDuplicates, activities: traceState.activities.slice(-200), errors: traceState.errors.slice(-100), failedJobs: traceState.failedJobs, results: traceState.results }; }
async function traceWriteCheckpoint() { try { const db = await traceOpenDb(); const record = { id: 'latest', state: traceCheckpoint(), updatedAt: new Date().toISOString() }; await new Promise((resolve, reject) => { const request = db.transaction(TRACE_DB_STORE, 'readwrite').objectStore(TRACE_DB_STORE).put(record); request.onsuccess = resolve; request.onerror = () => reject(request.error); }); localStorage.removeItem('domain_trace_state_v1'); } catch (error) { console.warn('Không lưu được checkpoint truy vết vào IndexedDB:', error); } }
function saveTraceState() { if (traceSaveTimer) clearTimeout(traceSaveTimer); traceSaveTimer = setTimeout(() => { traceSaveTimer = null; traceWriteCheckpoint(); }, 800); }
async function traceClearCheckpoint() { if (traceSaveTimer) { clearTimeout(traceSaveTimer); traceSaveTimer = null; } try { const db = await traceOpenDb(); await new Promise((resolve, reject) => { const request = db.transaction(TRACE_DB_STORE, 'readwrite').objectStore(TRACE_DB_STORE).clear(); request.onsuccess = resolve; request.onerror = () => reject(request.error); }); } catch (_) {} localStorage.removeItem('domain_trace_state_v1'); }

function traceRender(immediate = false) {
    if (immediate) { if (traceRenderTimer) clearTimeout(traceRenderTimer); traceRenderTimer = null; return traceRenderNow(); }
    if (!traceRenderTimer) traceRenderTimer = setTimeout(() => { traceRenderTimer = null; traceRenderNow(); }, traceState.results.size > 5000 ? 1000 : 250);
}

function traceRowInsight(item) {
    const cached = traceInsightCache.get(normalizeProjectDomain(item.domain)) || {};
    const period = calculateActivePeriod(item.ads || cached.ads || []);
    return {
        period,
        active: period.lastSeen ? isDomainCurrentlyActive(period) : null,
        volume: cached.volume ?? item.metrics?.volume ?? null,
        traffic: cached.traffic ?? item.traffic?.visits ?? null,
        niche: cached.niche || item.niche || ''
    };
}

function traceRenderNow() {
    if (!traceEls.tbody) return;
    if (traceRowsCacheVersion !== traceResultsVersion) { traceRowsCache = [...traceState.results.values()].sort((a, b) => a.level - b.level || a.domain.localeCompare(b.domain)); traceRowsCacheVersion = traceResultsVersion; }
    const deployment = traceEls.deploymentFilter?.value || '';
    const minDays = Number(traceEls.runningDaysFilter?.value || 0);
    const minVolume = traceEls.minVolumeFilter?.value === '' ? null : Number(traceEls.minVolumeFilter.value);
    const minTraffic = traceEls.minTrafficFilter?.value === '' ? null : Number(traceEls.minTrafficFilter.value);
    const niche = traceEls.nicheFilter?.value || '';
    const rows = traceRowsCache.filter(item => {
        const insight = traceRowInsight(item);
        if (deployment === 'active' && insight.active !== true) return false;
        if (minDays && insight.period.days < minDays) return false;
        if (minVolume !== null && (!Number.isFinite(Number(insight.volume)) || Number(insight.volume) < minVolume)) return false;
        if (minTraffic !== null && (!Number.isFinite(Number(insight.traffic)) || Number(insight.traffic) < minTraffic)) return false;
        if (niche && insight.niche !== niche) return false;
        return true;
    });
    const pageSize = Math.max(50, Number(traceEls.pageSize?.value) || 100), pageCount = Math.max(1, Math.ceil(rows.length / pageSize)); tracePage = Math.min(tracePage, pageCount); const start = (tracePage - 1) * pageSize; const visibleRows = rows.slice(start, start + pageSize);
    traceEls.tbody.innerHTML = visibleRows.length ? visibleRows.map((item, index) => { const insight = traceRowInsight(item); const metric = value => value === null || value === undefined ? '<span class="keyword-metric-empty">Chưa quét</span>' : Number(value).toLocaleString('vi-VN'); return `<tr><td>${start + index + 1}</td><td><strong>${traceEscape(item.domain)}</strong></td><td>${insight.active === true ? '<span class="trace-status-pill success">Đang triển khai</span>' : insight.active === false ? '<span class="trace-status-pill">Đã dừng</span>' : '<span class="keyword-metric-empty">Chưa có dữ liệu</span>'}</td><td>${insight.period.days ? `<strong>${insight.period.days.toLocaleString('vi-VN')} ngày</strong><small class="metric-vnd">${traceEscape(insight.period.firstSeen)} → ${traceEscape(insight.period.lastSeen)}</small>` : '<span class="keyword-metric-empty">Chưa có dữ liệu</span>'}</td><td>${metric(insight.volume)}</td><td>${metric(insight.traffic)}</td><td>${insight.niche ? `<span class="badge">${traceEscape(insight.niche)}</span>` : '<span class="keyword-metric-empty">Chưa phân loại</span>'}</td><td><span class="trace-level active">Cấp ${item.level}</span></td><td>${traceEscape([...item.parents].join(', ') || '—')}</td><td title="${traceEscape([...item.advertisers].join(', '))}">${traceEscape([...item.advertisers].slice(0, 3).join(', ') || '—')}${item.advertisers.size > 3 ? ` +${item.advertisers.size - 3}` : ''}</td><td>${traceEscape([...item.sources].join(', '))}</td><td>${item.sampleUrl ? `<a class="trace-source-link" href="${traceEscape(item.sampleUrl)}" target="_blank" rel="noopener">Xem mẫu nguồn</a>` : '—'}</td></tr>`; }).join('') : `<tr class="empty-row"><td colspan="12" class="text-center">${traceRowsCache.length ? 'Không có domain phù hợp bộ lọc.' : 'Chưa có domain nào được phát hiện.'}</td></tr>`;
    traceEls.domains.textContent = rows.length.toLocaleString('vi-VN');
    traceEls.advertisers.textContent = traceState.advertisers.size.toLocaleString('vi-VN');
    if (traceEls.advertiserScanned) traceEls.advertiserScanned.textContent = traceState.scannedAdvertisers.size.toLocaleString('vi-VN');
    if (traceEls.advertiserRunning) traceEls.advertiserRunning.textContent = traceState.activeAdvertisers.size.toLocaleString('vi-VN');
    traceEls.errors.textContent = traceState.errors.length.toLocaleString('vi-VN');
    traceEls.running.textContent = traceState.activeCount.toLocaleString('vi-VN');
    traceEls.summary.textContent = `${rows.length.toLocaleString('vi-VN')} / ${traceRowsCache.length.toLocaleString('vi-VN')} domain mới phù hợp bộ lọc · ${traceState.registeredDuplicates.size.toLocaleString('vi-VN')} domain trùng người đăng ký · ${traceState.advertisers.size.toLocaleString('vi-VN')} Nhà QC đã phát hiện · ${traceState.scannedAdvertisers.size.toLocaleString('vi-VN')} Nhà QC đã quét.`;
    if (traceEls.ownerSkipped) {
        traceEls.ownerSkipped.hidden = !traceState.skippedOwners.length;
        if (traceEls.ownerSkippedCount) traceEls.ownerSkippedCount.textContent = traceState.skippedOwners.length.toLocaleString('vi-VN');
        if (traceEls.ownerSkippedList) traceEls.ownerSkippedList.innerHTML = traceState.skippedOwners.slice(-200).map(item => `<div><a class="trace-advertiser-link" href="https://adstransparency.google.com/advertiser/${encodeURIComponent(item.id)}?authuser=0&region=anywhere" target="_blank" rel="noopener noreferrer"><strong>${traceEscape(item.id)}</strong> <i class="fa-solid fa-arrow-up-right-from-square"></i></a> · ${Number(item.creativeCount).toLocaleString('vi-VN')} mẫu trên <strong>${traceEscape(item.domain)}</strong> · ngưỡng ${Number(item.threshold).toLocaleString('vi-VN')} · cấp ${item.level}</div>`).join('');
    }
    if (traceEls.registeredDuplicates) {
        const duplicates = [...traceState.registeredDuplicates.values()];
        traceEls.registeredDuplicates.hidden = !duplicates.length;
        if (traceEls.registeredDuplicateCount) traceEls.registeredDuplicateCount.textContent = duplicates.length.toLocaleString('vi-VN');
        if (traceEls.registeredDuplicateList) traceEls.registeredDuplicateList.innerHTML = duplicates.slice(-500).map(item => `<div><strong>${traceEscape(item.domain)}</strong> · <span class="trace-status-pill success"><i class="fa-solid fa-user-check"></i> ${traceEscape(item.registrant || 'Đã có người đăng ký')}</span> · cấp ${Number(item.level || 0)}${item.parentDomain ? ` · từ ${traceEscape(item.parentDomain)}` : ''}</div>`).join('');
    }
    traceEls.copy.disabled = !rows.length; traceEls.export.disabled = !rows.length; if (traceEls.traffic) traceEls.traffic.disabled = !rows.length;
    traceEls.retry.hidden = !traceState.failedJobs.length;
    traceEls.errorDetails.hidden = !traceState.errors.length;
    traceEls.errorList.innerHTML = traceState.errors.slice(-100).reverse().map(error => `<div><strong>${traceEscape(error.stage)}</strong> · ${traceEscape(error.target)} · Cấp ${error.level}: ${traceEscape(error.message)}</div>`).join('');
    if (traceEls.pageSummary) traceEls.pageSummary.textContent = rows.length ? `${start + 1}–${Math.min(start + pageSize, rows.length)} / ${rows.length.toLocaleString('vi-VN')} kết quả` : '0 kết quả';
    if (traceEls.pageIndicator) traceEls.pageIndicator.textContent = `Trang ${tracePage} / ${pageCount}`;
    if (traceEls.pagePrev) traceEls.pagePrev.disabled = tracePage <= 1; if (traceEls.pageNext) traceEls.pageNext.disabled = tracePage >= pageCount;
    traceRenderActivity();
}

function traceRenderLevels() {
    if (!traceEls.levels) return;
    traceEls.levels.innerHTML = '<span class="trace-level done">Domain gốc</span>' + Array.from({ length: traceState.maxLevel }, (_, index) => `<span class="trace-level ${index + 1 < traceState.currentLevel ? 'done' : index + 1 === traceState.currentLevel ? 'active' : ''}">Cấp ${index + 1}</span>`).join('');
}

function traceProgress(message, percent) {
    const safePercent = Math.max(0, Math.min(100, Math.round(percent || 0)));
    traceEls.progressText.textContent = message; traceEls.progressPercent.textContent = `${safePercent}%`; traceEls.progressBar.style.width = `${safePercent}%`;
    traceEls.status.textContent = traceState.paused ? 'Đang tạm dừng' : traceState.running ? `Đang chạy cấp ${traceState.currentLevel}` : traceState.stopped ? 'Đã dừng' : safePercent === 100 ? 'Hoàn tất' : 'Sẵn sàng';
}

async function traceScanAdvertiser(advertiserId, parentDomain, level, config, newDomains) {
    await traceWaitWhilePaused();
    traceLog(`Đang lấy mẫu của Nhà QC ${advertiserId} trên ${parentDomain}.`);
    const creatives = [];
    await traceReadNdjson('/api/advertiser-creatives', { advId: advertiserId, fReq: {}, traceMode: true }, traceState.controller.signal, async chunk => {
        if (chunk.type === 'creatives') creatives.push(...(chunk.creatives || []), ...(chunk.filteredCreatives || []));
    });
    const uniqueCreatives = [...new Map(creatives.map(item => [item.creativeId || `${item.domain}-${item.imageUrl}-${item.previewUrl}`, item])).values()];
    uniqueCreatives.forEach(creative => {
        if (creative.domain) traceCollectCandidate(newDomains, creative.domain, advertiserId, 'Google API', creative.imageUrl || creative.previewUrl, creative);
    });
    // Trace only domains recognized from actual creative images. Text-ad preview
    // parsing is deliberately excluded because it produces the "Phân tích text" source.
    const ocrCandidates = uniqueCreatives.filter(item => item.imageUrl);
    traceLog(`Nhà QC ${advertiserId}: ${uniqueCreatives.length} mẫu, ${ocrCandidates.length} ảnh cần OCR.`);
    await tracePool(ocrCandidates, 4, async creative => {
        if (traceState.stopped) return;
        try {
            const target = creative.imageUrl;
            const result = await browserOcrResource(target, false, true);
            (result.domains || extractDomainsInBrowser(result.text || '')).forEach(domain => {
                traceCollectCandidate(newDomains, domain, advertiserId, 'OCR ảnh', target, creative);
            });
        } catch (error) {
            traceState.errors.push({ stage: 'OCR', target: creative.creativeId || advertiserId, level, message: error.message });
            traceLog(`OCR lỗi ở mẫu ${creative.creativeId || advertiserId}: ${error.message}`, 'error');
        }
    });
}

function traceCollectCandidate(collection, domain, advertiserId, source, sampleUrl, creative = null) {
    const clean = normalizeProjectDomain(domain);
    if (!clean || isExcludedDomain(clean) || isExcludedAdvertiser(advertiserId) || clean === normalizeProjectDomain(traceEls.root?.value)) return;
    const item = collection.get(clean) || { domain: clean, advertisers: new Set(), sources: new Set(), ads: [], sampleUrl: '' };
    if (advertiserId) item.advertisers.add(advertiserId);
    if (source) item.sources.add(source);
    if (!item.sampleUrl && sampleUrl) item.sampleUrl = sampleUrl;
    if (creative) item.ads = [...new Map([...(item.ads || []), creative].map(ad => [ad.creativeId || `${ad.firstSeen || ''}:${ad.lastSeen || ''}:${ad.imageUrl || ''}`, ad])).values()];
    collection.set(clean, item);
}

async function resolveTraceDomainCandidates(collection, level, parentDomain) {
    if (!collection.size) return new Set();
    const response = await fetch('/api/trace/domains/resolve', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ domains: [...collection.keys()], scanId: traceState.sessionId || '', rootDomain: normalizeProjectDomain(traceEls.root?.value), traceMode: true }), signal: traceState.controller.signal });
    const result = await response.json().catch(() => ({}));
    if (!response.ok) throw new Error(result.error || `Không thể kiểm tra kho domain: HTTP ${response.status}`);
    const added = new Set(result.added || []);
    let sheetDuplicates = 0;
    collection.forEach((item, domain) => {
        if (!added.has(domain)) return;
        const domainIdentity = projectDomainIdentity(domain);
        if (traceState.googleSheetDomains?.has(domainIdentity)) {
            sheetDuplicates++;
            const registrant = traceState.googleSheetRegistrations?.get(domainIdentity) || 'Đã có người đăng ký';
            if (!traceState.registeredDuplicates.has(domainIdentity)) traceState.registeredDuplicates.set(domainIdentity, { domain, registrant, level, parentDomain });
            return;
        }
        const advertisers = item.advertisers.size ? [...item.advertisers] : [''];
        const sources = item.sources.size ? [...item.sources] : [''];
        advertisers.forEach(advertiserId => sources.forEach(source => traceAddResult(domain, level, parentDomain, advertiserId, source, item.sampleUrl, item.ads || [])));
    });
    const allCandidates = new Set(collection.keys());
    traceLog(`Đối chiếu: ${added.size - sheetDuplicates} domain mới được hiển thị, ${Number(result.existing?.length || 0)} trùng Kho Domain, ${sheetDuplicates} trùng Google Sheet; tất cả vẫn tiếp tục được truy vết ở cấp sau.`);
    // Kho Domain chỉ lọc danh sách kết quả hiển thị. Mọi domain phát hiện được,
    // kể cả domain đã có trong kho, vẫn đi tiếp qua các cấp; traceState.visited
    // đảm nhiệm việc chống gọi API trùng trong chính phiên hiện tại.
    return allCandidates;
}

async function traceProcessDomain(job, config) {
    const domain = normalizeProjectDomain(job.domain);
    if (!domain || traceState.visited.has(domain) || traceState.stopped) return new Set();
    traceState.visited.add(domain); traceState.activeCount++; traceRender();
    traceLog(`Cấp ${job.level}: bắt đầu quét domain ${domain}.`);
    const discoveredDomains = new Map();
    let newDomains = new Set();
    try {
        let advertisers = [];
        await traceReadNdjson('/api/extract', { domain, fReq: {}, traceMode: true, limit: config.unlimited ? 1000000 : 1000, scanId: `trace-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` }, traceState.controller.signal, async chunk => {
            if (chunk.type === 'done') advertisers = Array.isArray(chunk.advertisers) ? chunk.advertisers.filter(id => !isExcludedAdvertiser(id)) : [];
            else if (chunk.type === 'checkpoint' && !advertisers.length) advertisers = Array.isArray(chunk.advertisers) ? chunk.advertisers.filter(id => !isExcludedAdvertiser(id)) : [];
        });
        const eligibleAdvertisers = [];
        advertisers.forEach(item => {
            const id = String(item.id || '').trim();
            if (!id) return;
            if (traceState.seenAdvertisers.has(id)) {
                traceLog(`Bỏ qua Nhà QC ${id} vì đã được quét trước đó trong phiên này.`, 'skip');
                return;
            }
            traceState.seenAdvertisers.add(id);
            const creativeCount = Math.max(0, Number(item.creativeCount) || 0);
            if (creativeCount >= config.ownerThreshold) {
                if (!traceState.skippedOwners.some(skipped => skipped.id === id && skipped.domain === domain)) {
                    traceState.skippedOwners.push({ id, domain, creativeCount, threshold: config.ownerThreshold, level: job.level });
                    traceLog(`Bỏ qua Nhà QC chính chủ ${id}: ${creativeCount} mẫu trên ${domain} (ngưỡng ${config.ownerThreshold}).`, 'skip');
                }
                return;
            }
            eligibleAdvertisers.push(id);
        });
        const ids = [...new Set(eligibleAdvertisers)].slice(0, config.advertiserLimit);
        ids.forEach(id => traceState.advertisers.add(id));
        await tracePool(ids, 3, async advertiserId => {
            traceState.activeAdvertisers.add(advertiserId); traceRender();
            try { await traceScanAdvertiser(advertiserId, domain, job.level, config, discoveredDomains); }
            catch (error) { traceState.errors.push({ stage: 'Nhà quảng cáo', target: advertiserId, level: job.level, message: error.message }); }
            finally { traceState.activeAdvertisers.delete(advertiserId); traceState.scannedAdvertisers.add(advertiserId); traceRender(); }
        });
        newDomains = await resolveTraceDomainCandidates(discoveredDomains, job.level, domain);
        traceState.failedJobs = traceState.failedJobs.filter(item => !(item.domain === domain && item.level === job.level));
        traceLog(`Hoàn thành domain ${domain}; tìm thêm ${newDomains.size} domain.`, 'success');
    } catch (error) {
        if (error.name !== 'AbortError') {
            traceState.errors.push({ stage: 'Domain', target: domain, level: job.level, message: error.message });
            traceLog(`Domain ${domain} bị lỗi: ${error.message}`, 'error');
            if (!traceState.failedJobs.some(item => item.domain === domain && item.level === job.level)) traceState.failedJobs.push({ domain, parent: job.parent || '', level: job.level });
        }
    } finally {
        traceState.activeCount = Math.max(0, traceState.activeCount - 1); traceRender();
    }
    return newDomains;
}

async function runDomainTrace(retryJobs = null) {
    if (!authenticatedUser?.canUseTrace) return showMonitorToast('Tài khoản chưa được Admin cấp quyền Truy vết theo cấp độ.', 'error');
    const root = normalizeProjectDomain(traceEls.root.value);
    if (!root && !retryJobs) return showMonitorToast('Hãy nhập domain gốc hợp lệ.', 'error');
    if (!retryJobs && isExcludedDomain(root)) return showMonitorToast(`Domain ${root} đang nằm trong danh sách loại trừ.`, 'warning');
    const unlimited = Boolean(traceEls.unlimited?.checked);
    const config = { unlimited, maxLevel: Math.max(1, Math.min(4, Number(traceEls.level.value) || 1)), domainLimit: unlimited ? Infinity : Math.max(1, Math.min(1000000, Number(traceEls.domainLimit.value) || 30)), advertiserLimit: unlimited ? Infinity : Math.max(1, Math.min(1000000, Number(traceEls.advertiserLimit.value) || 20)), ownerThreshold: Math.max(1, Math.min(1000000, Number(traceEls.ownerThreshold.value) || 15)) };
    const previousMaxLevel = Math.max(0, Number(traceState.maxLevel) || 0);
    const sameRoot = root && root === normalizeProjectDomain(traceState.rootDomain || '');
    const continuation = !retryJobs && !traceState.running && sameRoot && traceState.results.size > 0 && traceState.visited.size > 0 && config.maxLevel > previousMaxLevel;
    if (!retryJobs && !continuation) resetExclusionSessionStats();
    saveTraceOptions();
    if (!retryJobs && !continuation) { traceState = { running: true, paused: false, stopped: false, results: new Map(), advertisers: new Set(), scannedAdvertisers: new Set(), activeAdvertisers: new Set(), seenAdvertisers: new Set(), visited: new Set(), googleSheetDomains: new Set(), googleSheetRegistrations: new Map(), registeredDuplicates: new Map(), skippedOwners: [], activities: [], errors: [], failedJobs: [], activeCount: 0, controller: new AbortController(), maxLevel: config.maxLevel, currentLevel: 1, rootDomain: root, startedAt: new Date().toISOString(), sessionId: crypto.randomUUID?.() || `trace-${Date.now()}` }; traceResultsVersion++; traceRowsCacheVersion = -1; tracePage = 1; }
    else if (continuation) { traceState.running = true; traceState.paused = false; traceState.stopped = false; traceState.controller = new AbortController(); traceState.maxLevel = config.maxLevel; traceState.currentLevel = previousMaxLevel + 1; traceState.rootDomain = root; traceState.historySaved = false; }
    else { traceState.running = true; traceState.paused = false; traceState.stopped = false; traceState.controller = new AbortController(); }
    traceEls.start.disabled = true; traceEls.pause.disabled = false; traceEls.stop.disabled = false; traceEls.retry.hidden = true; traceLog(retryJobs ? 'Bắt đầu quét lại các tác vụ lỗi.' : continuation ? `Nối tiếp phiên truy vết từ cấp ${previousMaxLevel + 1} đến cấp ${config.maxLevel}; giữ nguyên kết quả Cấp 1–${previousMaxLevel}.` : `Bắt đầu truy vết ${root} đến cấp ${config.maxLevel}.`); traceRender(); traceRenderLevels();
    try {
        if (!retryJobs && traceEls.filterGoogleSheet?.checked) {
            if (traceEls.googleSheetStatus) traceEls.googleSheetStatus.textContent = 'Đang tải danh sách domain từ Google Sheet...';
            const statusResponse = await fetch('/api/trace/google-sheet/status?refresh=1', { cache: 'no-store' });
            const sheetResult = await statusResponse.json().catch(() => ({}));
            if (!statusResponse.ok) throw new Error(sheetResult.error || `HTTP ${statusResponse.status}`);
            const sheetName = sheetResult.sheetName || 'Data';
            const registrations = Array.isArray(sheetResult.lastSync?.registrations) ? sheetResult.lastSync.registrations : [];
            traceState.googleSheetRegistrations = new Map(registrations.map(item => [projectDomainIdentity(item.domain), String(item.registrant || (item.registrants || []).join(', ')).trim()]).filter(([domain]) => domain));
            traceState.googleSheetDomains = new Set([...(sheetResult.lastSync?.domains || []).map(projectDomainIdentity), ...traceState.googleSheetRegistrations.keys()].filter(Boolean));
            if (traceEls.googleSheetStatus) traceEls.googleSheetStatus.textContent = `Đã nạp ${traceState.googleSheetDomains.size.toLocaleString('vi-VN')} domain và thông tin người đăng ký từ tab “${sheetName}”.`;
            traceLog(`Google Sheet: đã nạp ${traceState.googleSheetDomains.size.toLocaleString('vi-VN')} domain để lọc kết quả.`, 'success');
            if (sheetResult.syncWarning) traceLog(`Cảnh báo đồng bộ Google Sheet: ${sheetResult.syncWarning}. Đang dùng dữ liệu gần nhất.`, 'skip');
        }
        if (retryJobs) {
            await tracePool(retryJobs, 2, async (job, index) => { traceProgress(`Đang quét lại ${job.domain}`, index / retryJobs.length * 100); await traceProcessDomain(job, config); });
        } else {
            let frontier = continuation
                ? [...traceState.results.values()].filter(item => Number(item.level) === previousMaxLevel && !traceState.visited.has(item.domain)).map(item => ({ domain: item.domain, parent: [...(item.parents || [])][0] || '', level: previousMaxLevel + 1 }))
                : [{ domain: root, parent: '', level: 1 }];
            const startLevel = continuation ? previousMaxLevel + 1 : 1;
            for (let level = startLevel; level <= config.maxLevel && frontier.length && !traceState.stopped; level++) {
                traceState.currentLevel = level; traceRenderLevels();
                const jobs = frontier.filter(job => !traceState.visited.has(job.domain)).slice(0, config.domainLimit);
                const nextDomains = new Map(); let completed = 0;
                await tracePool(jobs, 2, async job => {
                    traceProgress(`Cấp ${level}: đang truy vết ${job.domain}`, ((level - 1) + completed / Math.max(1, jobs.length)) / config.maxLevel * 100);
                    const found = await traceProcessDomain({ ...job, level }, config);
                    found.forEach(domain => { if (domain && !traceState.visited.has(domain)) nextDomains.set(domain, { domain, parent: job.domain, level: level + 1 }); });
                    completed++; traceRender();
                });
                frontier = [...nextDomains.values()]; saveTraceState();
            }
        }
        if (!traceState.stopped) { traceProgress('Hoàn tất truy vết và tổng hợp domain.', 100); showMonitorToast(`Truy vết hoàn tất: ${traceState.results.size} domain, ${traceState.errors.length} lỗi.`); }
    } finally {
        if (!retryJobs && !traceState.historySaved) {
            traceState.historySaved = true;
            await persistTraceHistory().catch(error => traceLog(`Không thể lưu lịch sử truy vết: ${error.message}`, 'error'));
        }
        traceState.running = false; traceState.paused = false; traceState.activeCount = 0; traceState.activeAdvertisers.clear(); traceEls.start.disabled = false; traceEls.pause.disabled = true; traceEls.pause.innerHTML = '<i class="fa-solid fa-pause"></i> Tạm dừng'; traceEls.stop.disabled = true; traceRender(); saveTraceState();
    }
}

async function persistTraceHistory() {
    const domains = [...traceState.results.keys()];
    const payload = {
        id: traceState.sessionId || `trace-${Date.now()}`,
        rootDomain: normalizeProjectDomain(traceEls.root.value),
        startedAt: traceState.startedAt || new Date().toISOString(),
        maxLevel: traceState.maxLevel,
        newDomains: domains,
        scannedDomains: traceState.visited.size,
        advertisers: traceState.advertisers.size,
        skippedOwners: traceState.skippedOwners.length,
        errors: traceState.errors.length,
        traceMode: true
    };
    // Kết quả lớn đã nằm trong IndexedDB; lịch sử local chỉ giữ thống kê để tránh
    // JSON.stringify hàng chục nghìn domain làm khóa luồng giao diện.
    saveScanHistory('trace', payload.rootDomain, { domainCount: domains.length, advertiserCount: payload.advertisers, scannedAdvertisers: traceState.scannedAdvertisers.size, maxLevel: payload.maxLevel }, { checkpoint: 'IndexedDB', sessionId: traceState.sessionId || '' });
    const response = await fetch('/api/trace/history', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
    const result = await response.json().catch(() => ({}));
    if (!response.ok) throw new Error(result.error || `HTTP ${response.status}`);
}

async function restoreTraceState() {
    if (!traceEls.root) return;
    try {
        let saved = null;
        try { const db = await traceOpenDb(); const record = await new Promise((resolve, reject) => { const request = db.transaction(TRACE_DB_STORE, 'readonly').objectStore(TRACE_DB_STORE).get('latest'); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); saved = record?.state || null; } catch (_) {}
        if (!saved) saved = JSON.parse(localStorage.getItem('domain_trace_state_v1') || 'null');
        if (!saved) return;
        if (!traceEls.root.value) traceEls.root.value = saved.root || ''; traceState.rootDomain = normalizeProjectDomain(saved.root || traceEls.root.value); traceState.maxLevel = Number(saved.maxLevel || 1); traceState.currentLevel = Number(saved.currentLevel || 0);
        traceState.advertisers = new Set(saved.advertisers || []); traceState.scannedAdvertisers = new Set(saved.scannedAdvertisers || saved.advertisers || []); traceState.activeAdvertisers = new Set(); traceState.seenAdvertisers = new Set(saved.seenAdvertisers || saved.advertisers || []); traceState.visited = new Set(saved.visited || []); traceState.skippedOwners = saved.skippedOwners || []; traceState.activities = saved.activities || []; traceState.errors = saved.errors || []; traceState.failedJobs = saved.failedJobs || []; traceState.startedAt = saved.startedAt || ''; traceState.sessionId = saved.sessionId || '';
        traceState.registeredDuplicates = saved.registeredDuplicates instanceof Map ? saved.registeredDuplicates : new Map((Array.isArray(saved.registeredDuplicates) ? saved.registeredDuplicates : []).map(item => [projectDomainIdentity(item.domain), item]));
        traceState.googleSheetDomains = new Set(); traceState.googleSheetRegistrations = new Map();
        const savedResults = saved.results instanceof Map ? [...saved.results.values()] : (saved.results || []); traceState.results = new Map(savedResults.map(item => ({ ...item, sources: [...(item.sources || [])].filter(source => source !== 'Phân tích text') })).filter(item => item.sources.length).map(item => [item.domain, { ...item, parents: new Set(item.parents || []), advertisers: new Set(item.advertisers || []), sources: new Set(item.sources) }])); traceResultsVersion++; traceRowsCacheVersion = -1;
        traceRender(true); traceRenderLevels(); traceProgress('Đã khôi phục kết quả truy vết gần nhất.', traceState.results.size ? 100 : 0);
    } catch (_) {}
}

function saveTraceOptions() {
    if (!traceEls.root) return;
    try {
        localStorage.setItem('domain_trace_options_v1', JSON.stringify({
            root: traceEls.root.value,
            maxLevel: traceEls.level.value,
            unlimited: Boolean(traceEls.unlimited.checked),
            filterGoogleSheet: Boolean(traceEls.filterGoogleSheet?.checked),
            domainLimit: traceEls.domainLimit.value,
            advertiserLimit: traceEls.advertiserLimit.value,
            ownerThreshold: traceEls.ownerThreshold.value
        }));
    } catch (_) {}
}

function restoreTraceOptions() {
    if (!traceEls.root) return;
    try {
        const saved = JSON.parse(localStorage.getItem('domain_trace_options_v1') || 'null');
        if (saved) {
            traceEls.root.value = saved.root || '';
            traceEls.level.value = String(saved.maxLevel || '2');
            traceEls.unlimited.checked = saved.unlimited !== false;
            if (traceEls.filterGoogleSheet) traceEls.filterGoogleSheet.checked = saved.filterGoogleSheet !== false;
            traceEls.domainLimit.value = saved.domainLimit || '30';
            traceEls.advertiserLimit.value = saved.advertiserLimit || '20';
            traceEls.ownerThreshold.value = saved.ownerThreshold || '15';
        }
        [traceEls.domainLimit, traceEls.advertiserLimit].forEach(input => { if (input) input.disabled = traceEls.unlimited.checked; });
    } catch (_) {}
}

if (traceEls.start) traceEls.start.addEventListener('click', () => runDomainTrace().catch(error => { traceProgress(`Lỗi: ${error.message}`, 0); showMonitorToast(error.message, 'error'); }));
if (traceEls.pause) traceEls.pause.addEventListener('click', () => {
    if (!traceState.running) return;
    traceState.paused = !traceState.paused;
    traceEls.pause.innerHTML = traceState.paused ? '<i class="fa-solid fa-play"></i> Tiếp tục' : '<i class="fa-solid fa-pause"></i> Tạm dừng';
    traceLog(traceState.paused ? 'Đã tạm dừng cấp tác vụ mới; đang chờ các tác vụ hiện tại hoàn tất.' : 'Đã tiếp tục xử lý hàng đợi.', traceState.paused ? 'skip' : 'success');
    traceProgress(traceState.paused ? 'Đang tạm dừng — dữ liệu đã lấy được giữ nguyên.' : 'Đã tiếp tục truy vết.', Number(traceEls.progressPercent.textContent.replace('%', '')) || 0);
});
if (traceEls.unlimited) traceEls.unlimited.addEventListener('change', () => {
    [traceEls.domainLimit, traceEls.advertiserLimit].forEach(input => { if (input) input.disabled = traceEls.unlimited.checked; });
    traceEls.progressText.textContent = traceEls.unlimited.checked ? 'Chế độ tối đa: không giới hạn domain và Nhà QC; OCR quét toàn bộ mẫu.' : 'Đang áp dụng giới hạn domain và Nhà QC đã chọn; OCR vẫn quét toàn bộ mẫu.';
    saveTraceOptions();
});
[
    [traceEls.root, 'input'], [traceEls.level, 'change'], [traceEls.domainLimit, 'input'], [traceEls.advertiserLimit, 'input'], [traceEls.ownerThreshold, 'input']
].forEach(([element, eventName]) => { if (element) element.addEventListener(eventName, saveTraceOptions); });
if (traceEls.stop) traceEls.stop.addEventListener('click', () => { traceState.paused = false; traceState.stopped = true; traceState.controller?.abort(); traceLog('Đã yêu cầu dừng hẳn quá trình truy vết.', 'error'); traceProgress('Đã yêu cầu dừng truy vết.', 0); });
if (traceEls.retry) traceEls.retry.addEventListener('click', () => { const jobs = traceState.failedJobs.map(item => ({ ...item })); jobs.forEach(job => traceState.visited.delete(job.domain)); runDomainTrace(jobs).catch(error => showMonitorToast(error.message, 'error')); });
if (traceEls.reset) traceEls.reset.addEventListener('click', async () => {
    const message = traceState.running ? 'Phiên truy vết đang chạy. Dừng phiên và xóa toàn bộ dữ liệu đang hiển thị để về trạng thái ban đầu?' : 'Xóa toàn bộ kết quả truy vết đang hiển thị và đưa giao diện về trạng thái ban đầu?';
    if (!await showAppConfirm(message, { danger: true, confirmText: 'Xóa và làm mới' })) return;
    const runningState = traceState; runningState.stopped = true; runningState.paused = false; runningState.controller?.abort();
    if (runningState.running) { const deadline = Date.now() + 30000; while (runningState.running && Date.now() < deadline) await new Promise(resolve => setTimeout(resolve, 100)); }
    await traceClearCheckpoint();
    traceState = { running: false, paused: false, stopped: false, results: new Map(), advertisers: new Set(), scannedAdvertisers: new Set(), activeAdvertisers: new Set(), seenAdvertisers: new Set(), visited: new Set(), googleSheetDomains: new Set(), googleSheetRegistrations: new Map(), registeredDuplicates: new Map(), skippedOwners: [], activities: [], errors: [], failedJobs: [], activeCount: 0, controller: null, maxLevel: Math.max(1, Number(traceEls.level?.value) || 1), currentLevel: 0 };
    tracePage = 1; traceResultsVersion++; traceRowsCacheVersion = -1; traceRowsCache = []; if (traceEls.root) traceEls.root.value = ''; traceEls.start.disabled = false; traceEls.pause.disabled = true; traceEls.pause.innerHTML = '<i class="fa-solid fa-pause"></i> Tạm dừng'; traceEls.stop.disabled = true; traceEls.retry.hidden = true;
    traceRender(true); traceRenderLevels(); traceProgress('Sẵn sàng truy vết.', 0); saveTraceOptions(); showMonitorToast('Đã làm mới giao diện truy vết.');
});
if (traceEls.pageSize) traceEls.pageSize.addEventListener('change', () => { tracePage = 1; traceRender(true); });
if (traceEls.pagePrev) traceEls.pagePrev.addEventListener('click', () => { if (tracePage > 1) { tracePage--; traceRender(true); } });
if (traceEls.pageNext) traceEls.pageNext.addEventListener('click', () => { tracePage++; traceRender(true); });
[traceEls.deploymentFilter, traceEls.runningDaysFilter, traceEls.nicheFilter].forEach(element => element?.addEventListener('change', () => { tracePage = 1; traceRender(true); }));
[traceEls.minVolumeFilter, traceEls.minTrafficFilter].forEach(element => element?.addEventListener('input', () => { tracePage = 1; traceRender(true); }));
if (traceEls.clearFilters) traceEls.clearFilters.addEventListener('click', () => {
    [traceEls.deploymentFilter, traceEls.runningDaysFilter, traceEls.nicheFilter].forEach(element => { if (element) element.value = ''; });
    [traceEls.minVolumeFilter, traceEls.minTrafficFilter].forEach(element => { if (element) element.value = ''; });
    tracePage = 1; traceRender(true);
});
if (traceEls.traffic) traceEls.traffic.addEventListener('click', () => {
    const domains = [...traceState.results.keys()];
    if (!domains.length) return showMonitorToast('Chưa có domain để quét Traffic.', 'error');
    if (!elBulkTrafficInput) return showMonitorToast('Chức năng Traffic không khả dụng.', 'error');
    elBulkTrafficInput.value = domains.join('\n');
    switchTab('tab-bulk-traffic');
    scanBulkTraffic(false).catch(error => showMonitorToast(error.message || 'Không thể quét Traffic.', 'error'));
});
if (traceEls.copy) traceEls.copy.addEventListener('click', async () => { await navigator.clipboard.writeText([...traceState.results.keys()].join('\n')); showMonitorToast(`Đã copy ${traceState.results.size} domain.`); });
if (traceEls.export) traceEls.export.addEventListener('click', () => { const rows = [...traceState.results.values()].sort((a,b) => a.level-b.level).map((item,index) => [index+1,item.domain,item.level,[...item.parents].join(', '),[...item.advertisers].join(', '),[...item.sources].join(', '),item.sampleUrl||'',item.status]); exportRowsToExcel(`Truy_vet_domain_${new Date().toISOString().slice(0,10)}.xls`, ['STT','Domain','Cấp độ','Domain nguồn','Nhà quảng cáo','Nguồn phát hiện','Mẫu nguồn','Trạng thái'], rows); });
restoreTraceOptions();
restoreTraceState();

const traceDomainHistoryEls = {
    total: document.getElementById('traceDomainHistoryTotal'), search: document.getElementById('traceDomainHistorySearch'), addedBy: document.getElementById('traceDomainHistoryAddedBy'), days: document.getElementById('traceDomainHistoryDays'), from: document.getElementById('traceDomainHistoryFrom'), to: document.getElementById('traceDomainHistoryTo'), scans: document.getElementById('traceDomainHistoryScans'), filter: document.getElementById('btnTraceDomainHistoryFilter'), refresh: document.getElementById('btnTraceDomainHistoryRefresh'), copy: document.getElementById('btnTraceDomainHistoryCopy'), summary: document.getElementById('traceDomainHistorySummary'), body: document.getElementById('traceDomainHistoryBody'), scanCount: document.getElementById('traceDomainScanHistoryCount'), scanBody: document.getElementById('traceDomainScanHistoryBody'), pageSize: document.getElementById('traceDomainHistoryPageSize'), pageSummary: document.getElementById('traceDomainHistoryPageSummary'), pageIndicator: document.getElementById('traceDomainHistoryPageIndicator'), prev: document.getElementById('traceDomainHistoryPrev'), next: document.getElementById('traceDomainHistoryNext'), sheetStatus: document.getElementById('traceGoogleSheetSyncStatus'), sheetSyncedAt: document.getElementById('traceGoogleSheetSyncedAt'), sheetIssues: document.getElementById('traceGoogleSheetIssues'), sheetIssuesBody: document.getElementById('traceGoogleSheetIssuesBody')
};
let traceDomainHistoryRows = [];
let traceDomainHistoryPage = 1;
let traceGoogleSheetRegistrations = new Map();

async function loadTraceGoogleSheetStatus() {
    if (!traceDomainHistoryEls.sheetStatus) return;
    const response = await fetch('/api/trace/google-sheet/status', { cache: 'no-store' });
    const result = await response.json().catch(() => ({}));
    const sync = result.lastSync;
    traceDomainHistoryEls.sheetStatus.textContent = sync?.message || (result.configured ? 'Admin đã cấu hình Google Sheet nhưng chưa đồng bộ dữ liệu.' : 'Admin chưa cấu hình Google Sheet của Kho Domain.');
    if (traceDomainHistoryEls.sheetSyncedAt) {
        const syncedAt = sync?.syncedAt ? new Date(sync.syncedAt) : null;
        const validDate = syncedAt && !Number.isNaN(syncedAt.getTime());
        traceDomainHistoryEls.sheetSyncedAt.textContent = validDate ? `Cập nhật gần nhất: ${syncedAt.toLocaleString('vi-VN', { timeZone: 'Asia/Ho_Chi_Minh' })}` : 'Chưa đồng bộ';
        traceDomainHistoryEls.sheetSyncedAt.dateTime = validDate ? syncedAt.toISOString() : '';
    }
    traceGoogleSheetRegistrations = new Map((sync?.registrations || []).map(item => [projectDomainIdentity(item.domain), String(item.registrant || '').trim()]));
    const duplicates = sync?.duplicateDetails || [], invalid = sync?.invalidDetails || [];
    if (traceDomainHistoryEls.sheetIssues) traceDomainHistoryEls.sheetIssues.hidden = !sync;
    if (traceDomainHistoryEls.sheetIssuesBody) traceDomainHistoryEls.sheetIssuesBody.innerHTML = `<section><strong>Dòng trùng (${Number(sync?.duplicateRows ?? duplicates.length)})</strong>${duplicates.length ? `<ul>${duplicates.map(item => `<li>Dòng ${Number(item.rowNumber)}: <code>${traceEscape(item.domain)}</code> — trùng với dòng ${Number(item.firstRowNumber)}</li>`).join('')}</ul>` : '<p>Không có dòng trùng.</p>'}</section><section><strong>Dòng lỗi / không hợp lệ (${Number(sync?.invalidRows ?? invalid.length)})</strong>${invalid.length ? `<ul>${invalid.map(item => `<li>Dòng ${Number(item.rowNumber)}: <code>${traceEscape(item.value || '(trống)')}</code></li>`).join('')}</ul>` : '<p>Không có dòng lỗi.</p>'}</section>`;
    return result;
}

async function loadTraceDomainHistory() {
    if (!traceDomainHistoryEls.body || (authenticatedUser?.role !== 'admin' && !authenticatedUser?.canUseDomainStore)) return;
    await loadTraceGoogleSheetStatus().catch(() => null);
    traceDomainHistoryEls.summary.textContent = 'Đang tải kho domain...';
    const params = new URLSearchParams();
    if (traceDomainHistoryEls.search.value.trim()) {
        const rawQuery = traceDomainHistoryEls.search.value.trim();
        params.set('q', normalizeProjectDomain(rawQuery) || rawQuery);
    }
    if (traceDomainHistoryEls.days.value) params.set('days', traceDomainHistoryEls.days.value);
    if (traceDomainHistoryEls.from.value) params.set('from', traceDomainHistoryEls.from.value);
    if (traceDomainHistoryEls.to.value) params.set('to', traceDomainHistoryEls.to.value);
    if (traceDomainHistoryEls.addedBy?.value) params.set('addedBy', traceDomainHistoryEls.addedBy.value);
    if (Number(traceDomainHistoryEls.scans.value) > 0) params.set('lastScans', traceDomainHistoryEls.scans.value);
    params.set('page', traceDomainHistoryPage);
    params.set('pageSize', Math.max(25, Number(traceDomainHistoryEls.pageSize?.value) || 100));
    const response = await fetch(`/api/trace/domains?${params}`, { cache: 'no-store' });
    const result = await response.json().catch(() => ({}));
    if (!response.ok) throw new Error(result.error || `HTTP ${response.status}`);
    traceDomainHistoryRows = result.domains || [];
    if (traceDomainHistoryEls.addedBy) {
        const selected = traceDomainHistoryEls.addedBy.value;
        const adders = [...new Set(result.adders || [])].filter(Boolean).sort((a, b) => a.localeCompare(b, 'vi'));
        traceDomainHistoryEls.addedBy.innerHTML = '<option value="">Tất cả người thêm</option>' + adders.map(name => `<option value="${traceEscape(name)}">${traceEscape(name)}</option>`).join('');
        traceDomainHistoryEls.addedBy.value = selected;
    }
    traceDomainHistoryEls.total.textContent = `Tổng: ${Number(result.total || 0).toLocaleString('vi-VN')}`;
    const searchedDomain = projectDomainIdentity(traceDomainHistoryEls.search.value.trim());
    const sheetRegistrant = searchedDomain ? traceGoogleSheetRegistrations.get(searchedDomain) : '';
    traceDomainHistoryEls.summary.textContent = `Đang hiển thị ${Number(result.filteredTotal || 0).toLocaleString('vi-VN')} / ${Number(result.total || 0).toLocaleString('vi-VN')} domain duy nhất.${sheetRegistrant !== undefined && searchedDomain ? ` Google Sheet: ${searchedDomain} đã được đăng ký${sheetRegistrant ? ` bởi ${sheetRegistrant}` : ''}.` : ''}`;
    traceDomainHistoryEls.copy.disabled = !traceDomainHistoryRows.length;
    const page = Number(result.page || traceDomainHistoryPage), pageSize = Number(result.pageSize || 100), pageCount = Number(result.pageCount || 1), offset = (page - 1) * pageSize;
    traceDomainHistoryPage = Math.min(page, pageCount);
    traceDomainHistoryEls.body.innerHTML = traceDomainHistoryRows.length ? traceDomainHistoryRows.map((row, index) => { const item = typeof row === 'string' ? { domain: row, addedByUsername: 'Dữ liệu cũ' } : row; const registration = item.registrant ? `<span class="trace-status-pill success"><i class="fa-solid fa-user-check"></i> ${traceEscape(item.registrant)}</span>` : '<span class="trace-status-pill"><i class="fa-solid fa-user-plus"></i> Chưa có người đăng ký</span>'; const niche = item.niche ? `<span class="badge">${traceEscape(item.niche)}</span>` : '<span class="keyword-metric-empty">Chưa phân loại</span>'; return `<tr><td>${offset + index + 1}</td><td><a class="table-link" href="https://${traceEscape(item.domain)}" target="_blank" rel="noopener noreferrer"><strong>${traceEscape(item.domain)}</strong></a></td><td>${niche}</td><td><span class="trace-user-chip"><i class="fa-solid fa-user"></i> ${traceEscape(item.addedByUsername || 'Không rõ')}</span></td><td>${item.firstSeenAt ? traceEscape(new Date(item.firstSeenAt).toLocaleString('vi-VN')) : '<span class="keyword-metric-empty">Dữ liệu cũ</span>'}</td><td>${registration}</td></tr>`; }).join('') : '<tr class="empty-row"><td colspan="6" class="text-center">Không có domain phù hợp bộ lọc.</td></tr>';
    if (traceDomainHistoryEls.pageSummary) traceDomainHistoryEls.pageSummary.textContent = result.filteredTotal ? `${offset + 1}–${Math.min(offset + pageSize, result.filteredTotal)} / ${Number(result.filteredTotal).toLocaleString('vi-VN')} kết quả` : '0 kết quả';
    if (traceDomainHistoryEls.pageIndicator) traceDomainHistoryEls.pageIndicator.textContent = `Trang ${page} / ${pageCount}`;
    if (traceDomainHistoryEls.prev) traceDomainHistoryEls.prev.disabled = page <= 1;
    if (traceDomainHistoryEls.next) traceDomainHistoryEls.next.disabled = page >= pageCount;
    const history = result.history || [];
    traceDomainHistoryEls.scanCount.textContent = history.length.toLocaleString('vi-VN');
    traceDomainHistoryEls.scanBody.innerHTML = history.length ? history.map(item => `<tr><td>${new Date(item.completedAt).toLocaleString('vi-VN')}</td><td>${traceEscape(item.username || '—')}</td><td><strong>${traceEscape(item.rootDomain || '—')}</strong></td><td>${Number(item.maxLevel || 1)}</td><td>${Number(item.newDomainCount ?? item.newDomains?.length ?? 0).toLocaleString('vi-VN')}</td><td>${Number(item.scannedDomains || 0).toLocaleString('vi-VN')}</td><td>${Number(item.advertisers || 0).toLocaleString('vi-VN')}</td><td>${Number(item.errors || 0).toLocaleString('vi-VN')}</td></tr>`).join('') : '<tr class="empty-row"><td colspan="8" class="text-center">Chưa có phiên truy vết.</td></tr>';
}

if (traceDomainHistoryEls.filter) traceDomainHistoryEls.filter.onclick = () => { traceDomainHistoryPage = 1; loadTraceDomainHistory().catch(error => showMonitorToast(error.message, 'error')); };
traceDomainHistoryEls.search?.closest('.trace-history-filters')?.addEventListener('keydown', event => {
    if (event.key !== 'Enter' || event.isComposing) return;
    event.preventDefault();
    traceDomainHistoryPage = 1;
    loadTraceDomainHistory().catch(error => showMonitorToast(error.message, 'error'));
});
if (traceDomainHistoryEls.refresh) traceDomainHistoryEls.refresh.onclick = () => loadTraceDomainHistory().catch(error => showMonitorToast(error.message, 'error'));
if (traceDomainHistoryEls.pageSize) traceDomainHistoryEls.pageSize.onchange = () => { traceDomainHistoryPage = 1; loadTraceDomainHistory().catch(error => showMonitorToast(error.message, 'error')); };
if (traceDomainHistoryEls.prev) traceDomainHistoryEls.prev.onclick = () => { if (traceDomainHistoryPage > 1) { traceDomainHistoryPage--; loadTraceDomainHistory().catch(error => showMonitorToast(error.message, 'error')); } };
if (traceDomainHistoryEls.next) traceDomainHistoryEls.next.onclick = () => { traceDomainHistoryPage++; loadTraceDomainHistory().catch(error => showMonitorToast(error.message, 'error')); };
if (traceDomainHistoryEls.copy) traceDomainHistoryEls.copy.onclick = async () => { await navigator.clipboard.writeText(traceDomainHistoryRows.map(row => typeof row === 'string' ? row : row.domain).join('\n')); showMonitorToast(`Đã copy ${traceDomainHistoryRows.length} domain.`); };

document.querySelectorAll('form').forEach(form => form.setAttribute('autocomplete', 'off'));
document.querySelectorAll('input, textarea').forEach(field => {
    field.setAttribute('autocomplete', field.type === 'password' ? 'new-password' : 'one-time-code');
    field.setAttribute('autocorrect', 'off');
    field.setAttribute('autocapitalize', 'off');
    field.setAttribute('spellcheck', 'false');
});
document.querySelectorAll('input[type="password"]').forEach(input => {
    const shell = document.createElement('div');
    shell.className = 'password-input-shell';
    input.parentNode.insertBefore(shell, input);
    shell.appendChild(input);
    const button = document.createElement('button');
    button.type = 'button';
    button.className = 'password-input-toggle';
    button.innerHTML = '<i class="fa-regular fa-eye"></i>';
    button.title = 'Hiện mật khẩu';
    button.setAttribute('aria-label', button.title);
    button.addEventListener('click', () => {
        const visible = input.type === 'text';
        input.type = visible ? 'password' : 'text';
        button.innerHTML = `<i class="fa-regular ${visible ? 'fa-eye' : 'fa-eye-slash'}"></i>`;
        button.title = visible ? 'Hiện mật khẩu' : 'Ẩn mật khẩu';
        button.setAttribute('aria-label', button.title);
    });
    shell.appendChild(button);
});

function renderUserExclusions() {
    const enabled = document.getElementById('exclusionEnabled'), domains = document.getElementById('exclusionDomains'), ids = document.getElementById('exclusionAdvertiserIds');
    if (enabled) enabled.checked = userExclusions.enabled; if (domains) domains.value = (userExclusions.domains || []).join('\n'); if (ids) ids.value = (userExclusions.advertisers || []).map(item => item.id).join('\n');
    const body = document.getElementById('exclusionAdvertiserBody');
    if (body) body.innerHTML = userExclusions.advertisers?.length ? userExclusions.advertisers.map((item, index) => `<tr><td>${index + 1}</td><td><strong>${traceEscape(item.id)}</strong></td><td>${traceEscape(item.name || 'Chưa lấy được tên')}</td></tr>`).join('') : '<tr class="empty-row"><td colspan="3" class="text-center">Chưa có Advertiser ID.</td></tr>';
    const summary = document.getElementById('exclusionSummary'); if (summary) summary.textContent = `${userExclusions.domains?.length || 0} domain · ${userExclusions.advertisers?.length || 0} nhà quảng cáo`;
    const state = document.getElementById('exclusionState'); if (state) state.textContent = userExclusions.enabled ? 'Đang bật lọc' : 'Đang tắt lọc';
    renderExclusionFilterNotice();
}
async function loadUserExclusions() { const response = await fetch('/api/user/exclusions', { cache: 'no-store' }); if (response.ok) { userExclusions = await response.json(); renderUserExclusions(); } }
async function resolveExcludedAdvertiserName(id) {
    const controller = new AbortController();
    try {
        const response = await fetch('/api/advertiser-creatives', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ advId: id, fReq: {} }), signal: controller.signal });
        if (!response.ok || !response.body) return '';
        const reader = response.body.getReader(), decoder = new TextDecoder(); let buffer = '';
        while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop(); for (const line of lines) { try { const chunk = JSON.parse(line); const name = chunk.creatives?.[0]?.rawName || chunk.filteredCreatives?.[0]?.rawName; if (name) { controller.abort(); return name; } } catch (_) {} } }
    } catch (_) {}
    return '';
}
async function saveUserExclusions() {
    const button = document.getElementById('btnSaveExclusions'); if (button) button.disabled = true;
    const domains = [...new Set(String(document.getElementById('exclusionDomains')?.value || '').split(/[\s,;]+/).map(normalizeProjectDomain).filter(Boolean))];
    const ids = [...new Set(String(document.getElementById('exclusionAdvertiserIds')?.value || '').split(/[\s,;]+/).map(value => value.trim()).filter(Boolean))];
    const oldNames = new Map((userExclusions.advertisers || []).map(item => [item.id, item.name]));
    const advertisers = [];
    for (const id of ids) { const name = oldNames.get(id) || await resolveExcludedAdvertiserName(id); advertisers.push({ id, name }); }
    const response = await fetch('/api/user/exclusions', { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ enabled: Boolean(document.getElementById('exclusionEnabled')?.checked), domains, advertisers }) });
    const result = await response.json().catch(() => ({})); if (!response.ok) throw new Error(result.error || `HTTP ${response.status}`); userExclusions = result; renderUserExclusions(); resetExclusionSessionStats(); showMonitorToast('Đã lưu danh sách loại trừ của tài khoản.');
    if (button) button.disabled = false;
}
document.getElementById('btnSaveExclusions')?.addEventListener('click', () => saveUserExclusions().catch(error => { document.getElementById('btnSaveExclusions').disabled = false; showMonitorToast(`Không thể lưu loại trừ: ${error.message}`, 'error'); }));
document.getElementById('btnOpenExclusionsFromNotice')?.addEventListener('click', () => switchTab('tab-exclusions'));
const settingsMenu = document.getElementById('settingsMenu');
const settingsDropdown = document.getElementById('settingsDropdown');
const settingsMenuButton = document.getElementById('btnSettingsMenu');
function setSettingsMenuOpen(open) {
    if (!settingsDropdown || !settingsMenuButton) return;
    settingsDropdown.hidden = !open;
    settingsMenuButton.setAttribute('aria-expanded', String(open));
    settingsMenu?.classList.toggle('open', open);
}
settingsMenuButton?.addEventListener('click', event => {
    event.stopPropagation();
    setSettingsMenuOpen(settingsDropdown?.hidden !== false);
});
document.getElementById('btnExclusionSettings')?.addEventListener('click', () => {
    setSettingsMenuOpen(false);
    switchTab('tab-exclusions');
});
document.getElementById('btnAffiliateFirecrawlSettings')?.addEventListener('click', () => {
    setSettingsMenuOpen(false);
    switchTab('tab-affiliate-firecrawl');
    loadAffiliateFirecrawlSettings().catch(error => showMonitorToast(`Không tải được cấu hình Firecrawl: ${error.message}`, 'error'));
});
document.addEventListener('click', event => {
    if (settingsDropdown && !settingsDropdown.hidden && !settingsMenu?.contains(event.target)) setSettingsMenuOpen(false);
});
document.addEventListener('keydown', event => {
    if (event.key === 'Escape') setSettingsMenuOpen(false);
});

async function initializeAuthenticatedUi() {
    try {
        const response = await fetch('/api/auth/me', { cache: 'no-store' });
        if (response.status === 401) return location.replace('/login');
        const data = await response.json();
        authenticatedUser = data.user;
        await loadUserExclusions().catch(() => {});
        loadProjectTrafficRows();
        affiliateLoadHistory();
        workspaceRestore().finally(workspaceStartAutosave);
        startPresenceHeartbeat();
        document.body.classList.remove('auth-pending');
        document.body.classList.toggle('is-admin', data.user?.role === 'admin');
        const canUseProxy = Boolean(data.user?.canUseProxy && data.proxyConfigured);
        document.body.classList.toggle('proxy-denied', !canUseProxy);
        document.querySelectorAll('.proxy-feature').forEach(element => {
            element.hidden = !canUseProxy;
            if (!canUseProxy) element.style.setProperty('display', 'none', 'important');
            else element.style.removeProperty('display');
        });
        document.querySelectorAll('.admin-feature').forEach(element => { element.hidden = data.user?.role !== 'admin'; });
        document.querySelectorAll('.advertiser-count-feature').forEach(element => {
            const allowed = Boolean(data.user?.canUseAdvertiserCount);
            if (!allowed) {
                element.hidden = true;
                element.style.setProperty('display', 'none', 'important');
            } else {
                element.style.removeProperty('display');
            }
        });
        document.querySelectorAll('.traffic-feature').forEach(element => {
            const allowed = Boolean(data.user?.canUseTraffic);
            if (!allowed) {
                element.hidden = true;
                element.style.setProperty('display', 'none', 'important');
            } else {
                element.style.removeProperty('display');
            }
        });
        document.querySelectorAll('.trace-feature').forEach(element => {
            const allowed = Boolean(data.user?.canUseTrace);
            if (!allowed) {
                element.hidden = true;
                element.style.setProperty('display', 'none', 'important');
            } else {
                element.style.removeProperty('display');
            }
        });
        document.querySelectorAll('.domain-store-feature').forEach(element => {
            const allowed = data.user?.role === 'admin' || Boolean(data.user?.canUseDomainStore);
            if (!allowed) {
                element.hidden = true;
                element.style.setProperty('display', 'none', 'important');
            } else {
                element.style.removeProperty('display');
            }
        });
        const applyFeaturePermission = (selector, allowed) => document.querySelectorAll(selector).forEach(element => {
            element.hidden = !allowed;
            if (!allowed) element.style.setProperty('display', 'none', 'important');
            else element.style.removeProperty('display');
        });
        const canUseNiche = data.user?.role === 'admin' || Boolean(data.user?.canUseNiche);
        const canUseDomainDuplicate = data.user?.role === 'admin' || Boolean(data.user?.canUseDomainDuplicate);
        const canUseAffiliate = data.user?.role === 'admin' || Boolean(data.user?.canUseAffiliate);
        applyFeaturePermission('.niche-feature', canUseNiche);
        applyFeaturePermission('.domain-duplicate-feature', canUseDomainDuplicate);
        applyFeaturePermission('.affiliate-feature', canUseAffiliate);
        if (!data.user?.canUseTrace && document.getElementById('tab-trace-content')?.classList.contains('active')) switchTab('tab-domain');
        const canUseDomainStore = data.user?.role === 'admin' || Boolean(data.user?.canUseDomainStore);
        if (!canUseDomainStore && document.getElementById('tab-trace-domains-content')?.classList.contains('active')) switchTab('tab-domain');
        if (!canUseNiche && document.getElementById('tab-niche-content')?.classList.contains('active')) switchTab('tab-domain');
        if (!canUseDomainDuplicate && document.getElementById('tab-domain-duplicate-content')?.classList.contains('active')) switchTab('tab-domain');
        if (!canUseAffiliate && document.getElementById('tab-affiliate-content')?.classList.contains('active')) switchTab('tab-domain');
        if (canUseDomainStore && document.getElementById('tab-trace-domains-content')?.classList.contains('active')) loadTraceDomainHistory().catch(() => {});
        // Quyền tính năng chỉ quyết định nút có được phép xuất hiện. Trạng thái
        // các nút retry vẫn phải dựa vào lỗi thật đang có trong bảng hiện tại.
        updateMetricRetryButtons();
        [elCurlCommand, elAdvCurlCommand, elMonitorCurlCommand].forEach(element => {
            if (element) element.closest('.form-group')?.setAttribute('hidden', '');
        });
        [elProxyUrl, elAdvProxyUrl, elMonitorProxyUrl].forEach(element => {
            if (!element) return;
            element.value = '';
            if (!canUseProxy) element.closest('.form-group')?.setAttribute('hidden', '');
        });
        localStorage.removeItem('saved_proxy_url');
        localStorage.removeItem('saved_adv_proxy_url');
        const badge = document.getElementById('currentUserBadge');
        if (badge) badge.textContent = data.user?.displayName || data.user?.username || '';
        const adminLink = document.getElementById('adminLink');
        if (adminLink) adminLink.hidden = data.user?.role !== 'admin';
        if (data.user?.role !== 'admin' && document.getElementById('tab-api-content')?.classList.contains('active')) switchTab('tab-domain');
        renderHistoryTable();
        renderLocalTrafficHistory();
        loadGoogleSheetsOAuthStatus();
        const oauthResult = new URLSearchParams(location.search).get('googleSheets');
        if (oauthResult) {
            showMonitorToast(oauthResult === 'connected' ? 'Đã kết nối Google Sheets thành công.' : 'Không thể kết nối Google Sheets.', oauthResult === 'connected' ? 'success' : 'error');
            const cleanUrl = new URL(location.href); cleanUrl.searchParams.delete('googleSheets'); history.replaceState({}, '', cleanUrl);
        }
    } catch (error) { location.replace('/login'); }
}

function normalizeProjectDomain(value) {
    let input = String(value || '').replace(/[\u200B-\u200D\u2060\uFEFF]/g, '').replace(/&amp;/gi, '&').trim();
    if (!input) return '';
    input = input.replace(/^(https?):\/(?!\/)/i, '$1://').replace(/([a-z0-9-])\s*\.\s*(?=[a-z0-9-])/gi, '$1.');
    const annotatedDomain = /^((?:[a-z0-9-]+\.)+[a-z]{2,63})(?:\s+\([^)]*\))$/i.exec(input)?.[1];
    if (annotatedDomain) input = annotatedDomain;

    // Hỗ trợ URL được copy dưới dạng Markdown: [nhãn](https://domain/path).
    const hyperlinkFormulaUrl = /^=HYPERLINK\(\s*["']([^"']+)["']/i.exec(input)?.[1];
    const markdownUrl = /^\s*\[[^\]]*\]\(\s*([^\s)]+)\s*\)\s*$/i.exec(input)?.[1];
    if (hyperlinkFormulaUrl) input = hyperlinkFormulaUrl;
    else if (markdownUrl) input = markdownUrl;
    else {
        const embeddedUrl = /https?:\/\/[^\s<>{}\[\]"']+/i.exec(input)?.[0];
        if (embeddedUrl) input = embeddedUrl.replace(/[),.;!?]+$/, '');
    }
    input = input.trim().replace(/^["'`(<\[]+|["'`)>\]]+$/g, '');
    input = input.replace(/^(https?):\/(?!\/)/i, '$1://').replace(/([a-z0-9-])\s*\.\s*(?=[a-z0-9-])/gi, '$1.');
    const labeledDomain = /^((?:[a-z0-9-]+\.)+[a-z]{2,63})(?:\s+\([^)]*\))$/i.exec(input)?.[1];
    if (labeledDomain) input = labeledDomain;

    try {
        const parsed = new URL(/^https?:\/\//i.test(input) ? input : `https://${input}`);
        const hostname = parsed.hostname.toLowerCase().replace(/^www\./i, '').replace(/\.$/, '');
        return hostname.includes('.') && !/\s/.test(hostname) ? hostname : '';
    } catch (error) {
        return '';
    }
}

const PROJECT_COUNTRY_SECOND_LEVEL_SUFFIXES = new Set(['ac','asn','biz','co','com','edu','firm','gen','go','gov','id','ind','info','ltd','me','mil','net','ne','nom','or','org','plc','pro','sch','web']);
const PROJECT_AFFILIATE_MERCHANT_HOSTS = new Set(['sjv.io', 'pxf.io', 'evyy.net']);
const PROJECT_SHARED_AFFILIATE_HOSTS = new Set([...PROJECT_AFFILIATE_MERCHANT_HOSTS, 'jdoqocy.com', 'kqzyfj.com', 'anrdoezrs.net', 'tkqlhce.com', 'dpbolvw.net', 'linksynergy.com', 'awin1.com', 'tidd.ly', 'prf.hn', 'rstyle.me', 'shopstyle.it', 'skimresources.com']);
const PROJECT_GENERIC_AFFILIATE_SUBDOMAINS = new Set(['www', 'go', 'click', 'track', 'tracking', 'link', 'links', 'affiliate', 'affiliates']);
function projectDomainIdentity(value) {
    const hostname = normalizeProjectDomain(value);
    if (!hostname) return '';
    const labels = hostname.split('.').filter(Boolean);
    if (labels.length < 2) return '';
    const affiliateBase = labels.slice(-2).join('.');
    if (labels.length >= 3 && PROJECT_AFFILIATE_MERCHANT_HOSTS.has(affiliateBase)) {
        const merchant = labels[0].toLowerCase();
        if (/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(merchant) && !PROJECT_GENERIC_AFFILIATE_SUBDOMAINS.has(merchant)) return `${merchant}.com`;
    }
    if (PROJECT_SHARED_AFFILIATE_HOSTS.has(affiliateBase)) return '';
    const hasCountrySuffix = labels.at(-1).length === 2 && labels.length >= 3 && PROJECT_COUNTRY_SECOND_LEVEL_SUFFIXES.has(labels.at(-2));
    const brand = labels.at(hasCountrySuffix ? -3 : -2);
    return /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i.test(brand || '') ? `${brand}.com` : '';
}

// Google Ads active Country geo targets, official dataset 2026-07-16.
const GOOGLE_COUNTRY_PAIRS = 'AF:2004,AL:2008,AQ:2010,DZ:2012,AS:2016,AD:2020,AO:2024,AG:2028,AZ:2031,AR:2032,AU:2036,AT:2040,BS:2044,BH:2048,BD:2050,AM:2051,BB:2052,BE:2056,BT:2064,BO:2068,BA:2070,BW:2072,BR:2076,BZ:2084,SB:2090,BN:2096,BG:2100,MM:2104,BI:2108,BY:2112,KH:2116,CM:2120,CA:2124,CV:2132,CF:2140,LK:2144,TD:2148,CL:2152,CN:2156,CX:2162,CC:2166,CO:2170,KM:2174,CG:2178,CD:2180,CK:2184,CR:2188,HR:2191,CY:2196,CZ:2203,BJ:2204,DK:2208,DM:2212,DO:2214,EC:2218,SV:2222,GQ:2226,ET:2231,ER:2232,EE:2233,GS:2239,FJ:2242,FI:2246,FR:2250,PF:2258,TF:2260,DJ:2262,GA:2266,GE:2268,GM:2270,DE:2276,GH:2288,KI:2296,GR:2300,GD:2308,GU:2316,GT:2320,GN:2324,GY:2328,HT:2332,HM:2334,VA:2336,HN:2340,HU:2348,IS:2352,IN:2356,ID:2360,IQ:2368,IE:2372,IL:2376,IT:2380,CI:2384,JM:2388,JP:2392,KZ:2398,JO:2400,KE:2404,KR:2410,KW:2414,KG:2417,LA:2418,LB:2422,LS:2426,LV:2428,LR:2430,LY:2434,LI:2438,LT:2440,LU:2442,MG:2450,MW:2454,MY:2458,MV:2462,ML:2466,MT:2470,MR:2478,MU:2480,MX:2484,MC:2492,MN:2496,MD:2498,ME:2499,MA:2504,MZ:2508,OM:2512,NA:2516,NR:2520,NP:2524,NL:2528,CW:2531,SX:2534,BQ:2535,NC:2540,VU:2548,NZ:2554,NI:2558,NE:2562,NG:2566,NU:2570,NF:2574,NO:2578,MP:2580,UM:2581,FM:2583,MH:2584,PW:2585,PK:2586,PA:2591,PG:2598,PY:2600,PE:2604,PH:2608,PN:2612,PL:2616,PT:2620,GW:2624,TL:2626,QA:2634,RO:2642,RU:2643,RW:2646,BL:2652,SH:2654,KN:2659,LC:2662,MF:2663,PM:2666,VC:2670,SM:2674,ST:2678,SA:2682,SN:2686,RS:2688,SC:2690,SL:2694,SG:2702,SK:2703,VN:2704,SI:2705,SO:2706,ZA:2710,ZW:2716,ES:2724,SS:2728,SD:2736,SR:2740,SZ:2748,SE:2752,CH:2756,SY:2760,TJ:2762,TH:2764,TG:2768,TK:2772,TO:2776,TT:2780,AE:2784,TN:2788,TR:2792,TM:2795,TV:2798,UG:2800,UA:2804,MK:2807,EG:2818,GB:2826,GG:2831,JE:2832,IM:2833,TZ:2834,US:2840,BF:2854,UY:2858,UZ:2860,VE:2862,WF:2876,WS:2882,YE:2887,ZM:2894';
const COUNTRY_PRIORITY = ['US','GB','FR','DE','CA','AU','JP','KR','SG','VN','TH','IN','ID','MY','PH','AE','CH','NL','IT','ES','BR','MX','NZ'];
const countryDisplayNames = new Intl.DisplayNames(['vi'], { type: 'region' });
const COUNTRY_NAME_OVERRIDES = { US: 'Mỹ', GB: 'Anh', FR: 'Pháp' };
const GOOGLE_COUNTRIES = GOOGLE_COUNTRY_PAIRS.split(',').map(pair => {
    const [code, id] = pair.split(':');
    return { code, id: Number(id), name: COUNTRY_NAME_OVERRIDES[code] || countryDisplayNames.of(code) || code };
}).sort((a, b) => {
    const ai = COUNTRY_PRIORITY.indexOf(a.code), bi = COUNTRY_PRIORITY.indexOf(b.code);
    if (ai !== -1 || bi !== -1) return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi);
    return a.name.localeCompare(b.name, 'vi');
});
const COUNTRY_LOOKUP = new Map();
for (const country of GOOGLE_COUNTRIES) {
    country.label = `${country.name} (${country.code})`;
    COUNTRY_LOOKUP.set(country.label.toLocaleLowerCase('vi'), country);
    COUNTRY_LOOKUP.set(country.code.toLowerCase(), country);
    COUNTRY_LOOKUP.set(country.name.toLocaleLowerCase('vi'), country);
}
COUNTRY_LOOKUP.set('tất cả quốc gia', { id: 0, code: 'ALL', name: 'Tất cả quốc gia', label: 'Tất cả quốc gia' });

function resolveCountryInput(input) {
    return COUNTRY_LOOKUP.get(String(input?.value || '').trim().toLocaleLowerCase('vi')) || null;
}

function applyCountryFilter(fReq, input) {
    const country = resolveCountryInput(input);
    if (!country) return false;
    if (!fReq['3']) fReq['3'] = {};
    // [] is only an internal signal that clears any country inherited from Admin's cURL.
    // The server removes field 8 entirely before calling Google for Global / Anywhere.
    fReq['3']['8'] = country.id ? [country.id] : [];
    return true;
}

function initializeCountrySelectors() {
    const bindSelector = (input, codeElement, storageKey) => {
        if (!input) return;
        const root = input.closest('.country-filter-group');
        const trigger = root?.querySelector('.country-select-trigger');
        const panel = root?.querySelector('.country-select-panel');
        const search = root?.querySelector('.country-search input');
        const list = root?.querySelector('.country-option-list');
        const title = root?.querySelector('.country-select-copy strong');
        const subtitle = root?.querySelector('.country-select-copy small');
        const savedCode = localStorage.getItem(storageKey) || 'ALL';
        const savedCountry = savedCode === 'ALL'
            ? COUNTRY_LOOKUP.get('tất cả quốc gia')
            : COUNTRY_LOOKUP.get(savedCode.toLowerCase());
        let selected = savedCountry || COUNTRY_LOOKUP.get('tất cả quốc gia');

        const normalizeSearch = value => String(value || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
        const close = () => {
            if (!panel || !trigger) return;
            panel.hidden = true;
            trigger.setAttribute('aria-expanded', 'false');
            root?.classList.remove('is-open');
        };
        const updateDisplay = (persist = false) => {
            input.value = selected.label;
            if (title) title.textContent = selected.name;
            if (subtitle) subtitle.textContent = selected.id ? `${selected.code} · Google geo ID ${selected.id}` : 'Global · Anywhere';
            if (codeElement) codeElement.textContent = selected.id ? `${selected.name} · ${selected.id}` : 'Global · Anywhere';
            if (persist) localStorage.setItem(storageKey, selected.code);
        };
        const renderOptions = query => {
            if (!list) return;
            const term = normalizeSearch(query);
            const allCountry = COUNTRY_LOOKUP.get('tất cả quốc gia');
            const matches = [allCountry, ...GOOGLE_COUNTRIES].filter(country =>
                !term || normalizeSearch(`${country.name} ${country.code} ${country.id} ${country.id ? '' : 'global anywhere toàn cầu tất cả'}`).includes(term)
            );
            list.replaceChildren();
            if (!matches.length) {
                const empty = document.createElement('div');
                empty.className = 'country-empty';
                empty.textContent = 'Không tìm thấy quốc gia phù hợp';
                list.appendChild(empty);
                return;
            }
            matches.forEach((country, index) => {
                const item = document.createElement('button');
                item.type = 'button';
                item.className = `country-option${country.code === selected.code ? ' is-selected' : ''}`;
                if (!term && index === 1) item.classList.add('popular-start');
                const badge = document.createElement('span');
                badge.className = `country-flag${country.id ? '' : ' global'}`;
                badge.textContent = country.id ? country.code : '◎';
                const copy = document.createElement('span');
                copy.className = 'country-option-copy';
                const name = document.createElement('strong');
                name.textContent = country.name;
                const meta = document.createElement('small');
                meta.textContent = country.id ? `${country.code} · Mã ${country.id}` : 'Không giới hạn khu vực';
                const check = document.createElement('i');
                check.className = 'fa-solid fa-check country-option-check';
                copy.append(name, meta);
                item.append(badge, copy, check);
                item.addEventListener('click', () => {
                    selected = country;
                    updateDisplay(true);
                    close();
                });
                list.appendChild(item);
            });
        };

        trigger?.addEventListener('click', () => {
            const opening = panel?.hidden;
            document.querySelectorAll('.country-select-panel:not([hidden])').forEach(openPanel => {
                openPanel.hidden = true;
                openPanel.closest('.country-filter-group')?.classList.remove('is-open');
                openPanel.previousElementSibling?.setAttribute('aria-expanded', 'false');
            });
            if (opening && panel) {
                panel.hidden = false;
                trigger.setAttribute('aria-expanded', 'true');
                root?.classList.add('is-open');
                if (search) search.value = '';
                renderOptions('');
                requestAnimationFrame(() => search?.focus());
            }
        });
        search?.addEventListener('input', () => renderOptions(search.value));
        document.addEventListener('click', event => {
            if (root && !root.contains(event.target)) close();
        });
        root?.addEventListener('keydown', event => {
            if (event.key === 'Escape') close();
        });
        updateDisplay(false);
    };

    bindSelector(elDomainCountry, elDomainCountryCode, 'saved_domain_country_code');
    bindSelector(elAdvCountry, elAdvCountryCode, 'saved_adv_country_code');
}

// DOM Elements - Tab 1
const elTargetDomain = document.getElementById('targetDomain');
const elBtnDomainQuickScan = document.getElementById('btnDomainQuickScan');
const elDomainCreativeLimit = document.getElementById('domainCreativeLimit');
const elDomainCountry = document.getElementById('domainCountry');
const elDomainCountryCode = document.getElementById('domainCountryCode');
const elBtnDomainMetrics = document.getElementById('btnDomainMetrics');
const elDomainMetricsResult = document.getElementById('domainMetricsResult');
const elDomainMetricVolume = document.getElementById('domainMetricVolume');
const elDomainMetricCpc = document.getElementById('domainMetricCpc');
const elDomainMetricCompetition = document.getElementById('domainMetricCompetition');
const elDomainMetricsStatus = document.getElementById('domainMetricsStatus');
const elCurlCommand = document.getElementById('curlCommand');
const elBtnStart = document.getElementById('btnStart');
const elBtnStop = document.getElementById('btnStop');
const elBtnFinish = document.getElementById('btnFinish');
const elBtnExport = document.getElementById('btnExport');
const elTableSearch = document.getElementById('tableSearch');
const elFormatFilter = document.getElementById('formatFilter');
const elDateRangeFilter = document.getElementById('dateRangeFilter');
const elCustomDateRangeBox = document.getElementById('customDateRangeBox');
const elFilterStartDate = document.getElementById('filterStartDate');
const elFilterEndDate = document.getElementById('filterEndDate');
const elBtnClearCustomDate = document.getElementById('btnClearCustomDate');
const elTableBody = document.getElementById('tableBody');
const elDomainPageSize = document.getElementById('domainPageSize');
const elDomainPagePrev = document.getElementById('domainPagePrev');
const elDomainPageNext = document.getElementById('domainPageNext');
const elDomainPageSummary = document.getElementById('domainPageSummary');
const elDomainPageIndicator = document.getElementById('domainPageIndicator');

const elStatPages = document.getElementById('statPages');
const elStatAds = document.getElementById('statAds');
const elStatAdvertisers = document.getElementById('statAdvertisers');
const elProgressContainer = document.getElementById('progressContainer');
const elProgressText = document.getElementById('progressText');
const elProgressPercent = document.getElementById('progressPercent');
const elProgressBarFill = document.getElementById('progressBarFill');
const elConsoleLog = document.getElementById('consoleLog');
const elAppStatus = document.getElementById('appStatus');
const elStartDate = document.getElementById('startDate');
const elEndDate = document.getElementById('endDate');
const elDateRangePreset = document.getElementById('dateRangePreset');
const elDatePickerRow = document.getElementById('datePickerRow');
const elProxyUrl = document.getElementById('proxyUrl');

// DOM Elements - Tab 2 (Adv & OCR)
const elTargetAdvId = document.getElementById('targetAdvId');
const elAdvCountry = document.getElementById('advCountry');
const elAdvCountryCode = document.getElementById('advCountryCode');
const elAdvCurlCommand = document.getElementById('advCurlCommand');
const elAdvProxyUrl = document.getElementById('advProxyUrl');
const elBtnAdvStart = document.getElementById('btnAdvStart');
const elBtnAdvStop = document.getElementById('btnAdvStop');
const elAdvConsoleLog = document.getElementById('advConsoleLog');
const elStatAdvTotal = document.getElementById('statAdvTotal');
const elStatAdvDomains = document.getElementById('statAdvDomains');
const elStatAdvFiltered = document.getElementById('statAdvFiltered');
const elFilteredAdsCount = document.getElementById('filteredAdsCount');
const elAdvDomainTableBody = document.getElementById('advDomainTableBody');
const elAdvDomainPageSize = document.getElementById('advDomainPageSize');
const elAdvDomainPagePrev = document.getElementById('advDomainPagePrev');
const elAdvDomainPageNext = document.getElementById('advDomainPageNext');
const elAdvDomainPageSummary = document.getElementById('advDomainPageSummary');
const elAdvDomainPageIndicator = document.getElementById('advDomainPageIndicator');
const elCreativeGrid = document.getElementById('creativeGrid');
const elFilteredAdsTableBody = document.getElementById('filteredAdsTableBody');
const elAdvDomainSearch = document.getElementById('advDomainSearch');
const elAdvDeploymentFilter = document.getElementById('advDeploymentFilter');
const elAdvRunningDaysFilter = document.getElementById('advRunningDaysFilter');
const elAdvMinVolumeFilter = document.getElementById('advMinVolumeFilter');
const elAdvMinTrafficFilter = document.getElementById('advMinTrafficFilter');
const elAdvNicheFilter = document.getElementById('advNicheFilter');
const elAdvDuplicateFilter = document.getElementById('advDuplicateFilter');
const elBtnClearAdvDomainFilters = document.getElementById('btnClearAdvDomainFilters');
const elBtnAddAdvDomainsToStore = document.getElementById('btnAddAdvDomainsToStore');
const elBtnCompareAdvDomains = document.getElementById('btnCompareAdvDomains');
const elBtnRunAutoOcr = document.getElementById('btnRunAutoOcr');
const elOcrProgressCard = document.getElementById('ocrProgressCard');
const elOcrProgressFill = document.getElementById('ocrProgressFill');
const elOcrStatusText = document.getElementById('ocrStatusText');
const elOcrStatusPercent = document.getElementById('ocrStatusPercent');
const elBtnPauseOcr = document.getElementById('btnPauseOcr');
const elBtnStopOcr = document.getElementById('btnStopOcr');
const elAdvDateRangePreset = document.getElementById('advDateRangePreset');
const elAdvStartDate = document.getElementById('advStartDate');
const elAdvEndDate = document.getElementById('advEndDate');
const elAdvDatePickerRow = document.getElementById('advDatePickerRow');
const elAdvScanMode = document.getElementById('advScanMode');
const elSavedAdvIdsBox = document.getElementById('savedAdvIdsBox');
const elSavedAdvIds = document.getElementById('savedAdvIds');
const elBtnSaveAdvIds = document.getElementById('btnSaveAdvIds');
const elSavedAdvIdsCount = document.getElementById('savedAdvIdsCount');
const elGoogleSheetWebAppUrl = document.getElementById('googleSheetWebAppUrl');
const elGoogleSpreadsheetUrl = document.getElementById('googleSpreadsheetUrl');
const elGoogleSheetName = document.getElementById('googleSheetName');
const elGoogleSheetSecret = document.getElementById('googleSheetSecret');
const elGoogleSheetsOAuthState = document.getElementById('googleSheetsOAuthState');
const elBtnConnectGoogleSheets = document.getElementById('btnConnectGoogleSheets');
const elBtnDisconnectGoogleSheets = document.getElementById('btnDisconnectGoogleSheets');
const elBtnPushGoogleSheet = document.getElementById('btnPushGoogleSheet');
const elGoogleSheetStatus = document.getElementById('googleSheetStatus');
const elBtnNotifications = document.getElementById('btnNotifications');
const elNotificationBadge = document.getElementById('notificationBadge');
const elNotificationModal = document.getElementById('notificationModal');
const elNotificationList = document.getElementById('notificationList');
const elNotificationSummary = document.getElementById('notificationSummary');
const elBtnReadAllNotifications = document.getElementById('btnReadAllNotifications');
const elMonitorIntervalValue = document.getElementById('monitorIntervalValue');
const elMonitorIntervalUnit = document.getElementById('monitorIntervalUnit');
const elBtnCreateMonitor = document.getElementById('btnCreateMonitor');
const elMonitorScheduleList = document.getElementById('monitorScheduleList');
const elMonitorAdvertiserIds = document.getElementById('monitorAdvertiserIds');
const elMonitorCurlCommand = document.getElementById('monitorCurlCommand');
const elMonitorProxyUrl = document.getElementById('monitorProxyUrl');
const elMonitorTelegramEnabled = document.getElementById('monitorTelegramEnabled');
const elMonitorTelegramFields = document.getElementById('monitorTelegramFields');
const elMonitorTelegramBotToken = document.getElementById('monitorTelegramBotToken');
const elMonitorTelegramChatId = document.getElementById('monitorTelegramChatId');
const elBtnTestMonitorTelegram = document.getElementById('btnTestMonitorTelegram');
const elBtnSaveMonitorTelegram = document.getElementById('btnSaveMonitorTelegram');
const elMonitorTelegramTestStatus = document.getElementById('monitorTelegramTestStatus');
const elMonitorDetailModal = document.getElementById('monitorDetailModal');
const elMonitorDetailContent = document.getElementById('monitorDetailContent');
const elMonitorDetailSummary = document.getElementById('monitorDetailSummary');
let latestMonitorData = { schedules: [], notifications: [] };
const activeBrowserMonitorJobs = new Set();
const cancelledBrowserMonitorJobs = new Set();
let monitorAuthRedirecting = false;

function handleMonitorUnauthorized(response) {
    if (response?.status !== 401) return false;
    if (!monitorAuthRedirecting) {
        monitorAuthRedirecting = true;
        location.replace('/login');
    }
    return true;
}
const elGoogleSheetFieldOptions = document.getElementById('googleSheetFieldOptions');
const elBtnToggleSheetFields = document.getElementById('btnToggleSheetFields');
const elBtnScanKeywordMetrics = document.getElementById('btnScanKeywordMetrics');
const elBtnRetryKeywordMetricsErrors = document.getElementById('btnRetryKeywordMetricsErrors');
const elBtnScanAdvertiserCounts = document.getElementById('btnScanAdvertiserCounts');
const elBtnRetryAdvertiserCountErrors = document.getElementById('btnRetryAdvertiserCountErrors');
const elBtnScanTraffic = document.getElementById('btnScanTraffic');
const elBtnRetryTrafficErrors = document.getElementById('btnRetryTrafficErrors');
let domainTableSort = { key: 'creativeCount', direction: 'desc' };
let advDomainTableSort = { key: 'count', direction: 'desc' };
const domainPagination = { page: 1, size: localStorage.getItem('domain_table_page_size') || '300' };
const advDomainPagination = { page: 1, size: localStorage.getItem('adv_domain_table_page_size') || '300' };
let ocrRunState = { running: false, paused: false, stopped: false };
let partialDomainAdvertisers = [];

function compareTableValues(left, right, direction = 'asc') {
    const multiplier = direction === 'asc' ? 1 : -1;
    const a = left === null || left === undefined ? '' : left;
    const b = right === null || right === undefined ? '' : right;
    if (typeof a === 'number' || typeof b === 'number') return ((Number(a) || 0) - (Number(b) || 0)) * multiplier;
    return String(a).localeCompare(String(b), 'vi', { numeric: true, sensitivity: 'base' }) * multiplier;
}

function paginateTableRows(rows, state, elements) {
    const total = rows.length;
    const size = state.size === 'all' ? Math.max(1, total) : Math.max(1, Number(state.size) || 300);
    const totalPages = state.size === 'all' ? 1 : Math.max(1, Math.ceil(total / size));
    state.page = Math.min(Math.max(1, state.page), totalPages);
    const start = state.size === 'all' ? 0 : (state.page - 1) * size;
    const end = state.size === 'all' ? total : Math.min(total, start + size);
    if (elements.summary) elements.summary.textContent = total ? `Hiển thị ${start + 1}–${end} trong ${total.toLocaleString('vi-VN')} kết quả` : '0 kết quả';
    if (elements.indicator) elements.indicator.textContent = `Trang ${state.page} / ${totalPages}`;
    if (elements.prev) elements.prev.disabled = state.page <= 1;
    if (elements.next) elements.next.disabled = state.page >= totalPages;
    return rows.slice(start, end);
}

function advertiserSortValue(adv, key) {
    if (key === 'formats') return Object.values(adv.formats || {}).reduce((sum, value) => sum + Number(value || 0), 0);
    if (key === 'runningDays') return adv.firstSeen && adv.lastSeen ? Math.max(1, Math.round((new Date(adv.lastSeen) - new Date(adv.firstSeen)) / 86400000) + 1) : 0;
    if (key === 'status') {
        const today = new Date();
        const yesterday = new Date(today); yesterday.setDate(today.getDate() - 1);
        const dateKey = date => date.toISOString().slice(0, 10);
        return [dateKey(today), dateKey(yesterday)].includes(String(adv.lastSeen || '').slice(0, 10)) ? 1 : 0;
    }
    return adv[key] ?? '';
}

function updateSortHeaders(selector, state) {
    document.querySelectorAll(selector).forEach(header => {
        const key = header.dataset.domainSort || header.dataset.advDomainSort;
        header.classList.toggle('sort-asc', key === state.key && state.direction === 'asc');
        header.classList.toggle('sort-desc', key === state.key && state.direction === 'desc');
    });
}

// DOM Elements - Tab 3 (History)
const elHistorySearch = document.getElementById('historySearch');
const elHistoryTableBody = document.getElementById('historyTableBody');
const elBtnClearHistory = document.getElementById('btnClearHistory');

// Global Proxy Toggle & Live IP Checking
const elBtnToggleProxy = document.getElementById('btnToggleProxy');
const elProxyStatusStateText = document.getElementById('proxyStatusStateText');
const elProxyTogglePill = document.getElementById('proxyTogglePill');
const elActiveIpAddress = document.getElementById('activeIpAddress');

function isProxyEnabledGlobally() {
    if (authenticatedUser && !authenticatedUser.canUseProxy) return false;
    const btn = elBtnToggleProxy || elProxyTogglePill;
    if (!btn) return true;
    return !btn.classList.contains('off');
}

document.getElementById('btnLogout')?.addEventListener('click', async () => {
    await fetch('/api/auth/logout', { method: 'POST' });
    location.href = '/login';
});
initializeAuthenticatedUi();

function updateProxyToggleUI(isEnabled) {
    const btn = elBtnToggleProxy || elProxyTogglePill;
    if (!btn) return;
    
    const knob = btn.querySelector('.proxy-pill-knob');
    if (isEnabled) {
        btn.classList.remove('off');
        btn.classList.add('active');
        btn.style.background = 'rgba(16, 185, 129, 0.15)';
        btn.style.borderColor = 'rgba(16, 185, 129, 0.4)';
        btn.style.color = '#34d399';
        if (knob) {
            knob.style.background = '#10b981';
            knob.style.boxShadow = '0 0 8px #10b981';
        }
        if (elProxyStatusStateText) elProxyStatusStateText.textContent = 'BẬT';
    } else {
        btn.classList.remove('active');
        btn.classList.add('off');
        btn.style.background = 'rgba(239, 68, 68, 0.15)';
        btn.style.borderColor = 'rgba(239, 68, 68, 0.4)';
        btn.style.color = '#f87171';
        if (knob) {
            knob.style.background = '#ef4444';
            knob.style.boxShadow = '0 0 8px #ef4444';
        }
        if (elProxyStatusStateText) elProxyStatusStateText.textContent = 'TẮT';
    }
}

async function fetchActiveIpAddress() {
    if (!elActiveIpAddress) return;
    elActiveIpAddress.innerHTML = '<i class="fa-solid fa-spinner fa-spin" style="color: var(--accent-green);"></i> <span style="font-size: 0.8rem; color: var(--text-muted);">Đang kiểm tra...</span>';
    
    const isProxy = isProxyEnabledGlobally();
    const proxyInput = elProxyUrl || elAdvProxyUrl;
    const proxyVal = isProxy ? (proxyInput ? proxyInput.value.trim() : '') : 'NONE';
    
    try {
        const query = isProxy ? `?proxy=${encodeURIComponent(proxyVal)}` : `?useProxy=false`;
        const res = await fetch(`/api/check-ip${query}`);
        const data = await res.json();
        
        if (data.success && data.ip) {
            if (data.isProxy) {
                elActiveIpAddress.innerHTML = `<span style="color: #34d399; font-weight: 700;">${data.ip}</span> <span class="badge-ip-proxy">🛡️ Proxy</span>`;
            } else {
                elActiveIpAddress.innerHTML = `<span style="color: #fbbf24; font-weight: 700;">${data.ip}</span> <span class="badge-ip-direct">⚡ Direct</span>`;
            }
        } else {
            elActiveIpAddress.innerHTML = `<span style="color: #f87171;">Lỗi kết nối IP</span>`;
        }
    } catch (e) {
        elActiveIpAddress.innerHTML = `<span style="color: #f87171;">Không thể kiểm tra IP</span>`;
    }
}

function initProxyToggleUI() {
    const btn = elBtnToggleProxy || elProxyTogglePill;
    if (!btn) return;
    const savedState = localStorage.getItem('app_global_proxy_enabled');
    const isEnabled = savedState === null ? true : savedState === 'true';
    updateProxyToggleUI(isEnabled);

    btn.addEventListener('click', (e) => {
        e.preventDefault();
        const newState = !isProxyEnabledGlobally();
        updateProxyToggleUI(newState);
        localStorage.setItem('app_global_proxy_enabled', newState ? 'true' : 'false');
        log(`[System] Proxy Web UI đã được ${newState ? 'BẬT 🛡️' : 'TẮT ⚠️'}`, newState ? 'info' : 'warning');
        
        // Re-check IP immediately when toggling
        fetchActiveIpAddress();
    });
}

document.addEventListener('DOMContentLoaded', () => {
    initProxyToggleUI();
    fetchActiveIpAddress();
});

// Add console log entry
function log(message, type = 'system') {
    if (!elConsoleLog) return;
    const item = document.createElement('div');
    item.className = `log-item ${type}`;
    const now = new Date().toLocaleTimeString();
    item.textContent = `[${now}] ${message}`;
    elConsoleLog.appendChild(item);
    elConsoleLog.scrollTop = elConsoleLog.scrollHeight;
}

// cURL command parser
function parseCurl(curlString) {
    if (!curlString) return null;
    
    // Replace backslash line wraps and carriage returns
    const cleanCurl = curlString.replace(/\\\r?\n/g, ' ').replace(/\n/g, ' ');
    const headers = {};
    
    // Parse headers: -H 'Key: Value' or -H "Key: Value" or --header "Key: Value"
    const headerRegex = /(?:-H|--header)\s+(['"])(.*?)\1/gi;
    let match;
    while ((match = headerRegex.exec(cleanCurl)) !== null) {
        const headerStr = match[2];
        const colonIndex = headerStr.indexOf(':');
        if (colonIndex !== -1) {
            const key = headerStr.substring(0, colonIndex).trim();
            const val = headerStr.substring(colonIndex + 1).trim();
            headers[key.toLowerCase()] = val;
        }
    }
    
    // Parse cookie flag if present: -b 'CookieString' or --cookie 'CookieString'
    const cookieRegex = /(?:-b|--cookie)\s+(['"])(.*?)\1/gi;
    const cookieMatch = cookieRegex.exec(cleanCurl);
    if (cookieMatch) {
        headers['cookie'] = cookieMatch[2];
    }
    
    if (Object.keys(headers).length === 0) {
        // Fallback: search for header-like shapes directly if quoting is mismatched
        const fallbackRegex = /(?:-H|--header)\s+([^\s'"\-]+:\s*[^\s'"]+)/gi;
        let fbMatch;
        while ((fbMatch = fallbackRegex.exec(cleanCurl)) !== null) {
            const headerStr = fbMatch[1];
            const colonIndex = headerStr.indexOf(':');
            if (colonIndex !== -1) {
                const key = headerStr.substring(0, colonIndex).trim();
                const val = headerStr.substring(colonIndex + 1).trim();
                headers[key.toLowerCase()] = val;
            }
        }
    }

    // Parse post data: --data-raw, --data, or -d
    const dataRegex = /(?:--data-raw|--data|-d)\s+(['"])(.*?)\1/gi;
    const dataMatch = dataRegex.exec(cleanCurl);
    let fReq = null;
    if (dataMatch) {
        let payload = dataMatch[2];
        if (payload.startsWith('f.req=')) {
            try {
                const encoded = payload.substring(6);
                const decoded = decodeURIComponent(encoded);
                fReq = JSON.parse(decoded);
            } catch (e) {
                console.error('Failed to parse f.req URLencoded:', e);
            }
        } else {
            try {
                fReq = JSON.parse(payload);
            } catch (e) {
                console.error('Failed to parse f.req JSON:', e);
            }
        }
    }
    
    return Object.keys(headers).length > 0 ? { headers, fReq } : null;
}

// Reset stats and UI before scan
function resetScanUI() {
    domainPagination.page = 1;
    elStatPages.textContent = '0';
    elStatAds.textContent = '0';
    elStatAdvertisers.textContent = '0';
    
    elProgressContainer.style.display = 'block';
    elProgressContainer.classList.remove('is-complete', 'is-paused', 'has-error');
    elProgressText.textContent = 'Đang bắt đầu quét...';
    elProgressPercent.textContent = '0%';
    elProgressBarFill.style.width = '0%';
    
    elTableBody.innerHTML = `
        <tr class="empty-row">
            <td colspan="8">Đang quét dữ liệu, vui lòng đợi...</td>
        </tr>
    `;
    
    elTableSearch.value = '';
    elTableSearch.disabled = true;
    elBtnExport.disabled = true;
    scrapedAdvertisers = [];
}

// Set UI status
function setStatus(status) {
    if (status === 'working') {
        elProgressContainer?.classList.remove('is-paused');
        elAppStatus.className = 'status-badge working';
        elAppStatus.innerHTML = '<span class="dot"></span> Đang chạy';
        elBtnStart.disabled = true;
        if (elBtnDomainQuickScan) elBtnDomainQuickScan.disabled = true;
        elBtnStop.disabled = false;
        elBtnFinish.disabled = false;
        isRunning = true;
    } else if (status === 'paused') {
        elProgressContainer?.classList.add('is-paused');
        elAppStatus.className = 'status-badge working';
        elAppStatus.innerHTML = '<span class="dot"></span> Tạm dừng';
        elBtnStart.disabled = true;
        if (elBtnDomainQuickScan) elBtnDomainQuickScan.disabled = true;
        elBtnStop.disabled = false;
        elBtnStop.innerHTML = '<i class="fa-solid fa-play"></i> Tiếp tục';
        elBtnFinish.disabled = false;
        isRunning = true;
    } else {
        elAppStatus.className = 'status-badge';
        elAppStatus.innerHTML = '<span class="dot"></span> Sẵn sàng';
        elBtnStart.disabled = false;
        if (elBtnDomainQuickScan) elBtnDomainQuickScan.disabled = false;
        elBtnStop.disabled = true;
        elBtnStop.innerHTML = '<i class="fa-solid fa-pause"></i> Tạm dừng';
        elBtnFinish.disabled = true;
        isRunning = false;
        isDomainScanPaused = false;
    }
}

// Render advertisers in the table
function renderTable(dataList) {
    workspaceScheduleSave();
    const visibleRows = paginateTableRows(dataList, domainPagination, {
        summary: elDomainPageSummary,
        indicator: elDomainPageIndicator,
        prev: elDomainPagePrev,
        next: elDomainPageNext
    });
    if (dataList.length === 0) {
        elTableBody.innerHTML = `
            <tr class="empty-row">
                <td colspan="9">Không tìm thấy nhà quảng cáo nào trùng khớp.</td>
            </tr>
        `;
        return;
    }

    elTableBody.innerHTML = '';
    const rowOffset = domainPagination.size === 'all' ? 0 : (domainPagination.page - 1) * Number(domainPagination.size || 300);
    visibleRows.forEach((adv, index) => {
        const row = document.createElement('tr');
        row.className = 'expandable-row';
        
        // STT with expandable arrow
        const tdIndex = document.createElement('td');
        if (adv.creativesList && adv.creativesList.length > 0) {
            tdIndex.innerHTML = `<button class="btn-expand-row" title="Mở/xem chi tiết từng mẫu QC"><i class="fa-solid fa-chevron-right icon-arrow"></i></button> ${rowOffset + index + 1}`;
        } else {
            tdIndex.textContent = rowOffset + index + 1;
        }
        row.appendChild(tdIndex);
        
        // Advertiser Name
        const tdName = document.createElement('td');
        tdName.style.fontWeight = '600';
        tdName.textContent = adv.name;
        row.appendChild(tdName);
        
        // Advertiser ID
        const tdId = document.createElement('td');
        tdId.style.fontFamily = 'monospace';
        tdId.style.fontSize = '0.85rem';
        if (adv.allIds && adv.allIds.length > 1) {
            tdId.innerHTML = `${adv.id} <span style="color: var(--accent-blue); font-size: 0.75rem;">(+${adv.allIds.length - 1} acc)</span>`;
            tdId.title = `Tất cả ID: ${adv.allIds.join(', ')}`;
        } else {
            tdId.textContent = adv.id;
        }
        row.appendChild(tdId);
        
        // Creative Count
        const tdCount = document.createElement('td');
        tdCount.className = 'text-center';
        tdCount.innerHTML = `<span class="badge-count" style="cursor: pointer;" title="Bấm để xem chi tiết">${adv.creativeCount}</span>`;
        row.appendChild(tdCount);
        
        // Formats
        const tdFormats = document.createElement('td');
        const formatContainer = document.createElement('div');
        formatContainer.className = 'format-tags';
        
        if (adv.formats['1']) formatContainer.innerHTML += `<span class="format-tag tag-image">Ảnh (${adv.formats['1']})</span>`;
        if (adv.formats['2']) formatContainer.innerHTML += `<span class="format-tag tag-text">Chữ (${adv.formats['2']})</span>`;
        if (adv.formats['3']) formatContainer.innerHTML += `<span class="format-tag tag-video">Video (${adv.formats['3']})</span>`;
        
        let otherCount = 0;
        Object.keys(adv.formats).forEach(key => {
            if (key !== '1' && key !== '2' && key !== '3') otherCount += adv.formats[key];
        });
        if (otherCount > 0) formatContainer.innerHTML += `<span class="format-tag tag-other">Khác (${otherCount})</span>`;
        
        tdFormats.appendChild(formatContainer);
        row.appendChild(tdFormats);
        
        // Total days running ads
        const tdTotalDays = document.createElement('td');
        tdTotalDays.className = 'text-center';
        if (adv.firstSeen && adv.lastSeen) {
            const dStart = new Date(adv.firstSeen);
            const dEnd = new Date(adv.lastSeen);
            const diffTime = Math.abs(dEnd - dStart);
            const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
            tdTotalDays.innerHTML = `<span class="badge-days">${diffDays} ngày</span>`;
        } else {
            tdTotalDays.innerHTML = `<span class="badge-days" style="opacity: 0.5;">N/A</span>`;
        }
        row.appendChild(tdTotalDays);
        
        // Duration active
        const tdDuration = document.createElement('td');
        tdDuration.style.fontSize = '0.85rem';
        tdDuration.style.color = 'var(--text-muted)';
        const start = adv.firstSeen || 'N/A';
        const end = adv.lastSeen || 'N/A';
        tdDuration.textContent = `${start} đến ${end}`;
        row.appendChild(tdDuration);

        const tdStatus = document.createElement('td');
        tdStatus.className = 'text-center';
        const today = new Date();
        const yesterday = new Date(today);
        yesterday.setDate(today.getDate() - 1);
        const toDateKey = value => `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}`;
        const isRunning = [toDateKey(today), toDateKey(yesterday)].includes(String(adv.lastSeen || '').slice(0, 10));
        adv.deploymentStatus = isRunning ? 'Đang triển khai' : 'Đã dừng';
        tdStatus.innerHTML = `<span class="deployment-status ${isRunning ? 'running' : 'stopped'}"><i class="fa-solid ${isRunning ? 'fa-circle-play' : 'fa-circle-stop'}"></i>${adv.deploymentStatus}</span>`;
        row.appendChild(tdStatus);
        
        // Link to transparency center
        const tdAction = document.createElement('td');
        tdAction.className = 'text-center';
        const link = document.createElement('a');
        link.className = 'table-link';
        link.href = `https://adstransparency.google.com/advertiser/${adv.id}?authuser=0&region=anywhere`;
        link.target = '_blank';
        link.rel = 'noopener noreferrer';
        link.className = 'advertiser-external-action';
        link.innerHTML = '<i class="fa-solid fa-arrow-up-right-from-square"></i><span>Chỉ mở trang Ads</span>';
        const lookupButton = document.createElement('button');
        lookupButton.type = 'button';
        lookupButton.className = 'btn btn-primary btn-sm advertiser-lookup-action';
        lookupButton.innerHTML = '<i class="fa-solid fa-magnifying-glass-chart"></i><span>Tra cứu & quét ngay</span>';
        lookupButton.title = `Chuyển sang Tra cứu Nhà QC và tự động quét ${adv.id}`;
        lookupButton.addEventListener('click', async event => {
            event.stopPropagation();
            await lookupAdvertiserFromDomainResult(adv.id);
        });
        const actionWrap = document.createElement('div');
        actionWrap.className = 'domain-advertiser-actions';
        actionWrap.append(lookupButton, link);
        tdAction.appendChild(actionWrap);
        row.appendChild(tdAction);
        
        // Create Expandable Details Row
        const detailsRow = document.createElement('tr');
        detailsRow.className = 'details-row';
        detailsRow.style.display = 'none';
        
        const tdDetails = document.createElement('td');
        tdDetails.colSpan = 9;
        tdDetails.style.padding = '0';
        
        const listHtml = (adv.creativesList && adv.creativesList.length > 0) ? adv.creativesList.map((c, idx) => {
            const fmtTag = c.format == 1 ? '<span class="format-tag tag-image">Ảnh</span>' 
                         : (c.format == 3 ? '<span class="format-tag tag-text">Chữ</span>' 
                         : (c.format == 2 ? '<span class="format-tag tag-video">Video</span>' 
                         : '<span class="format-tag tag-other">Khác</span>'));
            
            return `
                <tr>
                    <td class="text-center">${idx + 1}</td>
                    <td style="font-family: monospace; font-size: 0.8rem; color: #60a5fa;">${c.creativeId}</td>
                    <td>${fmtTag}</td>
                    <td>${c.firstSeen || 'N/A'}</td>
                    <td>${c.lastSeen || 'N/A'}</td>
                    <td class="text-center"><span class="badge-days">${c.daysActive} ngày</span></td>
                    <td class="text-center">
                        <a href="https://adstransparency.google.com/advertiser/${c.advertiserId}/creative/${c.creativeId}?authuser=0&region=anywhere" target="_blank" class="table-link" style="font-size: 0.8rem;">
                            <i class="fa-solid fa-up-right-from-square"></i> Xem mẫu QC
                        </a>
                    </td>
                </tr>
            `;
        }).join('') : '<tr><td colspan="7" class="text-center">Không có danh sách chi tiết</td></tr>';

        tdDetails.innerHTML = `
            <div class="details-accordion-content">
                <div class="details-accordion-header">
                    <i class="fa-solid fa-layer-group"></i> Thông số chi tiết ${adv.creativesList ? adv.creativesList.length : 0} mẫu quảng cáo của <strong>${adv.name}</strong>
                </div>
                <div class="table-scroll" style="margin: 0; max-height: 320px;">
                    <table class="sub-table">
                        <thead>
                            <tr>
                                <th class="text-center" style="width: 50px;">STT</th>
                                <th>Mã Mẫu QC (Creative ID)</th>
                                <th>Định dạng</th>
                                <th>Ngày bắt đầu</th>
                                <th>Ngày kết thúc</th>
                                <th class="text-center">Số ngày chạy mẫu này</th>
                                <th class="text-center">Hành động</th>
                            </tr>
                        </thead>
                        <tbody>
                            ${listHtml}
                        </tbody>
                    </table>
                </div>
            </div>
        `;
        
        detailsRow.appendChild(tdDetails);
        
        // Expand/Collapse event listener
        row.addEventListener('click', (e) => {
            if (e.target.closest('a')) return;
            const isHidden = detailsRow.style.display === 'none';
            detailsRow.style.display = isHidden ? 'table-row' : 'none';
            const arrow = row.querySelector('.icon-arrow');
            if (arrow) {
                arrow.style.transform = isHidden ? 'rotate(90deg)' : 'rotate(0deg)';
            }
        });

        elTableBody.appendChild(row);
        elTableBody.appendChild(detailsRow);
    });
}

// Start Scrape handler
async function startScrape() {
    resetExclusionSessionStats();
    const rawDomain = elTargetDomain.value.trim();
    const domain = normalizeProjectDomain(rawDomain);
    const curlCommandText = `curl 'https://adstransparency.google.com/' -H 'accept: application/json'`;
    const creativeLimit = Number(elDomainCreativeLimit ? elDomainCreativeLimit.value : 5000);
    partialDomainAdvertisers = [];
    
    if (!domain) {
        log('Lỗi: Bạn chưa nhập tên miền dự án!', 'error');
        alert('Vui lòng nhập tên miền cần quét!');
        return;
    }
    if (isExcludedDomain(domain)) { showMonitorToast(`Domain ${domain} đang nằm trong danh sách loại trừ.`, 'warning'); return; }

    if (domain !== rawDomain) {
        elTargetDomain.value = domain;
        try { localStorage.setItem('saved_target_domain', domain); } catch (e) {}
        log(`Domain đã được chuẩn hóa: ${rawDomain} → ${domain}`, 'info');
    }
    if (!Number.isSafeInteger(creativeLimit) || creativeLimit < 1 || creativeLimit > 1000000) {
        log('Lỗi: Giới hạn mẫu phải là số nguyên từ 1 đến 1.000.000.', 'error');
        alert('Vui lòng nhập giới hạn mẫu từ 1 đến 1.000.000.');
        return;
    }

    // Chạy song song để kết quả Volume & CPC sẵn sàng trong lúc quét quảng cáo.
    if (elBtnDomainMetrics && !elBtnDomainMetrics.disabled) void scanProjectDomainMetrics();
    
    if (!curlCommandText) {
        log('Lỗi: Cấu hình xác thực đang trống!', 'error');
        alert('Vui lòng liên hệ Admin để cập nhật xác thực Google!');
        return;
    }

    if (false && curlCommandText.includes('ABerymIoPaGmyJfTEct-ojebsfRZ')) {
        log('Lỗi: Cấu hình xác thực đã hết hạn!', 'error');
        alert('Cấu hình xác thực đã hết hạn. Vui lòng liên hệ Admin!');
        return;
    }
    
    log('Đang kiểm tra cấu hình xác thực...');
    const parsed = parseCurl(curlCommandText);
    
    if (!parsed) {
        log('Lỗi: Cấu hình xác thực không hợp lệ hoặc đã hết hạn!', 'error');
        alert('Cấu hình xác thực Google không hợp lệ. Vui lòng liên hệ Admin!');
        return;
    }

    const activeHeaders = parsed.headers;
    const activeFReq = parsed.fReq || {};

    const startDateVal = elStartDate ? elStartDate.value : '';
    const endDateVal = elEndDate ? elEndDate.value : '';

    if (startDateVal) {
        if (!activeFReq['3']) activeFReq['3'] = {};
        activeFReq['3']['6'] = parseInt(startDateVal.replace(/-/g, ''), 10);
    }
    if (endDateVal) {
        if (!activeFReq['3']) activeFReq['3'] = {};
        activeFReq['3']['7'] = parseInt(endDateVal.replace(/-/g, ''), 10);
    }
    if (!applyCountryFilter(activeFReq, elDomainCountry)) {
        showMonitorToast('Hãy chọn một quốc gia hợp lệ trong danh sách.', 'error');
        elDomainCountry?.focus();
        return;
    }
    
    log('Xác thực thành công! Đang khởi tạo kết nối...', 'success');
    setStatus('working');
    resetScanUI();
    
    const isProxyEnabled = isProxyEnabledGlobally();
    const proxyVal = isProxyEnabled ? (elProxyUrl ? elProxyUrl.value.trim() : '') : 'NONE';

    abortController = new AbortController();
    activeDomainScanId = (window.crypto && typeof window.crypto.randomUUID === 'function')
        ? window.crypto.randomUUID()
        : `scan-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
    isDomainScanPaused = false;
    
    try {
        const response = await fetch('/api/extract', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                domain,
                headers: activeHeaders,
                fReq: activeFReq,
                proxy: proxyVal,
                disableProxy: !isProxyEnabled,
                limit: creativeLimit,
                scanId: activeDomainScanId,
                dateRangePreset: elDateRangePreset?.value || '7'
            }),
            signal: abortController.signal
        });
        
        if (!response.ok) {
            const errorText = await response.text();
            throw new Error(errorText || `Yêu cầu thất bại với trạng thái ${response.status}`);
        }
        
        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let buffer = '';
        
        while (true) {
            const { done, value } = await reader.read();
            if (done) break;
            
            buffer += decoder.decode(value, { stream: true });
            const lines = buffer.split('\n');
            buffer = lines.pop(); // Keep last incomplete line in buffer
            
            for (const line of lines) {
                if (!line.trim()) continue;
                
                try {
                    const chunk = JSON.parse(line);
                    
                    if (chunk.type === 'status') {
                        log(chunk.message, 'info');
                    } else if (chunk.type === 'scan') {
                        activeDomainScanId = chunk.scanId;
                        log(`Phiên quét ${chunk.scanId} — giới hạn ${chunk.limit.toLocaleString('vi-VN')} mẫu.`, 'info');
                    } else if (chunk.type === 'paused') {
                        isDomainScanPaused = true;
                        setStatus('paused');
                        log(chunk.message, 'system');
                    } else if (chunk.type === 'resumed') {
                        isDomainScanPaused = false;
                        setStatus('working');
                        elBtnStop.innerHTML = '<i class="fa-solid fa-pause"></i> Tạm dừng';
                        log(chunk.message, 'success');
                    } else if (chunk.type === 'ip') {
                        const elIp = document.getElementById('activeIpAddress');
                        if (elIp) elIp.textContent = chunk.ip;
                    } else if (chunk.type === 'progress') {
                        // Update stats
                        elStatPages.textContent = chunk.page;
                        elStatAds.textContent = chunk.accumulatedCreativesCount;
                        elStatAdvertisers.textContent = chunk.accumulatedAdvertisersCount;
                        
                        // Calculate percentage dynamically
                        let percent = 0;
                        if (chunk.totalEstimatedAds > 0) {
                            const progressTarget = Math.min(chunk.totalEstimatedAds, chunk.limit || chunk.totalEstimatedAds);
                            percent = Math.min(99, Math.round((chunk.accumulatedCreativesCount / progressTarget) * 100));
                            elProgressText.textContent = `Đang quét: đã tải ${chunk.accumulatedCreativesCount} / ${progressTarget} mẫu quảng cáo.`;
                        } else {
                            percent = Math.min(95, chunk.page * 8);
                            elProgressText.textContent = `Đang quét: tìm thấy ${chunk.accumulatedCreativesCount} mẫu quảng cáo.`;
                        }
                        
                        elProgressPercent.textContent = `${percent}%`;
                        elProgressBarFill.style.width = `${percent}%`;
                    } else if (chunk.type === 'checkpoint') {
                        partialDomainAdvertisers = Array.isArray(chunk.advertisers) ? chunk.advertisers : partialDomainAdvertisers;
                    } else if (chunk.type === 'done') {
                        elProgressContainer.classList.remove('is-paused', 'has-error');
                        elProgressContainer.classList.add('is-complete');
                        const finishText = chunk.reason === 'user_finished'
                            ? 'Đã dừng lấy thêm và xử lý dữ liệu hiện có'
                            : chunk.reason === 'limit_reached'
                                ? `Đã đạt giới hạn ${chunk.limit.toLocaleString('vi-VN')} mẫu`
                                : 'Hoàn thành';
                        log(`${finishText}! Thu được ${chunk.totalCreatives} quảng cáo của ${chunk.totalAdvertisers} nhà quảng cáo.`, 'success');
                        
                        scrapedAdvertisers = (chunk.advertisers || []).filter(adv => !isExcludedAdvertiser(adv.id) && !(adv.allIds || []).some(isExcludedAdvertiser));
                        partialDomainAdvertisers = scrapedAdvertisers.slice();
                        
                        // Final UI update
                        elStatPages.textContent = elStatPages.textContent;
                        elStatAds.textContent = chunk.totalCreatives;
                        elStatAdvertisers.textContent = chunk.totalAdvertisers;
                        
                        elProgressText.textContent = 'Hoàn thành việc thu thập dữ liệu!';
                        elProgressPercent.textContent = '100%';
                        elProgressBarFill.style.width = '100%';
                        
                        renderTable(scrapedAdvertisers);
                        
                        // Enable export and filters
                        elTableSearch.disabled = false;
                        elFormatFilter.disabled = false;
                        elDateRangeFilter.disabled = false;
                        elFilterStartDate.disabled = false;
                        elFilterEndDate.disabled = false;
                        elBtnExport.disabled = false;

                        // Save snapshot into History Log
                        saveScanHistory('domain', elTargetDomain.value.trim(), {
                            advCount: scrapedAdvertisers.length,
                            creativeCount: chunk.totalCreatives || scrapedAdvertisers.reduce((acc, a) => acc + (a.creativeCount || 0), 0)
                        }, { scrapedAdvertisers });
                        showMonitorToast(`Quét domain hoàn tất: ${chunk.totalCreatives || 0} quảng cáo từ ${chunk.totalAdvertisers || 0} nhà quảng cáo.`);
                    } else if (chunk.type === 'error') {
                        elProgressContainer.classList.add('has-error');
                        if (Array.isArray(chunk.advertisers) && chunk.advertisers.length) {
                            partialDomainAdvertisers = chunk.advertisers;
                            log(`Đã giữ checkpoint gồm ${partialDomainAdvertisers.length} nhà quảng cáo. Có thể bấm “Xử lý dữ liệu đã lấy”.`, 'system');
                        }
                        if (/\b429\b|too many requests/i.test(String(chunk.error || ''))) showRateLimitNotice();
                        log(`Lỗi API: ${chunk.error}`, 'error');
                        alert(`Quá trình quét bị lỗi: ${chunk.error}`);
                    }
                } catch (e) {
                    console.error('Error parsing chunk:', e);
                }
            }
        }
        
    } catch (err) {
        if (err.name === 'AbortError') {
            log('Đã dừng quét theo yêu cầu của bạn.', 'system');
        } else {
            log(`Lỗi kết nối: ${err.message}`, 'error');
            alert(`Lỗi kết nối: ${err.message}`);
        }
    } finally {
        setStatus('idle');
        if (partialDomainAdvertisers.length) {
            elBtnFinish.disabled = false;
            elBtnFinish.innerHTML = '<i class="fa-solid fa-wand-magic-sparkles"></i> Xử lý dữ liệu đã lấy';
        }
        abortController = null;
        activeDomainScanId = null;
    }
}

async function scanProjectDomainMetrics() {
    const domain = normalizeProjectDomain(elTargetDomain?.value);
    if (!domain) {
        showMonitorToast('Hãy nhập tên miền dự án hợp lệ trước khi kiểm tra.', 'error');
        elTargetDomain?.focus();
        return;
    }

    const keyword = domainToBrandKeyword(domain);
    const originalHtml = elBtnDomainMetrics.innerHTML;
    elBtnDomainMetrics.disabled = true;
    elBtnDomainMetrics.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Đang kiểm tra';
    elDomainMetricsStatus.textContent = `Từ khóa đang tra cứu: ${keyword}`;
    elDomainMetricsStatus.classList.remove('error');

    try {
        const response = await fetch('/api/keyword-metrics', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ keyword, country: '', currency: 'usd' })
        });
        const result = await response.json();
        if (!response.ok || !result.success) throw new Error(result.error || `HTTP ${response.status}`);

        elDomainMetricVolume.textContent = result.volume == null ? 'Không có dữ liệu' : Number(result.volume).toLocaleString('vi-VN');
        elDomainMetricCpc.innerHTML = result.cpcValue == null
            ? 'Không có dữ liệu'
            : `${result.cpcCurrency || '$'}${result.cpcValue}<small class="metric-vnd">≈ ${(Number(result.cpcValue) * 26500).toLocaleString('vi-VN')} VNĐ</small>`;
        elDomainMetricCompetition.textContent = result.competition == null ? 'Không có dữ liệu' : Number(result.competition).toLocaleString('vi-VN', { maximumFractionDigits: 2 });
        elDomainMetricsResult.hidden = false;
        elDomainMetricsStatus.textContent = `Đã tra cứu từ khóa “${keyword}” thành công.`;
    } catch (error) {
        elDomainMetricsResult.hidden = true;
        elDomainMetricsStatus.textContent = `Không lấy được dữ liệu: ${error.message}`;
        elDomainMetricsStatus.classList.add('error');
    } finally {
        elBtnDomainMetrics.disabled = false;
        elBtnDomainMetrics.innerHTML = originalHtml;
    }
}

async function sendDomainScanAction(action) {
    if (!activeDomainScanId) return false;
    const response = await fetch(`/api/extract/${encodeURIComponent(activeDomainScanId)}/${action}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' }
    });
    const result = await response.json();
    if (!response.ok) throw new Error(result.error || `Không thể ${action} phiên quét.`);
    return true;
}

// Pause/resume without discarding data already collected by the server.
async function stopScrape() {
    try {
        if (isDomainScanPaused) {
            await sendDomainScanAction('resume');
            isDomainScanPaused = false;
            setStatus('working');
            elBtnStop.innerHTML = '<i class="fa-solid fa-pause"></i> Tạm dừng';
            log('Đã gửi lệnh tiếp tục quét.', 'success');
        } else {
            await sendDomainScanAction('pause');
            isDomainScanPaused = true;
            setStatus('paused');
            log('Đã gửi lệnh tạm dừng; hệ thống sẽ dừng sau trang đang xử lý.');
        }
    } catch (error) {
        log(`Không thể điều khiển phiên quét: ${error.message}`, 'error');
    }
}

async function finishDomainScrape() {
    try {
        elBtnFinish.disabled = true;
        if (!activeDomainScanId) {
            processPartialDomainData();
            return;
        }
        await sendDomainScanAction('finish');
        elProgressText.textContent = 'Đang hoàn tất và xử lý dữ liệu đã thu được...';
        log('Đã yêu cầu dừng lấy thêm và xử lý dữ liệu hiện có.');
    } catch (error) {
        elBtnFinish.disabled = false;
        log(`Không thể kết thúc phiên quét: ${error.message}`, 'error');
    }
}

function processPartialDomainData() {
    if (!partialDomainAdvertisers.length) {
        elBtnFinish.disabled = true;
        return;
    }
    scrapedAdvertisers = partialDomainAdvertisers.slice();
    const totalCreatives = scrapedAdvertisers.reduce((sum, advertiser) => sum + Number(advertiser.creativeCount || 0), 0);
    elStatAds.textContent = totalCreatives;
    elStatAdvertisers.textContent = scrapedAdvertisers.length;
    elTableSearch.disabled = false;
    elFormatFilter.disabled = false;
    elDateRangeFilter.disabled = false;
    elFilterStartDate.disabled = false;
    elFilterEndDate.disabled = false;
    elBtnExport.disabled = false;
    applyFilters();
    saveScanHistory('domain', elTargetDomain.value.trim(), {
        advCount: scrapedAdvertisers.length,
        creativeCount: totalCreatives
    }, { scrapedAdvertisers });
    elProgressText.textContent = `Đã xử lý dữ liệu tạm: ${totalCreatives} mẫu quảng cáo.`;
    elBtnFinish.disabled = true;
    elBtnFinish.innerHTML = '<i class="fa-solid fa-check"></i> Dừng & xử lý dữ liệu';
    showMonitorToast(`Đã khôi phục và xử lý ${totalCreatives} quảng cáo từ checkpoint.`);
}

function exportRowsToExcel(filename, headers, rows) {
    const escapeCell = value => String(value ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
    const table = `<table border="1"><thead><tr>${headers.map(value => `<th>${escapeCell(value)}</th>`).join('')}</tr></thead><tbody>${rows.map(row => `<tr>${row.map(value => `<td>${escapeCell(value)}</td>`).join('')}</tr>`).join('')}</tbody></table>`;
    const content = `\uFEFF<html><head><meta charset="UTF-8"></head><body>${table}</body></html>`;
    const blob = new Blob([content], { type: 'application/vnd.ms-excel;charset=utf-8;' });
    const url = URL.createObjectURL(blob);
    const anchor = document.createElement('a');
    anchor.href = url;
    anchor.download = filename.replace(/\.csv$/i, '.xls');
    document.body.appendChild(anchor);
    anchor.click();
    anchor.remove();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
}

// Export bảng Excel thật theo từng ô/cột, không phụ thuộc dấu phân cách vùng.
function exportToCSV() {
    if (scrapedAdvertisers.length === 0) return;
    
    log('Đang chuẩn bị file CSV...');
    const domain = elTargetDomain.value.trim().replace(/[^a-zA-Z0-9]/g, '_');
    const filename = `advertisers_${domain}_${new Date().toISOString().split('T')[0]}.xls`;
    const rows = scrapedAdvertisers.map((adv, index) => {
        const id = adv.id;
        const name = adv.name;
        const count = adv.creativeCount;
        const img = adv.formats['1'] || 0;
        const txt = adv.formats['2'] || 0;
        const vid = adv.formats['3'] || 0;
        
        let other = 0;
        Object.keys(adv.formats).forEach(k => {
            if (k !== '1' && k !== '2' && k !== '3') other += adv.formats[k];
        });
        
        const first = adv.firstSeen || 'N/A';
        const last = adv.lastSeen || 'N/A';
        const link = `https://adstransparency.google.com/?region=anywhere&advertiser=${adv.id}`;
        
        const today = new Date(); const yesterday = new Date(today); yesterday.setDate(today.getDate() - 1);
        const key = d => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
        const status = [key(today), key(yesterday)].includes(String(last).slice(0, 10)) ? 'Đang triển khai' : 'Đã dừng';
        return [index + 1, name, id, count, img, txt, vid, other, first, last, status, link];
    });
    exportRowsToExcel(filename, ['STT','Tên Nhà Quảng Cáo','ID Nhà Quảng Cáo','Số Lượng Quảng Cáo','Định Dạng Ảnh','Định Dạng Chữ','Định Dạng Video','Khác','Ngày Bắt Đầu','Ngày Cuối Cùng','Trạng Thái','Link Google Transparency'], rows);
    
    log(`Xuất thành công file ${filename}!`, 'success');
}

// Search & Multi-Criteria Filtering
function applyFilters() {
    const query = elTableSearch.value.toLowerCase().trim();
    const formatVal = elFormatFilter.value;
    const dateRangeVal = elDateRangeFilter.value;
    const customStart = elFilterStartDate.value;
    const customEnd = elFilterEndDate.value;
    
    const now = new Date();
    
    const filtered = scrapedAdvertisers.filter(adv => {
        // 1. Filter by Name or ID
        let matchQuery = true;
        if (query) {
            const matchName = adv.name.toLowerCase().includes(query);
            const matchId = adv.id.toLowerCase().includes(query) || (adv.allIds && adv.allIds.some(i => i.toLowerCase().includes(query)));
            matchQuery = matchName || matchId;
        }
        
        // 2. Filter by Creative Format (1=Image, 2=Text, 3=Video)
        let matchFormat = true;
        if (formatVal !== 'all') {
            matchFormat = (adv.formats[formatVal] || 0) > 0;
        }
        
        // 3. Filter by Time / Active Duration
        let matchTime = true;
        if (dateRangeVal === '7days' || dateRangeVal === '30days' || dateRangeVal === '90days') {
            const daysLimit = parseInt(dateRangeVal, 10);
            const cutoff = new Date();
            cutoff.setDate(now.getDate() - daysLimit);
            const cutoffStr = cutoff.toISOString().split('T')[0];
            if (adv.lastSeen) {
                matchTime = adv.lastSeen >= cutoffStr;
            }
        } else if (['2026', '2025', '2024', '2023'].includes(dateRangeVal)) {
            const startYear = adv.firstSeen ? adv.firstSeen.substring(0, 4) : '';
            const endYear = adv.lastSeen ? adv.lastSeen.substring(0, 4) : '';
            matchTime = (startYear === dateRangeVal || endYear === dateRangeVal);
        } else if (dateRangeVal === 'custom') {
            if (customStart && adv.lastSeen && adv.lastSeen < customStart) {
                matchTime = false;
            }
            if (customEnd && adv.firstSeen && adv.firstSeen > customEnd) {
                matchTime = false;
            }
        }
        
        return matchQuery && matchFormat && matchTime;
    });
    
    filtered.sort((a, b) => compareTableValues(
        advertiserSortValue(a, domainTableSort.key),
        advertiserSortValue(b, domainTableSort.key),
        domainTableSort.direction
    ));
    updateSortHeaders('[data-domain-sort]', domainTableSort);
    renderTable(filtered);
}

document.querySelectorAll('[data-domain-sort]').forEach(header => {
    header.addEventListener('click', () => {
        const key = header.dataset.domainSort;
        domainTableSort = {
            key,
            direction: domainTableSort.key === key && domainTableSort.direction === 'asc' ? 'desc' : 'asc'
        };
        domainPagination.page = 1;
        applyFilters();
    });
});

// Bind Events
elBtnStart.addEventListener('click', startScrape);
elBtnDomainQuickScan?.addEventListener('click', () => {
    if (isRunning || elBtnStart.disabled) return;
    startScrape();
});
elTargetDomain?.addEventListener('keydown', event => {
    if (event.key !== 'Enter' || event.isComposing || event.repeat) return;
    event.preventDefault();
    if (isRunning || elBtnStart.disabled) return;
    startScrape();
});
elBtnStop.addEventListener('click', stopScrape);
elBtnFinish.addEventListener('click', finishDomainScrape);
elBtnExport.addEventListener('click', exportToCSV);

// Default cURL Command from User Template
const DEFAULT_CURL = `curl 'https://adstransparency.google.com/anji/_/rpc/SearchService/SearchCreatives?authuser=0' \\
  -H 'accept: */*' \\
  -H 'accept-language: vi,en;q=0.9,en-GB;q=0.8,en-US;q=0.7' \\
  -H 'content-type: application/x-www-form-urlencoded' \\
  -b '_ga=GA1.1.638983587.1769479237; HSID=AOzfzwz4lpTUtSrfP; SSID=AM-u2SyiTK3zyh20E; APISID=JT69jJX1EJ5KbK5l/AUQLQC6Hjt_ipF-_S; SAPISID=qYDeKyIO5fEXHgU-/AB_lAYz0ekpzORXkA; __Secure-1PAPISID=qYDeKyIO5fEXHgU-/AB_lAYz0ekpzORXkA; __Secure-3PAPISID=qYDeKyIO5fEXHgU-/AB_lAYz0ekpzORXkA; __Secure-BUCKET=CP8F; _ga_YMYR0M0J94=deleted; ADS_VISITOR_ID=00000000-0000-0000-0000-000000000000; AEC=AdJVEas7C6eknlWP9HwpCYXChhML4flRix8uOJsGU2v4gzW8b19XllG9rg; SID=g.a000Agncval-tN-2p4c6S-mc9yGFOl8vQOmc4FS4atfZKvyzfPbmfCkxpo9i6t6LVtDVqxNltAACgYKAdgSARASFQHGX2MiZ5IPDKYXHXZ6YQ35kCUzAxoVAUF8yKoYcVkh6AH4ejWWeIkwqIDn0076; __Secure-1PSID=g.a000Agncval-tN-2p4c6S-mc9yGFOl8vQOmc4FS4atfZKvyzfPbmHSlx8yWATxOzvjvf1U3kawACgYKARMSARASFQHGX2MiJR7Vs3glrsEV-3YRKBCe8xoVAUF8yKozkqgauPK8OrE3aPhEoaUe0076; __Secure-3PSID=g.a000Agncval-tN-2p4c6S-mc9yGFOl8vQOmc4FS4atfZKvyzfPbmP5KKs0B7AYz4P2xATvbmLgACgYKAewSARASFQHGX2MiNWoXayWBYmTCc6tLcAXL_BoVAUF8yKpQNuAHfSgNefmu2RqiOu090076; S=billing-ui-v3=7vrSYsotZ7u8SsNEpe5i0TsCQCHBf592:billing-ui-v3-efe=7vrSYsotZ7u8SsNEpe5i0TsCQCHBf592:maestro=dSyFa6zpPMIv6rmD9k_z6-X--pynL7pmoVtFhOryE7I;  ; __Secure-3PSIDTS=sidts-CjIBPWEu2RmJW7IvD-3A00bEWGe7Z7zh3vemPakN_wyh7pbn-RkTW2lp4jJTjpQmgFG_5RAA; SIDCC=AKEyXzWSFVKdR3ihIlIcrlQngdu1iB6fy3GKfACTOty5om4NUd4pwVAAFG8HsuEdKwmZ732v58g; __Secure-1PSIDCC=AKEyXzV8QRaSmL-nfMjLU5AtIOPaSEzlqQamLFwQg23Gi5purS60Az17CrzG7qHIjxQzT7tY7g; __Secure-3PSIDCC=AKEyXzX3xjmc_ns8PKWEYr1I8TZDlLWg-2K5iDY0-UXm7JmbdU2DNjGt4usyzNYpygFhpvwiDOA' \\
  -H 'origin: https://adstransparency.google.com' \\
  -H 'priority: u=1, i' \\
  -H 'referer: https://adstransparency.google.com/?region=anywhere&domain=nuphy.com' \\
  -H 'sec-ch-ua: "Not;A=Brand";v="8", "Chromium";v="150", "Microsoft Edge";v="150"' \\
  -H 'sec-ch-ua-mobile: ?0' \\
  -H 'sec-ch-ua-platform: "macOS"' \\
  -H 'sec-fetch-dest: empty' \\
  -H 'sec-fetch-mode: cors' \\
  -H 'sec-fetch-site: same-origin' \\
  -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0' \\
  -H 'x-browser-channel: stable' \\
  -H 'x-browser-copyright: Copyright 2026 Google LLC. All Rights Reserved.' \\
  -H 'x-browser-validation: 9VleGbLep3vn3Yse2zzlhFbTCd4=' \\
  -H 'x-browser-year: 2026' \\
  -H 'x-framework-xsrf-token: ABerymIoPaGmyJfTEct-ojebsfRZ:1784516101536' \
  -H 'x-same-domain: 1' \
  --data-raw 'f.req=%7B%222%22%3A40%2C%223%22%3A%7B%2212%22%3A%7B%221%22%3A%22nuphy.com%22%2C%222%22%3Atrue%7D%7D%2C%227%22%3A%7B%221%22%3A1%2C%222%22%3A22%2C%223%22%3A2704%7D%7D'`;
elTableSearch.addEventListener('input', () => { domainPagination.page = 1; applyFilters(); });
elFormatFilter.addEventListener('change', () => { domainPagination.page = 1; applyFilters(); });
elDateRangeFilter.addEventListener('change', () => {
    if (elDateRangeFilter.value === 'custom') {
        elCustomDateRangeBox.style.display = 'flex';
    } else {
        elCustomDateRangeBox.style.display = 'none';
    }
    domainPagination.page = 1;
    applyFilters();
});
elFilterStartDate.addEventListener('change', () => { domainPagination.page = 1; applyFilters(); });
elFilterEndDate.addEventListener('change', () => { domainPagination.page = 1; applyFilters(); });
elBtnClearCustomDate.addEventListener('click', () => {
    elFilterStartDate.value = '';
    elFilterEndDate.value = '';
    elDateRangeFilter.value = 'all';
    elCustomDateRangeBox.style.display = 'none';
    domainPagination.page = 1;
    applyFilters();
});

if (elDomainPageSize) {
    elDomainPageSize.value = domainPagination.size;
    elDomainPageSize.addEventListener('change', () => {
        domainPagination.size = elDomainPageSize.value;
        domainPagination.page = 1;
        localStorage.setItem('domain_table_page_size', domainPagination.size);
        applyFilters();
    });
}
elDomainPagePrev?.addEventListener('click', () => { domainPagination.page--; applyFilters(); });
elDomainPageNext?.addEventListener('click', () => { domainPagination.page++; applyFilters(); });

// Default cURL Command from User Template

// Default cURL initialized


// Element bindings (declared at top of file)

let advController = null;
let advertiserCreatives = [];
let filteredAdvertiserCreatives = [];
let detectedDomainsMap = {}; // domain -> { count, sources: Set, ads: [], keyword, metrics }
let advNicheByDomain = new Map();

const SHEET_FIELD_LABELS = {
    domain: 'Domain dự án',
    firstSeen: 'Ngày bắt đầu',
    lastSeen: 'Ngày kết thúc',
    runningDays: 'Chạy trong bao nhiêu ngày rồi',
    keyword: 'Từ khóa thương hiệu',
    volume: 'Khối lượng tìm kiếm',
    cpcValue: 'Giá thầu trung bình',
    competition: 'Độ cạnh tranh',
    activeAdvertisersCount: 'Nhà quảng cáo đang chạy',
    adCount: 'Số quảng cáo',
    source: 'Nguồn nhận dạng'
};

function domainToBrandKeyword(domain) {
    const hostname = normalizeProjectDomain(domain);
    if (!hostname) return '';
    const parts = hostname.split('.').filter(Boolean);
    if (parts.length === 1) return parts[0].replace(/[-_]+/g, ' ').trim();
    const commonSecondLevelSuffixes = new Set(['co.uk', 'com.au', 'com.br', 'com.vn', 'co.jp', 'co.kr', 'co.nz', 'com.sg', 'com.my', 'com.tw']);
    const suffix2 = parts.slice(-2).join('.');
    const brandIndex = commonSecondLevelSuffixes.has(suffix2) ? parts.length - 3 : parts.length - 2;
    return (parts[Math.max(0, brandIndex)] || parts[0]).replace(/[-_]+/g, ' ').trim();
}

function logAdv(message, type = 'system') {
    const item = document.createElement('div');
    item.className = `log-item ${type}`;
    const now = new Date().toLocaleTimeString();
    item.textContent = `[${now}] ${message}`;
    elAdvConsoleLog.appendChild(item);
    elAdvConsoleLog.scrollTop = elAdvConsoleLog.scrollHeight;
}

// Tabs switching handler
const tabButtons = document.querySelectorAll('.tab-btn');
const tabContents = document.querySelectorAll('.tab-content');

function switchTab(targetTab) {
    tabButtons.forEach(b => {
        if (b.getAttribute('data-tab') === targetTab) {
            b.classList.add('active');
        } else {
            b.classList.remove('active');
        }
    });
    tabContents.forEach(c => {
        if (c.id === `${targetTab}-content`) {
            c.classList.add('active');
        } else {
            c.classList.remove('active');
        }
    });
    const historyMenu = document.getElementById('historyTabMenu');
    const historyTrigger = document.getElementById('historyTabTrigger');
    const isHistoryTab = ['tab-history', 'tab-api', 'tab-visitors'].includes(targetTab);
    historyMenu?.classList.toggle('has-active-tab', isHistoryTab);
    if (historyTrigger && isHistoryTab) {
        const activeLabel = historyMenu?.querySelector(`.history-menu-item[data-tab="${targetTab}"] strong`);
        historyTrigger.querySelector('span').textContent = activeLabel?.textContent || 'Lịch sử';
    } else if (historyTrigger) historyTrigger.querySelector('span').textContent = 'Lịch sử';
    try {
        localStorage.setItem('saved_active_tab', targetTab);
    } catch (e) {}
}

async function lookupAdvertiserFromDomainResult(advertiserId) {
    const id = String(advertiserId || '').trim();
    if (!id) return showMonitorToast('Không tìm thấy ID Nhà quảng cáo hợp lệ.', 'error');
    if (elBtnAdvStart?.disabled) return showMonitorToast('Một phiên Tra cứu Nhà QC đang chạy. Hãy đợi phiên hiện tại hoàn tất hoặc dừng lại.', 'warning');
    switchTab('tab-adv');
    if (elAdvScanMode) {
        elAdvScanMode.value = 'single';
        elAdvScanMode.dispatchEvent(new Event('change'));
    }
    elTargetAdvId.value = id;
    try { localStorage.setItem('saved_target_adv_id', id); } catch (_) {}
    elTargetAdvId.scrollIntoView({ behavior: 'smooth', block: 'center' });
    showMonitorToast(`Đang tự động tra cứu Nhà quảng cáo ${id}...`);
    await runAdvertiserScanByMode();
}

tabButtons.forEach(btn => {
    btn.addEventListener('click', () => {
        const targetTab = btn.getAttribute('data-tab');
        switchTab(targetTab);
        if (targetTab === 'tab-api') loadApiCatalog();
        if (targetTab === 'tab-visitors') loadVisitorHistory();
        if (targetTab === 'tab-trace-domains') loadTraceDomainHistory().catch(error => showMonitorToast(error.message, 'error'));
        closeHistoryTabMenu();
    });
});

const historyTabMenu = document.getElementById('historyTabMenu');
const historyTabTrigger = document.getElementById('historyTabTrigger');
const historyTabDropdown = document.getElementById('historyTabDropdown');
function closeHistoryTabMenu() {
    if (!historyTabDropdown || !historyTabTrigger) return;
    historyTabDropdown.hidden = true;
    historyTabTrigger.setAttribute('aria-expanded', 'false');
    historyTabMenu?.classList.remove('open');
}
if (historyTabTrigger) historyTabTrigger.addEventListener('click', event => {
    event.stopPropagation();
    const willOpen = Boolean(historyTabDropdown?.hidden);
    if (historyTabDropdown) historyTabDropdown.hidden = !willOpen;
    historyTabTrigger.setAttribute('aria-expanded', String(willOpen));
    historyTabMenu?.classList.toggle('open', willOpen);
});
document.addEventListener('click', event => { if (!historyTabMenu?.contains(event.target)) closeHistoryTabMenu(); });
document.addEventListener('keydown', event => { if (event.key === 'Escape') { closeHistoryTabMenu(); historyTabTrigger?.focus(); } });

let activeApiHistoryCategory = 'internal';

document.querySelectorAll('.api-history-tab').forEach(button => {
    button.addEventListener('click', () => {
        activeApiHistoryCategory = button.dataset.apiCategory;
        document.querySelectorAll('.api-history-tab').forEach(tab => {
            tab.classList.toggle('active', tab === button);
        });
        loadApiCatalog();
    });
});

const btnClearApiHistory = document.getElementById('btnClearApiHistory');
if (btnClearApiHistory) {
    btnClearApiHistory.addEventListener('click', async () => {
        if (!await showAppConfirm('Bạn có chắc muốn xóa toàn bộ lịch sử API call không?', { danger: true, confirmText: 'Xóa lịch sử' })) return;
        btnClearApiHistory.disabled = true;
        try {
            const response = await fetch('/api/call-history', { method: 'DELETE' });
            const result = await response.json();
            if (!response.ok) throw new Error(result.error || `HTTP ${response.status}`);
            await loadApiCatalog();
        } catch (error) {
            alert(`Không thể xóa lịch sử API: ${error.message}`);
        } finally {
            btnClearApiHistory.disabled = false;
        }
    });
}

let apiCatalogRequestController = null;

async function loadApiCatalog(force = false) {
    const baseUrl = window.location.origin;
    const baseUrlEl = document.getElementById('apiBaseUrl');
    const statusEl = document.getElementById('apiStatus');
    const listEl = document.getElementById('apiEndpointList');
    const environmentEl = document.getElementById('apiEnvironment');
    const copyBaseButton = document.getElementById('btnCopyBaseUrl');
    if (!baseUrlEl || !statusEl || !listEl || !environmentEl || !copyBaseButton) return;

    if (apiCatalogRequestController && !force) return;
    if (apiCatalogRequestController && force) apiCatalogRequestController.abort();
    const requestController = new AbortController();
    apiCatalogRequestController = requestController;
    const requestTimeout = setTimeout(() => requestController.abort(), 8000);

    baseUrlEl.textContent = baseUrl;
    copyBaseButton.onclick = () => loadApiCatalog(true);
    statusEl.textContent = 'Đang tải lịch sử API...';

    try {
        const openDetailKeys = new Set(
            Array.from(listEl.querySelectorAll('details[open][data-detail-key]'))
                .map(details => details.dataset.detailKey)
        );
        const response = await fetch('/api/call-history', {
            cache: 'no-store',
            signal: requestController.signal
        });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const history = await response.json();

        const elChkHideCompetitorMonitor = document.getElementById('chkHideCompetitorMonitor');
        if (elChkHideCompetitorMonitor && !elChkHideCompetitorMonitor.dataset.bound) {
            elChkHideCompetitorMonitor.dataset.bound = 'true';
            const savedSetting = localStorage.getItem('hide_competitor_monitor_api');
            // Mặc định là ẩn (checked = true) nếu người dùng chưa chọn
            elChkHideCompetitorMonitor.checked = savedSetting === null ? true : savedSetting === 'true';
            elChkHideCompetitorMonitor.addEventListener('change', () => {
                localStorage.setItem('hide_competitor_monitor_api', elChkHideCompetitorMonitor.checked ? 'true' : 'false');
                loadApiCatalog();
            });
        }
        const isHideMonitor = elChkHideCompetitorMonitor ? elChkHideCompetitorMonitor.checked : false;

        environmentEl.textContent = 'Tự động cập nhật';
        environmentEl.classList.add('is-local');
        const counts = history.calls.reduce((result, call) => {
            if (isHideMonitor && call.target && call.target.includes('/api/competitor-monitor')) {
                return result;
            }
            const category = call.category || (call.direction.includes('nội bộ') ? 'internal' : 'external');
            result[category] = (result[category] || 0) + 1;
            return result;
        }, { internal: 0, external: 0, proxy: 0 });
        document.getElementById('apiCountInternal').textContent = counts.internal;
        document.getElementById('apiCountExternal').textContent = counts.external;
        document.getElementById('apiCountProxy').textContent = counts.proxy;

        const filteredCalls = history.calls.filter(call => {
            if (isHideMonitor && call.target && call.target.includes('/api/competitor-monitor')) {
                return false;
            }
            const category = call.category || (call.direction.includes('nội bộ') ? 'internal' : 'external');
            return category === activeApiHistoryCategory;
        });
        statusEl.textContent = `${filteredCalls.length}/${history.total} API call • ${history.persistent ? 'Redis dùng chung' : 'RAM tạm thời'} • Cập nhật lúc ${new Date().toLocaleTimeString('vi-VN')}`;
        statusEl.classList.toggle('warning', !history.persistent && location.hostname.endsWith('.vercel.app'));
        listEl.innerHTML = '';

        if (!history.persistent && location.hostname.endsWith('.vercel.app')) {
            const warning = document.createElement('div');
            warning.className = 'api-storage-warning';
            warning.textContent = 'Vercel chưa kết nối Redis nên lịch sử không được chia sẻ giữa các instance. Hãy cấu hình UPSTASH_REDIS_REST_URL và UPSTASH_REDIS_REST_TOKEN.';
            listEl.appendChild(warning);
        }

        if (!filteredCalls.length) {
            const empty = document.createElement('div');
            empty.className = 'api-history-empty';
            empty.textContent = 'Chưa có API call nào trong nhóm này. Hãy thực hiện một thao tác quét để xem lịch sử.';
            listEl.appendChild(empty);
            return;
        }

        filteredCalls.forEach(call => {
            const card = document.createElement('article');
            card.className = 'api-endpoint-card';
            const top = document.createElement('div');
            top.className = 'api-endpoint-top';
            const title = document.createElement('div');
            title.className = 'api-endpoint-title';
            const method = document.createElement('span');
            method.className = `api-method method-${call.method.toLowerCase()}`;
            method.textContent = call.method;
            const name = document.createElement('strong');
            name.textContent = call.direction;
            title.append(method, name);

            const status = document.createElement('span');
            const successful = Number(call.status) >= 200 && Number(call.status) < 400;
            status.className = `api-call-status ${successful ? 'success' : 'error'}`;
            status.textContent = `${call.status} • ${call.durationMs} ms`;
            top.append(title, status);

            const url = document.createElement('code');
            url.className = 'api-endpoint-url';
            url.textContent = call.target;
            const meta = document.createElement('div');
            meta.className = 'api-endpoint-meta';
            meta.textContent = new Date(call.timestamp).toLocaleString('vi-VN');
            card.append(top, url, meta);

            const connectionName = call.connection || call.proxyConnection;
            if (connectionName) {
                const connection = document.createElement('div');
                connection.className = 'api-proxy-ip';
                const connectionLabel = document.createElement('span');
                connectionLabel.textContent = 'Kết nối';
                const connectionValue = document.createElement('strong');
                connectionValue.textContent = connectionName;
                connection.append(connectionLabel, connectionValue);
                card.appendChild(connection);
            }

            if (call.proxyIpChecked) {
                const proxyIp = document.createElement('div');
                proxyIp.className = 'api-proxy-ip';
                const proxyLabel = document.createElement('span');
                proxyLabel.textContent = call.proxyIpSource === 'TrafficLens response' ? 'IP được TrafficLens xác nhận' : 'IP proxy kiểm tra gần thời điểm call';
                const proxyValue = document.createElement('strong');
                proxyValue.textContent = call.proxyIpChecked;
                proxyIp.append(proxyLabel, proxyValue);
                card.appendChild(proxyIp);
            } else if (call.trafficProvider === 'trafficlens' && call.proxyIpUnavailable) {
                const proxyIp = document.createElement('div');
                proxyIp.className = 'api-proxy-ip';
                const proxyLabel = document.createElement('span');
                proxyLabel.textContent = 'IP của request';
                const proxyValue = document.createElement('strong');
                proxyValue.textContent = 'TrafficLens không phản hồi IP nguồn — không gắn IP suy đoán';
                proxyIp.append(proxyLabel, proxyValue);
                card.appendChild(proxyIp);
            }

            if (call.error) {
                const errorBox = document.createElement('div');
                errorBox.className = 'api-error-message';
                const errorTitle = document.createElement('strong');
                errorTitle.textContent = 'Lỗi: ';
                const errorText = document.createElement('span');
                errorText.textContent = call.error;
                errorBox.append(errorTitle, errorText);
                card.appendChild(errorBox);
            }

            const receivedData = call.responseBody ?? call.errorResponse;
            if (receivedData !== null && receivedData !== undefined) {
                const resDetails = document.createElement('details');
                resDetails.className = 'api-payload-details api-response-details';
                resDetails.dataset.detailKey = `${call.id}-response`;
                resDetails.open = openDetailKeys.has(resDetails.dataset.detailKey) || (Boolean(call.error) && openDetailKeys.size === 0);
                const resSummary = document.createElement('summary');
                resSummary.textContent = 'Dữ liệu nhận về (Response)';
                const resPayload = document.createElement('pre');
                resPayload.textContent = typeof receivedData === 'string'
                    ? receivedData
                    : JSON.stringify(receivedData, null, 2);
                resDetails.append(resSummary, resPayload);
                card.appendChild(resDetails);
            }

            if (call.payload && Object.keys(call.payload).length) {
                const details = document.createElement('details');
                details.className = 'api-payload-details';
                details.dataset.detailKey = `${call.id}-payload`;
                details.open = openDetailKeys.has(details.dataset.detailKey);
                const summary = document.createElement('summary');
                summary.textContent = 'Dữ liệu gửi đi (Request)';
                const payload = document.createElement('pre');
                payload.textContent = JSON.stringify(call.payload, null, 2);
                details.append(summary, payload);
                card.appendChild(details);
            }

            listEl.appendChild(card);
        });
    } catch (error) {
        if (requestController.signal.aborted && apiCatalogRequestController !== requestController) return;
        environmentEl.textContent = 'Không kết nối';
        statusEl.textContent = requestController.signal.aborted
            ? 'Không tải được lịch sử API sau 8 giây. Hãy bấm Làm mới để thử lại.'
            : `Không tải được thông tin API: ${error.message}`;
        statusEl.classList.add('error');
    } finally {
        clearTimeout(requestTimeout);
        if (apiCatalogRequestController === requestController) apiCatalogRequestController = null;
    }
}

setInterval(() => {
    if (document.getElementById('tab-api-content')?.classList.contains('active')) {
        loadApiCatalog();
    }
}, 5000);

async function collectWebRtcInfo() {
    if (!window.RTCPeerConnection) {
        return { supported: false, status: 'unsupported', ips: [], candidates: [] };
    }

    return new Promise(async resolve => {
        const ips = new Set();
        const candidates = [];
        let finished = false;
        const peer = new RTCPeerConnection({
            iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
        });

        const finish = status => {
            if (finished) return;
            finished = true;
            peer.close();
            resolve({ supported: true, status, ips: Array.from(ips), candidates });
        };

        const timeout = setTimeout(() => finish(candidates.length ? 'partial' : 'not-exposed'), 3500);
        peer.addEventListener('icecandidate', event => {
            if (!event.candidate) {
                clearTimeout(timeout);
                finish(candidates.length ? 'collected' : 'not-exposed');
                return;
            }
            const candidate = event.candidate;
            const address = candidate.address || null;
            const masked = Boolean(address && address.endsWith('.local'));
            candidates.push({
                address,
                masked,
                type: candidate.type || null,
                protocol: candidate.protocol || null,
                port: candidate.port || null
            });
            if (address && !masked) ips.add(address);
        });

        try {
            peer.createDataChannel('visitor-ip-check');
            const offer = await peer.createOffer();
            await peer.setLocalDescription(offer);
        } catch (error) {
            clearTimeout(timeout);
            finish(`error: ${error.message}`);
        }
    });
}

async function captureVisitorAccess() {
    try {
        const webrtc = await collectWebRtcInfo();
        await fetch('/api/visitor-log', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                pageUrl: location.href,
                language: navigator.language,
                languages: navigator.languages,
                platform: navigator.userAgentData?.platform || navigator.platform || null,
                timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
                viewport: { width: innerWidth, height: innerHeight },
                screen: { width: screen.width, height: screen.height, colorDepth: screen.colorDepth },
                devicePixelRatio: window.devicePixelRatio,
                hardwareConcurrency: navigator.hardwareConcurrency,
                deviceMemory: navigator.deviceMemory,
                cookieEnabled: navigator.cookieEnabled,
                online: navigator.onLine,
                webrtc
            }),
            keepalive: true
        });
    } catch (error) {
        console.warn('Visitor access logging failed:', error);
    }
}

function appendVisitorField(container, label, value) {
    const row = document.createElement('div');
    row.className = 'visitor-field';
    const labelEl = document.createElement('span');
    labelEl.textContent = label;
    const valueEl = document.createElement('strong');
    valueEl.textContent = value === null || value === undefined || value === '' ? 'Không có' : String(value);
    row.append(labelEl, valueEl);
    container.appendChild(row);
}

function formatTimeAgo(timestamp) {
    const seconds = Math.max(0, Math.floor((Date.now() - new Date(timestamp).getTime()) / 1000));
    if (seconds < 10) return 'Vừa xong';
    if (seconds < 60) return `${seconds} giây trước`;
    const minutes = Math.floor(seconds / 60);
    if (minutes < 60) return `${minutes} phút trước`;
    const hours = Math.floor(minutes / 60);
    if (hours < 24) return `${hours} giờ trước`;
    const days = Math.floor(hours / 24);
    return `${days} ngày trước`;
}

async function loadVisitorHistory() {
    const list = document.getElementById('visitorHistoryList');
    const status = document.getElementById('visitorHistoryStatus');
    if (!list || !status) return;

    const openKeys = new Set(
        Array.from(list.querySelectorAll('details[open][data-visitor-key]'))
            .map(details => details.dataset.visitorKey)
    );

    try {
        const response = await fetch('/api/visitor-history', { cache: 'no-store' });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const data = await response.json();
        status.textContent = `${data.total} IP duy nhất • ${data.totalAccesses || data.total} tổng lượt • ${data.persistent ? 'Redis dùng chung' : 'RAM tạm thời'} • ${new Date().toLocaleTimeString('vi-VN')}`;
        list.innerHTML = '';

        if (!data.visitors.length) {
            const empty = document.createElement('div');
            empty.className = 'api-history-empty';
            empty.textContent = 'Chưa có lịch sử IP truy cập.';
            list.appendChild(empty);
            return;
        }

        data.visitors.forEach(visitor => {
            const card = document.createElement('article');
            card.className = 'visitor-card';
            const header = document.createElement('div');
            header.className = 'visitor-card-header';
            const ipGroup = document.createElement('div');
            const requestIp = document.createElement('strong');
            requestIp.className = 'visitor-primary-ip';
            requestIp.textContent = `IP request: ${visitor.requestIp || 'Không lấy được'}`;
            const time = document.createElement('span');
            time.className = 'visitor-last-seen';
            time.textContent = `Truy cập gần nhất: ${formatTimeAgo(visitor.latestAccess || visitor.timestamp)} • ${new Date(visitor.latestAccess || visitor.timestamp).toLocaleString('vi-VN')}`;
            const count = document.createElement('span');
            count.className = 'visitor-access-count';
            count.textContent = `${visitor.accessCount || 1} lượt truy cập`;
            ipGroup.append(requestIp, time, count);
            const rtcIp = document.createElement('span');
            const rtcIps = visitor.webrtc?.ips || [];
            rtcIp.className = `visitor-webrtc-badge ${rtcIps.length ? 'found' : 'missing'}`;
            rtcIp.textContent = rtcIps.length ? `WebRTC: ${rtcIps.join(', ')}` : `WebRTC: ${visitor.webrtc?.status || 'Không lấy được'}`;
            header.append(ipGroup, rtcIp);

            const fields = document.createElement('div');
            fields.className = 'visitor-fields-grid';
            appendVisitorField(fields, 'Vị trí', [visitor.geo?.city, visitor.geo?.region, visitor.geo?.country].filter(Boolean).join(', '));
            appendVisitorField(fields, 'Tọa độ', visitor.geo?.latitude && visitor.geo?.longitude ? `${visitor.geo.latitude}, ${visitor.geo.longitude}` : null);
            appendVisitorField(fields, 'Múi giờ', visitor.geo?.timezone || visitor.browser?.timezone);
            appendVisitorField(fields, 'Referer', visitor.request?.referer);
            appendVisitorField(fields, 'Origin', visitor.request?.origin);
            appendVisitorField(fields, 'Trang truy cập', visitor.request?.pageUrl);
            appendVisitorField(fields, 'Thiết bị', visitor.browser?.secChUaPlatform || visitor.browser?.platform);
            appendVisitorField(fields, 'Ngôn ngữ', visitor.browser?.acceptLanguage || visitor.browser?.language);

            const details = document.createElement('details');
            details.className = 'visitor-details';
            details.dataset.visitorKey = visitor.id;
            details.open = openKeys.has(visitor.id);
            const summary = document.createElement('summary');
            summary.textContent = 'Xem toàn bộ thông tin';
            const pre = document.createElement('pre');
            pre.textContent = JSON.stringify(visitor, null, 2);
            details.append(summary, pre);
            card.append(header, fields, details);
            list.appendChild(card);
        });
    } catch (error) {
        status.textContent = `Không tải được lịch sử IP: ${error.message}`;
    }
}

document.getElementById('btnRefreshVisitorHistory')?.addEventListener('click', loadVisitorHistory);
document.getElementById('btnClearVisitorHistory')?.addEventListener('click', async () => {
    if (!await showAppConfirm('Bạn có chắc muốn xóa toàn bộ lịch sử IP truy cập không?', { danger: true, confirmText: 'Xóa lịch sử' })) return;
    try {
        const response = await fetch('/api/visitor-history', { method: 'DELETE' });
        const result = await response.json();
        if (!response.ok) throw new Error(result.error || `HTTP ${response.status}`);
        await loadVisitorHistory();
    } catch (error) {
        alert(`Không thể xóa lịch sử IP: ${error.message}`);
    }
});

setInterval(() => {
    if (document.getElementById('tab-visitors-content')?.classList.contains('active')) {
        loadVisitorHistory();
    }
}, 5000);

captureVisitorAccess();

// Restore saved active tab on page load
try {
    const savedTab = localStorage.getItem('saved_active_tab');
    if (savedTab && document.getElementById(`${savedTab}-content`)) {
        switchTab(savedTab);
        if (savedTab === 'tab-api') loadApiCatalog();
        if (savedTab === 'tab-visitors') loadVisitorHistory();
    }
} catch (e) {}

// Helper: Add domain to map
function addDetectedDomain(domain, source, creative = null, skipHistoryUpdate = false) {
    const cleanDomain = normalizeProjectDomain(domain);
    if (!cleanDomain || cleanDomain === 'null' || isExcludedDomain(cleanDomain) || isExcludedAdvertiser(creative?.advertiserId)) return;

    if (!detectedDomainsMap[cleanDomain]) {
        detectedDomainsMap[cleanDomain] = {
            count: 0,
            sources: new Set(),
            ads: [],
            keyword: domainToBrandKeyword(cleanDomain),
            metrics: null,
            metricsLoading: false,
            metricsError: '',
            metricsErrorDetails: null,
            summaryLoading: false,
            summaryError: '',
            summaryErrorDetails: null,
            trafficError: '',
            trafficErrorDetails: null,
            inDomainStore: null,
            inGoogleSheet: null
        };
    }
    const info = detectedDomainsMap[cleanDomain];
    info.sources.add(source);

    if (creative && creative.creativeId) {
        const existingAd = info.ads.find(ad => ad.creativeId === creative.creativeId);
        if (!existingAd) {
            info.ads.push({
                creativeId: creative.creativeId,
                advertiserId: creative.advertiserId || (elTargetAdvId ? elTargetAdvId.value.trim() : ''),
                format: creative.format,
                firstSeen: creative.firstSeen,
                lastSeen: creative.lastSeen
            });
        }
        info.count = info.ads.length;
    } else if (info.ads.length === 0) {
        info.count++;
    }

    elStatAdvDomains.textContent = Object.keys(detectedDomainsMap).length;
    if (elBtnScanKeywordMetrics) elBtnScanKeywordMetrics.disabled = Object.keys(detectedDomainsMap).length === 0;
    if (elBtnScanAdvertiserCounts) elBtnScanAdvertiserCounts.disabled = Object.keys(detectedDomainsMap).length === 0;
    if (elBtnScanTraffic) elBtnScanTraffic.disabled = Object.keys(detectedDomainsMap).length === 0;
    if (elBtnPushGoogleSheet) elBtnPushGoogleSheet.disabled = Object.keys(detectedDomainsMap).length === 0;
    if (!skipHistoryUpdate) updateCurrentAdvHistoryRecord();
}

async function compareAdvertiserDomainDuplicates() {
    const candidates = [...new Set(Object.keys(detectedDomainsMap).map(normalizeProjectDomain).filter(Boolean))];
    if (!candidates.length) return;
    const sheetDomains = new Set();
    let sheetCompared = false;
    try {
        const response = await fetch('/api/project-registrations/check', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ domains: candidates }), cache: 'no-store' });
        const result = await response.json().catch(() => ({}));
        if (!response.ok) throw new Error(result.error || `HTTP ${response.status}`);
        (result.registrations || []).map(item => projectDomainIdentity(item.domain)).filter(Boolean).forEach(domain => sheetDomains.add(domain));
        var sheetRegistrants = new Map((result.registrations || []).map(item => [projectDomainIdentity(item.domain), String(item.registrant || '').trim()]));
        sheetCompared = Boolean(result.synced);
    } catch (error) { logAdv(`Không thể đối chiếu Google Sheet: ${error.message}`, 'warning'); }
    if (authenticatedUser?.role !== 'admin' && !authenticatedUser?.canUseDomainStore) {
        Object.entries(detectedDomainsMap).forEach(([domain, info]) => { const clean = projectDomainIdentity(domain); info.inDomainStore = null; info.domainComparisonError = ''; info.inGoogleSheet = sheetCompared ? sheetDomains.has(clean) : null; info.sheetRegistrant = sheetCompared ? (sheetRegistrants?.get(clean) || '') : ''; });
        renderAdvDomainTable();
        updateCurrentAdvHistoryRecord();
        return;
    }
    let storeDomains = new Set();
    try {
        const response = await fetch('/api/advertiser/domains/check', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ domains: candidates }) });
        const result = await response.json().catch(() => ({}));
        if (response.status === 404) {
            // Tương thích với tiến trình server cũ chưa nạp route check hàng loạt.
            const checks = [];
            await runDomainWorkerPool(candidates, 5, async domain => {
                const params = new URLSearchParams({ q: domain, page: '1', pageSize: '25' });
                const fallbackResponse = await fetch(`/api/trace/domains?${params}`, { cache: 'no-store' });
                const fallback = await fallbackResponse.json().catch(() => ({}));
                if (!fallbackResponse.ok) throw new Error(fallback.error || `HTTP ${fallbackResponse.status}`);
                if ((fallback.domains || []).map(normalizeProjectDomain).includes(domain)) checks.push(domain);
            });
            storeDomains = new Set(checks);
        } else {
            if (!response.ok) throw new Error(result.error || `HTTP ${response.status}`);
            storeDomains = new Set((result.existing || []).map(projectDomainIdentity).filter(Boolean));
        }
    } catch (error) {
        Object.entries(detectedDomainsMap).forEach(([domain, info]) => { const clean = projectDomainIdentity(domain); info.inDomainStore = null; info.domainComparisonError = error.message; info.inGoogleSheet = sheetCompared ? sheetDomains.has(clean) : null; info.sheetRegistrant = sheetCompared ? (sheetRegistrants?.get(clean) || '') : ''; });
        renderAdvDomainTable();
        throw new Error(`Không thể kiểm tra Kho Domain: ${error.message}`);
    }
    Object.entries(detectedDomainsMap).forEach(([domain, info]) => { const clean = projectDomainIdentity(domain); info.inDomainStore = storeDomains.has(clean); info.domainComparisonError = ''; info.inGoogleSheet = sheetCompared ? sheetDomains.has(clean) : null; info.sheetRegistrant = sheetCompared ? (sheetRegistrants?.get(clean) || '') : ''; });
    renderAdvDomainTable();
    updateCurrentAdvHistoryRecord();
    logAdv(`Đối chiếu hoàn tất: ${storeDomains.size} domain đã có trong Kho${sheetCompared ? `, ${[...sheetDomains].filter(domain => candidates.includes(domain)).length} domain đã có trong Google Sheet` : ', Google Sheet chưa được đối chiếu'}. Không tự động thêm hoặc xóa domain.`, 'success');
}

function calculateActivePeriod(ads) {
    const starts = ads.map(ad => ad.firstSeen).filter(Boolean).sort();
    const ends = ads.map(ad => ad.lastSeen).filter(Boolean).sort();
    if (starts.length === 0 || ends.length === 0) {
        return { firstSeen: null, lastSeen: null, days: 0 };
    }

    const firstSeen = starts[0];
    const lastSeen = ends[ends.length - 1];
    const start = new Date(`${firstSeen}T00:00:00`);
    const end = new Date(`${lastSeen}T00:00:00`);
    const days = Math.max(1, Math.round((end - start) / 86400000) + 1);
    return { firstSeen, lastSeen, days };
}

// Helper: Render Domain stats table
function isDomainCurrentlyActive(period) {
    if (!period.lastSeen) return false;
    const lastSeen = new Date(`${period.lastSeen}T23:59:59`);
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    return !Number.isNaN(lastSeen.getTime()) && lastSeen >= today;
}

function getFilteredAdvDomainEntries() {
    const searchQuery = elAdvDomainSearch ? elAdvDomainSearch.value.toLowerCase().trim() : '';
    const deployment = elAdvDeploymentFilter?.value || '';
    const daysRange = elAdvRunningDaysFilter?.value || '';
    const minVolume = elAdvMinVolumeFilter?.value === '' ? null : Math.max(0, Number(elAdvMinVolumeFilter.value));
    const minTraffic = elAdvMinTrafficFilter?.value === '' ? null : Math.max(0, Number(elAdvMinTrafficFilter.value));
    const niche = elAdvNicheFilter?.value || '';
    const duplicateStatus = elAdvDuplicateFilter?.value || '';
    Object.entries(detectedDomainsMap).forEach(([domain, info]) => {
        const clean = normalizeProjectDomain(domain), current = traceInsightCache.get(clean) || {};
        traceInsightCache.set(clean, { ...current, ads: Array.isArray(info.ads) ? info.ads : current.ads, volume: info.metrics?.volume ?? current.volume, traffic: info.traffic?.visits ?? current.traffic, niche: advNicheByDomain.get(clean) || info.niche || current.niche || '' });
    });
    return Object.entries(detectedDomainsMap)
        .filter(([domain, info]) => {
            const period = calculateActivePeriod(Array.isArray(info.ads) ? info.ads : []);
            const volume = info.metrics?.volume;
            const traffic = info.traffic?.visits;
            const domainNiche = advNicheByDomain.get(normalizeProjectDomain(domain)) || info.niche || '';
            if (searchQuery && !domain.toLowerCase().includes(searchQuery)) return false;
            if (deployment === 'active' && !isDomainCurrentlyActive(period)) return false;
            if (daysRange && period.days < Number(daysRange)) return false;
            if (minVolume !== null && (!Number.isFinite(Number(volume)) || Number(volume) < minVolume)) return false;
            if (minTraffic !== null && (!Number.isFinite(Number(traffic)) || Number(traffic) < minTraffic)) return false;
            if (niche && domainNiche !== niche) return false;
            if (duplicateStatus === 'new-both' && !(info.inDomainStore === false && info.inGoogleSheet === false)) return false;
            if (duplicateStatus === 'duplicate-any' && !(info.inDomainStore === true || info.inGoogleSheet === true)) return false;
            if (duplicateStatus === 'store' && info.inDomainStore !== true) return false;
            if (duplicateStatus === 'sheet' && info.inGoogleSheet !== true) return false;
            if (duplicateStatus === 'both' && !(info.inDomainStore === true && info.inGoogleSheet === true)) return false;
            if (duplicateStatus === 'not-store' && info.inDomainStore !== false) return false;
            if (duplicateStatus === 'not-sheet' && info.inGoogleSheet !== false) return false;
            return true;
        })
        .sort((a, b) => {
            const value = ([domain, info]) => {
                const period = calculateActivePeriod(Array.isArray(info.ads) ? info.ads : []);
                const metrics = info.metrics || {};
                const values = {
                    domain,
                    keyword: info.keyword || domainToBrandKeyword(domain),
                    count: Number(info.count || 0),
                    days: Number(period.days || 0),
                    volume: Number(metrics.volume || 0),
                    cpc: Number(metrics.cpcValue || 0),
                    competition: Number(metrics.competition || 0),
                    traffic: Number(info.traffic?.visits || 0),
                    niche: advNicheByDomain.get(normalizeProjectDomain(domain)) || info.niche || '',
                    activeAdvertisers: Number(metrics.activeAdvertisersCount || 0),
                    source: Array.from(info.sources || []).join(', ')
                };
                return values[advDomainTableSort.key];
            };
            return compareTableValues(value(a), value(b), advDomainTableSort.direction);
        });
}

const ADV_DOMAIN_COLUMN_KEYS = ['registration', 'domain', 'keyword', 'adCount', 'runningTime', 'volume', 'cpc', 'competition', 'traffic', 'niche', 'activeAdvertisers', 'source', 'details'];
const ADV_DOMAIN_COLUMN_ORDER_KEY = 'adv_domain_column_order_v1';
let advDomainDraggedColumn = '';
let advDomainColumnDragJustEnded = false;

function getAdvDomainColumnOrder() {
    try {
        const saved = JSON.parse(localStorage.getItem(ADV_DOMAIN_COLUMN_ORDER_KEY) || '[]');
        if (Array.isArray(saved) && saved.length === ADV_DOMAIN_COLUMN_KEYS.length && ADV_DOMAIN_COLUMN_KEYS.every(key => saved.includes(key))) return saved;
    } catch (_) {}
    return [...ADV_DOMAIN_COLUMN_KEYS];
}

function applyAdvDomainColumnOrder() {
    const table = document.getElementById('advDomainResultsTable');
    if (!table) return;
    const order = getAdvDomainColumnOrder();
    const headerRow = table.tHead?.rows?.[0];
    if (headerRow) {
        [...headerRow.cells].forEach((cell, index) => { if (!cell.dataset.columnKey) cell.dataset.columnKey = ADV_DOMAIN_COLUMN_KEYS[index]; });
        const cells = new Map([...headerRow.cells].map(cell => [cell.dataset.columnKey, cell]));
        order.forEach(key => cells.get(key) && headerRow.appendChild(cells.get(key)));
    }
    [...(table.tBodies?.[0]?.rows || [])].forEach(row => {
        if (row.cells.length !== ADV_DOMAIN_COLUMN_KEYS.length) return;
        [...row.cells].forEach((cell, index) => { if (!cell.dataset.columnKey) cell.dataset.columnKey = ADV_DOMAIN_COLUMN_KEYS[index]; });
        const cells = new Map([...row.cells].map(cell => [cell.dataset.columnKey, cell]));
        order.forEach(key => cells.get(key) && row.appendChild(cells.get(key)));
    });
}

function initializeAdvDomainColumnReorder() {
    const table = document.getElementById('advDomainResultsTable');
    const headers = [...(table?.tHead?.rows?.[0]?.cells || [])];
    if (!headers.length) return;
    headers.forEach((header, index) => {
        header.dataset.columnKey = ADV_DOMAIN_COLUMN_KEYS[index];
        header.draggable = true;
        header.title = `${header.title ? `${header.title} · ` : ''}Giữ và kéo để đổi vị trí cột`;
        header.addEventListener('dragstart', event => {
            advDomainDraggedColumn = header.dataset.columnKey;
            event.dataTransfer.effectAllowed = 'move';
            event.dataTransfer.setData('text/plain', advDomainDraggedColumn);
            header.classList.add('column-dragging');
        });
        header.addEventListener('dragover', event => {
            if (!advDomainDraggedColumn || advDomainDraggedColumn === header.dataset.columnKey) return;
            event.preventDefault();
            event.dataTransfer.dropEffect = 'move';
            headers.forEach(item => item.classList.remove('column-drop-before', 'column-drop-after'));
            header.classList.add(event.clientX > header.getBoundingClientRect().left + header.offsetWidth / 2 ? 'column-drop-after' : 'column-drop-before');
        });
        header.addEventListener('drop', event => {
            event.preventDefault();
            const source = advDomainDraggedColumn || event.dataTransfer.getData('text/plain');
            const target = header.dataset.columnKey;
            if (!source || !target || source === target) return;
            const order = getAdvDomainColumnOrder().filter(key => key !== source);
            let targetIndex = order.indexOf(target);
            if (event.clientX > header.getBoundingClientRect().left + header.offsetWidth / 2) targetIndex++;
            order.splice(targetIndex, 0, source);
            localStorage.setItem(ADV_DOMAIN_COLUMN_ORDER_KEY, JSON.stringify(order));
            applyAdvDomainColumnOrder();
        });
        header.addEventListener('dragend', () => {
            advDomainDraggedColumn = '';
            headers.forEach(item => item.classList.remove('column-dragging', 'column-drop-before', 'column-drop-after'));
            advDomainColumnDragJustEnded = true;
            setTimeout(() => { advDomainColumnDragJustEnded = false; }, 0);
        });
    });
    applyAdvDomainColumnOrder();
}

function renderAdvDomainTable() {
    const sorted = getFilteredAdvDomainEntries();
    if (elBtnCompareAdvDomains) elBtnCompareAdvDomains.disabled = Object.keys(detectedDomainsMap).length === 0;
    if (elBtnAddAdvDomainsToStore) { const addable = sorted.filter(([, info]) => info.inDomainStore === false).length; elBtnAddAdvDomainsToStore.disabled = addable === 0; elBtnAddAdvDomainsToStore.innerHTML = `<i class="fa-solid fa-database"></i> Thêm vào Kho Domain${addable ? ` (${addable})` : ''}`; }
    updateSortHeaders('[data-adv-domain-sort]', advDomainTableSort);
    const visibleDomains = paginateTableRows(sorted, advDomainPagination, {
        summary: elAdvDomainPageSummary,
        indicator: elAdvDomainPageIndicator,
        prev: elAdvDomainPagePrev,
        next: elAdvDomainPageNext
    });
    if (sorted.length === 0) {
        elAdvDomainTableBody.innerHTML = `<tr class="empty-row"><td colspan="13" class="text-center">${Object.keys(detectedDomainsMap).length ? 'Không có tên miền phù hợp với bộ lọc.' : 'Chưa phát hiện được dự án nào'}</td></tr>`;
        return;
    }

    elAdvDomainTableBody.innerHTML = visibleDomains.map(([domain, info], index) => {
        const sourcesStr = Array.from(info.sources).join(', ');
        const ads = Array.isArray(info.ads) ? info.ads : [];
        const period = calculateActivePeriod(ads);
        const periodHtml = period.firstSeen
            ? `<strong>${period.days} ngày</strong><br><span style="font-size:0.72rem; color:var(--text-muted);">${period.firstSeen} → ${period.lastSeen}</span>`
            : '<span style="color:var(--text-muted);">Chưa có dữ liệu ngày</span>';
        const inlineErrorDetails = (message, details) => {
            const detailText = JSON.stringify(details || { message }, null, 2);
            const limitedText = detailText.length > 10000
                ? `${detailText.slice(0, 10000)}\n… [Đã rút gọn ${detailText.length - 10000} ký tự]`
                : detailText;
            return `<details class="keyword-metric-error" style="min-width:120px; max-width:360px;">
                <summary style="cursor:pointer; font-weight:700;">Lỗi — bấm xem</summary>
                <pre style="white-space:pre-wrap; max-height:260px; overflow:auto; margin-top:6px;">${escapeMonitorHtml(limitedText)}</pre>
            </details>`;
        };
        const metricCell = (value, formatter, showErrorDetails = false) => {
            if (info.metricsLoading) return '<span class="keyword-metric-empty">Đang lấy...</span>';
            if (info.metricsError) {
                return showErrorDetails
                    ? inlineErrorDetails(info.metricsError, info.metricsErrorDetails)
                    : `<span class="keyword-metric-error" title="Xem chi tiết tại cột Khối lượng tìm kiếm">Lỗi</span>`;
            }
            if (value === null || value === undefined || value === '') return '<span class="keyword-metric-empty">Chưa quét</span>';
            return `<span class="keyword-metric-value">${formatter ? formatter(value) : value}</span>`;
        };
        const metrics = info.metrics || {};
        const niche = advNicheByDomain.get(normalizeProjectDomain(domain)) || info.niche || '';
        const trafficCell = info.trafficLoading
            ? '<span class="keyword-metric-empty">Đang lấy...</span>'
            : info.trafficError
                ? inlineErrorDetails(info.trafficError, info.trafficErrorDetails)
                : info.traffic?.visits === null || info.traffic?.visits === undefined
                    ? '<span class="keyword-metric-empty">Chưa quét</span>'
                    : (() => {
                        const t = info.traffic;
                        const eng = t.engagements || {};
                        const tooltipLines = [
                            `Domain: ${t.domain || domain}`,
                            `Lượng truy cập: ${Number(t.visits).toLocaleString('vi-VN')} (${t.latestMonth || 'Gần đây'})`,
                            t.globalRank ? `Hạng thế giới: #${Number(t.globalRank).toLocaleString('vi-VN')}` : null,
                            t.countryRank ? `Hạng quốc gia: #${Number(t.countryRank).toLocaleString('vi-VN')}` : null,
                            t.category ? `Danh mục: ${t.category}${t.categoryRank ? ` (#${t.categoryRank})` : ''}` : null,
                            eng.BounceRate !== undefined ? `Tỷ lệ thoát (Bounce Rate): ${(Number(eng.BounceRate) * 100).toFixed(1)}%` : null,
                            eng.TimeOnSite !== undefined ? `Thời gian trên trang: ${Math.round(Number(eng.TimeOnSite))}s` : null,
                            eng.PageViews !== undefined ? `Trang/Lượt xem: ${Number(eng.PageViews).toFixed(2)}` : null,
                            t.profile ? `Nguồn dữ liệu: Profile ${t.profile}` : null
                        ].filter(Boolean).join('\n');
                        return `<div class="traffic-cell-with-trend"><div><span class="keyword-metric-value" title="${tooltipLines.replace(/"/g, '&quot;')}">${Number(t.visits).toLocaleString('vi-VN')} lượt</span>${t.latestMonth ? `<small class="metric-vnd">${t.latestMonth}</small>` : ''}</div>${renderTrafficTrend(t)}</div>`;
                    })();
        const detailRows = ads.length > 0 ? ads.map(ad => {
            const adPeriod = calculateActivePeriod([ad]);
            const advertiserId = ad.advertiserId || (elTargetAdvId ? elTargetAdvId.value.trim() : '');
            const adUrl = `https://adstransparency.google.com/advertiser/${encodeURIComponent(advertiserId)}/creative/${encodeURIComponent(ad.creativeId)}?region=anywhere`;
            return `
                <div style="display:grid; grid-template-columns:minmax(130px, 1fr) minmax(190px, 1.4fr) auto; gap:12px; align-items:center; padding:8px 10px; border-bottom:1px solid rgba(255,255,255,0.05);">
                    <strong style="font-family:monospace; color:var(--accent-blue);">${ad.creativeId}</strong>
                    <span>${ad.firstSeen || '-'} → ${ad.lastSeen || '-'}${adPeriod.days ? ` (${adPeriod.days} ngày)` : ''}</span>
                    <a href="${adUrl}" target="_blank" rel="noopener noreferrer" style="color:var(--accent-blue); text-decoration:underline;">Mở quảng cáo</a>
                </div>
            `;
        }).join('') : '<div style="padding:10px; color:var(--text-muted);">Domain được phát hiện qua OCR nhưng chưa liên kết được thông tin mẫu quảng cáo.</div>';

        return `
            <tr>
                <td><div class="domain-duplicate-status"><span class="domain-duplicate-label">Đăng ký</span>${info.inGoogleSheet === true ? `<span class="trace-status-pill success"><i class="fa-solid fa-user-check"></i> ${escapeMonitorHtml(info.sheetRegistrant || 'Đã có người đăng ký')}</span>` : info.inGoogleSheet === false ? '<span class="trace-status-pill"><i class="fa-solid fa-user-plus"></i> Chưa có người đăng ký</span>' : '<span class="keyword-metric-empty">Chưa đối chiếu Sheet</span>'}</div></td>
                <td style="font-weight:600;"><a href="https://${domain}" target="_blank" rel="noopener noreferrer" style="color:var(--accent-blue); text-decoration:none;">${domain} <i class="fa-solid fa-arrow-up-right-from-square" style="font-size:.65rem; opacity:.55;"></i></a></td>
                <td><strong>${info.keyword || domainToBrandKeyword(domain)}</strong></td>
                <td class="text-center"><span class="badge-count">${info.count}</span></td>
                <td>${periodHtml}</td>
                <td>${metricCell(metrics.volume, value => Number(value).toLocaleString('vi-VN'), true)}</td>
                <td>${metricCell(metrics.cpcValue, value => `${metrics.cpcCurrency || '$'}${value}<small class="metric-vnd">≈ ${(Number(value) * 26500).toLocaleString('vi-VN')} VNĐ</small>`)}</td>
                <td>${metricCell(metrics.competition, value => Number(value).toLocaleString('vi-VN', { maximumFractionDigits: 2 }))}</td>
                <td>${trafficCell}</td>
                <td>${niche ? `<span class="badge">${escapeMonitorHtml(niche)}</span>` : '<span class="keyword-metric-empty">Chưa phân loại</span>'}</td>
                <td>${info.summaryLoading
                    ? '<span class="keyword-metric-empty">Đang lấy...</span>'
                    : info.summaryError
                        ? `<details class="keyword-metric-error" style="max-width:260px;">
                            <summary style="cursor:pointer;">Lỗi: ${escapeMonitorHtml(String(info.summaryError).slice(0, 42))}</summary>
                            <pre style="white-space:pre-wrap; max-height:220px; overflow:auto; margin-top:6px;">${escapeMonitorHtml(JSON.stringify(info.summaryErrorDetails || [{ message: info.summaryError }], null, 2))}</pre>
                           </details>`
                        : metrics.activeAdvertisersCount === null || metrics.activeAdvertisersCount === undefined
                            ? '<span class="keyword-metric-empty">Chưa quét</span>'
                            : `<span class="keyword-metric-value">${Number(metrics.activeAdvertisersCount).toLocaleString('vi-VN')} nhà QC</span>`}</td>
                <td><span style="font-size:0.8rem; color:var(--text-muted); background:rgba(255,255,255,0.05); padding: 0.2rem 0.4rem; border-radius:4px;">${sourcesStr}</span></td>
                <td class="text-center">
                    <button class="btn btn-secondary btn-sm btn-domain-details" data-details-index="${index}" style="padding:0.3rem 0.55rem; font-size:0.76rem;">
                        <i class="fa-solid fa-chevron-down"></i> Xem ${ads.length || ''}
                    </button>
                </td>
            </tr>
            <tr id="domain-details-${index}" style="display:none;">
                <td colspan="13" style="padding:0; background:rgba(0,0,0,0.14);">
                    ${detailRows}
                </td>
            </tr>
        `;
    }).join('');

    applyAdvDomainColumnOrder();
    elAdvDomainTableBody.querySelectorAll('.btn-domain-details').forEach(button => {
        button.addEventListener('click', () => {
            const index = button.getAttribute('data-details-index');
            const detailRow = document.getElementById(`domain-details-${index}`);
            const isOpen = detailRow.style.display !== 'none';
            detailRow.style.display = isOpen ? 'none' : 'table-row';
            button.querySelector('i').className = `fa-solid fa-chevron-${isOpen ? 'down' : 'up'}`;
        });
    });
}

async function runDomainWorkerPool(entries, concurrency, worker) {
    if (!entries.length) return;
    let cursor = 0;
    const workerCount = Math.min(Math.max(1, concurrency), entries.length);
    await Promise.all(Array.from({ length: workerCount }, async () => {
        while (cursor < entries.length) {
            const currentIndex = cursor++;
            await worker(entries[currentIndex], currentIndex);
        }
    }));
}

function updateMetricRetryButtons() {
    const infos = Object.values(detectedDomainsMap);
    const metricErrors = infos.filter(info => Boolean(info.metricsError)).length;
    const advertiserErrors = infos.filter(info => Boolean(info.summaryError)).length;
    const trafficErrors = infos.filter(info => Boolean(info.trafficError)).length;
    if (elBtnRetryKeywordMetricsErrors) {
        elBtnRetryKeywordMetricsErrors.hidden = metricErrors === 0;
        elBtnRetryKeywordMetricsErrors.innerHTML = `<i class="fa-solid fa-rotate"></i> Quét lại CPC lỗi (${metricErrors})`;
    }
    if (elBtnRetryAdvertiserCountErrors) {
        elBtnRetryAdvertiserCountErrors.hidden = advertiserErrors === 0;
        elBtnRetryAdvertiserCountErrors.innerHTML = `<i class="fa-solid fa-rotate"></i> Quét lại Nhà QC lỗi (${advertiserErrors})`;
    }
    if (elBtnRetryTrafficErrors) {
        elBtnRetryTrafficErrors.hidden = trafficErrors === 0;
        elBtnRetryTrafficErrors.innerHTML = `<i class="fa-solid fa-rotate"></i> Quét lại Traffic lỗi (${trafficErrors})`;
    }
}

async function scanKeywordMetrics(onlyFailed = false) {
    const entries = Object.entries(detectedDomainsMap)
        .filter(([, info]) => !onlyFailed || Boolean(info.metricsError));
    if (!entries.length || !elBtnScanKeywordMetrics) return;
    elBtnScanKeywordMetrics.disabled = true;
    if (elBtnRetryKeywordMetricsErrors) elBtnRetryKeywordMetricsErrors.disabled = true;
    const originalHtml = elBtnScanKeywordMetrics.innerHTML;
    let completed = 0;
    // Worker pool động tối đa 50 request: xong domain nào lấy domain kế tiếp
    // ngay, không chờ cả batch và không bao giờ vượt quá 50 luồng.
    await runDomainWorkerPool(entries, 50, async ([domain, info]) => {
        info.keyword = info.keyword || domainToBrandKeyword(domain);
        info.metricsLoading = true;
        info.metricsError = '';
        info.metricsErrorDetails = null;
        renderAdvDomainTable();
        const startedAt = Date.now();
        try {
            const response = await fetch('/api/keyword-metrics', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ keyword: info.keyword, country: '', currency: 'usd' })
            });
            const responseText = await response.text();
            let result = null;
            try { result = responseText ? JSON.parse(responseText) : null; } catch (_) {}
            if (!response.ok || !result?.success) {
                const requestError = new Error(result?.error || `HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ''}`);
                requestError.details = {
                    type: 'api-error', domain, keyword: info.keyword,
                    endpoint: '/api/keyword-metrics', status: response.status,
                    statusText: response.statusText || '', durationMs: Date.now() - startedAt,
                    response: result || responseText.slice(0, 10000) || '(Phản hồi rỗng)'
                };
                throw requestError;
            }
            info.metrics = info.metrics || {};
            Object.assign(info.metrics, {
                volume: result.volume,
                cpcValue: result.cpcValue,
                cpcCurrency: result.cpcCurrency || '$',
                competition: result.competition
            });
        } catch (error) {
            info.metricsError = error.message || 'Không thể lấy CPC/Volume.';
            info.metricsErrorDetails = error.details || {
                type: error.name === 'AbortError' ? 'timeout' : 'network-error',
                domain, keyword: info.keyword, endpoint: '/api/keyword-metrics',
                durationMs: Date.now() - startedAt, name: error.name || 'Error',
                message: error.message || String(error)
            };
        } finally {
            info.metricsLoading = false;
            completed++;
            elBtnScanKeywordMetrics.innerHTML = `<i class="fa-solid fa-spinner fa-spin"></i> ${completed}/${entries.length}`;
            renderAdvDomainTable();
            updateCurrentAdvHistoryRecord();
        }
    });
    elBtnScanKeywordMetrics.innerHTML = originalHtml;
    elBtnScanKeywordMetrics.disabled = false;
    if (elBtnRetryKeywordMetricsErrors) elBtnRetryKeywordMetricsErrors.disabled = false;
    updateMetricRetryButtons();
}

async function scanAdvertiserCounts(onlyFailed = false) {
    const entries = Object.entries(detectedDomainsMap)
        .filter(([, info]) => !onlyFailed || Boolean(info.summaryError));
    if (!entries.length || !elBtnScanAdvertiserCounts) return;
    const proxy = elAdvProxyUrl ? elAdvProxyUrl.value.trim() : '';

    elBtnScanAdvertiserCounts.disabled = true;
    if (elBtnRetryAdvertiserCountErrors) elBtnRetryAdvertiserCountErrors.disabled = true;
    const originalHtml = elBtnScanAdvertiserCounts.innerHTML;
    // Chạy 3 domain song song: tăng tốc rõ rệt nhưng vẫn hạn chế 429 khi
    // proxy không tunnel được Google và backend phải fallback qua IP trực tiếp.
    const CONCURRENCY = 3;
    let completed = 0;
    await runDomainWorkerPool(entries, CONCURRENCY, async ([domain, info]) => {
            info.summaryLoading = true;
            info.summaryError = '';
            info.summaryErrorDetails = null;
            renderAdvDomainTable();
            try {
                const response = await fetch('/api/domain-summary', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    // Với tài khoản dùng Proxy Admin, backend tự gắn proxy bí mật
                    // vào request; frontend không cần và không được thấy URL proxy.
                    body: JSON.stringify({ domain, proxy, disableProxy: false, requireProxy: true })
                });
                const result = await response.json();
                if (!response.ok || result.error) {
                    const requestError = new Error(result.error || `HTTP ${response.status}`);
                    requestError.details = result.details || null;
                    throw requestError;
                }
                info.metrics = info.metrics || {};
                info.metrics.activeAdvertisersCount = result.activeAdvertisersCount;
            } catch (error) {
                info.summaryError = error.message || 'Không thể lấy số Nhà quảng cáo.';
                info.summaryErrorDetails = error.details || null;
            } finally {
                info.summaryLoading = false;
                completed++;
                elBtnScanAdvertiserCounts.innerHTML = `<i class="fa-solid fa-spinner fa-spin"></i> ${completed}/${entries.length}`;
                renderAdvDomainTable();
                updateCurrentAdvHistoryRecord();
            }
    });
    elBtnScanAdvertiserCounts.innerHTML = originalHtml;
    elBtnScanAdvertiserCounts.disabled = false;
    if (elBtnRetryAdvertiserCountErrors) elBtnRetryAdvertiserCountErrors.disabled = false;
    updateMetricRetryButtons();
}

async function scanTraffic(onlyFailed = false) {
    const entries = Object.entries(detectedDomainsMap)
        .filter(([, info]) => !onlyFailed || Boolean(info.trafficError));
    if (!entries.length || !elBtnScanTraffic) return;

    elBtnScanTraffic.disabled = true;
    if (elBtnRetryTrafficErrors) elBtnRetryTrafficErrors.disabled = true;
    const originalHtml = elBtnScanTraffic.innerHTML;
    let completed = 0;

    // Worker pool động tối đa 50 request, dùng chung cơ chế xác thực Workfast.
    await runDomainWorkerPool(entries, 50, async ([domain, info]) => {
        info.trafficLoading = true;
        info.trafficError = '';
        info.trafficErrorDetails = null;
        renderAdvDomainTable();
        const startedAt = Date.now();
        const endpoint = `/api/domain-traffic?domain=${encodeURIComponent(domain)}`;
        try {
            let profileAttempt = 0;
            let rateLimitWaits = 0;
            while (true) {
                const response = await fetch(endpoint);
                const responseText = await response.text();
                let result = null;
                try { result = responseText ? JSON.parse(responseText) : null; } catch (_) {}
                appendLocalTrafficHistory(domain, result, response.status);
                if (response.status === 429 && result?.rateLimited && Number(result?.retryAfterMs) > 0 && rateLimitWaits < 3) {
                    rateLimitWaits++;
                    const waitMs = Math.min(61000, Math.max(1000, Number(result.retryAfterMs)));
                    info.trafficError = `Đang chờ hạn mức TrafficLens API · ${Math.ceil(waitMs / 1000)} giây`;
                    renderAdvDomainTable();
                    await new Promise(resolve => setTimeout(resolve, waitMs));
                    info.trafficError = '';
                    continue;
                }
                const profileCount = Math.max(1, Number(result?.profileCount || 1));
                if (response.status === 429 && result?.retryable && result?.nextProfile && profileAttempt < profileCount - 1) {
                    profileAttempt++;
                    continue;
                }
                if (!response.ok || !result?.success) {
                    const requestError = new Error(result?.error || `HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ''}`);
                    requestError.details = {
                        type: 'api-error', domain, endpoint, status: response.status,
                        statusText: response.statusText || '', profileAttempt: profileAttempt + 1,
                        durationMs: Date.now() - startedAt,
                        response: result || responseText.slice(0, 10000) || '(Phản hồi rỗng)'
                    };
                    throw requestError;
                }
                info.traffic = result;
                break;
            }
        } catch (error) {
            info.trafficError = error.message || 'Không thể lấy Traffic web.';
            info.trafficErrorDetails = error.details || {
                type: error.name === 'AbortError' ? 'timeout' : 'network-error',
                domain, endpoint, durationMs: Date.now() - startedAt,
                name: error.name || 'Error', message: error.message || String(error)
            };
        } finally {
            info.trafficLoading = false;
            completed++;
            elBtnScanTraffic.innerHTML = `<i class="fa-solid fa-spinner fa-spin"></i> ${completed}/${entries.length}`;
            renderAdvDomainTable();
            updateCurrentAdvHistoryRecord();
        }
    });

    elBtnScanTraffic.innerHTML = originalHtml;
    elBtnScanTraffic.disabled = false;
    if (elBtnRetryTrafficErrors) elBtnRetryTrafficErrors.disabled = false;
    updateMetricRetryButtons();
}

function renderFilteredAdsTable() {
    workspaceScheduleSave();
    const total = filteredAdvertiserCreatives.length;
    elStatAdvFiltered.textContent = total;
    elFilteredAdsCount.textContent = total;

    if (total === 0) {
        elFilteredAdsTableBody.innerHTML = `<tr class="empty-row"><td colspan="4" class="text-center">Chưa có mẫu quảng cáo nào bị lọc.</td></tr>`;
        return;
    }

    elFilteredAdsTableBody.innerHTML = filteredAdvertiserCreatives.map(c => {
        const advertiserId = c.advertiserId || (elTargetAdvId ? elTargetAdvId.value.trim() : '');
        const adUrl = `https://adstransparency.google.com/advertiser/${encodeURIComponent(advertiserId)}/creative/${encodeURIComponent(c.creativeId)}?region=anywhere`;
        const formatLabel = c.format === 1 ? 'Hình ảnh' : (c.format === 2 ? 'Video' : (c.format === 3 ? 'Văn bản' : `Định dạng ${c.format || '-'}`));
        return `
            <tr>
                <td><strong style="font-family:monospace; color:var(--accent-blue);">${c.creativeId || '-'}</strong></td>
                <td>${formatLabel}</td>
                <td>${c.firstSeen || '-'} - ${c.lastSeen || '-'}</td>
                <td class="text-center">
                    <a class="btn btn-primary btn-sm" href="${adUrl}" target="_blank" rel="noopener noreferrer" style="display:inline-flex; padding:0.35rem 0.65rem; font-size:0.78rem;">
                        <i class="fa-solid fa-arrow-up-right-from-square"></i>&nbsp; Mở quảng cáo
                    </a>
                </td>
            </tr>
        `;
    }).join('');
}

if (elAdvDomainSearch) {
    elAdvDomainSearch.addEventListener('input', () => {
        advDomainPagination.page = 1;
        renderAdvDomainTable();
    });
}

[elAdvDeploymentFilter, elAdvRunningDaysFilter, elAdvNicheFilter, elAdvDuplicateFilter].forEach(element => element?.addEventListener('change', () => {
    advDomainPagination.page = 1;
    renderAdvDomainTable();
}));
[elAdvMinVolumeFilter, elAdvMinTrafficFilter].forEach(element => element?.addEventListener('input', () => {
    advDomainPagination.page = 1;
    renderAdvDomainTable();
}));
elBtnClearAdvDomainFilters?.addEventListener('click', () => {
    [elAdvDeploymentFilter, elAdvRunningDaysFilter, elAdvNicheFilter, elAdvDuplicateFilter].forEach(element => { if (element) element.value = ''; });
    [elAdvMinVolumeFilter, elAdvMinTrafficFilter].forEach(element => { if (element) element.value = ''; });
    if (elAdvDomainSearch) elAdvDomainSearch.value = '';
    advDomainPagination.page = 1;
    renderAdvDomainTable();
});
elBtnAddAdvDomainsToStore?.addEventListener('click', async () => {
    const domains = getFilteredAdvDomainEntries().filter(([, info]) => info.inDomainStore === false).map(([domain]) => normalizeProjectDomain(domain)).filter(Boolean);
    if (!domains.length) return showMonitorToast('Không có domain mới trong bộ lọc hiện tại.', 'warning');
    if (!await showAppConfirm(`Bạn sắp thêm ${domains.length.toLocaleString('vi-VN')} domain đang lọc vào Kho Domain.\nCác domain này sẽ được dùng để đối chiếu ở những lần quét sau.`, { title: 'Thêm vào Kho Domain', confirmText: 'Thêm vào kho' })) return;
    elBtnAddAdvDomainsToStore.disabled = true;
    try {
        let added = 0;
        for (let index = 0; index < domains.length; index += 1000) {
            const response = await fetch('/api/advertiser/domains/resolve', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ domains: domains.slice(index, index + 1000), scanId: `advertiser-manual:${elTargetAdvId?.value.trim() || 'lookup'}`, rootDomain: '' }) });
            const result = await response.json().catch(() => ({}));
            if (!response.ok) throw new Error(result.error || `HTTP ${response.status}`);
            added += Number(result.added?.length || 0);
        }
        await compareAdvertiserDomainDuplicates();
        showMonitorToast(`Đã thêm ${added.toLocaleString('vi-VN')} domain mới vào Kho Domain.`);
    } catch (error) { showMonitorToast(`Không thể thêm vào Kho Domain: ${error.message}`, 'error'); }
    finally { renderAdvDomainTable(); }
});
elBtnCompareAdvDomains?.addEventListener('click', async () => {
    elBtnCompareAdvDomains.disabled = true;
    elBtnCompareAdvDomains.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Đang đối chiếu';
    try { await compareAdvertiserDomainDuplicates(); showMonitorToast('Đã cập nhật trạng thái trùng của toàn bộ domain.'); }
    catch (error) { showMonitorToast(`Không thể đối chiếu: ${error.message}`, 'error'); }
    finally { elBtnCompareAdvDomains.innerHTML = '<i class="fa-solid fa-arrows-rotate"></i> Đối chiếu lại'; renderAdvDomainTable(); }
});

document.querySelectorAll('[data-adv-domain-sort]').forEach(header => {
    header.addEventListener('click', () => {
        if (advDomainColumnDragJustEnded) return;
        const key = header.dataset.advDomainSort;
        advDomainTableSort = {
            key,
            direction: advDomainTableSort.key === key && advDomainTableSort.direction === 'asc' ? 'desc' : 'asc'
        };
        advDomainPagination.page = 1;
        renderAdvDomainTable();
    });
});
initializeAdvDomainColumnReorder();

if (elAdvDomainPageSize) {
    elAdvDomainPageSize.value = advDomainPagination.size;
    elAdvDomainPageSize.addEventListener('change', () => {
        advDomainPagination.size = elAdvDomainPageSize.value;
        advDomainPagination.page = 1;
        localStorage.setItem('adv_domain_table_page_size', advDomainPagination.size);
        renderAdvDomainTable();
    });
}
elAdvDomainPagePrev?.addEventListener('click', () => {
    advDomainPagination.page -= 1;
    renderAdvDomainTable();
});
elAdvDomainPageNext?.addEventListener('click', () => {
    advDomainPagination.page += 1;
    renderAdvDomainTable();
});

// Render creatives grid
function renderCreativeGrid(creativesList) {
    workspaceScheduleSave();
    if (elCreativeGrid.querySelector('.text-center')) {
        elCreativeGrid.innerHTML = '';
    }

    creativesList.forEach(c => {
        const card = document.createElement('div');
        card.className = 'creative-card';
        card.id = `creative-card-${c.creativeId}`;

        let mediaHtml = '';
        if (c.imageUrl) {
            mediaHtml = `<img src="/api/image-proxy?url=${encodeURIComponent(c.imageUrl)}" alt="Ad Preview" loading="lazy">`;
        } else if (c.previewUrl) {
            mediaHtml = `<iframe src="/api/ad-preview-html?url=${encodeURIComponent(c.previewUrl)}" style="width: 100%; height: 100%; border: none; border-radius: 8px; background: white;" loading="lazy"></iframe>`;
        } else {
            const formatLabel = c.format === 2 ? '📝 Quảng cáo văn bản' : (c.format === 3 ? '🎬 Quảng cáo Video' : '🔗 Link/Mẫu khác');
            mediaHtml = `<div class="no-img-placeholder"><i class="fa-solid fa-file-invoice"></i><span>${formatLabel}</span></div>`;
        }

        const domainLabel = c.domain ? `<span class="ocr-badge-domain" style="background:rgba(59,130,246,0.1); color:var(--accent-blue); border-color:rgba(59,130,246,0.2);">${c.domain} (Google)</span>` : '';

        const targetUrl = c.imageUrl || c.previewUrl || '';
        const urlLabel = c.imageUrl ? 'Đường dẫn ảnh:' : (c.previewUrl ? 'Đường dẫn HTML:' : 'Đường dẫn:');

        card.innerHTML = `
            <div class="creative-media-box">
                ${mediaHtml}
            </div>
            <div class="creative-details">
                <div class="creative-meta-row">
                    <span class="creative-meta-label">ID Mẫu:</span>
                    <span class="creative-meta-value">${c.creativeId.replace('CR', '')}</span>
                </div>
                <div class="creative-meta-row">
                    <span class="creative-meta-label">Advertiser:</span>
                    <span class="creative-meta-value" style="font-size:0.72rem; word-break:break-all;">${c.advertiserId || '-'}</span>
                </div>
                <div class="creative-meta-row">
                    <span class="creative-meta-label">Loại Ads:</span>
                    <span class="creative-meta-value">${c.imageUrl ? 'Hình ảnh' : (c.previewUrl ? 'Văn bản (Search)' : 'Mẫu khác')}</span>
                </div>
                <div class="creative-meta-row">
                    <span class="creative-meta-label">Hoạt động:</span>
                    <span class="creative-meta-value" style="font-size:0.75rem;">${c.firstSeen} - ${c.lastSeen}</span>
                </div>
                ${targetUrl ? `
                <div class="creative-meta-row" style="flex-direction: column; align-items: flex-start; gap: 4px; margin-top: 4px; border-top: 1px dashed rgba(255,255,255,0.05); padding-top: 4px; width: 100%;">
                    <span class="creative-meta-label" style="font-size: 0.8rem;">${urlLabel}</span>
                    <a href="${targetUrl}" target="_blank" style="color: var(--accent-blue); word-break: break-all; font-size: 0.75rem; text-decoration: underline;" title="${targetUrl}">
                        ${targetUrl}
                    </a>
                </div>
                ` : ''}
                ${domainLabel}
                
                ${targetUrl ? `
                <div class="ocr-action-area">
                    <button class="btn btn-primary btn-sm btn-ocr-run" data-url="${targetUrl}" data-is-preview="${c.previewUrl ? 'true' : 'false'}" data-id="${c.creativeId}">
                        <i class="fa-solid fa-brain"></i> ${c.imageUrl ? 'Quét OCR chữ trên ảnh' : 'Phân tích text & tìm dự án'}
                    </button>
                    <div class="ocr-result-box" style="display:none;" id="ocr-box-${c.creativeId}">Đang chờ quét...</div>
                </div>
                ` : ''}
            </div>
        `;

        const btnOcr = card.querySelector('.btn-ocr-run');
        if (btnOcr) {
            const isPreview = btnOcr.getAttribute('data-is-preview') === 'true';
            btnOcr.addEventListener('click', () => runOcrOnCreative(targetUrl, c.creativeId, isPreview));
        }

        elCreativeGrid.appendChild(card);
    });
}

// Single OCR handler
const browserOcrPoolSize = Math.max(1, Math.min(4, Math.floor((navigator.hardwareConcurrency || 2) / 2)));
const browserOcrSlots = Array.from({ length: browserOcrPoolSize }, () => Promise.resolve());
let browserOcrSlotIndex = 0;

const OCR_GENERIC_TLDS = new Set([
    'com','net','org','info','biz','edu','gov','mil','int','app','dev','shop','store','online','site','website','xyz','tech','digital','agency','cloud','space','live','news','blog','pro','mobi','travel','solutions','services','company','world','today','email','media','marketing','design','finance','games','studio','social','network','academy','support','systems','global','group','life','one','top','link','club','art','work','care','center','ai','io'
]);
const OCR_PRIORITY_SUFFIXES = new Set([
    'com.au','net.au','org.au','co.uk','org.uk','me.uk','co.nz','co.jp','co.kr','co.in','co.id','com.vn','com.sg','com.my','com.ph','com.br','com.mx','com.tr','com.cn','com.hk','com.tw','co.th','co.za','com.ar','com.co','com.pe','com.ua','com.de'
]);
const OCR_FILE_EXTENSIONS = new Set([
    'jpg','jpeg','png','gif','webp','svg','bmp','ico','pdf','doc','docx','xls','xlsx','ppt','pptx','zip','rar','css','js','json','xml','txt','mp3','mp4','avi','mov','woff','woff2','ttf','exe','apk'
]);

function scoreOcrDomain(domain, rawCandidate, index) {
    const labels = domain.split('.');
    const tld = labels.at(-1);
    const suffix2 = labels.slice(-2).join('.');
    let score = tld === 'com' ? 600 : OCR_PRIORITY_SUFFIXES.has(suffix2) ? 500 : /^[a-z]{2}$/.test(tld) ? 350 : 250;
    if (/^https?:\/\//i.test(rawCandidate)) score += 40;
    if (/^(?:https?:\/\/)?www\./i.test(rawCandidate)) score += 20;
    if (labels.length === 2) score += 10;
    return score - index / 100000;
}

function extractDomainsInBrowser(text) {
    // OCR often inserts spaces or Unicode full stops around a domain: "brand . com".
    const normalizedText = String(text || '')
        .normalize('NFKC')
        .replace(/[。．｡]/g, '.')
        .replace(/([a-z0-9-])(\s*)\.([ \t\r\n]*)(?=([a-z0-9-]))/gi, (match, left, spaceBefore, spaceAfter, nextChar) => {
            // "1996. Shop" is a sentence boundary, not the domain 1996.shop.
            // Still repair common OCR forms such as "brand . com" and "brand. com".
            if (!spaceBefore && spaceAfter && /^[A-Z]$/.test(nextChar)) return match;
            return `${left}.`;
        });
    const candidatePattern = /(?:https?:\/\/)?(?:www\.)?[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?)+/gi;
    const candidates = normalizedText.match(candidatePattern) || [];
    const accepted = new Map();

    candidates.forEach((rawCandidate, index) => {
        const domain = normalizeProjectDomain(rawCandidate);
        if (!domain || domain.length > 253 || domain.includes('..')) return;
        const labels = domain.split('.');
        if (labels.length < 2 || labels.some(label => !label || label.length > 63 || label.startsWith('-') || label.endsWith('-'))) return;
        const tld = labels.at(-1);
        const suffix2 = labels.slice(-2).join('.');
        const suffixLabelCount = OCR_PRIORITY_SUFFIXES.has(suffix2) ? 2 : 1;
        const registrableLabel = labels[labels.length - suffixLabelCount - 1] || '';
        // A website name cannot be inferred safely when its main label is only a number.
        // This removes OCR sentence artifacts such as "1996.shop" and "2026.com".
        if (!/[a-z]/i.test(registrableLabel)) return;
        if (OCR_FILE_EXTENSIONS.has(tld) || /^\d+$/.test(tld)) return;
        if (!OCR_PRIORITY_SUFFIXES.has(suffix2) && !OCR_GENERIC_TLDS.has(tld) && !/^[a-z]{2}$/.test(tld) && !/^xn--[a-z0-9-]+$/.test(tld)) return;
        const score = scoreOcrDomain(domain, rawCandidate, index);
        if (!accepted.has(domain) || accepted.get(domain).score < score) accepted.set(domain, { domain, score, index });
    });

    return [...accepted.values()].sort((a, b) => b.score - a.score || a.index - b.index).map(item => item.domain);
}

async function browserOcrResource(url, isPreview = false, traceMode = false) {
    if (isPreview) {
        const response = await fetch('/api/ocr', {
            method: 'POST', headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ previewUrl: url })
        });
        if (handleMonitorUnauthorized(response)) {
            const error = new Error('Phiên đăng nhập đã hết hạn.');
            error.status = 401;
            throw error;
        }
        const result = await response.json();
        if (!response.ok) throw new Error(result.error || `HTTP ${response.status}`);
        return result;
    }
    if (!window.Tesseract) throw new Error('Không tải được bộ OCR Tesseract trên trình duyệt.');
    const traceQuery = traceMode ? '&traceMode=1' : '';
    const imageResponse = await fetch(`/api/image-proxy?url=${encodeURIComponent(url)}${traceQuery}`);
    if (handleMonitorUnauthorized(imageResponse)) {
        const error = new Error('Phiên đăng nhập đã hết hạn.');
        error.status = 401;
        throw error;
    }
    if (!imageResponse.ok) throw new Error(`Không tải được ảnh OCR: HTTP ${imageResponse.status}`);
    const imageBlob = await imageResponse.blob();
    const slot = browserOcrSlotIndex++ % browserOcrPoolSize;
    const task = browserOcrSlots[slot].then(async () => {
        const result = await Tesseract.recognize(imageBlob, 'eng');
        const text = result.data?.text || '';
        return { text, domains: extractDomainsInBrowser(text), processor: 'user-browser' };
    });
    browserOcrSlots[slot] = task.catch(() => {});
    return task;
}

async function runOcrOnCreative(url, creativeId, isPreview = false) {
    const box = document.getElementById(`ocr-box-${creativeId}`);
    const btn = document.querySelector(`#creative-card-${creativeId} .btn-ocr-run`);

    if (box) {
        box.style.display = 'block';
        box.textContent = isPreview ? 'Đang phân tích text ad...' : 'Đang nhận dạng OCR...';
    }
    if (btn) btn.disabled = true;

    try {
        const resData = await browserOcrResource(url, isPreview);
        box.textContent = resData.text ? `[Văn bản đọc được]:\n${resData.text}` : '[Không tìm thấy chữ nào]';
        
        if (resData.domains && resData.domains.length > 0) {
            const creative = advertiserCreatives.find(c => c.creativeId === creativeId) || null;
            resData.domains.forEach(d => {
                addDetectedDomain(d, 'OCR Ảnh', creative);
            });
            renderAdvDomainTable();
            if (!ocrRunState.running) await compareAdvertiserDomainDuplicates();

            const detailsDiv = document.querySelector(`#creative-card-${creativeId} .creative-details`);
            resData.domains.forEach(d => {
                if (!detailsDiv.innerHTML.includes(d)) {
                    const badge = document.createElement('span');
                    badge.className = 'ocr-badge-domain';
                    badge.textContent = `${d} (OCR)`;
                    detailsDiv.appendChild(badge);
                }
            });
        }
    } catch (e) {
        box.textContent = `Lỗi OCR: ${e.message}`;
    } finally {
        if (btn) btn.disabled = false;
    }
}

// Automatic parallel-batch OCR handler for ultra speed
async function runAutoOcr() {
    const scannableAds = advertiserCreatives.filter(c => c.imageUrl || c.previewUrl);
    if (scannableAds.length === 0) return;

    elOcrProgressCard.style.display = 'block';
    elBtnRunAutoOcr.disabled = true;
    ocrRunState = { running: true, paused: false, stopped: false };
    if (elBtnPauseOcr) {
        elBtnPauseOcr.disabled = false;
        elBtnPauseOcr.innerHTML = '<i class="fa-solid fa-pause"></i> Tạm dừng OCR';
    }
    if (elBtnStopOcr) elBtnStopOcr.disabled = false;

    let completedCount = 0;
    const totalCount = scannableAds.length;
    const CONCURRENCY = 4; // Scan 4 ads concurrently in parallel

    for (let i = 0; i < scannableAds.length; i += CONCURRENCY) {
        if (ocrRunState.stopped) break;
        while (ocrRunState.paused && !ocrRunState.stopped) {
            elOcrStatusText.textContent = `Đã tạm dừng tại ${completedCount}/${totalCount} mẫu`;
            await new Promise(resolve => setTimeout(resolve, 250));
        }
        if (ocrRunState.stopped) break;
        const batch = scannableAds.slice(i, i + CONCURRENCY);
        await Promise.all(batch.map(async (ad) => {
            const card = document.getElementById(`creative-card-${ad.creativeId}`);
            if (card) {
                card.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
            }

            const targetUrl = ad.imageUrl || ad.previewUrl;
            const isPreview = !!ad.previewUrl;
            await runOcrOnCreative(targetUrl, ad.creativeId, isPreview);

            completedCount++;
            const pct = Math.round((completedCount / totalCount) * 100);
            elOcrProgressFill.style.width = `${pct}%`;
            elOcrStatusText.textContent = `Đang quét siêu tốc OCR mẫu QC ${completedCount}/${totalCount}...`;
            elOcrStatusPercent.textContent = `${pct}%`;
        }));
    }

    if (Object.keys(detectedDomainsMap).length) await compareAdvertiserDomainDuplicates();

    elOcrStatusText.textContent = ocrRunState.stopped
        ? `Đã dừng OCR — giữ lại kết quả của ${completedCount}/${totalCount} mẫu.`
        : '⚡ Hoàn tất quét OCR siêu tốc toàn bộ!';
    ocrRunState.running = false;
    if (elBtnPauseOcr) elBtnPauseOcr.disabled = true;
    if (elBtnStopOcr) elBtnStopOcr.disabled = true;
    elBtnRunAutoOcr.disabled = false;
}

elBtnPauseOcr?.addEventListener('click', () => {
    if (!ocrRunState.running) return;
    ocrRunState.paused = !ocrRunState.paused;
    elBtnPauseOcr.innerHTML = ocrRunState.paused
        ? '<i class="fa-solid fa-play"></i> Tiếp tục OCR'
        : '<i class="fa-solid fa-pause"></i> Tạm dừng OCR';
    if (!ocrRunState.paused) elOcrStatusText.textContent = 'Đang tiếp tục OCR...';
});

elBtnStopOcr?.addEventListener('click', () => {
    if (!ocrRunState.running) return;
    ocrRunState.stopped = true;
    ocrRunState.paused = false;
    elOcrStatusText.textContent = 'Đang dừng sau nhóm OCR hiện tại...';
    elBtnStopOcr.disabled = true;
});

// Main lookup routine
async function startAdvScrape(requestedAdvId = '') {
    if (!requestedAdvId) resetExclusionSessionStats();
    const advId = String(requestedAdvId || elTargetAdvId.value).trim();
    if (!advId) {
        alert('Vui lòng nhập ID nhà quảng cáo!');
        return;
    }
    if (isExcludedAdvertiser(advId)) { showMonitorToast(`Nhà quảng cáo ${advId} đang nằm trong danh sách loại trừ.`, 'warning'); return; }

    let curlStr = `curl 'https://adstransparency.google.com/' -H 'accept: application/json'`;
    if (!curlStr) {
        curlStr = elCurlCommand.value.trim();
    }

    if (!curlStr) {
        alert('Chưa có cấu hình xác thực Google. Vui lòng liên hệ Admin!');
        return;
    }

    if (false && curlStr.includes('ABerymIoPaGmyJfTEct-ojebsfRZ')) {
        logAdv('Lỗi: Cấu hình xác thực đã hết hạn!', 'error');
        alert('Cấu hình xác thực Google đã hết hạn. Vui lòng liên hệ Admin!');
        return;
    }

    // Reset stats
    advertiserCreatives = [];
    filteredAdvertiserCreatives = [];
    detectedDomainsMap = {};
    advDomainPagination.page = 1;
    elCreativeGrid.innerHTML = '';
    elAdvDomainTableBody.innerHTML = `<tr class="empty-row"><td colspan="3" class="text-center">Đang tải...</td></tr>`;
    elStatAdvTotal.textContent = '0';
    elStatAdvDomains.textContent = '0';
    renderFilteredAdsTable();
    const proxyVal = elAdvProxyUrl ? elAdvProxyUrl.value.trim() : '';

    elBtnAdvStart.disabled = true;
    elBtnAdvStop.disabled = false;
    advController = new AbortController();

    try {
        const parsed = parseCurl(curlStr);
        const activeHeaders = parsed ? parsed.headers : {};
        const activeFReq = (parsed && parsed.fReq) ? parsed.fReq : {};

        const advStartDateVal = elAdvStartDate ? elAdvStartDate.value : '';
        const advEndDateVal = elAdvEndDate ? elAdvEndDate.value : '';

        if (advStartDateVal) {
            if (!activeFReq['3']) activeFReq['3'] = {};
            activeFReq['3']['6'] = parseInt(advStartDateVal.replace(/-/g, ''), 10);
        }
        if (advEndDateVal) {
            if (!activeFReq['3']) activeFReq['3'] = {};
            activeFReq['3']['7'] = parseInt(advEndDateVal.replace(/-/g, ''), 10);
        }
        if (!applyCountryFilter(activeFReq, elAdvCountry)) {
            showMonitorToast('Hãy chọn một quốc gia hợp lệ trong danh sách.', 'error');
            elAdvCountry?.focus();
            return;
        }

        const response = await fetch('/api/advertiser-creatives', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ advId, headers: activeHeaders, fReq: activeFReq, proxy: proxyVal, dateRangePreset: elAdvDateRangePreset?.value || '7' }),
            signal: advController.signal
        });

        if (!response.ok) {
            throw new Error(`Server returned HTTP ${response.status}`);
        }

        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let buffer = '';

        while (true) {
            const { done, value } = await reader.read();
            if (done) break;

            buffer += decoder.decode(value, { stream: true });
            const lines = buffer.split('\n');
            buffer = lines.pop();

            for (const line of lines) {
                if (!line.trim()) continue;
                try {
                    const chunk = JSON.parse(line);
                    if (chunk.type === 'status') {
                        logAdv(chunk.message);
                    } else if (chunk.type === 'ip') {
                        if (elActiveIpAddress) elActiveIpAddress.textContent = chunk.ip;
                    } else if (chunk.type === 'creatives') {
                        const taggedCreatives = chunk.creatives.map(creative => ({
                            ...creative,
                            advertiserId: creative.advertiserId || advId
                        }));
                        const taggedFiltered = (chunk.filteredCreatives || []).map(creative => ({
                            ...creative,
                            advertiserId: creative.advertiserId || advId
                        }));
                        advertiserCreatives = advertiserCreatives.concat(taggedCreatives);
                        filteredAdvertiserCreatives = filteredAdvertiserCreatives.concat(taggedFiltered);
                        elStatAdvTotal.textContent = chunk.totalFetched;
                        
                        taggedCreatives.forEach(c => {
                            if (c.domain) {
                                addDetectedDomain(c.domain, 'Google API', c);
                            }
                        });

                        renderAdvDomainTable();
                        renderCreativeGrid(taggedCreatives);
                        renderFilteredAdsTable();
                    } else if (chunk.type === 'done') {
                        logAdv(chunk.message, 'success');
                        showMonitorToast(`Tra cứu nhà quảng cáo ${advId} đã hoàn tất.`);
                    } else if (chunk.type === 'error') {
                        if (/\b429\b|too many requests/i.test(String(chunk.error || ''))) showRateLimitNotice();
                        logAdv(`Lỗi: ${chunk.error}`, 'error');
                        alert(`Lỗi quét: ${chunk.error}`);
                    }
                } catch(e) {
                    console.error('Error parsing chunk:', e);
                }
            }
        }

        logAdv('Thu thập mẫu quảng cáo hoàn tất!', 'success');
        if (advertiserCreatives.some(c => c.imageUrl || c.previewUrl)) {
            elBtnRunAutoOcr.disabled = false;
        }

        await compareAdvertiserDomainDuplicates();

        // Save snapshot into History Log
        saveScanHistory('advertiser', advId, {
            creativeCount: advertiserCreatives.length,
            domainCount: Object.keys(detectedDomainsMap).length,
            filteredCount: filteredAdvertiserCreatives.length
        }, { advertiserCreatives, filteredAdvertiserCreatives, detectedDomainsMap });

        const records = advertiserCreatives.map(creative => ({
            advertiserId: advId,
            creativeId: creative.creativeId || '',
            domain: creative.domain || '',
            adType: creative.imageUrl ? 'Hình ảnh' : (creative.previewUrl ? 'Văn bản' : 'Khác'),
            firstSeen: creative.firstSeen || '',
            lastSeen: creative.lastSeen || '',
            imageUrl: creative.imageUrl || '',
            previewUrl: creative.previewUrl || '',
            source: 'Google Ads Transparency'
        }));
        sheetExportRecords.push(...records);
        if (elBtnPushGoogleSheet) elBtnPushGoogleSheet.disabled = sheetExportRecords.length === 0;
        return {
            advId,
            records,
            creatives: advertiserCreatives.slice(),
            filteredCreatives: filteredAdvertiserCreatives.slice()
        };
    } catch(err) {
        if (err.name === 'AbortError') {
            logAdv('Đã dừng tiến trình quét theo yêu cầu.', 'warning');
        } else {
            logAdv(`Lỗi: ${err.message}`, 'error');
        }
    } finally {
        elBtnAdvStart.disabled = false;
        elBtnAdvStop.disabled = true;
    }
}

function stopAdvScrape() {
    if (advController) {
        advController.abort();
    }
}

function parseSavedAdvertiserIds(value) {
    return [...new Set(String(value || '').split(/[\s,;]+/).map(id => id.trim()).filter(Boolean))];
}

function escapeMonitorHtml(value) {
    return String(value ?? '').replace(/[&<>"']/g, char => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[char]));
}

function showMonitorToast(message, type = 'success') {
    let stack = document.querySelector('.app-toast-stack');
    if (!stack) {
        stack = document.createElement('div');
        stack.className = 'app-toast-stack';
        document.body.appendChild(stack);
    }
    const toast = document.createElement('div');
    toast.className = `monitor-toast ${type}`;
    const title = type === 'success' ? 'Thành công' : type === 'warning' ? 'Cần chú ý' : 'Không thể thực hiện';
    const icon = type === 'success' ? 'fa-circle-check' : type === 'warning' ? 'fa-triangle-exclamation' : 'fa-circle-exclamation';
    toast.innerHTML = `<i class="fa-solid ${icon}"></i><div class="toast-copy"><strong>${title}</strong><span>${escapeMonitorHtml(message)}</span></div><button type="button" class="toast-dismiss" aria-label="Đóng">&times;</button><i class="toast-progress"></i>`;
    stack.appendChild(toast);
    const close = () => { if (toast.classList.contains('leaving')) return; toast.classList.add('leaving'); setTimeout(() => toast.remove(), 280); };
    toast.querySelector('.toast-dismiss').addEventListener('click', close);
    toast.querySelector('.toast-progress').addEventListener('animationend', close);
    requestAnimationFrame(() => toast.classList.add('show'));
}

function showRateLimitNotice() {
    if (authenticatedUser?.canUseProxy) return;
    showMonitorToast('Google đang giới hạn kết nối (429). Tài khoản chưa có quyền Proxy; hãy đổi Wi-Fi, dùng dữ liệu di động hoặc khởi động lại modem rồi thử lại.', 'warning');
}

async function loadCompetitorMonitor() {
    if (!elMonitorScheduleList || monitorAuthRedirecting || !authenticatedUser) return;
    try {
        const response = await fetch('/api/competitor-monitor', { cache: 'no-store' });
        if (handleMonitorUnauthorized(response)) return;
        const data = await response.json();
        if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`);
        latestMonitorData = data;
        processPendingBrowserMonitorJobs(data).catch(error => console.error('Browser monitor OCR:', error));
        if (elMonitorTelegramEnabled) {
            elMonitorTelegramEnabled.checked = Boolean(data.telegram?.enabled);
            elMonitorTelegramFields.hidden = !elMonitorTelegramEnabled.checked;
        }
        if (elMonitorTelegramChatId && !elMonitorTelegramChatId.value) elMonitorTelegramChatId.value = data.telegram?.chatId || '';
        if (elMonitorTelegramBotToken && !elMonitorTelegramBotToken.value) {
            elMonitorTelegramBotToken.placeholder = data.telegram?.configured
                ? `Đã lưu: ${data.telegram.tokenHint || '••••••••'}`
                : '123456789:AA...';
            elMonitorTelegramBotToken.classList.toggle('has-saved-secret', Boolean(data.telegram?.configured));
        }
        if (elMonitorTelegramTestStatus && data.telegram?.configured && !elMonitorTelegramTestStatus.textContent) {
            elMonitorTelegramTestStatus.className = 'success';
            elMonitorTelegramTestStatus.textContent = 'Đã có cấu hình Telegram của tài khoản này.';
        }
        const unread = Number(data.unread || 0);
        if (elNotificationBadge) {
            elNotificationBadge.hidden = unread === 0;
            elNotificationBadge.textContent = unread > 99 ? '99+' : String(unread);
        }
        elMonitorScheduleList.innerHTML = data.schedules.length ? data.schedules.map(schedule => {
            const minutes = Number(schedule.intervalMinutes || schedule.intervalHours * 60 || 1440);
            const intervalLabel = minutes < 60 ? `${minutes} phút` : minutes % 1440 === 0 ? `${minutes / 1440} ngày` : `${minutes / 60} giờ`;
            const progress = schedule.scanProgress;
            const progressHtml = schedule.running && progress ? `<div class="monitor-live-progress">
                <div class="monitor-progress-copy"><span><i class="fa-solid fa-spinner fa-spin"></i> ${escapeMonitorHtml(progress.message || 'Đang xử lý dữ liệu')}</span><strong>${Number(progress.percent || 0)}%</strong></div>
                <div class="monitor-progress-track"><i style="width:${Math.max(2, Math.min(100, Number(progress.percent || 0)))}%"></i></div>
                <small>Cập nhật ${progress.updatedAt ? new Date(progress.updatedAt).toLocaleTimeString('vi-VN') : 'vừa xong'}${progress.total ? ` • ${Number(progress.processed || 0).toLocaleString('vi-VN')}/${Number(progress.total).toLocaleString('vi-VN')} mẫu` : ''}</small>
            </div>` : '';
            return `
            <div class="monitor-schedule-item">
                <div><strong>${schedule.advertiserIds.length} đối thủ • mỗi ${intervalLabel}</strong>
                    <span>${schedule.telegramEnabled ? '<i class="fa-brands fa-telegram"></i> Telegram • ' : ''}${schedule.running ? 'Máy người dùng đang OCR...' : schedule.awaitingClientOcr ? 'Chờ thiết bị OCR...' : schedule.lastError ? 'Có lỗi' : 'Đang theo dõi'}</span></div>
                <div><span>Lần cuối: ${schedule.lastRunAt ? new Date(schedule.lastRunAt).toLocaleString('vi-VN') : 'đang tạo mốc'}</span>
                    <span><button class="btn-link monitor-detail" data-id="${schedule.id}">Xem chi tiết</button> · <button class="btn-link monitor-edit" data-id="${schedule.id}">Sửa chu kỳ</button> · <button class="btn-link monitor-run" data-id="${schedule.id}">Quét ngay</button> · <button class="btn-link monitor-delete" data-id="${schedule.id}">Xóa</button></span></div>
                <div><span>Lần kế tiếp: ${schedule.nextRunAt ? new Date(schedule.nextRunAt).toLocaleString('vi-VN') : '-'}</span></div>
                <div class="monitor-edit-row" data-edit-id="${schedule.id}" hidden>
                    <input type="number" class="monitor-edit-value" min="1" value="${minutes < 60 ? minutes : minutes % 1440 === 0 ? minutes / 1440 : minutes / 60}">
                    <select class="monitor-edit-unit form-select"><option value="minutes" ${minutes < 60 ? 'selected' : ''}>Phút</option><option value="hours" ${minutes >= 60 && minutes % 1440 !== 0 ? 'selected' : ''}>Giờ</option><option value="days" ${minutes % 1440 === 0 ? 'selected' : ''}>Ngày</option></select>
                    <button class="btn btn-primary btn-sm monitor-edit-save" data-id="${schedule.id}">Lưu</button>
                    <button class="btn btn-secondary btn-sm monitor-edit-cancel" data-id="${schedule.id}">Hủy</button>
                </div>
                <div class="monitor-id-breakdown">${schedule.advertiserIds.map(id => `<span><strong>${escapeMonitorHtml(id)}</strong>: ${(schedule.snapshot?.[id] || []).length} domain đã biết</span>`).join('')}</div>
                ${progressHtml}
                ${schedule.lastError ? `<div class="keyword-metric-error">${escapeMonitorHtml(schedule.lastError)}</div>` : ''}
                ${schedule.lastTelegramError ? `<div class="keyword-metric-error">Telegram: ${escapeMonitorHtml(schedule.lastTelegramError)}</div>` : ''}
            </div>`;
        }).join('') : 'Chưa có lịch theo dõi.';
        if (elNotificationList) {
            const orderedNotifications = [...data.notifications].sort((a, b) => Number(a.read) - Number(b.read));
            elNotificationList.innerHTML = orderedNotifications.length ? orderedNotifications.map(item => `
                <article class="notification-item ${item.read ? 'read' : 'unread'}" data-id="${item.id}">
                    <div class="notification-item-accent"><i class="fa-solid ${item.type === 'system' ? 'fa-triangle-exclamation' : 'fa-bullhorn'}"></i></div>
                    <div class="notification-item-body">
                    <div class="notification-item-top"><span class="notification-item-domain">${escapeMonitorHtml(item.type === 'system' ? (item.title || 'Thông báo hệ thống') : item.domain)}</span>${item.read ? '<span class="notification-read-check" title="Đã xem"><i class="fa-solid fa-check"></i></span>' : '<span class="notification-new-dot" title="Chưa đọc"></span>'}</div>
                    <div class="notification-meta">
                        ${item.type === 'system' ? `<p>${escapeMonitorHtml(item.message || '')}</p>` : `<p><strong>Đối thủ:</strong> ${escapeMonitorHtml(item.advertiserId)}</p>`}
                        ${item.type === 'system' ? '' : `<p><strong>Mẫu quảng cáo:</strong> ${escapeMonitorHtml(item.creativeId || 'Không có ID')}</p>`}
                        <time><i class="fa-regular fa-clock"></i> ${new Date(item.detectedAt).toLocaleString('vi-VN')}</time>
                    </div>
                    <div class="notification-actions">
                        ${item.type === 'system' ? '' : `<a class="notification-primary-action notification-open-ad" data-id="${item.id}" href="${item.adUrl}" target="_blank" rel="noopener noreferrer"><i class="fa-solid fa-arrow-up-right-from-square"></i> Mở quảng cáo</a>`}
                        ${item.read ? '' : `<button class="notification-read-action notification-toggle-read" data-id="${item.id}" data-read="false"><i class="fa-solid fa-check"></i> Đã xem</button>`}
                    </div>
                    </div>
                </article>`).join('') : '<div class="notification-empty"><i class="fa-regular fa-bell-slash"></i><strong>Chưa có thông báo</strong><span>Thông báo mới sẽ xuất hiện tại đây.</span></div>';
        }
        if (elNotificationSummary) elNotificationSummary.textContent = `${unread} chưa đọc · ${data.notifications.length} tổng thông báo`;
        if (elBtnReadAllNotifications) elBtnReadAllNotifications.hidden = unread === 0;
    } catch (error) {
        elMonitorScheduleList.textContent = `Không tải được lịch: ${error.message}`;
    }
}

async function processPendingBrowserMonitorJobs(data) {
    if (!data.curlConfigured) {
        if (!processPendingBrowserMonitorJobs.curlWarningShown && (data.schedules || []).some(schedule => schedule.awaitingClientOcr)) {
            processPendingBrowserMonitorJobs.curlWarningShown = true;
            showMonitorToast('Lịch OCR đang tạm chờ vì chưa có cấu hình xác thực Google.', 'warning');
        }
        return;
    }
    processPendingBrowserMonitorJobs.curlWarningShown = false;
    let browserOcrDeviceId = localStorage.getItem('browser_ocr_device_id');
    if (!browserOcrDeviceId) {
        browserOcrDeviceId = globalThis.crypto?.randomUUID?.() || `ocr-${Date.now()}-${Math.random().toString(36).slice(2)}`;
        localStorage.setItem('browser_ocr_device_id', browserOcrDeviceId);
    }
    const pending = (data.schedules || []).filter(schedule => schedule.awaitingClientOcr && (!schedule.running || schedule.ocrDeviceId === browserOcrDeviceId) && !activeBrowserMonitorJobs.has(schedule.id));
    for (const schedule of pending) {
        if (cancelledBrowserMonitorJobs.has(schedule.id) || monitorAuthRedirecting) continue;
        activeBrowserMonitorJobs.add(schedule.id);
        try {
            showMonitorToast(`Máy của bạn đang nhận tác vụ OCR cho ${schedule.advertiserIds.length} Advertiser ID.`);
            const prepareResponse = await fetch(`/api/competitor-monitor/${encodeURIComponent(schedule.id)}/client-scan`, {
                method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ deviceId: browserOcrDeviceId })
            });
            if (handleMonitorUnauthorized(prepareResponse)) return;
            const prepared = await prepareResponse.json();
            if (!prepareResponse.ok) throw new Error(prepared.error || `HTTP ${prepareResponse.status}`);
            const results = {};
            const totalCandidates = Object.values(prepared.candidates || {}).reduce((sum, candidates) => sum + candidates.length, 0);
            let completedCandidates = 0;
            let lastProgressSentAt = 0;
            const sendProgress = async force => {
                if (cancelledBrowserMonitorJobs.has(schedule.id) || monitorAuthRedirecting) return;
                if (!force && Date.now() - lastProgressSentAt < 4000) return;
                lastProgressSentAt = Date.now();
                const progressResponse = await fetch(`/api/competitor-monitor/${encodeURIComponent(schedule.id)}/client-progress`, {
                    method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ deviceId: browserOcrDeviceId, processed: completedCandidates, total: totalCandidates })
                }).catch(() => null);
                if (progressResponse) handleMonitorUnauthorized(progressResponse);
            };
            await sendProgress(true);
            for (const [advertiserId, candidates] of Object.entries(prepared.candidates || {})) {
                const domains = new Map();
                const concurrency = browserOcrPoolSize;
                for (let start = 0; start < candidates.length; start += concurrency) {
                    if (cancelledBrowserMonitorJobs.has(schedule.id) || monitorAuthRedirecting) break;
                    await Promise.all(candidates.slice(start, start + concurrency).map(async candidate => {
                        try {
                            const isPreview = Boolean(candidate.previewUrl);
                            const ocr = await browserOcrResource(candidate.previewUrl || candidate.imageUrl, isPreview);
                            for (const rawDomain of ocr.domains || []) {
                                const domain = normalizeProjectDomain(rawDomain);
                                if (domain && !domains.has(domain)) domains.set(domain, candidate.detail || {});
                            }
                        } catch (error) {
                            if (error.status === 401) throw error;
                            console.warn('Bỏ qua mẫu OCR lỗi:', error.message);
                        }
                    }));
                    completedCandidates += Math.min(concurrency, candidates.length - start);
                    await sendProgress(completedCandidates >= totalCandidates);
                }
                results[advertiserId] = Array.from(domains, ([domain, detail]) => ({ domain, detail }));
            }
            if (cancelledBrowserMonitorJobs.has(schedule.id) || monitorAuthRedirecting) continue;
            const submitResponse = await fetch(`/api/competitor-monitor/${encodeURIComponent(schedule.id)}/client-results`, {
                method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ deviceId: browserOcrDeviceId, results })
            });
            if (handleMonitorUnauthorized(submitResponse)) return;
            const submitted = await submitResponse.json();
            if (!submitResponse.ok) throw new Error(submitted.error || `HTTP ${submitResponse.status}`);
            showMonitorToast('OCR lịch theo dõi đã hoàn tất bằng cấu hình máy của bạn.');
        } catch (error) {
            if (error.status === 401 || monitorAuthRedirecting) return;
            if (cancelledBrowserMonitorJobs.has(schedule.id)) continue;
            showMonitorToast(`OCR lịch theo dõi lỗi: ${error.message}`, 'error');
        } finally {
            activeBrowserMonitorJobs.delete(schedule.id);
            if (!monitorAuthRedirecting) setTimeout(loadCompetitorMonitor, 500);
        }
    }
}

window.alert = message => showMonitorToast(String(message || ''), 'warning');

async function createCompetitorMonitor() {
    const ids = parseSavedAdvertiserIds(elMonitorAdvertiserIds ? elMonitorAdvertiserIds.value : '');
    if (!ids.length) return alert('Hãy nhập ít nhất một Advertiser ID cần theo dõi.');
    const curlStr = `curl 'https://adstransparency.google.com/' -H 'accept: application/json'`;
    const parsed = parseCurl(curlStr);
    if (!parsed) return alert('Cấu hình xác thực dùng chung chưa hợp lệ. Vui lòng liên hệ Admin.');
    elBtnCreateMonitor.disabled = true;
    try {
        const unitMultiplier = { minutes: 1, hours: 60, days: 1440 };
        const intervalMinutes = Number(elMonitorIntervalValue.value) * (unitMultiplier[elMonitorIntervalUnit.value] || 1);
        const response = await fetch('/api/competitor-monitor', {
            method: 'POST', headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                advertiserIds: ids, intervalMinutes,
                headers: parsed.headers, fReq: parsed.fReq || {},
                proxy: elMonitorProxyUrl?.value.trim() || elAdvProxyUrl?.value.trim() || elProxyUrl?.value.trim() || '', disableProxy: !isProxyEnabledGlobally()
            })
        });
        const result = await response.json();
        if (!response.ok) throw new Error(result.error || `HTTP ${response.status}`);
        await loadCompetitorMonitor();
        showMonitorToast(`Đang quét OCR lần đầu cho ${ids.length} Advertiser ID để tạo dữ liệu mốc.`);
    } catch (error) { showMonitorToast(error.message, 'error'); }
    finally { elBtnCreateMonitor.disabled = false; }
}

async function testMonitorTelegram() {
    const botToken = elMonitorTelegramBotToken?.value.trim() || '';
    const chatId = elMonitorTelegramChatId?.value.trim() || '';
    if ((!botToken && !latestMonitorData.telegram?.configured) || (!chatId && !latestMonitorData.telegram?.chatId)) {
        return showMonitorToast('Hãy nhập đủ Bot Token và Chat ID để test.', 'error');
    }

    const originalHtml = elBtnTestMonitorTelegram.innerHTML;
    elBtnTestMonitorTelegram.disabled = true;
    elBtnTestMonitorTelegram.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Đang gửi';
    elMonitorTelegramTestStatus.className = '';
    elMonitorTelegramTestStatus.textContent = 'Đang kết nối Telegram...';
    try {
        const response = await fetch('/api/competitor-monitor/telegram/test', {
            method: 'POST', headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ botToken, chatId })
        });
        const result = await response.json();
        if (!response.ok || !result.ok) throw new Error(result.error || `HTTP ${response.status}`);
        elMonitorTelegramTestStatus.className = 'success';
        elMonitorTelegramTestStatus.textContent = `@${result.botUsername || 'bot'} đã gửi thành công tới ${result.chatTitle || result.chatId}.`;
        showMonitorToast('Telegram Bot hoạt động tốt. Hãy kiểm tra tin nhắn vừa nhận.');
    } catch (error) {
        elMonitorTelegramTestStatus.className = 'error';
        elMonitorTelegramTestStatus.textContent = error.message;
        showMonitorToast(`Test Telegram thất bại: ${error.message}`, 'error');
    } finally {
        elBtnTestMonitorTelegram.disabled = false;
        elBtnTestMonitorTelegram.innerHTML = originalHtml;
    }
}

async function saveMonitorTelegramSettings() {
    const enabled = Boolean(elMonitorTelegramEnabled?.checked);
    const botToken = elMonitorTelegramBotToken?.value.trim() || '';
    const chatId = elMonitorTelegramChatId?.value.trim() || '';
    elBtnSaveMonitorTelegram.disabled = true;
    try {
        const response = await fetch('/api/competitor-monitor/telegram', {
            method: 'PUT', headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ enabled, botToken, chatId })
        });
        const result = await response.json();
        if (!response.ok || !result.ok) throw new Error(result.error || `HTTP ${response.status}`);
        elMonitorTelegramBotToken.value = '';
        await loadCompetitorMonitor();
        elMonitorTelegramTestStatus.className = 'success';
        elMonitorTelegramTestStatus.textContent = enabled ? 'Đã lưu cấu hình Telegram cá nhân.' : 'Đã tắt thông báo Telegram của tài khoản.';
        showMonitorToast(elMonitorTelegramTestStatus.textContent);
    } catch (error) {
        showMonitorToast(`Không lưu được Telegram: ${error.message}`, 'error');
    } finally { elBtnSaveMonitorTelegram.disabled = false; }
}

function openMonitorDetail(scheduleId) {
    const schedule = latestMonitorData.schedules.find(item => item.id === scheduleId);
    if (!schedule || !elMonitorDetailModal) return;
    const results = schedule.lastScanResults || {};
    elMonitorDetailSummary.textContent = `Lần cuối ${schedule.lastRunAt ? new Date(schedule.lastRunAt).toLocaleString('vi-VN') : 'chưa quét'} • kế tiếp ${schedule.nextRunAt ? new Date(schedule.nextRunAt).toLocaleString('vi-VN') : '-'}`;
    elMonitorDetailContent.innerHTML = schedule.advertiserIds.map(advertiserId => {
        const result = results[advertiserId] || {};
        const newDomains = result.newDomains || [];
        const knownDomains = result.knownDomains || (schedule.snapshot?.[advertiserId] || []).map(domain => ({ domain }));
        const renderDomains = (items, isNew) => items.length ? items.map(item => `
            <div class="monitor-domain-row"><span class="${isNew ? 'monitor-new-label' : 'monitor-old-label'}">${escapeMonitorHtml(item.domain)}</span>
                ${item.adUrl ? `<a class="monitor-ad-link" href="${item.adUrl}" target="_blank" rel="noopener noreferrer"><i class="fa-solid fa-arrow-up-right-from-square"></i><span>Mở mẫu</span><small>${escapeMonitorHtml(item.creativeId || '')}</small></a>` : '<span>-</span>'}</div>`).join('') : '<div class="keyword-metric-empty">Không có</div>';
        return `<section class="monitor-detail-id">
            <h3>${escapeMonitorHtml(advertiserId)}</h3>
            <div class="notification-meta">Quét lúc: ${result.scannedAt ? new Date(result.scannedAt).toLocaleString('vi-VN') : '-'} • Tổng ${result.totalDomains ?? (schedule.snapshot?.[advertiserId] || []).length} domain OCR</div>
            <div class="monitor-domain-groups">
                <div class="monitor-domain-group"><strong class="monitor-new-label">Dự án mới (${newDomains.length})</strong>${renderDomains(newDomains, true)}</div>
                <div class="monitor-domain-group"><strong class="monitor-old-label">Dự án đã biết (${knownDomains.length})</strong>${renderDomains(knownDomains, false)}</div>
            </div></section>`;
    }).join('');
    elMonitorDetailModal.hidden = false;
}

function updateSavedAdvertiserIdCount() {
    const ids = parseSavedAdvertiserIds(elSavedAdvIds ? elSavedAdvIds.value : '');
    if (elSavedAdvIdsCount) elSavedAdvIdsCount.textContent = `${ids.length} ID`;
    return ids;
}

async function runAdvertiserScanByMode() {
    sheetExportRecords = [];
    if (elBtnPushGoogleSheet) elBtnPushGoogleSheet.disabled = true;
    if (!elAdvScanMode || elAdvScanMode.value === 'single') {
        await startAdvScrape();
        return;
    }

    const ids = updateSavedAdvertiserIdCount();
    if (!ids.length) {
        alert('Danh sách ID đã lưu đang trống.');
        return;
    }

    const allCreatives = [];
    const allFilteredCreatives = [];
    elBtnAdvStart.disabled = true;
    for (let index = 0; index < ids.length; index++) {
        elTargetAdvId.value = ids[index];
        logAdv(`Quét danh sách ID: ${index + 1}/${ids.length} — ${ids[index]}`, 'info');
        const result = await startAdvScrape(ids[index]);
        if (result) {
            allCreatives.push(...result.creatives);
            allFilteredCreatives.push(...result.filteredCreatives);
        }
        if (advController && advController.signal.aborted) break;
    }

    advertiserCreatives = allCreatives;
    filteredAdvertiserCreatives = allFilteredCreatives;
    detectedDomainsMap = {};
    advertiserCreatives.forEach(creative => {
        if (creative.domain) addDetectedDomain(creative.domain, 'Google API', creative, true);
    });
    elCreativeGrid.innerHTML = '';
    renderCreativeGrid(advertiserCreatives);
    renderAdvDomainTable();
    renderFilteredAdsTable();
    elStatAdvTotal.textContent = advertiserCreatives.length;
    elStatAdvDomains.textContent = Object.keys(detectedDomainsMap).length;
    elStatAdvFiltered.textContent = filteredAdvertiserCreatives.length;
    if (advertiserCreatives.some(creative => creative.imageUrl || creative.previewUrl)) {
        elBtnRunAutoOcr.disabled = false;
    }
    logAdv(`Hoàn tất danh sách: ${ids.length} ID, ${sheetExportRecords.length} mẫu sẵn sàng gửi Sheet.`, 'success');
}

async function pushResultsToGoogleSheet() {
    const spreadsheetUrl = elGoogleSpreadsheetUrl ? elGoogleSpreadsheetUrl.value.trim() : '';
    const webAppUrl = elGoogleSheetWebAppUrl ? elGoogleSheetWebAppUrl.value.trim() : '';
    const sheetName = elGoogleSheetName ? elGoogleSheetName.value.trim() : 'Competitor Ads';
    const secret = elGoogleSheetSecret ? elGoogleSheetSecret.value : '';
    if (!spreadsheetUrl && (!webAppUrl || !secret)) return alert('Hãy kết nối Google và nhập link Google Sheet, hoặc dùng cấu hình Apps Script dự phòng.');
    const selectedFields = elGoogleSheetFieldOptions
        ? Array.from(elGoogleSheetFieldOptions.querySelectorAll('input[type="checkbox"]:checked')).map(input => input.value)
        : ['domain', 'firstSeen', 'lastSeen', 'runningDays'];
    if (!selectedFields.length) return alert('Hãy chọn ít nhất một cột dữ liệu để gửi.');

    const completeRecords = Object.entries(detectedDomainsMap).map(([domain, info]) => {
        const period = calculateActivePeriod(Array.isArray(info.ads) ? info.ads : []);
        return {
            domain,
            firstSeen: period.firstSeen || '',
            lastSeen: period.lastSeen || '',
            runningDays: period.days || 0,
            keyword: info.keyword || domainToBrandKeyword(domain),
            volume: info.metrics?.volume ?? '',
            cpcValue: info.metrics?.cpcValue ?? '',
            competition: info.metrics?.competition ?? '',
            activeAdvertisersCount: info.metrics?.activeAdvertisersCount ?? '',
            adCount: info.count || 0,
            source: Array.from(info.sources || []).join(', ')
        };
    });
    const records = completeRecords.map(record => Object.fromEntries(
        selectedFields.map(field => [field, record[field] ?? ''])
    ));
    if (!records.length) return alert('Chưa có domain nào để gửi. Hãy quét ID hoặc OCR trước.');
    const columns = selectedFields.map(key => ({ key, label: SHEET_FIELD_LABELS[key] || key }));

    elBtnPushGoogleSheet.disabled = true;
    elGoogleSheetStatus.className = 'sheet-export-status';
    elGoogleSheetStatus.textContent = `Đang gửi ${records.length} domain với ${columns.length} cột...`;
    try {
        const response = await fetch('/api/google-sheet-export', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ spreadsheetUrl, webAppUrl, sheetName, secret, columns, records })
        });
        const result = await response.json();
        if (!response.ok || !result.ok) throw new Error(result.error || `HTTP ${response.status}`);
        if (result.mode !== 'oauth' && result.receiverVersion !== 2) {
            throw new Error('Apps Script Web App vẫn đang chạy phiên bản cũ. Hãy tạo Phiên bản mới trong Quản lý hoạt động triển khai rồi thử lại.');
        }
        elGoogleSheetStatus.className = 'sheet-export-status success';
        elGoogleSheetStatus.textContent = result.mode === 'oauth'
            ? `Đã ghi ${result.inserted || 0} dòng trực tiếp bằng Google Sheets API.`
            : `Đã thêm ${result.inserted || 0} dòng; cập nhật ${result.updated || 0} dòng domain đã có.`;
    } catch (error) {
        elGoogleSheetStatus.className = 'sheet-export-status error';
        elGoogleSheetStatus.textContent = `Không thể gửi: ${error.message}`;
    } finally {
        elBtnPushGoogleSheet.disabled = Object.keys(detectedDomainsMap).length === 0;
    }
}

async function loadGoogleSheetsOAuthStatus() {
    if (!elGoogleSheetsOAuthState && !traceDomainHistoryEls.googleStatus) return;
    try {
        const response = await fetch('/api/google-sheets/oauth/status');
        const status = await response.json();
        if (!response.ok) throw new Error(status.error || `HTTP ${response.status}`);
        elGoogleSheetsOAuthState.className = `sheet-export-status ${status.connected ? 'success' : ''}`;
        elGoogleSheetsOAuthState.textContent = status.connected
            ? `Đã kết nối${status.email ? `: ${status.email}` : ' Google Sheets'}.`
            : status.configured ? 'Chưa kết nối Google Sheets.' : 'Admin chưa cấu hình Google OAuth trên server.';
        if (elBtnConnectGoogleSheets) {
            elBtnConnectGoogleSheets.hidden = status.connected;
            elBtnConnectGoogleSheets.disabled = !status.configured;
        }
        if (elBtnDisconnectGoogleSheets) elBtnDisconnectGoogleSheets.hidden = !status.connected;
        if (traceDomainHistoryEls.googleStatus) {
            traceDomainHistoryEls.googleStatus.className = `sheet-export-status ${status.connected ? 'success' : status.configured ? '' : 'error'}`;
            traceDomainHistoryEls.googleStatus.textContent = status.connected ? `Đã kết nối${status.email ? `: ${status.email}` : ' Google Sheets'}.` : status.configured ? 'Ứng dụng đã cấu hình, nhưng tài khoản này chưa kết nối Google.' : 'Admin chưa cấu hình Google OAuth.';
        }
        if (traceDomainHistoryEls.googleConnect) { traceDomainHistoryEls.googleConnect.hidden = status.connected; traceDomainHistoryEls.googleConnect.disabled = !status.configured; }
    } catch (error) {
        if (elGoogleSheetsOAuthState) { elGoogleSheetsOAuthState.className = 'sheet-export-status error'; elGoogleSheetsOAuthState.textContent = `Không kiểm tra được Google OAuth: ${error.message}`; }
        if (traceDomainHistoryEls.googleStatus) { traceDomainHistoryEls.googleStatus.className = 'sheet-export-status error'; traceDomainHistoryEls.googleStatus.textContent = `Không kiểm tra được Google OAuth: ${error.message}`; }
    }
}

async function connectGoogleSheetsOAuth() {
    const response = await fetch('/api/google-sheets/oauth/start');
    const result = await response.json();
    if (!response.ok || !result.url) return alert(result.error || 'Không tạo được liên kết Google OAuth.');
    location.href = result.url;
}

async function disconnectGoogleSheetsOAuth() {
    if (!await showAppConfirm('Ngắt kết nối Google Sheets của tài khoản này?', { danger: true, confirmText: 'Ngắt kết nối' })) return;
    const response = await fetch('/api/google-sheets/oauth', { method: 'DELETE' });
    if (!response.ok) return alert('Không thể ngắt kết nối Google Sheets.');
    await loadGoogleSheetsOAuthStatus();
}

// Bind events
elBtnAdvStart.addEventListener('click', runAdvertiserScanByMode);
elBtnAdvStop.addEventListener('click', stopAdvScrape);
elBtnRunAutoOcr.addEventListener('click', runAutoOcr);
if (elAdvScanMode) elAdvScanMode.addEventListener('change', () => {
    if (elSavedAdvIdsBox) elSavedAdvIdsBox.hidden = elAdvScanMode.value !== 'saved';
});
if (elSavedAdvIds) elSavedAdvIds.addEventListener('input', updateSavedAdvertiserIdCount);
if (elBtnSaveAdvIds) elBtnSaveAdvIds.addEventListener('click', () => {
    const ids = updateSavedAdvertiserIdCount();
    elSavedAdvIds.value = ids.join('\n');
    localStorage.setItem('saved_competitor_adv_ids', elSavedAdvIds.value);
    logAdv(`Đã lưu ${ids.length} ID đối thủ.`, 'success');
});
if (elBtnPushGoogleSheet) elBtnPushGoogleSheet.addEventListener('click', pushResultsToGoogleSheet);
if (elBtnConnectGoogleSheets) elBtnConnectGoogleSheets.addEventListener('click', connectGoogleSheetsOAuth);
if (elBtnDisconnectGoogleSheets) elBtnDisconnectGoogleSheets.addEventListener('click', disconnectGoogleSheetsOAuth);
if (elBtnScanKeywordMetrics) elBtnScanKeywordMetrics.addEventListener('click', () => scanKeywordMetrics(false));
if (elBtnRetryKeywordMetricsErrors) elBtnRetryKeywordMetricsErrors.addEventListener('click', () => scanKeywordMetrics(true));
if (elBtnScanAdvertiserCounts) elBtnScanAdvertiserCounts.addEventListener('click', () => scanAdvertiserCounts(false));
if (elBtnRetryAdvertiserCountErrors) elBtnRetryAdvertiserCountErrors.addEventListener('click', () => scanAdvertiserCounts(true));
if (elBtnScanTraffic) elBtnScanTraffic.addEventListener('click', () => scanTraffic(false));
if (elBtnRetryTrafficErrors) elBtnRetryTrafficErrors.addEventListener('click', () => scanTraffic(true));
if (elBtnCreateMonitor) elBtnCreateMonitor.addEventListener('click', createCompetitorMonitor);
if (elMonitorTelegramEnabled) elMonitorTelegramEnabled.addEventListener('change', () => {
    elMonitorTelegramFields.hidden = !elMonitorTelegramEnabled.checked;
});
if (elBtnTestMonitorTelegram) elBtnTestMonitorTelegram.addEventListener('click', testMonitorTelegram);
if (elBtnSaveMonitorTelegram) elBtnSaveMonitorTelegram.addEventListener('click', saveMonitorTelegramSettings);
if (elMonitorAdvertiserIds) {
    elMonitorAdvertiserIds.value = localStorage.getItem('monitor_advertiser_ids') || '';
    elMonitorAdvertiserIds.addEventListener('input', () => localStorage.setItem('monitor_advertiser_ids', elMonitorAdvertiserIds.value));
}
if (elMonitorCurlCommand) {
    elMonitorCurlCommand.value = localStorage.getItem('saved_adv_curl_command') || localStorage.getItem('saved_curl_command') || DEFAULT_CURL;
    elMonitorCurlCommand.addEventListener('input', () => {
        localStorage.setItem('saved_adv_curl_command', elMonitorCurlCommand.value);
        if (elAdvCurlCommand) elAdvCurlCommand.value = elMonitorCurlCommand.value;
    });
}
if (elMonitorProxyUrl) {
    elMonitorProxyUrl.value = localStorage.getItem('saved_adv_proxy_url') || localStorage.getItem('saved_proxy_url') || elAdvProxyUrl?.value || elProxyUrl?.value || '';
    elMonitorProxyUrl.addEventListener('input', () => {
        localStorage.setItem('saved_adv_proxy_url', elMonitorProxyUrl.value);
        if (elAdvProxyUrl) elAdvProxyUrl.value = elMonitorProxyUrl.value;
    });
}
if (elBtnNotifications) elBtnNotifications.addEventListener('click', async () => {
    elNotificationModal.hidden = false;
    await loadCompetitorMonitor();
});
document.querySelectorAll('[data-close-notifications]').forEach(element => element.addEventListener('click', () => {
    elNotificationModal.hidden = true;
}));
if (elBtnReadAllNotifications) elBtnReadAllNotifications.addEventListener('click', async () => {
    await fetch('/api/competitor-monitor/notifications/all', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: '{}' });
    await loadCompetitorMonitor();
});
if (elMonitorScheduleList) elMonitorScheduleList.addEventListener('click', async event => {
    const button = event.target.closest('button[data-id]');
    if (!button) return;
    if (button.classList.contains('monitor-detail')) return openMonitorDetail(button.dataset.id);
    const editRow = elMonitorScheduleList.querySelector(`[data-edit-id="${button.dataset.id}"]`);
    if (button.classList.contains('monitor-edit')) {
        editRow.hidden = false;
        return;
    }
    if (button.classList.contains('monitor-edit-cancel')) {
        editRow.hidden = true;
        return;
    }
    if (button.classList.contains('monitor-edit-save')) {
        const multiplier = { minutes: 1, hours: 60, days: 1440 };
        const intervalMinutes = Number(editRow.querySelector('.monitor-edit-value').value) * multiplier[editRow.querySelector('.monitor-edit-unit').value];
        const response = await fetch(`/api/competitor-monitor/${encodeURIComponent(button.dataset.id)}`, {
            method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ intervalMinutes })
        });
        const result = await response.json();
        if (!response.ok) return showMonitorToast(result.error || 'Không sửa được chu kỳ.', 'error');
        showMonitorToast('Chu kỳ và thời điểm quét kế tiếp đã được cập nhật.');
        return loadCompetitorMonitor();
    }
    if (button.classList.contains('monitor-delete') && !await showAppConfirm('Xóa lịch theo dõi này?', { danger: true, confirmText: 'Xóa lịch' })) return;
    const isRun = button.classList.contains('monitor-run');
    let options = { method: isRun ? 'POST' : 'DELETE' };
    if (isRun) {
        const parsed = parseCurl(`curl 'https://adstransparency.google.com/' -H 'accept: application/json'`);
        if (!parsed) return alert('Cấu hình xác thực dùng chung chưa hợp lệ. Vui lòng liên hệ Admin.');
        options = {
            method: 'POST', headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                headers: parsed.headers,
                proxy: elMonitorProxyUrl?.value.trim() || elAdvProxyUrl?.value.trim() || elProxyUrl?.value.trim() || '',
                disableProxy: !isProxyEnabledGlobally()
            })
        };
    }
    const scheduleId = button.dataset.id;
    const response = await fetch(`/api/competitor-monitor/${encodeURIComponent(scheduleId)}${isRun ? '/run' : ''}`, options);
    if (handleMonitorUnauthorized(response)) return;
    const result = await response.json().catch(() => ({}));
    if (!response.ok) return showMonitorToast(result.error || `${isRun ? 'Không thể quét lịch' : 'Không thể xóa lịch'}: HTTP ${response.status}`, 'error');
    if (!isRun) {
        cancelledBrowserMonitorJobs.add(scheduleId);
        activeBrowserMonitorJobs.delete(scheduleId);
        showMonitorToast('Đã xóa lịch theo dõi.');
    }
    await loadCompetitorMonitor();
});
document.querySelectorAll('[data-close-monitor-detail]').forEach(element => element.addEventListener('click', () => {
    elMonitorDetailModal.hidden = true;
}));
if (elNotificationList) elNotificationList.addEventListener('click', async event => {
    const control = event.target.closest('.notification-open-ad, .notification-toggle-read');
    if (!control) return;
    const read = control.classList.contains('notification-toggle-read') ? control.dataset.read !== 'true' : true;
    const notificationItem = control.closest('.notification-item');
    if (!notificationItem?.classList.contains('read')) notificationItem?.classList.add('dismissing');
    await fetch(`/api/competitor-monitor/notifications/${encodeURIComponent(control.dataset.id)}`, {
        method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ read })
    });
    setTimeout(loadCompetitorMonitor, 300);
});
const monitorStartupTimer = setInterval(() => {
    if (!authenticatedUser || monitorAuthRedirecting) return;
    clearInterval(monitorStartupTimer);
    loadCompetitorMonitor();
    setInterval(loadCompetitorMonitor, 10000);
}, 100);
if (elGoogleSheetFieldOptions) {
    elGoogleSheetFieldOptions.addEventListener('change', () => {
        const selected = Array.from(elGoogleSheetFieldOptions.querySelectorAll('input:checked')).map(input => input.value);
        localStorage.setItem('saved_google_sheet_fields', JSON.stringify(selected));
    });
}
if (elBtnToggleSheetFields && elGoogleSheetFieldOptions) {
    elBtnToggleSheetFields.addEventListener('click', () => {
        const inputs = Array.from(elGoogleSheetFieldOptions.querySelectorAll('input[type="checkbox"]'));
        const shouldSelectAll = inputs.some(input => !input.checked);
        inputs.forEach(input => { input.checked = shouldSelectAll; });
        elBtnToggleSheetFields.textContent = shouldSelectAll ? 'Bỏ chọn tất cả' : 'Chọn tất cả';
        elGoogleSheetFieldOptions.dispatchEvent(new Event('change'));
    });
}

// Preset Date Logic
function handlePresetChange(selectEl, startInput, endInput, rowEl) {
    if (!selectEl || !startInput || !endInput) return;

    const val = selectEl.value;
    if (val === 'custom') {
        if (rowEl) rowEl.style.display = 'grid';
    } else if (val === 'all') {
        if (rowEl) rowEl.style.display = 'none';
        startInput.value = '';
        endInput.value = '';
        try {
            localStorage.setItem(startInput.id === 'startDate' ? 'saved_start_date' : 'saved_adv_start_date', '');
            localStorage.setItem(endInput.id === 'endDate' ? 'saved_end_date' : 'saved_adv_end_date', '');
        } catch (e) {}
    } else {
        if (rowEl) rowEl.style.display = 'none';
        
        const days = parseInt(val, 10);
        const todayObj = new Date();
        const startObj = new Date();
        startObj.setDate(todayObj.getDate() - days);

        const formatDate = (d) => {
            const y = d.getFullYear();
            const m = String(d.getMonth() + 1).padStart(2, '0');
            const date = String(d.getDate()).padStart(2, '0');
            return `${y}-${m}-${date}`;
        };

        startInput.value = formatDate(startObj);
        endInput.value = formatDate(todayObj);

        try {
            localStorage.setItem(startInput.id === 'startDate' ? 'saved_start_date' : 'saved_adv_start_date', startInput.value);
            localStorage.setItem(endInput.id === 'endDate' ? 'saved_end_date' : 'saved_adv_end_date', endInput.value);
        } catch (e) {}
    }
}

if (elDateRangePreset) {
    elDateRangePreset.addEventListener('change', () => {
        handlePresetChange(elDateRangePreset, elStartDate, elEndDate, elDatePickerRow);
        try { localStorage.setItem('saved_date_range_preset', elDateRangePreset.value); } catch(e) {}
    });
}
if (elAdvDateRangePreset) {
    elAdvDateRangePreset.addEventListener('change', () => {
        handlePresetChange(elAdvDateRangePreset, elAdvStartDate, elAdvEndDate, elAdvDatePickerRow);
        try { localStorage.setItem('saved_adv_date_range_preset', elAdvDateRangePreset.value); } catch(e) {}
    });
}

// ==========================================
// FEATURE: LocalStorage Persistence
// ==========================================
function loadSavedInputs() {
    try {
        if (elSavedAdvIds) {
            elSavedAdvIds.value = localStorage.getItem('saved_competitor_adv_ids') || '';
            updateSavedAdvertiserIdCount();
        }
        if (elAdvScanMode) {
            elAdvScanMode.value = localStorage.getItem('saved_adv_scan_mode') || 'single';
            if (elSavedAdvIdsBox) elSavedAdvIdsBox.hidden = elAdvScanMode.value !== 'saved';
        }
        if (elGoogleSheetWebAppUrl) elGoogleSheetWebAppUrl.value = localStorage.getItem('saved_google_sheet_webapp_url') || '';
        if (elGoogleSpreadsheetUrl) elGoogleSpreadsheetUrl.value = localStorage.getItem('saved_google_spreadsheet_url') || '';
        if (elGoogleSheetName) elGoogleSheetName.value = localStorage.getItem('saved_google_sheet_name') || 'Competitor Ads';
        if (elGoogleSheetFieldOptions) {
            const savedFields = JSON.parse(localStorage.getItem('saved_google_sheet_fields') || 'null');
            if (Array.isArray(savedFields)) {
                elGoogleSheetFieldOptions.querySelectorAll('input[type="checkbox"]').forEach(input => {
                    input.checked = savedFields.includes(input.value);
                });
            }
        }

        const savedDomain = localStorage.getItem('saved_target_domain');
        if (savedDomain !== null) elTargetDomain.value = savedDomain;

        const savedDomainLimit = localStorage.getItem('saved_domain_creative_limit');
        if (savedDomainLimit !== null && elDomainCreativeLimit) elDomainCreativeLimit.value = savedDomainLimit;

        const savedAdvId = localStorage.getItem('saved_target_adv_id');
        if (savedAdvId !== null) elTargetAdvId.value = savedAdvId;

        const savedProxy = localStorage.getItem('saved_proxy_url');
        if (savedProxy !== null) elProxyUrl.value = savedProxy;

        const savedAdvProxy = localStorage.getItem('saved_adv_proxy_url');
        if (savedAdvProxy !== null) elAdvProxyUrl.value = savedAdvProxy;

        const savedCurl = localStorage.getItem('saved_curl_command');
        elCurlCommand.value = savedCurl !== null ? savedCurl : DEFAULT_CURL;

        const savedAdvCurl = localStorage.getItem('saved_adv_curl_command');
        elAdvCurlCommand.value = savedAdvCurl !== null ? savedAdvCurl : DEFAULT_CURL;

        // Load Preset
        const savedPreset = localStorage.getItem('saved_date_range_preset');
        if (savedPreset !== null && elDateRangePreset) {
            elDateRangePreset.value = savedPreset;
        }
        const savedAdvPreset = localStorage.getItem('saved_adv_date_range_preset');
        if (savedAdvPreset !== null && elAdvDateRangePreset) {
            elAdvDateRangePreset.value = savedAdvPreset;
        }

        // Load custom dates if preset is custom, otherwise run handler
        if (elDateRangePreset && elDateRangePreset.value === 'custom') {
            if (elDatePickerRow) elDatePickerRow.style.display = 'grid';
            const savedStartDate = localStorage.getItem('saved_start_date');
            if (savedStartDate !== null && elStartDate) elStartDate.value = savedStartDate;
            const savedEndDate = localStorage.getItem('saved_end_date');
            if (savedEndDate !== null && elEndDate) elEndDate.value = savedEndDate;
        } else {
            handlePresetChange(elDateRangePreset, elStartDate, elEndDate, elDatePickerRow);
        }

        if (elAdvDateRangePreset && elAdvDateRangePreset.value === 'custom') {
            if (elAdvDatePickerRow) elAdvDatePickerRow.style.display = 'grid';
            const savedAdvStartDate = localStorage.getItem('saved_adv_start_date');
            if (savedAdvStartDate !== null && elAdvStartDate) elAdvStartDate.value = savedAdvStartDate;
            const savedAdvEndDate = localStorage.getItem('saved_adv_end_date');
            if (savedAdvEndDate !== null && elAdvEndDate) elAdvEndDate.value = savedAdvEndDate;
        } else {
            handlePresetChange(elAdvDateRangePreset, elAdvStartDate, elAdvEndDate, elAdvDatePickerRow);
        }

    } catch (e) {
        console.error('Failed to load saved inputs from localStorage:', e);
    }
}

function bindLocalStorageSavers() {
    const inputs = [
        { el: elTargetDomain, key: 'saved_target_domain' },
        { el: elDomainCreativeLimit, key: 'saved_domain_creative_limit' },
        { el: elTargetAdvId, key: 'saved_target_adv_id' },
        { el: elProxyUrl, key: 'saved_proxy_url' },
        { el: elAdvProxyUrl, key: 'saved_adv_proxy_url' },
        { el: elCurlCommand, key: 'saved_curl_command' },
        { el: elAdvCurlCommand, key: 'saved_adv_curl_command' },
        { el: elStartDate, key: 'saved_start_date' },
        { el: elEndDate, key: 'saved_end_date' },
        { el: elAdvStartDate, key: 'saved_adv_start_date' },
        { el: elAdvEndDate, key: 'saved_adv_end_date' },
        { el: elAdvScanMode, key: 'saved_adv_scan_mode' },
        { el: elGoogleSheetWebAppUrl, key: 'saved_google_sheet_webapp_url' },
        { el: elGoogleSpreadsheetUrl, key: 'saved_google_spreadsheet_url' },
        { el: elGoogleSheetName, key: 'saved_google_sheet_name' }
    ];

    inputs.forEach(item => {
        if (item.el) {
            item.el.addEventListener('input', () => {
                try {
                    localStorage.setItem(item.key, item.el.value);
                } catch (e) {
                    console.error(`Failed to save ${item.key} to localStorage:`, e);
                }
            });
        }
    });
}

// Run persistence
loadSavedInputs();
bindLocalStorageSavers();

// ==========================================
// FEATURE: Scan History Log (Local Storage)
// ==========================================

function scanHistoryStorageKey() {
    const key = `app_scan_history_list:${authenticatedUser?.id || 'anonymous'}`;
    if (authenticatedUser?.role === 'admin' && !localStorage.getItem(key) && localStorage.getItem('app_scan_history_list')) {
        localStorage.setItem(key, localStorage.getItem('app_scan_history_list'));
        localStorage.removeItem('app_scan_history_list');
    }
    return key;
}

function serializeDetectedDomainsForHistory(domainMap = detectedDomainsMap) {
    const serializableMap = {};
    Object.entries(domainMap || {}).forEach(([domain, info]) => {
        const traffic = info.traffic && typeof info.traffic === 'object'
            ? { ...info.traffic }
            : info.traffic || null;
        // Workfast's raw response contains a full country directory and can
        // quickly exceed the browser's localStorage quota. All displayed
        // traffic fields already exist on the normalized result.
        if (traffic && typeof traffic === 'object') delete traffic.raw;

        serializableMap[domain] = {
            count: info.count,
            sources: info.sources instanceof Set ? Array.from(info.sources) : info.sources,
            ads: Array.isArray(info.ads) ? info.ads : [],
            keyword: info.keyword || domainToBrandKeyword(domain),
            metrics: info.metrics && typeof info.metrics === 'object' ? { ...info.metrics } : null,
            traffic,
            metricsError: info.metricsError || '',
            metricsErrorDetails: info.metricsErrorDetails || null,
            summaryError: info.summaryError || '',
            summaryErrorDetails: info.summaryErrorDetails || null,
            trafficError: info.trafficError || '',
            trafficErrorDetails: info.trafficErrorDetails || null
        };
    });
    return serializableMap;
}

function saveScanHistory(type, target, summaryStats, fullData) {
    if (!target) return;

    try {
        let historyList = [];
        try {
            historyList = JSON.parse(localStorage.getItem(scanHistoryStorageKey())) || [];
        } catch (e) {
            historyList = [];
        }

        // Convert Sets in detectedDomainsMap if any for JSON serialization
        if (fullData && fullData.detectedDomainsMap) {
            fullData.detectedDomainsMap = serializeDetectedDomainsForHistory(fullData.detectedDomainsMap);
        }

        const now = new Date();
        const displayTime = `${now.toLocaleDateString('vi-VN')} ${now.toLocaleTimeString('vi-VN')}`;

        const item = {
            id: 'hist_' + Date.now(),
            timestamp: now.getTime(),
            displayTime,
            type, // 'domain' or 'advertiser'
            target,
            summaryStats,
            fullData
        };

        historyList.unshift(item);

        if (historyList.length > 30) {
            historyList = historyList.slice(0, 30);
        }

        localStorage.setItem(scanHistoryStorageKey(), JSON.stringify(historyList));
        renderHistoryTable();
    } catch (e) {
        console.error('Failed to save scan history to localStorage:', e);
    }
}

function updateCurrentAdvHistoryRecord() {
    try {
        let historyList = JSON.parse(localStorage.getItem(scanHistoryStorageKey())) || [];
        if (historyList.length === 0) return;

        const currentAdvId = elTargetAdvId ? elTargetAdvId.value.trim() : '';
        const activeRecord = historyList.find(r => r.type === 'advertiser' && r.target === currentAdvId);

        if (activeRecord) {
            const domainCount = Object.keys(detectedDomainsMap).length;
            activeRecord.summaryStats.domainCount = domainCount;
            activeRecord.summaryStats.filteredCount = filteredAdvertiserCreatives.length;

            activeRecord.fullData.detectedDomainsMap = serializeDetectedDomainsForHistory();
            activeRecord.fullData.advertiserCreatives = advertiserCreatives;
            activeRecord.fullData.filteredAdvertiserCreatives = filteredAdvertiserCreatives;

            localStorage.setItem(scanHistoryStorageKey(), JSON.stringify(historyList));
            renderHistoryTable();
        }
    } catch (e) {
        console.error('Failed to update OCR domains in history:', e);
    }
}

function renderHistoryTable() {
    if (!elHistoryTableBody) return;

    let historyList = [];
    try {
        historyList = JSON.parse(localStorage.getItem(scanHistoryStorageKey())) || [];
    } catch (e) {
        historyList = [];
    }

    const searchQuery = elHistorySearch ? elHistorySearch.value.toLowerCase().trim() : '';

    const filtered = historyList.filter(item => {
        if (!searchQuery) return true;
        return item.target.toLowerCase().includes(searchQuery) || item.type.toLowerCase().includes(searchQuery);
    });

    if (filtered.length === 0) {
        elHistoryTableBody.innerHTML = `
            <tr class="empty-row">
                <td colspan="6" class="text-center">Chưa có lịch sử quét nào được lưu.</td>
            </tr>
        `;
        return;
    }

    elHistoryTableBody.innerHTML = '';
    filtered.forEach((item, index) => {
        const row = document.createElement('tr');

        // STT
        const tdIdx = document.createElement('td');
        tdIdx.className = 'text-center';
        tdIdx.textContent = index + 1;
        row.appendChild(tdIdx);

        // Display Time
        const tdTime = document.createElement('td');
        tdTime.style.fontSize = '0.85rem';
        tdTime.style.color = 'var(--text-muted)';
        tdTime.textContent = item.displayTime;
        row.appendChild(tdTime);

        // Type Badge
        const tdType = document.createElement('td');
        const isDomain = item.type === 'domain';
        const isTrace = item.type === 'trace';
        tdType.innerHTML = isTrace
            ? '<span class="format-tag tag-text"><i class="fa-solid fa-diagram-project"></i> Truy vết cấp độ</span>'
            : isDomain
            ? '<span class="format-tag tag-image"><i class="fa-solid fa-globe"></i> Quét Domain</span>' 
            : '<span class="format-tag tag-video"><i class="fa-solid fa-address-card"></i> Tra cứu Nhà QC</span>';
        row.appendChild(tdType);

        // Target
        const tdTarget = document.createElement('td');
        tdTarget.innerHTML = `<strong style="color: var(--accent-blue); font-family: monospace;">${item.target}</strong>`;
        row.appendChild(tdTarget);
        // Summary stats
        const tdSummary = document.createElement('td');
        tdSummary.className = 'text-center';
        if (isTrace) {
            tdSummary.innerHTML = `<span class="badge-days" style="background:rgba(139,92,246,.15);color:#c4b5fd">${item.summaryStats.domainCount || 0} domain mới | ${item.summaryStats.advertiserCount || 0} Nhà QC | cấp ${item.summaryStats.maxLevel || 1}</span>`;
        } else if (isDomain) {
            tdSummary.innerHTML = `<span class="badge-days" style="background: rgba(59,130,246,0.15); color: #60a5fa;">${item.summaryStats.advCount || 0} đối thủ | ${item.summaryStats.creativeCount || 0} mẫu QC</span>`;
        } else {
            tdSummary.innerHTML = `<span class="badge-days" style="background: rgba(168,85,247,0.15); color: #c084fc;">${item.summaryStats.creativeCount || 0} mẫu QC | ${item.summaryStats.domainCount || 0} domain OCR</span>`;
        }
        row.appendChild(tdSummary);

        // Actions
        const tdActions = document.createElement('td');
        tdActions.className = 'text-center';
        tdActions.innerHTML = `
            <div style="display: flex; gap: 0.5rem; justify-content: center;">
                <button class="btn btn-primary btn-sm btn-load-hist" data-id="${item.id}" style="padding: 0.3rem 0.6rem; font-size: 0.78rem;">
                    <i class="fa-solid fa-rotate-left"></i> Xem lại kết quả
                </button>
                <button class="btn btn-secondary btn-sm btn-delete-hist" data-id="${item.id}" style="padding: 0.3rem 0.5rem; font-size: 0.78rem; background: rgba(239, 68, 68, 0.15); color: #ef4444; border-color: rgba(239, 68, 68, 0.3);">
                    <i class="fa-solid fa-trash"></i>
                </button>
            </div>
        `;
        row.appendChild(tdActions);

        elHistoryTableBody.appendChild(row);
    });

    // Bind event listeners
    elHistoryTableBody.querySelectorAll('.btn-load-hist').forEach(btn => {
        btn.addEventListener('click', () => {
            const histId = btn.getAttribute('data-id');
            loadHistoryRecord(histId);
        });
    });

    elHistoryTableBody.querySelectorAll('.btn-delete-hist').forEach(btn => {
        btn.addEventListener('click', () => {
            const histId = btn.getAttribute('data-id');
            deleteHistoryRecord(histId);
        });
    });
}
if (elBtnDomainMetrics) elBtnDomainMetrics.addEventListener('click', scanProjectDomainMetrics);

async function loadHistoryRecord(histId) {
    try {
        const historyList = JSON.parse(localStorage.getItem(scanHistoryStorageKey())) || [];
        const record = historyList.find(r => r.id === histId);
        if (!record) return;

        if (record.type === 'domain') {
            scrapedAdvertisers = record.fullData.scrapedAdvertisers || [];
            elTargetDomain.value = record.target;
            elStatPages.textContent = '-';
            elStatAds.textContent = record.summaryStats.creativeCount || 0;
            elStatAdvertisers.textContent = scrapedAdvertisers.length;

            elTableSearch.disabled = false;
            elFormatFilter.disabled = false;
            elDateRangeFilter.disabled = false;
            elBtnExport.disabled = false;

            applyFilters();
            switchTab('tab-domain');
            log(`[System] Đã tải lại kết quả lịch sử quét cho miền ${record.target} (${record.displayTime})`, 'success');
        } else if (record.type === 'advertiser') {
            advertiserCreatives = record.fullData.advertiserCreatives || [];
            filteredAdvertiserCreatives = record.fullData.filteredAdvertiserCreatives || [];
            
            // Reconstruct Set for sources in detectedDomainsMap
            const rawMap = record.fullData.detectedDomainsMap || {};
            detectedDomainsMap = {};
            Object.entries(rawMap).forEach(([domain, info]) => {
                detectedDomainsMap[domain] = {
                    ...info,
                    count: info.count || 1,
                    sources: new Set(Array.isArray(info.sources) ? info.sources : [info.sources || 'Lịch sử']),
                    ads: Array.isArray(info.ads) ? info.ads : [],
                    metricsLoading: false,
                    summaryLoading: false,
                    trafficLoading: false
                };
            });

            elTargetAdvId.value = record.target;

            elStatAdvTotal.textContent = advertiserCreatives.length;
            elStatAdvDomains.textContent = Object.keys(detectedDomainsMap).length;

            renderAdvDomainTable();
            updateMetricRetryButtons();
            renderCreativeGrid(advertiserCreatives);
            renderFilteredAdsTable();

            if (advertiserCreatives.some(c => c.imageUrl || c.previewUrl)) {
                elBtnRunAutoOcr.disabled = false;
            }

            switchTab('tab-adv');
            logAdv(`[System] Đã tải lại kết quả tra cứu lịch sử cho ID ${record.target} (${record.displayTime})`, 'success');
            await compareAdvertiserDomainDuplicates().catch(error => logAdv(`Không thể tự đối chiếu kết quả lịch sử: ${error.message}`, 'warning'));
        } else if (record.type === 'trace') {
            const saved = record.fullData?.trace || {};
            traceEls.root.value = saved.root || record.target || '';
            traceState.maxLevel = Number(saved.maxLevel || record.summaryStats?.maxLevel || 1);
            traceState.currentLevel = Number(saved.currentLevel || traceState.maxLevel);
            traceState.advertisers = new Set(saved.advertisers || []);
            traceState.activeAdvertisers = new Set();
            traceState.seenAdvertisers = new Set(saved.seenAdvertisers || saved.advertisers || []);
            traceState.visited = new Set(saved.visited || []);
            traceState.skippedOwners = saved.skippedOwners || [];
            traceState.activities = saved.activities || [];
            traceState.errors = saved.errors || [];
            traceState.failedJobs = saved.failedJobs || [];
            traceState.results = new Map((saved.results || []).map(item => [item.domain, { ...item, parents: new Set(item.parents || []), advertisers: new Set(item.advertisers || []), sources: new Set(item.sources || []) }]));
            traceRender(); traceRenderLevels(); traceProgress(`Đã tải lịch sử truy vết ${record.displayTime}.`, 100); switchTab('tab-trace');
        }
    } catch (e) {
        console.error('Failed to load history record:', e);
    }
}

function deleteHistoryRecord(histId) {
    try {
        let historyList = JSON.parse(localStorage.getItem(scanHistoryStorageKey())) || [];
        historyList = historyList.filter(r => r.id !== histId);
        localStorage.setItem(scanHistoryStorageKey(), JSON.stringify(historyList));
        renderHistoryTable();
    } catch (e) {
        console.error('Failed to delete history record:', e);
    }
}

if (elBtnClearHistory) {
    elBtnClearHistory.addEventListener('click', async () => {
        if (await showAppConfirm('Bạn có chắc chắn muốn xoá toàn bộ nhật ký lịch sử quét?', { danger: true, confirmText: 'Xóa nhật ký' })) {
            localStorage.removeItem(scanHistoryStorageKey());
            renderHistoryTable();
        }
    });
}

if (elHistorySearch) {
    elHistorySearch.addEventListener('input', renderHistoryTable);
}

// Initial render
initializeCountrySelectors();
renderHistoryTable();

// Export OCR Detected Domains to CSV (Excel UTF-8 with BOM)
function exportOcrDomainsToCSV() {
    const domainEntries = getFilteredAdvDomainEntries();
    if (domainEntries.length === 0) {
        alert('Không có tên miền phù hợp với bộ lọc để xuất Excel!');
        return;
    }

    logAdv('Đang xuất danh sách tên miền phát hiện (OCR) ra file Excel...', 'info');
    const advId = (elTargetAdvId ? elTargetAdvId.value.trim() : 'Adv').replace(/[^a-zA-Z0-9]/g, '_');
    const filename = `OCR_Domains_${advId}_${new Date().toISOString().split('T')[0]}.xls`;
    const rows = domainEntries.map(([domain, info], index) => {
        const count = info.count || 0;
        const sources = Array.from(info.sources || []).join(' | ');
        const ads = Array.isArray(info.ads) ? info.ads : [];
        const period = calculateActivePeriod(ads);
        const days = period.days || 0;
        const firstSeen = period.firstSeen || 'N/A';
        const lastSeen = period.lastSeen || 'N/A';
        const creativeIds = ads.map(a => a.creativeId).join('; ');
        const metrics = info.metrics || {};
        const niche = advNicheByDomain.get(normalizeProjectDomain(domain)) || info.niche || '';
        const active = isDomainCurrentlyActive(period) ? 'Đang triển khai' : 'Không hoạt động';

        const storeState = info.inDomainStore === true ? 'Đã có' : info.inDomainStore === false ? 'Chưa có' : 'Chưa đối chiếu';
        const registrationState = info.inGoogleSheet === true ? (info.sheetRegistrant || 'Đã có người đăng ký') : info.inGoogleSheet === false ? 'Chưa có người đăng ký' : 'Chưa đối chiếu Sheet';
        return [index + 1, registrationState, `${storeState} trong Kho`, domain, active, count, days, firstSeen, lastSeen, metrics.volume ?? '', info.traffic?.visits ?? '', niche, metrics.cpcValue ?? '', metrics.competition ?? '', metrics.activeAdvertisersCount ?? '', sources, creativeIds];
    });
    exportRowsToExcel(filename, ['STT','Dự Án Đã Có Người Đăng Ký','Trạng Thái Kho Domain','Domain Dự Án','Trạng Thái','Số Lượng QC','Thời Gian Chạy (Ngày)','Ngày Đầu Tiên','Ngày Cuối Cùng','Volume','Traffic','Ngách','Giá Thầu TB','Độ Cạnh Tranh','Nhà QC Đang Chạy','Nguồn Nhận Dạng','Danh Sách Creative IDs'], rows);

    logAdv(`✅ Xuất thành công file Excel ${filename}!`, 'success');
}

const elBtnExportOcrExcel = document.getElementById('btnExportOcrExcel');
if (elBtnExportOcrExcel) {
    elBtnExportOcrExcel.addEventListener('click', exportOcrDomainsToCSV);
}

// ==========================================
// BULK TRAFFIC LOOKUP
// ==========================================
const elBulkTrafficInput = document.getElementById('bulkTrafficInput');
const elBulkTrafficProvider = document.getElementById('bulkTrafficProvider');
const elBtnBulkTrafficScan = document.getElementById('btnBulkTrafficScan');
const elBtnBulkTrafficPause = document.getElementById('btnBulkTrafficPause');
const elBtnBulkTrafficStop = document.getElementById('btnBulkTrafficStop');
const elBtnBulkTrafficRetry = document.getElementById('btnBulkTrafficRetry');
const elBtnBulkTrafficExport = document.getElementById('btnBulkTrafficExport');
const elBulkTrafficStatus = document.getElementById('bulkTrafficStatus');
const elBulkTrafficTableBody = document.getElementById('bulkTrafficTableBody');
const elLocalTrafficHistoryBody = document.getElementById('localTrafficHistoryBody');
const elBtnClearLocalTrafficHistory = document.getElementById('btnClearLocalTrafficHistory');
let bulkTrafficRows = [];
let bulkTrafficControl = { running: false, paused: false, stopped: false, controller: null };
const bulkTrafficFilterEls = {
    domain: document.getElementById('bulkTrafficDomainFilter'),
    status: document.getElementById('bulkTrafficStatusFilter'),
    min: document.getElementById('bulkTrafficMinFilter'),
    max: document.getElementById('bulkTrafficMaxFilter'),
    clear: document.getElementById('btnClearBulkTrafficFilters'),
    summary: document.getElementById('bulkTrafficFilterSummary')
};

function filteredBulkTrafficRows() {
    const query = String(bulkTrafficFilterEls.domain?.value || '').trim().toLowerCase();
    const status = bulkTrafficFilterEls.status?.value || '';
    const min = bulkTrafficFilterEls.min?.value === '' ? null : Number(bulkTrafficFilterEls.min.value);
    const max = bulkTrafficFilterEls.max?.value === '' ? null : Number(bulkTrafficFilterEls.max.value);
    return bulkTrafficRows.filter(row => {
        if (query && !row.domain.toLowerCase().includes(query)) return false;
        const rowStatus = row.loading ? 'running' : row.error ? 'error' : row.completed ? 'success' : 'pending';
        if (status && rowStatus !== status) return false;
        const visits = Number(row.data?.visits);
        if (min !== null && (!Number.isFinite(visits) || visits < min)) return false;
        if (max !== null && (!Number.isFinite(visits) || visits > max)) return false;
        return true;
    });
}

async function waitForBulkTrafficResume() {
    while (bulkTrafficControl.paused && !bulkTrafficControl.stopped) await new Promise(resolve => setTimeout(resolve, 200));
}

async function runBulkTrafficWorkerPool(entries, concurrency, worker) {
    let cursor = 0;
    const workers = Array.from({ length: Math.min(concurrency, entries.length) }, async () => {
        while (cursor < entries.length && !bulkTrafficControl.stopped) {
            await waitForBulkTrafficResume();
            if (bulkTrafficControl.stopped) break;
            const index = cursor++;
            try {
                await worker(entries[index], index);
            } catch (error) {
                const row = entries[index];
                if (row) {
                    row.loading = false;
                    row.completed = true;
                    row.error = error.message || 'KhÃ´ng thá»ƒ láº¥y Traffic.';
                    row.errorDetails = row.errorDetails || { domain: row.domain, message: error.message || String(error) };
                    scheduleRenderBulkTrafficRows();
                }
            }
        }
    });
    await Promise.all(workers);
}

function localTrafficHistoryKey() {
    return `traffic_api_diagnostics:${authenticatedUser?.id || 'anonymous'}`;
}

function readLocalTrafficHistory() {
    try { return JSON.parse(localStorage.getItem(localTrafficHistoryKey())) || []; } catch (_) { return []; }
}

function appendLocalTrafficHistory(domain, result, httpStatus) {
    const diagnostics = result?.authDiagnostics;
    if (!diagnostics) return;
    const rows = readLocalTrafficHistory();
    rows.unshift({
        id: `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
        timestamp: new Date().toISOString(), domain,
        profile: diagnostics.profileName || diagnostics.profile || 'Không xác định',
        variablesReceived: diagnostics.variablesReceived || null,
        request: diagnostics.request || null,
        status: httpStatus ?? diagnostics.status ?? null,
        success: Boolean(result?.success),
        error: result?.error || null
    });
    localStorage.setItem(localTrafficHistoryKey(), JSON.stringify(rows.slice(0, 300)));
    scheduleRenderLocalTrafficHistory();
}

let localTrafficRenderFrame = null;
function scheduleRenderLocalTrafficHistory() {
    if (localTrafficRenderFrame !== null) return;
    localTrafficRenderFrame = requestAnimationFrame(() => {
        localTrafficRenderFrame = null;
        renderLocalTrafficHistory();
    });
}

function renderLocalTrafficHistory() {
    if (!elLocalTrafficHistoryBody) return;
    const rows = readLocalTrafficHistory();
    elLocalTrafficHistoryBody.innerHTML = rows.length ? rows.map(item => {
        const variables = JSON.stringify(item.variablesReceived || {}, null, 2);
        const request = JSON.stringify(item.request || {}, null, 2);
        return `<tr><td>${new Date(item.timestamp).toLocaleString('vi-VN')}</td><td><strong>${escapeMonitorHtml(item.domain)}</strong></td><td>${escapeMonitorHtml(item.profile)}</td><td><details><summary>${item.variablesReceived?.complete ? 'Đã nhận đủ biến' : 'Thiếu biến — xem'}</summary><pre style="white-space:pre-wrap;max-width:320px;max-height:220px;overflow:auto">${escapeMonitorHtml(variables)}</pre></details></td><td><details><summary>Xem request</summary><pre style="white-space:pre-wrap;max-width:440px;max-height:220px;overflow:auto">${escapeMonitorHtml(request)}</pre></details></td><td class="${item.success ? '' : 'keyword-metric-error'}"><strong>${escapeMonitorHtml(item.status)}</strong>${item.error ? `<br><small>${escapeMonitorHtml(item.error)}</small>` : ''}</td></tr>`;
    }).join('') : '<tr class="empty-row"><td colspan="6" class="text-center">Chưa có nhật ký local.</td></tr>';
}

function parseBulkTrafficDomains(text) {
    const values = String(text || '').split(/[\r\n,;\t ]+/).map(normalizeProjectDomain).filter(Boolean);
    return [...new Set(values)];
}

function bulkTrafficMetric(row, key) {
    const engagements = row.data?.engagements || {};
    if (key === 'domain') return row.domain;
    if (key === 'visits') return row.data?.visits ?? '';
    if (key === 'bounceRate') {
        const value = row.data?.bounceRate ?? engagements.BounceRate ?? engagements.Bounce_Rate;
        if (value === null || value === undefined) return '';
        const numeric = Number(value);
        return Number.isFinite(numeric) ? `${(Math.abs(numeric) <= 1 ? numeric * 100 : numeric).toFixed(2)}%` : '';
    }
    if (key === 'timeOnSite') {
        const value = row.data?.timeOnSite ?? engagements.TimeOnSite ?? engagements.AvgVisitDuration ?? engagements.VisitDuration;
        return value === null || value === undefined ? '' : Math.round(Number(value));
    }
    if (key === 'pageViews') {
        const value = row.data?.pagesPerVisit ?? engagements.PageViews ?? engagements.PagesPerVisit ?? engagements.PagePerVisit ?? engagements.PagesPerSession;
        return value === null || value === undefined ? '' : Number(value).toFixed(2);
    }
    return '';
}

function trafficTrendData(data) {
    const monthly = data?.monthlyVisits && typeof data.monthlyVisits === 'object' ? data.monthlyVisits : {};
    const points = Object.entries(monthly)
        .map(([month, visits]) => ({ month: String(month).slice(0, 7), visits: Number(visits) }))
        .filter(item => /^\d{4}-\d{2}$/.test(item.month) && Number.isFinite(item.visits))
        .sort((a, b) => a.month.localeCompare(b.month))
        .slice(-3);
    if (points.length < 2) return { points, change: null };
    const first = points[0].visits;
    const last = points[points.length - 1].visits;
    return { points, change: first === 0 ? (last === 0 ? 0 : null) : ((last - first) / first) * 100 };
}

function renderTrafficTrend(data) {
    const { points, change } = trafficTrendData(data);
    if (points.length < 2) return '<span class="traffic-trend-empty">Chưa đủ dữ liệu</span>';
    const width = 112, height = 38, pad = 3;
    const values = points.map(item => item.visits);
    const min = Math.min(...values), max = Math.max(...values), range = max - min || 1;
    const coords = points.map((item, index) => {
        const x = pad + index * ((width - pad * 2) / Math.max(1, points.length - 1));
        const y = height - pad - ((item.visits - min) / range) * (height - pad * 2);
        return `${x.toFixed(1)},${y.toFixed(1)}`;
    }).join(' ');
    const direction = change === null ? 'neutral' : change > 0 ? 'up' : change < 0 ? 'down' : 'neutral';
    const changeText = change === null ? '—' : `${change > 0 ? '+' : ''}${change.toFixed(1)}%`;
    const detail = points.map(item => `${item.month}: ${item.visits.toLocaleString('vi-VN')}`).join(' · ');
    const circles = coords.split(' ').map((point, index) => {
        const [cx, cy] = point.split(',');
        const attributes = `cx="${cx}" cy="${cy}" data-trend-month="${escapeMonitorHtml(points[index].month)}" data-trend-visits="${points[index].visits}"`;
        return `<circle class="traffic-trend-hit" ${attributes} r="10" tabindex="0"></circle><circle class="traffic-trend-point" cx="${cx}" cy="${cy}" r="3.2"></circle>`;
    }).join('');
    return `<div class="traffic-trend-card ${direction}" aria-label="${escapeMonitorHtml(detail)}"><svg viewBox="0 0 ${width} ${height}"><polyline points="${coords}"></polyline>${circles}</svg><div><span>${changeText}</span><small>${escapeMonitorHtml(points.map(item => item.month.slice(5)).join(' → '))}</small></div><div class="traffic-trend-tooltip" role="tooltip" hidden><small></small><strong></strong></div></div>`;
}

function showTrafficTrendPoint(circle, event) {
    const card = circle?.closest('.traffic-trend-card');
    const tooltip = card?.querySelector('.traffic-trend-tooltip');
    if (!card || !tooltip) return;
    const month = circle.dataset.trendMonth || '—';
    const visits = Number(circle.dataset.trendVisits);
    tooltip.querySelector('small').textContent = `Tháng ${month}`;
    tooltip.querySelector('strong').textContent = `${Number.isFinite(visits) ? visits.toLocaleString('vi-VN') : '—'} lượt truy cập`;
    tooltip.hidden = false;
    const rect = card.getBoundingClientRect();
    const clientX = event?.clientX || (rect.left + rect.width / 2);
    tooltip.style.left = `${Math.max(52, Math.min(rect.width - 52, clientX - rect.left))}px`;
}
document.addEventListener('pointerover', event => {
    const circle = event.target.closest?.('.traffic-trend-card circle[data-trend-month]');
    if (circle) showTrafficTrendPoint(circle, event);
});
document.addEventListener('pointerout', event => {
    const circle = event.target.closest?.('.traffic-trend-card circle[data-trend-month]');
    if (!circle) return;
    const tooltip = circle.closest('.traffic-trend-card')?.querySelector('.traffic-trend-tooltip');
    if (tooltip) tooltip.hidden = true;
});
document.addEventListener('focusin', event => {
    const circle = event.target.closest?.('.traffic-trend-card circle[data-trend-month]');
    if (circle) showTrafficTrendPoint(circle);
});
document.addEventListener('focusout', event => {
    const tooltip = event.target.closest?.('.traffic-trend-card')?.querySelector('.traffic-trend-tooltip');
    if (tooltip) tooltip.hidden = true;
});

function trafficTrendExport(data) {
    const { points, change } = trafficTrendData(data);
    return {
        months: points.map(item => `${item.month}: ${item.visits}`).join(' | '),
        change: change === null ? '' : Number(change.toFixed(2))
    };
}

function renderBulkTrafficRows() {
    workspaceScheduleSave();
    if (!elBulkTrafficTableBody) return;
    const visibleRows = filteredBulkTrafficRows();
    elBulkTrafficTableBody.innerHTML = visibleRows.length ? visibleRows.map((row, index) => {
        const errorTitle = row.errorDetails ? JSON.stringify(row.errorDetails, null, 2) : row.error;
        const state = row.loading ? 'running' : row.error ? 'error' : row.completed ? 'success' : 'pending';
        return `<tr><td>${index + 1}</td><td><strong>${escapeMonitorHtml(row.domain)}</strong></td><td>${row.loading ? 'Đang lấy…' : row.data ? Number(row.data.visits ?? 0).toLocaleString('vi-VN') : '—'}</td><td>${row.loading ? '—' : renderTrafficTrend(row.data)}</td><td>${row.loading ? '—' : escapeMonitorHtml(bulkTrafficMetric(row, 'bounceRate') || 'Không có')}</td><td>${row.loading ? '—' : (bulkTrafficMetric(row, 'timeOnSite') === '' ? 'Không có' : `${bulkTrafficMetric(row, 'timeOnSite')} giây`)}</td><td>${row.loading ? '—' : escapeMonitorHtml(bulkTrafficMetric(row, 'pageViews') || 'Không có')}</td><td>${escapeMonitorHtml(row.data?.latestMonth || '—')}</td><td>${state === 'running' ? '<span class="keyword-metric-empty">Đang quét</span>' : state === 'error' ? `<details class="keyword-metric-error"><summary>Lỗi — bấm xem</summary><pre style="white-space:pre-wrap;max-width:360px;max-height:220px;overflow:auto">${escapeMonitorHtml(errorTitle)}</pre></details>` : state === 'success' ? '<span style="color:var(--accent-green)">Thành công</span>' : '<span class="keyword-metric-empty">Đang chờ</span>'}</td></tr>`;
    }).join('') : '<tr class="empty-row"><td colspan="9" class="text-center">Chưa có dữ liệu.</td></tr>';
    if (bulkTrafficFilterEls.summary) bulkTrafficFilterEls.summary.textContent = `Đang hiển thị ${visibleRows.length.toLocaleString('vi-VN')}/${bulkTrafficRows.length.toLocaleString('vi-VN')} kết quả. Excel và Copy sẽ dùng danh sách này.`;
    const errors = bulkTrafficRows.filter(row => row.error).length;
    const running = bulkTrafficRows.filter(row => row.loading).length;
    const success = bulkTrafficRows.filter(row => row.completed && !row.error && row.data).length;
    const pending = bulkTrafficRows.filter(row => !row.loading && !row.completed && !row.error).length;
    const setCount = (id, value) => { const element = document.getElementById(id); if (element) element.textContent = Number(value).toLocaleString('vi-VN'); };
    setCount('bulkCountTotal', bulkTrafficRows.length);
    setCount('bulkCountRunning', running);
    setCount('bulkCountSuccess', success);
    setCount('bulkCountError', errors);
    setCount('bulkCountPending', pending);
    if (elBtnBulkTrafficRetry) {
        elBtnBulkTrafficRetry.hidden = errors === 0;
        elBtnBulkTrafficRetry.innerHTML = `<i class="fa-solid fa-rotate"></i> Quét lại lỗi (${errors})`;
    }
    if (elBtnBulkTrafficExport) elBtnBulkTrafficExport.disabled = !visibleRows.some(row => row.data);
}

let bulkTrafficRenderFrame = null;
function scheduleRenderBulkTrafficRows() {
    if (bulkTrafficRenderFrame !== null) return;
    bulkTrafficRenderFrame = requestAnimationFrame(() => {
        bulkTrafficRenderFrame = null;
        renderBulkTrafficRows();
    });
}

async function scanBulkTrafficWithExtension(entries) {
    bulkTrafficControl = { running: true, paused: false, stopped: false, controller: new AbortController(), extensionJobId: '' };
    elBtnBulkTrafficScan.disabled = true;
    if (elBulkTrafficProvider) elBulkTrafficProvider.disabled = true;
    if (elBtnBulkTrafficPause) elBtnBulkTrafficPause.disabled = true;
    if (elBtnBulkTrafficStop) elBtnBulkTrafficStop.disabled = false;
    entries.forEach(row => { row.loading = false; row.completed = false; row.error = ''; row.errorDetails = null; });
    renderBulkTrafficRows();
    try {
        const createResponse = await fetch('/api/extension/jobs', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ action: 'scan_traffic', domains: entries.map(row => row.domain) }), signal: bulkTrafficControl.controller.signal });
        const created = await createResponse.json().catch(() => ({}));
        if (!createResponse.ok || !created.job?.id) throw new Error(created.error || `HTTP ${createResponse.status}`);
        bulkTrafficControl.extensionJobId = created.job.id;
        if (elBulkTrafficStatus) elBulkTrafficStatus.textContent = `Đã gửi job ${created.job.id} · chờ Extension nhận`;
        const rowsByDomain = new Map(entries.map(row => [normalizeProjectDomain(row.domain), row]));
        while (!bulkTrafficControl.stopped) {
            await new Promise(resolve => setTimeout(resolve, 1200));
            const response = await fetch(`/api/extension/jobs/${encodeURIComponent(created.job.id)}`, { cache: 'no-store', signal: bulkTrafficControl.controller.signal });
            const payload = await response.json().catch(() => ({}));
            if (!response.ok || !payload.job) throw new Error(payload.error || `HTTP ${response.status}`);
            for (const result of payload.job.results || []) {
                const row = rowsByDomain.get(normalizeProjectDomain(result.domain));
                if (!row) continue;
                row.completed = true; row.loading = false; row.data = result.success === false ? null : (result.data || {}); row.error = result.success === false ? (result.error || 'Extension không lấy được dữ liệu.') : ''; row.errorDetails = result.success === false ? result : null;
            }
            if (elBulkTrafficStatus) elBulkTrafficStatus.textContent = `${payload.job.completed}/${payload.job.total} · ${payload.job.status}`;
            renderBulkTrafficRows();
            if (['completed', 'failed', 'cancelled'].includes(payload.job.status)) {
                if (payload.job.status !== 'completed') entries.filter(row => !row.completed).forEach(row => { row.completed = true; row.error = payload.job.message || `Extension job ${payload.job.status}`; });
                break;
            }
        }
    } catch (error) {
        if (error.name !== 'AbortError') entries.filter(row => !row.completed).forEach(row => { row.completed = true; row.error = error.message || 'Không thể kết nối Extension.'; row.errorDetails = { message: row.error }; });
    } finally {
        bulkTrafficControl.running = false; bulkTrafficControl.paused = false;
        elBtnBulkTrafficScan.disabled = false;
        if (elBulkTrafficProvider) elBulkTrafficProvider.disabled = false;
        if (elBtnBulkTrafficPause) elBtnBulkTrafficPause.disabled = true;
        if (elBtnBulkTrafficStop) elBtnBulkTrafficStop.disabled = true;
        renderBulkTrafficRows();
        const success = entries.filter(row => row.completed && row.data && !row.error).length, errors = entries.filter(row => row.error).length;
        if (elBulkTrafficStatus) elBulkTrafficStatus.textContent = bulkTrafficControl.stopped ? `Đã dừng Extension job · ${success} thành công` : `Hoàn tất: ${success} thành công, ${errors} lỗi`;
    }
}

async function scanBulkTraffic(onlyFailed = false) {
    if (!onlyFailed) {
        const domains = parseBulkTrafficDomains(elBulkTrafficInput?.value);
        if (!domains.length) return alert('Vui lòng nhập ít nhất một domain hợp lệ.');
        bulkTrafficRows = domains.map(domain => ({ domain, data: null, loading: false, completed: false, error: '', errorDetails: null }));
    }
    const entries = bulkTrafficRows.filter(row => !onlyFailed || row.error).slice();
    if (!entries.length) return;
    const selectedProvider = elBulkTrafficProvider?.value || '';
    if (selectedProvider === 'chrome_extension') return scanBulkTrafficWithExtension(entries);
    bulkTrafficControl = { running: true, paused: false, stopped: false, controller: new AbortController() };
    elBtnBulkTrafficScan.disabled = true;
    if (elBulkTrafficProvider) elBulkTrafficProvider.disabled = true;
    if (elBtnBulkTrafficPause) { elBtnBulkTrafficPause.disabled = false; elBtnBulkTrafficPause.innerHTML = '<i class="fa-solid fa-pause"></i> Tạm dừng'; }
    if (elBtnBulkTrafficStop) elBtnBulkTrafficStop.disabled = false;
    if (elBtnBulkTrafficRetry) elBtnBulkTrafficRetry.disabled = true;
    let completed = 0;
    await runBulkTrafficWorkerPool(entries, 50, async row => {
        while (!bulkTrafficControl.stopped) {
            await waitForBulkTrafficResume();
            if (bulkTrafficControl.stopped) break;
            row.loading = true; row.completed = false; row.error = ''; row.errorDetails = null; scheduleRenderBulkTrafficRows();
            const endpoint = `/api/domain-traffic?domain=${encodeURIComponent(row.domain)}${selectedProvider ? `&provider=${encodeURIComponent(selectedProvider)}` : ''}`;
            const startedAt = Date.now();
            try {
                let profileAttempt = 0;
                let rateLimitWaits = 0;
                while (true) {
                    const response = await fetch(endpoint, { signal: bulkTrafficControl.controller.signal });
                const text = await response.text();
                let result = null; try { result = text ? JSON.parse(text) : null; } catch (_) {}
                appendLocalTrafficHistory(row.domain, result, response.status);
                if (response.status === 429 && result?.rateLimited && Number(result?.retryAfterMs) > 0 && rateLimitWaits < 3) {
                    rateLimitWaits++;
                    const waitMs = Math.min(61000, Math.max(1000, Number(result.retryAfterMs)));
                    row.error = `Đang chờ hạn mức TrafficLens API · ${Math.ceil(waitMs / 1000)} giây`;
                    scheduleRenderBulkTrafficRows();
                    await new Promise(resolve => setTimeout(resolve, waitMs));
                    row.error = '';
                    continue;
                }
                const profileCount = Math.max(1, Number(result?.profileCount || 1));
                if (response.status === 429 && result?.retryable && result?.nextProfile && profileAttempt < profileCount - 1) {
                    profileAttempt++;
                    row.profileAttempt = profileAttempt + 1;
                    scheduleRenderBulkTrafficRows();
                    continue;
                }
                if (!response.ok || !result?.success) {
                    const error = new Error(result?.error || `HTTP ${response.status}`);
                    error.details = { domain: row.domain, endpoint, status: response.status, profileAttempt: profileAttempt + 1, durationMs: Date.now() - startedAt, response: result || text.slice(0, 10000) };
                    throw error;
                }
                row.data = result;
                const clean = normalizeProjectDomain(row.domain), current = traceInsightCache.get(clean) || {};
                traceInsightCache.set(clean, { ...current, traffic: result.visits ?? current.traffic });
                traceRender();
                break;
                }
                row.completed = true;
            } catch (error) {
                if (error.name === 'AbortError') {
                    row.error = ''; row.errorDetails = null; row.completed = false;
                } else {
                    row.error = error.message || 'Không thể lấy Traffic.';
                    row.errorDetails = error.details || { domain: row.domain, endpoint, durationMs: Date.now() - startedAt, message: error.message };
                    row.completed = true;
                }
            } finally {
                row.loading = false;
                if (row.completed) completed++;
                if (elBulkTrafficStatus && !bulkTrafficControl.paused && !bulkTrafficControl.stopped) elBulkTrafficStatus.textContent = `${completed}/${entries.length} hoàn thành`;
                scheduleRenderBulkTrafficRows();
            }
            if (row.completed || bulkTrafficControl.stopped) break;
        }
    });
    const wasStopped = bulkTrafficControl.stopped;
    bulkTrafficControl.running = false; bulkTrafficControl.paused = false;
    elBtnBulkTrafficScan.disabled = false;
    if (elBulkTrafficProvider) elBulkTrafficProvider.disabled = false;
    if (elBtnBulkTrafficPause) { elBtnBulkTrafficPause.disabled = true; elBtnBulkTrafficPause.innerHTML = '<i class="fa-solid fa-pause"></i> Tạm dừng'; }
    if (elBtnBulkTrafficStop) elBtnBulkTrafficStop.disabled = true;
    if (elBtnBulkTrafficRetry) elBtnBulkTrafficRetry.disabled = false;
    renderBulkTrafficRows();
    const successCount = bulkTrafficRows.filter(row => row.completed && row.data && !row.error).length;
    const errorCount = bulkTrafficRows.filter(row => row.error).length;
    const pendingCount = bulkTrafficRows.filter(row => !row.completed && !row.error).length;
    if (elBulkTrafficStatus) elBulkTrafficStatus.textContent = wasStopped ? `Đã dừng: ${successCount} thành công, ${pendingCount} còn chờ` : `Hoàn tất: ${successCount} thành công, ${errorCount} lỗi`;
    showMonitorToast(wasStopped ? `Đã dừng quét Traffic · còn ${pendingCount.toLocaleString('vi-VN')} domain` : `Quét Traffic hoàn tất · ${successCount.toLocaleString('vi-VN')} thành công${errorCount ? ` · ${errorCount.toLocaleString('vi-VN')} lỗi` : ''}`);
}

if (elBtnBulkTrafficScan) elBtnBulkTrafficScan.onclick = () => scanBulkTraffic(false);
if (elBtnBulkTrafficPause) elBtnBulkTrafficPause.onclick = () => {
    if (!bulkTrafficControl.running) return;
    if (bulkTrafficControl.paused) {
        bulkTrafficControl.controller = new AbortController();
        bulkTrafficControl.paused = false;
        elBtnBulkTrafficPause.innerHTML = '<i class="fa-solid fa-pause"></i> Tạm dừng';
        if (elBulkTrafficStatus) elBulkTrafficStatus.textContent = 'Đang tiếp tục quét...';
    } else {
        bulkTrafficControl.paused = true;
        bulkTrafficControl.controller?.abort();
        elBtnBulkTrafficPause.innerHTML = '<i class="fa-solid fa-play"></i> Tiếp tục';
        if (elBulkTrafficStatus) elBulkTrafficStatus.textContent = 'Đang tạm dừng — kết quả đã lấy được giữ nguyên';
    }
};
if (elBtnBulkTrafficStop) elBtnBulkTrafficStop.onclick = () => {
    if (!bulkTrafficControl.running) return;
    bulkTrafficControl.stopped = true;
    bulkTrafficControl.paused = false;
    if (bulkTrafficControl.extensionJobId) fetch(`/api/extension/jobs/${encodeURIComponent(bulkTrafficControl.extensionJobId)}/cancel`, { method: 'POST' }).catch(() => {});
    bulkTrafficControl.controller?.abort();
    if (elBulkTrafficStatus) elBulkTrafficStatus.textContent = 'Đang dừng hẳn...';
};
if (elBtnBulkTrafficRetry) elBtnBulkTrafficRetry.onclick = () => scanBulkTraffic(true);
document.querySelectorAll('.bulk-copy-column').forEach(button => button.onclick = async () => {
    const values = filteredBulkTrafficRows().map(row => bulkTrafficMetric(row, button.dataset.column));
    if (!values.length) return alert('Chưa có dữ liệu để copy.');
    await navigator.clipboard.writeText(values.join('\n'));
    showMonitorToast(`Đã copy ${values.length} dòng.`);
});
if (elBtnBulkTrafficExport) elBtnBulkTrafficExport.onclick = () => {
    const rows = filteredBulkTrafficRows().map((row, index) => { const trend = trafficTrendExport(row.data); return [index + 1, row.domain, bulkTrafficMetric(row, 'visits'), trend.months, trend.change, bulkTrafficMetric(row, 'bounceRate'), bulkTrafficMetric(row, 'timeOnSite'), bulkTrafficMetric(row, 'pageViews'), row.data?.latestMonth || '', row.error || 'Thành công']; });
    exportRowsToExcel(`Traffic_hang_loat_${new Date().toISOString().slice(0, 10)}.xls`, ['STT','Domain','Traffic','Traffic 3 tháng','Thay đổi 3 tháng (%)','Tỷ lệ thoát trang','Thời gian ở lại (giây)','Pages Per Visit','Tháng dữ liệu','Trạng thái'], rows);
};
Object.values(bulkTrafficFilterEls).filter(element => element && element !== bulkTrafficFilterEls.clear && element !== bulkTrafficFilterEls.summary).forEach(element => {
    element.addEventListener(element.tagName === 'SELECT' ? 'change' : 'input', renderBulkTrafficRows);
});
if (bulkTrafficFilterEls.clear) bulkTrafficFilterEls.clear.onclick = () => {
    [bulkTrafficFilterEls.domain, bulkTrafficFilterEls.min, bulkTrafficFilterEls.max].forEach(element => { if (element) element.value = ''; });
    if (bulkTrafficFilterEls.status) bulkTrafficFilterEls.status.value = '';
    renderBulkTrafficRows();
};
if (elBtnClearLocalTrafficHistory) elBtnClearLocalTrafficHistory.onclick = async () => {
    if (!await showAppConfirm('Xóa toàn bộ nhật ký biến và API Traffic lưu trên trình duyệt này?', { danger: true, confirmText: 'Xóa nhật ký' })) return;
    localStorage.removeItem(localTrafficHistoryKey());
    renderLocalTrafficHistory();
};
renderLocalTrafficHistory();

// Quét Traffic từ tên dự án: link được nhập theo cùng số dòng; dòng trống sẽ được tìm bằng Tavily + AI.
const projectTrafficUi = {
    names: document.getElementById('projectTrafficNames'), links: document.getElementById('projectTrafficLinks'), concurrency: document.getElementById('projectTrafficConcurrency'), provider: document.getElementById('projectTrafficProvider'), status: document.getElementById('projectTrafficStatus'), body: document.getElementById('projectTrafficTableBody'), start: document.getElementById('btnProjectTrafficStart'), resume: document.getElementById('btnProjectTrafficResume'), pause: document.getElementById('btnProjectTrafficPause'), stop: document.getElementById('btnProjectTrafficStop'), retry: document.getElementById('btnProjectTrafficRetry'), export: document.getElementById('btnProjectTrafficExport'), clear: document.getElementById('btnProjectTrafficClear'), total: document.getElementById('projectTrafficTotal'), running: document.getElementById('projectTrafficRunning'), success: document.getElementById('projectTrafficSuccess'), errors: document.getElementById('projectTrafficErrors'), pending: document.getElementById('projectTrafficPending'), pageSize: document.getElementById('projectTrafficPageSize'), pageSummary: document.getElementById('projectTrafficPageSummary'), pageIndicator: document.getElementById('projectTrafficPageIndicator'), pagePrev: document.getElementById('projectTrafficPagePrev'), pageNext: document.getElementById('projectTrafficPageNext')
};
let projectTrafficRows = [], projectTrafficPage = 1, projectTrafficControl = { running: false, paused: false, stopped: false, controller: null };
const projectTrafficStorageKey = () => `project_traffic_results_v1:${authenticatedUser?.id || 'anonymous'}`;
const PROJECT_TRAFFIC_DB_NAME = 'checkggads_project_traffic_v1', PROJECT_TRAFFIC_DB_STORE = 'sessions';
let projectTrafficDbPromise = null, projectTrafficSaveRevision = 0, projectTrafficSavedRevision = 0, projectTrafficSavePromise = null, projectTrafficStorageWarningShown = false;
function projectTrafficOpenDb() {
    if (!('indexedDB' in window)) return Promise.reject(new Error('Trình duyệt không hỗ trợ IndexedDB.'));
    if (projectTrafficDbPromise) return projectTrafficDbPromise;
    projectTrafficDbPromise = new Promise((resolve, reject) => {
        const request = indexedDB.open(PROJECT_TRAFFIC_DB_NAME, 1);
        request.onupgradeneeded = () => { if (!request.result.objectStoreNames.contains(PROJECT_TRAFFIC_DB_STORE)) request.result.createObjectStore(PROJECT_TRAFFIC_DB_STORE, { keyPath: 'id' }); };
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
    });
    return projectTrafficDbPromise;
}
async function projectTrafficWriteDb(rows) {
    const db = await projectTrafficOpenDb();
    const record = { id: projectTrafficStorageKey(), rows, updatedAt: new Date().toISOString() };
    await new Promise((resolve, reject) => {
        const transaction = db.transaction(PROJECT_TRAFFIC_DB_STORE, 'readwrite');
        transaction.objectStore(PROJECT_TRAFFIC_DB_STORE).put(record);
        transaction.oncomplete = resolve;
        transaction.onerror = () => reject(transaction.error);
        transaction.onabort = () => reject(transaction.error || new Error('Giao dịch IndexedDB bị hủy.'));
    });
}
async function projectTrafficReadDb() {
    const db = await projectTrafficOpenDb();
    return new Promise((resolve, reject) => {
        const request = db.transaction(PROJECT_TRAFFIC_DB_STORE, 'readonly').objectStore(PROJECT_TRAFFIC_DB_STORE).get(projectTrafficStorageKey());
        request.onsuccess = () => resolve(Array.isArray(request.result?.rows) ? request.result.rows : []);
        request.onerror = () => reject(request.error);
    });
}
async function projectTrafficClearDb() {
    const db = await projectTrafficOpenDb();
    await new Promise((resolve, reject) => {
        const request = db.transaction(PROJECT_TRAFFIC_DB_STORE, 'readwrite').objectStore(PROJECT_TRAFFIC_DB_STORE).delete(projectTrafficStorageKey());
        request.onsuccess = resolve;
        request.onerror = () => reject(request.error);
    });
}
function saveProjectTrafficRows() {
    projectTrafficSaveRevision++;
    if (projectTrafficSavePromise) return projectTrafficSavePromise;
    projectTrafficSavePromise = (async () => {
        try {
            while (projectTrafficSavedRevision < projectTrafficSaveRevision) {
                const revision = projectTrafficSaveRevision;
                await projectTrafficWriteDb(projectTrafficRows.slice(0, 10000));
                projectTrafficSavedRevision = revision;
            }
            localStorage.removeItem(projectTrafficStorageKey());
        } catch (error) {
            console.warn('Không lưu được phiên Quét traffic vào IndexedDB:', error);
            if (!projectTrafficStorageWarningShown) { projectTrafficStorageWarningShown = true; showMonitorToast(`Không lưu được phiên quét: ${error.message}`, 'error'); }
        } finally {
            projectTrafficSavePromise = null;
        }
    })();
    return projectTrafficSavePromise;
}
function projectTrafficMetric(row, key) { return bulkTrafficMetric({ domain: row.domain || row.resolvedDomain || '', data: row.data }, key); }
function renderProjectTrafficRows() {
    if (!projectTrafficUi.body) return;
    const pageSize = Math.max(50, Number(projectTrafficUi.pageSize?.value) || 100);
    const pageCount = Math.max(1, Math.ceil(projectTrafficRows.length / pageSize));
    projectTrafficPage = Math.max(1, Math.min(projectTrafficPage, pageCount));
    const pageStart = (projectTrafficPage - 1) * pageSize;
    const visibleRows = projectTrafficRows.slice(pageStart, pageStart + pageSize);
    projectTrafficUi.body.innerHTML = visibleRows.length ? visibleRows.map((row, index) => {
        const state = row.loading ? 'running' : row.error ? 'error' : row.completed ? 'success' : 'pending';
        const link = value => value ? `<a class="table-link" href="${escapeMonitorHtml(/^https?:\/\//i.test(value) ? value : `https://${value}`)}" target="_blank" rel="noopener">${escapeMonitorHtml(value)}</a>` : '<span class="keyword-metric-empty">Để trống</span>';
        return `<tr><td>${index + 1}</td><td><strong>${escapeMonitorHtml(row.project)}</strong></td><td>${link(row.inputLink)}</td><td>${link(row.finalUrl)}</td><td>${escapeMonitorHtml(row.linkSource || '—')}</td><td>${row.data ? Number(row.data.visits ?? 0).toLocaleString('vi-VN') : '—'}</td><td>${row.loading ? '—' : renderTrafficTrend(row.data)}</td><td>${escapeMonitorHtml(projectTrafficMetric(row, 'bounceRate') || '—')}</td><td>${projectTrafficMetric(row, 'timeOnSite') === '' ? '—' : `${escapeMonitorHtml(projectTrafficMetric(row, 'timeOnSite'))} giây`}</td><td>${escapeMonitorHtml(projectTrafficMetric(row, 'pageViews') || '—')}</td><td>${escapeMonitorHtml(row.data?.latestMonth || '—')}</td><td>${state === 'running' ? `<span class="keyword-metric-empty">${escapeMonitorHtml(row.stage || 'Đang xử lý')}</span>` : state === 'error' ? `<details class="keyword-metric-error"><summary>Lỗi — xem</summary><pre style="white-space:pre-wrap;max-width:380px;max-height:220px;overflow:auto">${escapeMonitorHtml(row.errorDetails ? JSON.stringify(row.errorDetails, null, 2) : row.error)}</pre></details>` : state === 'success' ? '<span class="trace-status-pill success">Thành công</span>' : '<span class="keyword-metric-empty">Đang chờ</span>'}</td></tr>`;
    }).join('') : '<tr class="empty-row"><td colspan="12" class="text-center">Chưa có dữ liệu.</td></tr>';
    const running = projectTrafficRows.filter(row => row.loading).length, success = projectTrafficRows.filter(row => row.completed && row.data && !row.error).length, errors = projectTrafficRows.filter(row => row.error).length, pending = projectTrafficRows.filter(row => !row.loading && !row.completed && !row.error).length;
    if (projectTrafficUi.total) projectTrafficUi.total.textContent = projectTrafficRows.length.toLocaleString('vi-VN');
    if (projectTrafficUi.running) projectTrafficUi.running.textContent = running.toLocaleString('vi-VN');
    if (projectTrafficUi.success) projectTrafficUi.success.textContent = success.toLocaleString('vi-VN');
    if (projectTrafficUi.errors) projectTrafficUi.errors.textContent = errors.toLocaleString('vi-VN');
    if (projectTrafficUi.pending) projectTrafficUi.pending.textContent = pending.toLocaleString('vi-VN');
    if (projectTrafficUi.retry) { projectTrafficUi.retry.hidden = errors === 0; projectTrafficUi.retry.disabled = projectTrafficControl.running; projectTrafficUi.retry.innerHTML = `<i class="fa-solid fa-rotate"></i> Quét lại lỗi${errors ? ` (${errors})` : ''}`; }
    if (projectTrafficUi.resume) { projectTrafficUi.resume.hidden = pending === 0 || projectTrafficControl.running; projectTrafficUi.resume.disabled = projectTrafficControl.running; projectTrafficUi.resume.innerHTML = `<i class="fa-solid fa-forward"></i> Tiếp tục phiên dở${pending ? ` (${pending})` : ''}`; }
    if (projectTrafficUi.export) projectTrafficUi.export.disabled = !projectTrafficRows.length;
    if (projectTrafficUi.pageSummary) projectTrafficUi.pageSummary.textContent = projectTrafficRows.length ? `Hiển thị ${pageStart + 1}–${Math.min(pageStart + pageSize, projectTrafficRows.length)} / ${projectTrafficRows.length.toLocaleString('vi-VN')} kết quả` : 'Chưa có kết quả';
    if (projectTrafficUi.pageIndicator) projectTrafficUi.pageIndicator.textContent = `Trang ${projectTrafficPage} / ${pageCount}`;
    if (projectTrafficUi.pagePrev) projectTrafficUi.pagePrev.disabled = projectTrafficPage <= 1;
    if (projectTrafficUi.pageNext) projectTrafficUi.pageNext.disabled = projectTrafficPage >= pageCount;
}
async function loadProjectTrafficRows() {
    let saved = [];
    try {
        saved = await projectTrafficReadDb();
        if (!saved.length) {
            try { const legacy = JSON.parse(localStorage.getItem(projectTrafficStorageKey()) || '[]'); saved = Array.isArray(legacy) ? legacy : []; } catch (_) { saved = []; }
            if (saved.length) await projectTrafficWriteDb(saved);
        }
        localStorage.removeItem(projectTrafficStorageKey());
    } catch (error) {
        console.warn('Không tải được phiên Quét traffic từ IndexedDB:', error);
        try { const legacy = JSON.parse(localStorage.getItem(projectTrafficStorageKey()) || '[]'); saved = Array.isArray(legacy) ? legacy : []; } catch (_) { saved = []; }
    }
    projectTrafficRows = saved.map(row => row.loading ? { ...row, loading: false, completed: false, stage: '' } : row);
    projectTrafficSavedRevision = projectTrafficSaveRevision;
    renderProjectTrafficRows();
}
function parseProjectTrafficInputs() {
    const names = String(projectTrafficUi.names?.value || '').split(/\r?\n/), links = String(projectTrafficUi.links?.value || '').split(/\r?\n/);
    return names.map((project, index) => ({ project: project.trim(), inputLink: String(links[index] || '').trim() })).filter(row => row.project).map((row, index) => ({ id: `${Date.now()}-${index}-${Math.random().toString(36).slice(2, 7)}`, ...row, finalUrl: '', resolvedDomain: '', linkSource: '', data: null, loading: false, completed: false, error: '', errorDetails: null, stage: '' }));
}
async function waitProjectTrafficResume() { while (projectTrafficControl.paused && !projectTrafficControl.stopped) await new Promise(resolve => setTimeout(resolve, 200)); }
async function fetchProjectTrafficWithSharedAuth(domain, provider, signal, row) {
    const endpoint = `/api/domain-traffic?domain=${encodeURIComponent(domain)}${provider ? `&provider=${encodeURIComponent(provider)}` : ''}`;
    let profileAttempt = 0, rateLimitWaits = 0;
    while (true) {
        const response = await fetch(endpoint, { signal });
        const text = await response.text();
        let result = {}; try { result = text ? JSON.parse(text) : {}; } catch (_) {}
        appendLocalTrafficHistory(domain, result, response.status);
        if (response.status === 429 && result?.rateLimited && Number(result?.retryAfterMs) > 0 && rateLimitWaits < 3) {
            rateLimitWaits++;
            const waitMs = Math.min(61000, Math.max(1000, Number(result.retryAfterMs)));
            row.stage = `Chờ hạn mức TrafficLens · ${Math.ceil(waitMs / 1000)} giây`; renderProjectTrafficRows();
            await new Promise((resolve, reject) => { const onAbort = () => { clearTimeout(timer); reject(new DOMException('Đã dừng', 'AbortError')); }; const timer = setTimeout(() => { signal.removeEventListener('abort', onAbort); resolve(); }, waitMs); signal.addEventListener('abort', onAbort, { once: true }); });
            row.stage = 'Đang quét Traffic'; renderProjectTrafficRows();
            continue;
        }
        const profileCount = Math.max(1, Number(result?.profileCount || 1));
        if (response.status === 429 && result?.retryable && result?.nextProfile && profileAttempt < profileCount - 1) {
            profileAttempt++; row.stage = `Chuyển ${result.nextProfile.name || `profile ${profileAttempt + 1}`}`; renderProjectTrafficRows();
            continue;
        }
        return { response, result, text, endpoint, profileAttempt };
    }
}
async function runProjectTraffic(onlyFailed = false) {
    if (projectTrafficControl.running) return;
    if (!onlyFailed) { const parsed = parseProjectTrafficInputs(); if (!parsed.length) return showMonitorToast('Hãy nhập ít nhất một tên dự án.', 'error'); projectTrafficRows = parsed; saveProjectTrafficRows(); }
    const entries = projectTrafficRows.filter(row => onlyFailed ? Boolean(row.error) : !row.completed).map(row => { row.error = ''; row.errorDetails = null; row.completed = false; return row; });
    if (!entries.length) return showMonitorToast('Không có dự án cần quét.', 'warning');
    const concurrency = Math.max(1, Math.min(5, Number(projectTrafficUi.concurrency?.value) || 3)); localStorage.setItem('project_traffic_concurrency', String(concurrency));
    projectTrafficControl = { running: true, paused: false, stopped: false, controller: new AbortController() }; let cursor = 0, completed = 0;
    projectTrafficUi.start.disabled = true; projectTrafficUi.pause.disabled = false; projectTrafficUi.stop.disabled = false; if (projectTrafficUi.status) projectTrafficUi.status.textContent = `Đang chạy ${concurrency} luồng`;
    const workers = Array.from({ length: Math.min(concurrency, entries.length) }, async () => { while (cursor < entries.length && !projectTrafficControl.stopped) {
        await waitProjectTrafficResume(); if (projectTrafficControl.stopped) break; const row = entries[cursor++]; row.loading = true; row.stage = row.inputLink ? 'Đang chuẩn hóa link' : 'Đang tìm website'; renderProjectTrafficRows();
        try {
            let domain = normalizeProjectDomain(row.inputLink || row.resolvedDomain || row.finalUrl);
            if (!domain) { const response = await fetch('/api/project-traffic/resolve-website', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ project: row.project }), signal: projectTrafficControl.controller.signal }); const result = await response.json().catch(() => ({})); if (!response.ok) { const error = new Error(result.error || `HTTP ${response.status}`); error.details = result; throw error; } domain = normalizeProjectDomain(result.domain || result.finalUrl); row.finalUrl = result.finalUrl || `https://${domain}`; row.linkSource = result.source || 'Tavily + AI'; }
            else { row.finalUrl = `https://${domain}`; row.linkSource = 'Người dùng nhập'; }
            if (!domain) throw new Error('Không xác định được domain hợp lệ.'); row.resolvedDomain = domain; row.stage = 'Đang quét Traffic'; renderProjectTrafficRows();
            const provider = projectTrafficUi.provider?.value || '';
            const { response, result, text, endpoint, profileAttempt } = await fetchProjectTrafficWithSharedAuth(domain, provider, projectTrafficControl.controller.signal, row);
            if (!response.ok || !result.success) { const authStatus = Number(result?.status || result?.authDiagnostics?.status || 0); if ((response.status === 429 || [401, 403, 419].includes(authStatus)) && !projectTrafficControl.stopped) { projectTrafficControl.stopped = true; projectTrafficControl.stopReason = response.status === 429 ? 'Tất cả profile Traffic khả dụng đều đã hết hạn mức. Phiên đã tự dừng.' : `Sign Traffic bị từ chối (HTTP ${authStatus}). Phiên đã tự dừng 60 giây để không gọi get-sign liên tục.`; if (projectTrafficUi.status) projectTrafficUi.status.textContent = projectTrafficControl.stopReason; showMonitorToast(projectTrafficControl.stopReason, 'warning'); } const error = new Error(result.error || `HTTP ${response.status}`); error.details = { endpoint, profileAttempt: profileAttempt + 1, response: result || text }; throw error; } row.data = result; row.completed = true; completed++;
        } catch (error) { if (error.name !== 'AbortError') { row.error = error.message || 'Không thể xử lý dự án.'; row.errorDetails = error.details || { project: row.project, message: error.message }; row.completed = true; completed++; } }
        finally { row.loading = false; row.stage = ''; saveProjectTrafficRows(); renderProjectTrafficRows(); if (projectTrafficUi.status && !projectTrafficControl.paused) projectTrafficUi.status.textContent = `${completed}/${entries.length} hoàn thành`; }
    }}); await Promise.all(workers);
    const stopped = projectTrafficControl.stopped; const stopReason = projectTrafficControl.stopReason || ''; projectTrafficControl.running = false; projectTrafficControl.paused = false; projectTrafficUi.start.disabled = false; projectTrafficUi.pause.disabled = true; projectTrafficUi.stop.disabled = true; renderProjectTrafficRows(); const success = projectTrafficRows.filter(row => row.data && !row.error).length, errors = projectTrafficRows.filter(row => row.error).length; if (projectTrafficUi.status) projectTrafficUi.status.textContent = stopped ? (stopReason || 'Đã dừng') : `Hoàn tất: ${success} thành công, ${errors} lỗi`;
}
if (projectTrafficUi.start) projectTrafficUi.start.onclick = () => runProjectTraffic(false);
if (projectTrafficUi.retry) projectTrafficUi.retry.onclick = () => runProjectTraffic(true);
if (projectTrafficUi.resume) projectTrafficUi.resume.onclick = () => {
    projectTrafficRows.forEach(row => { if (!row.loading && !row.completed && !row.error) row.error = 'Tiếp tục phiên dở'; });
    saveProjectTrafficRows();
    runProjectTraffic(true);
};
if (projectTrafficUi.pause) projectTrafficUi.pause.onclick = () => { if (!projectTrafficControl.running) return; projectTrafficControl.paused = !projectTrafficControl.paused; projectTrafficUi.pause.innerHTML = projectTrafficControl.paused ? '<i class="fa-solid fa-play"></i> Tiếp tục' : '<i class="fa-solid fa-pause"></i> Tạm dừng'; if (projectTrafficUi.status) projectTrafficUi.status.textContent = projectTrafficControl.paused ? 'Đang tạm dừng — chờ các tác vụ hiện tại hoàn tất' : 'Đang tiếp tục chạy'; };
if (projectTrafficUi.stop) projectTrafficUi.stop.onclick = () => { projectTrafficControl.stopped = true; projectTrafficControl.paused = false; projectTrafficControl.controller?.abort(); };
if (projectTrafficUi.clear) projectTrafficUi.clear.onclick = async () => { if (projectTrafficControl.running) return showMonitorToast('Hãy dừng phiên đang chạy trước.', 'warning'); if (!await showAppConfirm('Xóa toàn bộ kết quả Quét traffic từ tên dự án trên trình duyệt này?', { danger: true, confirmText: 'Xóa kết quả' })) return; projectTrafficRows = []; projectTrafficPage = 1; try { await projectTrafficClearDb(); } catch (error) { console.warn('Không xóa được phiên Quét traffic trong IndexedDB:', error); } localStorage.removeItem(projectTrafficStorageKey()); renderProjectTrafficRows(); };
if (projectTrafficUi.export) projectTrafficUi.export.onclick = () => exportRowsToExcel(`Traffic_tu_ten_du_an_${new Date().toISOString().slice(0, 10)}.xls`, ['STT','Tên dự án','Link nhập','Website chính thức','Nguồn link','Traffic','Traffic 3 tháng','Thay đổi 3 tháng (%)','Bounce Rate','Thời gian trên trang (giây)','Pages/Visit','Tháng dữ liệu','Trạng thái'], projectTrafficRows.map((row, index) => { const trend = trafficTrendExport(row.data); return [index + 1,row.project,row.inputLink,row.finalUrl,row.linkSource,projectTrafficMetric(row,'visits'),trend.months,trend.change,projectTrafficMetric(row,'bounceRate'),projectTrafficMetric(row,'timeOnSite'),projectTrafficMetric(row,'pageViews'),row.data?.latestMonth || '',row.error || (row.completed ? 'Thành công' : 'Đang chờ')]; }));
if (projectTrafficUi.concurrency) projectTrafficUi.concurrency.value = localStorage.getItem('project_traffic_concurrency') || '3';
if (projectTrafficUi.pageSize) {
    projectTrafficUi.pageSize.value = localStorage.getItem('project_traffic_page_size') || '100';
    projectTrafficUi.pageSize.onchange = () => { localStorage.setItem('project_traffic_page_size', projectTrafficUi.pageSize.value); projectTrafficPage = 1; renderProjectTrafficRows(); };
}
if (projectTrafficUi.pagePrev) projectTrafficUi.pagePrev.onclick = () => { if (projectTrafficPage > 1) { projectTrafficPage--; renderProjectTrafficRows(); } };
if (projectTrafficUi.pageNext) projectTrafficUi.pageNext.onclick = () => { const pageSize = Math.max(50, Number(projectTrafficUi.pageSize?.value) || 100); if (projectTrafficPage < Math.ceil(projectTrafficRows.length / pageSize)) { projectTrafficPage++; renderProjectTrafficRows(); } };

// Check trùng domain với dữ liệu đăng ký đã đồng bộ từ Google Sheet.
const domainDuplicateUi = {
    input: document.getElementById('domainDuplicateInput'), check: document.getElementById('btnDomainDuplicateCheck'), clear: document.getElementById('btnDomainDuplicateClear'),
    copy: document.getElementById('btnCopyUniqueDomains'), body: document.getElementById('domainDuplicateBody'), summary: document.getElementById('domainDuplicateSummary'), state: document.getElementById('domainDuplicateState'),
    sheetUrl: document.getElementById('domainDuplicateSheetUrl'), sheetName: document.getElementById('domainDuplicateSheetName'), domainColumn: document.getElementById('domainDuplicateDomainColumn'), registrantColumn: document.getElementById('domainDuplicateRegistrantColumn'), googleStatus: document.getElementById('domainDuplicateGoogleStatus'), googleConnect: document.getElementById('btnDomainDuplicateGoogleConnect')
};
let domainDuplicateUnique = [];
const DOMAIN_DUPLICATE_SESSION_KEY = 'domain_duplicate_last_session_v1';
let domainDuplicateInputSaveTimer = null;
function renderDomainDuplicateRows(rows = []) {
    if (!domainDuplicateUi.body) return;
    domainDuplicateUi.body.innerHTML = rows.length ? rows.map((row, index) => `<tr><td>${index + 1}</td><td><strong>${traceEscape(row.domain)}</strong></td><td>${row.duplicate ? '<span style="color:#f59e0b">Đã trùng</span>' : '<span style="color:#10b981">Không trùng</span>'}</td><td>${traceEscape(row.sources || '—')}</td><td>${traceEscape(row.registrants || '—')}</td></tr>`).join('') : '<tr class="empty-row"><td colspan="5" class="text-center">Nhập domain và bấm Kiểm tra trùng.</td></tr>';
}
function saveDomainDuplicateSession(session = {}) {
    try {
        const previous = JSON.parse(localStorage.getItem(DOMAIN_DUPLICATE_SESSION_KEY) || '{}');
        localStorage.setItem(DOMAIN_DUPLICATE_SESSION_KEY, JSON.stringify({ ...(previous && typeof previous === 'object' ? previous : {}), ...session, input: String(domainDuplicateUi.input?.value || ''), savedAt: new Date().toISOString() }));
    }
    catch (error) { console.warn('Không thể lưu phiên check trùng domain:', error); }
}
function restoreDomainDuplicateSession() {
    try {
        const session = JSON.parse(localStorage.getItem(DOMAIN_DUPLICATE_SESSION_KEY) || 'null');
        if (!session || typeof session !== 'object') return;
        if (domainDuplicateUi.input) domainDuplicateUi.input.value = String(session.input || '');
        const rows = session.checkedInput === session.input && Array.isArray(session.rows) ? session.rows : [];
        domainDuplicateUnique = rows.length && Array.isArray(session.unique) ? session.unique.map(normalizeProjectDomain).filter(Boolean) : [];
        if (rows.length) renderDomainDuplicateRows(rows);
        if (domainDuplicateUi.summary && rows.length && session.summary) domainDuplicateUi.summary.textContent = session.summary;
        if (domainDuplicateUi.state && rows.length) domainDuplicateUi.state.textContent = 'Phiên gần nhất';
        if (domainDuplicateUi.copy) domainDuplicateUi.copy.disabled = !domainDuplicateUnique.length;
    } catch (error) { localStorage.removeItem(DOMAIN_DUPLICATE_SESSION_KEY); }
}
if (domainDuplicateUi.sheetUrl) domainDuplicateUi.sheetUrl.value = localStorage.getItem('domain_duplicate_sheet_url') || '';
if (domainDuplicateUi.sheetName) domainDuplicateUi.sheetName.value = localStorage.getItem('domain_duplicate_sheet_name') || 'Data';
if (domainDuplicateUi.domainColumn) domainDuplicateUi.domainColumn.value = localStorage.getItem('domain_duplicate_domain_column') || 'Domain';
if (domainDuplicateUi.registrantColumn) domainDuplicateUi.registrantColumn.value = localStorage.getItem('domain_duplicate_registrant_column') || '';
domainDuplicateUi.sheetUrl?.addEventListener('input', () => localStorage.setItem('domain_duplicate_sheet_url', domainDuplicateUi.sheetUrl.value.trim()));
domainDuplicateUi.sheetName?.addEventListener('input', () => localStorage.setItem('domain_duplicate_sheet_name', domainDuplicateUi.sheetName.value.trim()));
domainDuplicateUi.domainColumn?.addEventListener('input', () => localStorage.setItem('domain_duplicate_domain_column', domainDuplicateUi.domainColumn.value.trim()));
domainDuplicateUi.registrantColumn?.addEventListener('input', () => localStorage.setItem('domain_duplicate_registrant_column', domainDuplicateUi.registrantColumn.value.trim()));
domainDuplicateUi.input?.addEventListener('input', () => { clearTimeout(domainDuplicateInputSaveTimer); domainDuplicateInputSaveTimer = setTimeout(() => saveDomainDuplicateSession(), 250); });
async function loadDomainDuplicateGoogleStatus() {
    if (!domainDuplicateUi.googleStatus) return;
    try {
        const response = await fetch('/api/google-sheets/oauth/status', { cache: 'no-store' });
        const status = await response.json().catch(() => ({}));
        if (!response.ok) throw new Error(status.error || `HTTP ${response.status}`);
        domainDuplicateUi.googleStatus.className = `sheet-export-status ${status.connected ? 'success' : status.configured ? '' : 'error'}`;
        domainDuplicateUi.googleStatus.textContent = status.connected ? `Đã kết nối Google Sheets${status.email ? `: ${status.email}` : ''}.` : status.configured ? 'Chưa kết nối Google Sheets. Hãy kết nối để đọc file riêng.' : 'Admin chưa cấu hình Google OAuth trên server.';
        if (domainDuplicateUi.googleConnect) { domainDuplicateUi.googleConnect.hidden = status.connected; domainDuplicateUi.googleConnect.disabled = !status.configured; }
    } catch (error) { domainDuplicateUi.googleStatus.className = 'sheet-export-status error'; domainDuplicateUi.googleStatus.textContent = `Không kiểm tra được kết nối Google: ${error.message}`; }
}
if (domainDuplicateUi.googleConnect) domainDuplicateUi.googleConnect.onclick = connectGoogleSheetsOAuth;
loadDomainDuplicateGoogleStatus();
restoreDomainDuplicateSession();
function parseDomainDuplicateInput() {
    return [...new Set(String(domainDuplicateUi.input?.value || '').split(/[\s,;]+/).map(normalizeProjectDomain).filter(Boolean))];
}
if (domainDuplicateUi.check) domainDuplicateUi.check.onclick = async () => {
    const domains = parseDomainDuplicateInput();
    if (!domains.length) return showMonitorToast('Hãy nhập ít nhất một domain hợp lệ.', 'warning');
    domainDuplicateUi.check.disabled = true; domainDuplicateUi.state.textContent = 'Đang kiểm tra...';
    try {
        const sheetUrl = String(domainDuplicateUi.sheetUrl?.value || '').trim();
        const sheetName = String(domainDuplicateUi.sheetName?.value || 'Data').trim() || 'Data';
        const domainColumnName = String(domainDuplicateUi.domainColumn?.value || '').trim();
        const registrantColumnName = String(domainDuplicateUi.registrantColumn?.value || '').trim();
        const commonRequest = fetch('/api/project-registrations/check', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ domains }) });
        const customRequest = sheetUrl ? fetch('/api/google-sheets/domains', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ spreadsheetUrl: sheetUrl, sheetName, domainColumnName, registrantColumnName, refresh: true }) }) : null;
        const [response, customResponse] = await Promise.all([commonRequest, customRequest]);
        const data = await response.json().catch(() => ({}));
        if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`);
        const customData = customResponse ? await customResponse.json().catch(() => ({})) : null;
        if (customResponse && !customResponse.ok) throw new Error(customData?.error || `Google Sheet riêng HTTP ${customResponse.status}`);
        const candidateIdentities = new Set(domains.map(projectDomainIdentity).filter(Boolean));
        const registered = new Map();
        const mergeRegistrations = (items, source) => (items || []).forEach(item => {
            const key = projectDomainIdentity(item.domain); if (!key) return;
            if (!candidateIdentities.has(key)) return;
            if (!registered.has(key)) registered.set(key, { domain: key, sources: new Set(), registrants: new Set() });
            const entry = registered.get(key); entry.sources.add(source);
            const names = Array.isArray(item.registrants) ? item.registrants : [item.registrant];
            names.map(String).map(name => name.trim()).filter(Boolean).forEach(name => entry.registrants.add(name));
        });
        mergeRegistrations(data.registrations, 'Sheet chung');
        mergeRegistrations(customData?.registrations, `Sheet riêng: ${customData?.sheetName || sheetName}`);
        domainDuplicateUnique = domains.filter(domain => !registered.has(projectDomainIdentity(domain)));
        const resultRows = domains.map(domain => { const match = registered.get(projectDomainIdentity(domain)); return { domain, duplicate: Boolean(match), sources: match ? [...match.sources].join(', ') : '', registrants: match ? [...match.registrants].join(', ') : '' }; });
        renderDomainDuplicateRows(resultRows);
        const syncTime = data.syncedAt ? new Date(data.syncedAt).toLocaleString('vi-VN') : '';
        const customMatched = new Set((customData?.registrations || []).map(item => projectDomainIdentity(item.domain)).filter(domain => candidateIdentities.has(domain))).size;
        domainDuplicateUi.summary.textContent = `${domains.length.toLocaleString('vi-VN')} domain · ${registered.size.toLocaleString('vi-VN')} trùng tổng · ${domainDuplicateUnique.length.toLocaleString('vi-VN')} không trùng${customData ? ` · ${customMatched.toLocaleString('vi-VN')} trùng trong “${customData.spreadsheetTitle || 'Sheet riêng'} / ${customData.sheetName}”` : ''}${syncTime ? ` · Sheet chung cập nhật ${syncTime}` : ''}${data.syncWarning ? ` · Cảnh báo: ${data.syncWarning}` : ''}`;
        domainDuplicateUi.state.textContent = data.syncWarning ? 'Dùng dữ liệu gần nhất' : 'Đã tự đồng bộ'; domainDuplicateUi.copy.disabled = !domainDuplicateUnique.length;
        saveDomainDuplicateSession({ checkedInput: String(domainDuplicateUi.input?.value || ''), rows: resultRows, unique: domainDuplicateUnique, summary: domainDuplicateUi.summary.textContent });
        showMonitorToast(data.syncWarning ? `Đã kiểm tra bằng dữ liệu gần nhất; tự đồng bộ lỗi: ${data.syncWarning}` : `Đã tự đồng bộ Sheet và kiểm tra ${domains.length} domain: ${domainDuplicateUnique.length} domain không trùng.`, data.syncWarning ? 'warning' : 'success');
    } catch (error) { domainDuplicateUi.state.textContent = 'Kiểm tra lỗi'; showMonitorToast(`Không thể check trùng: ${error.message}`, 'error'); }
    finally { domainDuplicateUi.check.disabled = false; }
};
if (domainDuplicateUi.copy) domainDuplicateUi.copy.onclick = async () => { await navigator.clipboard.writeText(domainDuplicateUnique.join('\n')); showMonitorToast(`Đã copy ${domainDuplicateUnique.length} domain không trùng.`); };
if (domainDuplicateUi.clear) domainDuplicateUi.clear.onclick = () => { clearTimeout(domainDuplicateInputSaveTimer); localStorage.removeItem(DOMAIN_DUPLICATE_SESSION_KEY); if (domainDuplicateUi.input) domainDuplicateUi.input.value = ''; domainDuplicateUnique = []; domainDuplicateUi.copy.disabled = true; domainDuplicateUi.state.textContent = 'Sẵn sàng'; domainDuplicateUi.summary.textContent = 'Chưa kiểm tra danh sách domain.'; renderDomainDuplicateRows(); };

// Phân loại ngách: Tavily Extract -> AI; kết quả lớn lưu trong IndexedDB và chỉ render theo trang.
const nicheUi = {
    domains: document.getElementById('nicheDomains'), scraperEngine: document.getElementById('nicheScraperEngine'), concurrency: document.getElementById('nicheConcurrency'), start: document.getElementById('btnNicheStart'), stop: document.getElementById('btnNicheStop'),
    resume: document.getElementById('btnNicheResume'), retryErrors: document.getElementById('btnNicheRetryErrors'), clear: document.getElementById('btnNicheClear'), export: document.getElementById('btnNicheExport'), filter: document.getElementById('nicheCategoryFilter'), body: document.getElementById('nicheResultsBody'), summary: document.getElementById('nicheSummary'), state: document.getElementById('nicheRunState'),
    pageSize: document.getElementById('nichePageSize'), pageSummary: document.getElementById('nichePageSummary'), pageIndicator: document.getElementById('nichePageIndicator'), pagePrev: document.getElementById('nichePagePrev'), pageNext: document.getElementById('nichePageNext')
};
let nicheResults = [], nicheRun = { running: false, stopped: false, controller: null, jobId: null }, nichePage = 1, nicheRenderTimer = null;
const NICHE_RESULTS_LOCAL_KEY = 'niche_classification_results_v1';
const NICHE_DB_NAME = 'check_gg_ads_niche_v1', NICHE_DB_STORE = 'results';
let nicheDbPromise = null, nichePendingWrites = new Map(), nicheWriteTimer = null;
function nicheOpenDb() {
    if (!('indexedDB' in window)) return Promise.reject(new Error('Trình duyệt không hỗ trợ IndexedDB.'));
    if (nicheDbPromise) return nicheDbPromise;
    nicheDbPromise = new Promise((resolve, reject) => {
        const request = indexedDB.open(NICHE_DB_NAME, 1);
        request.onupgradeneeded = () => { const db = request.result; if (!db.objectStoreNames.contains(NICHE_DB_STORE)) db.createObjectStore(NICHE_DB_STORE, { keyPath: 'domain' }); };
        request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error);
    });
    return nicheDbPromise;
}
async function nicheReadDb() {
    const db = await nicheOpenDb();
    return new Promise((resolve, reject) => { const request = db.transaction(NICHE_DB_STORE, 'readonly').objectStore(NICHE_DB_STORE).getAll(); request.onsuccess = () => resolve(request.result || []); request.onerror = () => reject(request.error); });
}
async function nicheFlushWrites() {
    if (nicheWriteTimer) { clearTimeout(nicheWriteTimer); nicheWriteTimer = null; }
    const rows = [...nichePendingWrites.values()]; nichePendingWrites.clear(); if (!rows.length) return;
    try { const db = await nicheOpenDb(); await new Promise((resolve, reject) => { const transaction = db.transaction(NICHE_DB_STORE, 'readwrite'); const store = transaction.objectStore(NICHE_DB_STORE); rows.forEach(row => store.put(row)); transaction.oncomplete = resolve; transaction.onerror = () => reject(transaction.error); }); } catch (error) { console.warn('Không thể lưu kết quả phân loại vào IndexedDB:', error); }
}
function nicheQueueSave(rows) {
    (Array.isArray(rows) ? rows : [rows]).forEach(row => { if (row?.domain) nichePendingWrites.set(row.domain, row); });
    if (!nicheWriteTimer) nicheWriteTimer = setTimeout(nicheFlushWrites, 300);
}
async function nicheClearDb() { const db = await nicheOpenDb(); await new Promise((resolve, reject) => { const request = db.transaction(NICHE_DB_STORE, 'readwrite').objectStore(NICHE_DB_STORE).clear(); request.onsuccess = resolve; request.onerror = () => reject(request.error); }); }
function nicheScheduleRender(immediate = false) { if (nicheRenderTimer) return; const run = () => { nicheRenderTimer = null; nicheRender(); }; if (immediate) run(); else nicheRenderTimer = setTimeout(run, 250); }
function isNicheNonRetryable(item) { return Boolean(item?.nonRetryable) || /failed to fetch|error fetching content|không tải được nội dung/i.test(String(item?.error || '')); }
function nicheRender() {
    if (!nicheUi.body) return;
    const category = nicheUi.filter?.value || '';
    const rows = nicheResults.filter(item => !category || item.category === category);
    const pageSize = Math.max(50, Number(nicheUi.pageSize?.value) || 100), pageCount = Math.max(1, Math.ceil(rows.length / pageSize)); nichePage = Math.min(nichePage, pageCount);
    const start = (nichePage - 1) * pageSize, visibleRows = rows.slice(start, start + pageSize);
    nicheUi.body.innerHTML = visibleRows.length ? visibleRows.map((item, index) => { const errorDetail = item.tavilyResponse ? `<details style="margin-top:6px"><summary style="cursor:pointer;color:#f59e0b">Xem response Tavily</summary><pre style="max-width:720px;max-height:260px;overflow:auto;white-space:pre-wrap;font-size:11px;color:var(--text-muted)">${traceEscape(JSON.stringify(item.tavilyResponse, null, 2))}</pre></details>` : ''; const skipped = isNicheNonRetryable(item); return `<tr><td>${start + index + 1}</td><td><strong>${traceEscape(item.domain)}</strong></td><td><span class="badge">${traceEscape(item.category || '—')}</span></td><td>${traceEscape(item.scraperUsed || item.tavilyKey || '—')}</td><td>${item.updatedAt ? new Date(item.updatedAt).toLocaleString('vi-VN') : '—'}</td><td>${item.pending ? '<span style="color:#f59e0b">Đang xử lý</span>' : item.interrupted ? `<span style="color:#f59e0b">Chưa chạy · ${traceEscape(item.error || 'Phiên bị gián đoạn')}</span>` : item.error ? `<span style="color:${skipped ? '#f59e0b' : '#ef4444'}">${skipped ? 'Bỏ qua · ' : ''}${traceEscape(item.error)}</span>${errorDetail}` : '<span style="color:#10b981">Thành công</span>'}</td></tr>`; }).join('') : '<tr class="empty-row"><td colspan="6" class="text-center">Chưa có kết quả phù hợp.</td></tr>';
    const success = nicheResults.filter(item => !item.error && !item.pending).length;
    const skipped = nicheResults.filter(item => item.error && isNicheNonRetryable(item)).length;
    const interrupted = nicheResults.filter(item => item.interrupted && !isNicheNonRetryable(item)).length;
    const errors = nicheResults.filter(item => item.error && !item.interrupted && !isNicheNonRetryable(item)).length;
    if (nicheUi.summary) nicheUi.summary.textContent = `${nicheResults.length} domain · ${success} thành công${skipped ? ` · ${skipped} bỏ qua` : ''}${errors ? ` · ${errors} lỗi` : ''}${interrupted ? ` · ${interrupted} chưa chạy` : ''}`;
    if (nicheUi.resume) { nicheUi.resume.hidden = interrupted === 0; nicheUi.resume.disabled = nicheRun.running || interrupted === 0; nicheUi.resume.innerHTML = `<i class="fa-solid fa-forward-step"></i> Tiếp tục phiên dở${interrupted ? ` (${interrupted})` : ''}`; }
    if (nicheUi.retryErrors) { nicheUi.retryErrors.disabled = nicheRun.running || errors === 0; nicheUi.retryErrors.innerHTML = `<i class="fa-solid fa-rotate-right"></i> Quét lại lỗi${errors ? ` (${errors})` : ''}`; }
    if (nicheUi.export) { nicheUi.export.disabled = rows.length === 0; nicheUi.export.innerHTML = `<i class="fa-solid fa-file-excel"></i> ${category ? `Xuất ngách ${traceEscape(category)} (${rows.length.toLocaleString('vi-VN')})` : `Xuất tất cả (${rows.length.toLocaleString('vi-VN')})`}`; }
    if (nicheUi.pageSummary) nicheUi.pageSummary.textContent = rows.length ? `${start + 1}–${Math.min(start + pageSize, rows.length)} / ${rows.length} kết quả` : '0 kết quả';
    if (nicheUi.pageIndicator) nicheUi.pageIndicator.textContent = `Trang ${nichePage} / ${pageCount}`;
    if (nicheUi.pagePrev) nicheUi.pagePrev.disabled = nichePage <= 1; if (nicheUi.pageNext) nicheUi.pageNext.disabled = nichePage >= pageCount;
}
async function nicheLoadResults() {
    if (!nicheUi.body) return;
    try {
        nicheResults = await nicheReadDb();
        const legacy = JSON.parse(localStorage.getItem(NICHE_RESULTS_LOCAL_KEY) || '[]');
        if (!nicheResults.length && Array.isArray(legacy) && legacy.length) { nicheResults = legacy; nicheQueueSave(legacy); await nicheFlushWrites(); }
        localStorage.removeItem(NICHE_RESULTS_LOCAL_KEY);
    } catch (error) { console.warn('Không tải được IndexedDB, dùng dữ liệu local cũ:', error); try { const saved = JSON.parse(localStorage.getItem(NICHE_RESULTS_LOCAL_KEY) || '[]'); nicheResults = Array.isArray(saved) ? saved : []; } catch (_) { nicheResults = []; } }
    const interruptedAt = new Date().toISOString(); let repairedPending = false;
    nicheResults = nicheResults.map(item => {
        if (!item.pending && String(item.error || '').trim() !== 'Đã dừng') return item;
        repairedPending = true;
        return { domain: item.domain, inputOrder: item.inputOrder, error: item.pending ? 'Phiên trước bị gián đoạn' : 'Phiên đã dừng trước khi domain này được xử lý.', interrupted: true, updatedAt: item.updatedAt || interruptedAt };
    });
    if (repairedPending) { nicheQueueSave(nicheResults.filter(item => item.interrupted)); await nicheFlushWrites(); }
    nicheResults.sort((a, b) => Number(a.inputOrder ?? Number.MAX_SAFE_INTEGER) - Number(b.inputOrder ?? Number.MAX_SAFE_INTEGER) || new Date(b.updatedAt || 0) - new Date(a.updatedAt || 0));
    advNicheByDomain = new Map(nicheResults.filter(item => item.domain && item.category && !item.error).map(item => [normalizeProjectDomain(item.domain), item.category]));
    const categories = [...new Set(['AI', 'Vật lý', 'Khác', ...nicheResults.map(item => item.category).filter(Boolean)])];
    if (nicheUi.filter) { const selected = nicheUi.filter.value; nicheUi.filter.innerHTML = '<option value="">Tất cả ngách</option>' + categories.map(value => `<option value="${traceEscape(value)}">${traceEscape(value)}</option>`).join(''); nicheUi.filter.value = selected; }
    nicheResults.filter(item => item.domain && item.category && !item.error).forEach(item => { const clean = normalizeProjectDomain(item.domain), current = traceInsightCache.get(clean) || {}; traceInsightCache.set(clean, { ...current, niche: item.category }); });
    if (traceEls.nicheFilter) { const selected = traceEls.nicheFilter.value; traceEls.nicheFilter.innerHTML = '<option value="">Tất cả ngách</option>' + categories.map(value => `<option value="${traceEscape(value)}">${traceEscape(value)}</option>`).join(''); traceEls.nicheFilter.value = selected; }
    if (elAdvNicheFilter) { const selected = elAdvNicheFilter.value; elAdvNicheFilter.innerHTML = '<option value="">Tất cả ngách</option>' + categories.map(value => `<option value="${traceEscape(value)}">${traceEscape(value)}</option>`).join(''); elAdvNicheFilter.value = categories.includes(selected) ? selected : ''; }
    renderAdvDomainTable();
    nicheRender();
    traceRender(true);
}
async function nicheRunDirectFallback(domains, cursorByDomain, scraperEngine, concurrency, completedStart = 0) {
    const remaining = domains.filter(domain => nicheResults[cursorByDomain.get(domain)]?.pending);
    let cursor = 0, completed = completedStart;
    nicheUi.state.textContent = `Phiên nền đã chuyển instance · tiếp tục trực tiếp ${remaining.length} domain`;
    showMonitorToast(`Phiên nền không còn trên instance hiện tại. Đang tự động tiếp tục ${remaining.length.toLocaleString('vi-VN')} domain còn lại.`, 'warning');
    const workers = Array.from({ length: Math.min(concurrency, remaining.length) }, async () => {
        while (nicheRun.running && cursor < remaining.length) {
            const domain = remaining[cursor++];
            const inputOrder = cursorByDomain.get(domain);
            try {
                const response = await fetch('/api/niche/classify', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ domain, scraperEngine }), signal: nicheRun.controller.signal });
                const payload = await response.json().catch(() => ({}));
                if (!response.ok) throw Object.assign(new Error(payload.error || `HTTP ${response.status}`), { payload });
                const row = { ...payload, domain, inputOrder, pending: false };
                nicheResults[inputOrder] = row; nicheQueueSave(row);
            } catch (error) {
                if (error.name === 'AbortError' || nicheRun.stopped) break;
                const quotaExhausted = error.payload?.quotaExhausted === true;
                const row = { domain, inputOrder, error: error.message || 'Không thể phân loại domain.', interrupted: quotaExhausted, nonRetryable: isNicheNonRetryable({ error: error.message }), tavilyResponse: error.payload?.tavilyResponse || error.payload?.firecrawlResponse || null, updatedAt: new Date().toISOString() };
                nicheResults[inputOrder] = row; nicheQueueSave(row);
                if (quotaExhausted) { nicheRun.stopped = true; nicheRun.running = false; nicheRun.controller.abort(); }
            } finally {
                completed++;
                nicheUi.state.textContent = `Đang tiếp tục trực tiếp ${completed}/${domains.length}`;
                nicheScheduleRender();
            }
        }
    });
    await Promise.all(workers);
    return completed;
}
async function nicheStart(domainOverride = null) {
    if (nicheRun.running) { showMonitorToast('Một phiên phân loại đang chạy.', 'warning'); return false; }
    const retryMode = Array.isArray(domainOverride);
    const domains = retryMode ? [...new Set(domainOverride.map(normalizeProjectDomain).filter(Boolean))] : [...new Set(String(nicheUi.domains?.value || '').split(/[\s,;]+/).map(normalizeProjectDomain).filter(Boolean))];
    if (!domains.length) { showMonitorToast('Không có domain hợp lệ để phân loại.', 'warning'); return false; }
    const requested = new Set(domains);
    let cursorByDomain;
    if (retryMode) {
        cursorByDomain = new Map(nicheResults.map((item, index) => [item.domain, index]));
        const pendingRows = [];
        domains.forEach(domain => { const index = cursorByDomain.get(domain); if (index === undefined) return; const row = { domain, inputOrder: nicheResults[index].inputOrder ?? index, pending: true }; nicheResults[index] = row; pendingRows.push(row); });
        nicheQueueSave(pendingRows);
    } else {
        const previous = nicheResults.filter(item => !requested.has(item.domain)).map((item, index) => ({ ...item, inputOrder: domains.length + index }));
        nicheResults = [...domains.map((domain, inputOrder) => ({ domain, inputOrder, pending: true })), ...previous];
        cursorByDomain = new Map(domains.map((domain, index) => [domain, index])); nicheQueueSave(nicheResults);
    }
    await nicheFlushWrites();
    nicheRun = { running: true, stopped: false, controller: new AbortController(), jobId: null };
    nicheScheduleRender(true);
    nicheUi.start.disabled = true; nicheUi.stop.disabled = false; nicheUi.state.textContent = `Đang chạy 0/${domains.length}`;
    let completed = 0, runError = null;
    try {
        const scraperEngine = nicheUi.scraperEngine?.value || 'tavily';
        const concurrency = Math.max(1, Math.min(5, Number(nicheUi.concurrency?.value) || 2));
        const startResponse = await fetch('/api/niche/jobs', {
            method: 'POST',
            headers: { 'content-type': 'application/json' },
            body: JSON.stringify({ domains, scraperEngine, concurrency }),
            signal: nicheRun.controller.signal
        });
        const started = await startResponse.json().catch(() => ({}));
        if (!startResponse.ok) throw new Error(started.error || `HTTP ${startResponse.status}`);
        nicheRun.jobId = started.id;
        let resultCursor = 0, missingJobRetries = 0;
        while (nicheRun.running) {
            const statusResponse = await fetch(`/api/niche/jobs/${encodeURIComponent(nicheRun.jobId)}?since=${resultCursor}`, { cache: 'no-store', signal: nicheRun.controller.signal });
            const status = await statusResponse.json().catch(() => ({}));
            if (statusResponse.status === 404) {
                if (missingJobRetries++ < 2) { await new Promise(resolve => setTimeout(resolve, 1200)); continue; }
                completed = await nicheRunDirectFallback(domains, cursorByDomain, scraperEngine, concurrency, completed);
                break;
            }
            if (!statusResponse.ok) throw new Error(status.error || `HTTP ${statusResponse.status}`);
            missingJobRetries = 0;
            for (const item of (status.results || [])) {
                const domain = domains[item.inputOrder];
                const inputOrder = cursorByDomain.get(domain);
                if (inputOrder === undefined) continue;
                const row = { ...(item.row || {}), domain, inputOrder, pending: false };
                nicheResults[inputOrder] = row; nicheQueueSave(row);
            }
            resultCursor = Number(status.nextCursor || resultCursor);
            completed = Number(status.completed || 0);
            nicheUi.state.textContent = `Đang chạy ${completed}/${domains.length}`;
            nicheScheduleRender();
            if (status.status !== 'running') {
                if (status.status === 'stopped') {
                    nicheRun.stopped = true;
                    if (status.quotaExhausted) {
                        showMonitorToast(`${status.provider || 'API'} đã xác nhận hết lượt trên tất cả key. Phiên dừng tại ${completed}/${domains.length} domain.`, 'warning');
                        loadCompetitorMonitor().catch(() => {});
                    }
                }
                break;
            }
            await new Promise(resolve => setTimeout(resolve, 900));
        }
    } catch (error) {
        if (!(error.name === 'AbortError' && nicheRun.stopped)) {
            runError = error;
            nicheResults = nicheResults.map(item => item.pending && requested.has(item.domain) ? { domain: item.domain, inputOrder: item.inputOrder, error: `Mất kết nối phiên chạy nền: ${error.message}`, interrupted: true, updatedAt: new Date().toISOString() } : item);
            nicheQueueSave(nicheResults.filter(item => item.interrupted));
            showMonitorToast(`Không thể chạy tiếp phiên phân loại: ${error.message}`, 'error');
        }
    }
    if (nicheRun.stopped) {
        const interruptedAt = new Date().toISOString();
        nicheResults = nicheResults.map(item => item.pending && requested.has(item.domain) ? {
            domain: item.domain,
            inputOrder: item.inputOrder,
            error: 'Phiên đã dừng trước khi domain này được xử lý.',
            interrupted: true,
            updatedAt: interruptedAt
        } : item);
        nicheQueueSave(nicheResults.filter(item => item.interrupted && requested.has(item.domain)));
    }
    await nicheFlushWrites();
    nicheRun.running = false; nicheUi.start.disabled = false; nicheUi.stop.disabled = true; nicheUi.state.textContent = runError ? `Lỗi khởi chạy · ${completed}/${domains.length}` : nicheRun.stopped ? `Đã dừng ${completed}/${domains.length}` : `Hoàn tất ${completed}/${domains.length}`;
    await nicheLoadResults();
    return !runError;
}
if (nicheUi.start) nicheUi.start.onclick = () => nicheStart();
if (nicheUi.resume) nicheUi.resume.onclick = () => { const interruptedDomains = nicheResults.filter(item => item.interrupted && !isNicheNonRetryable(item)).map(item => item.domain); if (interruptedDomains.length) nicheStart(interruptedDomains); };
if (nicheUi.retryErrors) nicheUi.retryErrors.onclick = async () => {
    const failedDomains = [...new Set(nicheResults.filter(item => item.error && !item.interrupted && !isNicheNonRetryable(item)).map(item => normalizeProjectDomain(item.domain)).filter(Boolean))];
    if (!failedDomains.length) return showMonitorToast('Không có lỗi nào đủ điều kiện quét lại. Lỗi tải nội dung đã được đánh dấu bỏ qua.', 'warning');
    if (!await showAppConfirm(`Quét lại ${failedDomains.length.toLocaleString('vi-VN')} domain bị lỗi?\nCác lỗi “Failed to fetch/Error fetching content” sẽ không được chạy lại.`, { confirmText: 'Quét lại' })) return;
    nicheUi.retryErrors.disabled = true;
    nicheUi.state.textContent = `Đang chuẩn bị quét lại ${failedDomains.length.toLocaleString('vi-VN')} domain...`;
    showMonitorToast(`Đã tạo hàng đợi quét lại ${failedDomains.length.toLocaleString('vi-VN')} domain.`);
    try { await nicheStart(failedDomains); }
    catch (error) { showMonitorToast(`Không thể quét lại lỗi: ${error.message}`, 'error'); }
    finally { nicheRender(); }
};
if (nicheUi.stop) nicheUi.stop.onclick = async () => {
    nicheRun.stopped = true; nicheRun.running = false; nicheUi.state.textContent = 'Đang dừng...';
    if (nicheRun.jobId) {
        try { await fetch(`/api/niche/jobs/${encodeURIComponent(nicheRun.jobId)}/stop`, { method: 'POST' }); }
        catch (_) {}
    }
    nicheRun.controller?.abort();
};
if (nicheUi.clear) nicheUi.clear.onclick = async () => {
    if (nicheRun.running) return alert('Hãy dừng phiên phân loại trước khi xóa lịch sử.');
    if (!await showAppConfirm('Xóa toàn bộ lịch sử kết quả phân loại lưu trên trình duyệt này?', { danger: true, confirmText: 'Xóa lịch sử' })) return;
    if (nicheWriteTimer) { clearTimeout(nicheWriteTimer); nicheWriteTimer = null; } nichePendingWrites.clear();
    try { await nicheClearDb(); } catch (error) { console.warn('Không xóa được IndexedDB:', error); }
    localStorage.removeItem(NICHE_RESULTS_LOCAL_KEY); nicheResults = []; nichePage = 1; nicheRender();
};
if (nicheUi.filter) nicheUi.filter.onchange = () => { nichePage = 1; nicheRender(); };
if (nicheUi.pageSize) nicheUi.pageSize.onchange = () => { nichePage = 1; nicheRender(); };
if (nicheUi.pagePrev) nicheUi.pagePrev.onclick = () => { if (nichePage > 1) { nichePage--; nicheRender(); } };
if (nicheUi.pageNext) nicheUi.pageNext.onclick = () => { nichePage++; nicheRender(); };
if (nicheUi.export) nicheUi.export.onclick = () => {
    const category = nicheUi.filter?.value || '';
    const rows = nicheResults.filter(item => !category || item.category === category);
    if (!rows.length) return showMonitorToast('Không có kết quả nào khớp bộ lọc ngách để xuất.', 'warning');
    const categorySlug = category ? `_${category.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]+/gi, '_').replace(/^_|_$/g, '')}` : '_Tat_ca';
    exportRowsToExcel(`Phan_loai_ngach${categorySlug}_${new Date().toISOString().slice(0, 10)}.xls`, ['STT','Domain','Ngách','Nguồn / Key','Thời gian','Trạng thái'], rows.map((item, index) => [index + 1, item.domain, item.category || '', item.scraperUsed || item.tavilyKey || '', item.updatedAt || '', item.pending ? 'Đang xử lý' : item.error || 'Thành công']));
    showMonitorToast(`Đã xuất ${rows.length.toLocaleString('vi-VN')} domain${category ? ` thuộc ngách ${category}` : ''}.`);
};
nicheLoadResults();

// --- Quét Nền tảng Affiliate (Firecrawl Search v2) ---
let affiliateFirecrawlSettings = { source: 'user', canUseAdmin: false, personalKeyConfigured: false };
async function loadAffiliateFirecrawlSettings() {
    const response = await fetch('/api/affiliate/firecrawl-settings', { cache: 'no-store' });
    const data = await response.json().catch(() => ({}));
    if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`);
    affiliateFirecrawlSettings = data;
    const source = document.getElementById('affiliateFirecrawlSource');
    const keyInput = document.getElementById('affiliatePersonalFirecrawlKey');
    const personalHint = document.getElementById('affiliatePersonalFirecrawlHint');
    const adminHint = document.getElementById('affiliateAdminFirecrawlHint');
    const state = document.getElementById('affiliateFirecrawlState');
    const keyList = document.getElementById('affiliatePersonalFirecrawlList');
    if (source) {
        [...source.options].forEach(option => { if (['admin', 'both'].includes(option.value)) option.disabled = !data.canUseAdmin; });
        source.value = data.source;
        if (source.selectedIndex < 0 || source.selectedOptions[0]?.disabled) source.value = data.personalKeyConfigured ? 'user' : '';
    }
    if (keyInput) keyInput.value = '';
    if (personalHint) personalHint.textContent = data.personalKeyConfigured ? `Đã lưu ${(data.personalKeys || []).length} key cá nhân; nhập thêm key mới nếu cần.` : 'Chưa cấu hình key cá nhân.';
    if (keyList) keyList.innerHTML = (data.personalKeys || []).length ? data.personalKeys.map(key => `<article class="affiliate-personal-key"><div><strong>${escapeMonitorHtml(key.name)}</strong><small>${key.active ? 'Đang bật' : 'Đã tắt'}${key.lastUsedAt ? ` · dùng gần nhất ${new Date(key.lastUsedAt).toLocaleString('vi-VN')}` : ''}</small>${key.lastError ? `<small class="key-error">${escapeMonitorHtml(key.lastError)}</small>` : ''}</div><div><button class="btn btn-secondary btn-sm" data-personal-firecrawl-toggle="${escapeMonitorHtml(key.id)}" data-active="${key.active}">${key.active ? 'Tắt' : 'Bật'}</button><button class="btn btn-danger btn-sm" data-personal-firecrawl-delete="${escapeMonitorHtml(key.id)}">Xóa</button></div></article>`).join('') : '<div class="empty-row">Chưa có Firecrawl API key cá nhân.</div>';
    if (adminHint) adminHint.textContent = data.canUseAdmin ? 'Admin đã cấp quyền dùng kho Firecrawl chung.' : 'Admin chưa cấp quyền dùng kho Firecrawl chung.';
    if (state) state.textContent = data.personalKeyConfigured || data.canUseAdmin ? 'Sẵn sàng' : 'Chưa có API key';
    return data;
}

document.getElementById('btnSaveAffiliateFirecrawl')?.addEventListener('click', async () => {
    const button = document.getElementById('btnSaveAffiliateFirecrawl');
    const source = document.getElementById('affiliateFirecrawlSource')?.value;
    const apiKeys = document.getElementById('affiliatePersonalFirecrawlKey')?.value.trim() || '';
    if (!source) return showMonitorToast('Chưa có nguồn Firecrawl khả dụng. Hãy nhập key cá nhân hoặc liên hệ Admin.', 'warning');
    button.disabled = true;
    try {
        const response = await fetch('/api/affiliate/firecrawl-settings', { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ source, apiKeys }) });
        const data = await response.json().catch(() => ({}));
        if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`);
        await loadAffiliateFirecrawlSettings();
        showMonitorToast('Đã lưu cấu hình Firecrawl cho tính năng Affiliate.');
    } catch (error) { showMonitorToast(`Không lưu được cấu hình Firecrawl: ${error.message}`, 'error'); }
    finally { button.disabled = false; }
});

document.getElementById('affiliatePersonalFirecrawlList')?.addEventListener('click', async event => {
    const toggle = event.target.closest('[data-personal-firecrawl-toggle]');
    const remove = event.target.closest('[data-personal-firecrawl-delete]');
    const button = toggle || remove; if (!button) return;
    button.disabled = true;
    try {
        const id = toggle?.dataset.personalFirecrawlToggle || remove.dataset.personalFirecrawlDelete;
        const response = await fetch(`/api/affiliate/firecrawl-settings/keys/${encodeURIComponent(id)}`, toggle ? { method: 'PATCH', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ active: toggle.dataset.active !== 'true' }) } : { method: 'DELETE' });
        const data = await response.json().catch(() => ({}));
        if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`);
        await loadAffiliateFirecrawlSettings();
        showMonitorToast(remove ? 'Đã xóa Firecrawl key cá nhân.' : 'Đã cập nhật trạng thái Firecrawl key.');
    } catch (error) { showMonitorToast(`Không cập nhật được Firecrawl key: ${error.message}`, 'error'); }
    finally { button.disabled = false; }
});

document.getElementById('btnClearAffiliateFirecrawl')?.addEventListener('click', async () => {
    if (!await showAppConfirm('Xóa Firecrawl API key cá nhân đã lưu?', { danger: true, confirmText: 'Xóa API key' })) return;
    const source = affiliateFirecrawlSettings.canUseAdmin ? 'admin' : 'user';
    try {
        const response = await fetch('/api/affiliate/firecrawl-settings', { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ source, clearPersonalKey: true }) });
        const data = await response.json().catch(() => ({}));
        if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`);
        await loadAffiliateFirecrawlSettings();
        showMonitorToast('Đã xóa Firecrawl API key cá nhân.');
    } catch (error) { showMonitorToast(`Không xóa được Firecrawl key: ${error.message}`, 'error'); }
});

const affiliateUi = {
    projects: document.getElementById('affiliateProjects'),
    concurrency: document.getElementById('affiliateConcurrency'),
    start: document.getElementById('btnAffiliateStart'),
    resume: document.getElementById('btnAffiliateResume'),
    retry: document.getElementById('btnAffiliateRetry'),
    stop: document.getElementById('btnAffiliateStop'),
    clear: document.getElementById('btnAffiliateClear'),
    export: document.getElementById('btnAffiliateExport'),
    body: document.getElementById('affiliateResultsBody'),
    summary: document.getElementById('affiliateSummary'),
    state: document.getElementById('affiliateRunState'),
    historyFilter: document.getElementById('affiliateHistoryFilter'),
    networkFilter: document.getElementById('affiliateNetworkFilter'),
    clearFilter: document.getElementById('btnAffiliateClearFilter'),
    pageSize: document.getElementById('affiliatePageSize'),
    pageSummary: document.getElementById('affiliatePageSummary'),
    pageIndicator: document.getElementById('affiliatePageIndicator'),
    pagePrev: document.getElementById('affiliatePagePrev'),
    pageNext: document.getElementById('affiliatePageNext')
};
let affiliateResults = [], affiliateRun = { running: false, stopped: false, jobId: null }, affiliatePage = 1;
const affiliateSelectedNetworks = new Set();
let affiliateSelectedBatchId = '';
let affiliatePageUnloading = false;
window.addEventListener('pagehide', () => { affiliatePageUnloading = true; });
window.addEventListener('pageshow', () => { affiliatePageUnloading = false; });
const AFFILIATE_LOCAL_KEY = 'affiliate_finder_results_v1';
const AFFILIATE_DB_NAME = 'check_gg_ads_affiliate_v1', AFFILIATE_DB_STORE = 'results', AFFILIATE_DB_OWNER_INDEX = 'ownerKey';
let affiliateDbPromise = null;
const affiliateOwnerKey = () => `affiliate:${authenticatedUser?.id || 'anonymous'}`;
function affiliateNewId(prefix = 'affiliate') { return `${prefix}:${Date.now()}:${crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2)}`; }
function affiliateOpenDb() {
    if (!('indexedDB' in window)) return Promise.reject(new Error('Trình duyệt không hỗ trợ IndexedDB.'));
    if (affiliateDbPromise) return affiliateDbPromise;
    affiliateDbPromise = new Promise((resolve, reject) => {
        const request = indexedDB.open(AFFILIATE_DB_NAME, 1);
        request.onupgradeneeded = () => {
            const db = request.result;
            const store = db.objectStoreNames.contains(AFFILIATE_DB_STORE)
                ? request.transaction.objectStore(AFFILIATE_DB_STORE)
                : db.createObjectStore(AFFILIATE_DB_STORE, { keyPath: 'historyId' });
            if (!store.indexNames.contains(AFFILIATE_DB_OWNER_INDEX)) store.createIndex(AFFILIATE_DB_OWNER_INDEX, 'ownerKey', { unique: false });
        };
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
    });
    return affiliateDbPromise;
}
async function affiliateReadDb() {
    const db = await affiliateOpenDb();
    return new Promise((resolve, reject) => {
        const request = db.transaction(AFFILIATE_DB_STORE, 'readonly').objectStore(AFFILIATE_DB_STORE).index(AFFILIATE_DB_OWNER_INDEX).getAll(affiliateOwnerKey());
        request.onsuccess = () => resolve((request.result || []).sort((a, b) => new Date(b.updatedAt || b.createdAt || 0) - new Date(a.updatedAt || a.createdAt || 0)));
        request.onerror = () => reject(request.error);
    });
}
async function affiliateWriteDb(rows) {
    if (!rows?.length) return;
    const db = await affiliateOpenDb();
    await new Promise((resolve, reject) => {
        const transaction = db.transaction(AFFILIATE_DB_STORE, 'readwrite');
        const store = transaction.objectStore(AFFILIATE_DB_STORE);
        rows.forEach(row => store.put({ ...row, historyId: row.historyId || affiliateNewId(), ownerKey: affiliateOwnerKey() }));
        transaction.oncomplete = resolve;
        transaction.onerror = () => reject(transaction.error);
        transaction.onabort = () => reject(transaction.error || new Error('Giao dịch IndexedDB bị hủy.'));
    });
}
async function affiliateClearDb() {
    const db = await affiliateOpenDb();
    await new Promise((resolve, reject) => {
        const transaction = db.transaction(AFFILIATE_DB_STORE, 'readwrite');
        const request = transaction.objectStore(AFFILIATE_DB_STORE).index(AFFILIATE_DB_OWNER_INDEX).openCursor(IDBKeyRange.only(affiliateOwnerKey()));
        request.onsuccess = () => { const cursor = request.result; if (!cursor) return; cursor.delete(); cursor.continue(); };
        transaction.oncomplete = resolve;
        transaction.onerror = () => reject(transaction.error);
        transaction.onabort = () => reject(transaction.error || new Error('Giao dịch IndexedDB bị hủy.'));
    });
}

function affiliateTagStyle(network) {
    const palette = [
        ['#818cf8', 'rgba(99,102,241,.18)', 'rgba(129,140,248,.45)'],
        ['#22d3ee', 'rgba(6,182,212,.16)', 'rgba(34,211,238,.42)'],
        ['#34d399', 'rgba(16,185,129,.16)', 'rgba(52,211,153,.42)'],
        ['#fbbf24', 'rgba(245,158,11,.16)', 'rgba(251,191,36,.42)'],
        ['#fb7185', 'rgba(244,63,94,.16)', 'rgba(251,113,133,.42)'],
        ['#c084fc', 'rgba(168,85,247,.16)', 'rgba(192,132,252,.42)'],
        ['#60a5fa', 'rgba(59,130,246,.16)', 'rgba(96,165,250,.42)'],
        ['#fb923c', 'rgba(249,115,22,.16)', 'rgba(251,146,60,.42)']
    ];
    let hash = 0;
    for (const char of String(network || '').toLowerCase()) hash = ((hash * 31) + char.charCodeAt(0)) >>> 0;
    const [color, background, border] = palette[hash % palette.length];
    return `color:${color};background:${background};border:1px solid ${border}`;
}

function affiliateSyncHistoryOptions() {
    if (!affiliateUi.historyFilter) return;
    const batches = new Map();
    affiliateResults.forEach(item => {
        const id = String(item.batchId || 'legacy');
        const timestamp = item.createdAt || item.updatedAt || '';
        const batch = batches.get(id) || { id, timestamp, count: 0, completed: 0 };
        batch.count++;
        if (!item.pending && !item.error) batch.completed++;
        if (new Date(timestamp).getTime() > new Date(batch.timestamp || 0).getTime()) batch.timestamp = timestamp;
        batches.set(id, batch);
    });
    const sorted = [...batches.values()].sort((a, b) => new Date(b.timestamp || 0) - new Date(a.timestamp || 0));
    if (affiliateSelectedBatchId && !batches.has(affiliateSelectedBatchId)) affiliateSelectedBatchId = '';
    affiliateUi.historyFilter.innerHTML = '<option value="">Tất cả phiên đã lưu</option>' + sorted.map(batch => `<option value="${escapeMonitorHtml(batch.id)}">${escapeMonitorHtml(batch.timestamp ? new Date(batch.timestamp).toLocaleString('vi-VN') : 'Dữ liệu cũ')} · ${batch.count.toLocaleString('vi-VN')} dự án · ${batch.completed.toLocaleString('vi-VN')} xong</option>`).join('');
    affiliateUi.historyFilter.value = affiliateSelectedBatchId;
}

function affiliateHistoryResults() {
    return affiliateSelectedBatchId ? affiliateResults.filter(item => String(item.batchId || 'legacy') === affiliateSelectedBatchId) : affiliateResults;
}

function affiliateSyncNetworkFilterOptions() {
    if (!affiliateUi.networkFilter) return;
    const networks = [...new Set(affiliateHistoryResults().flatMap(item => Array.isArray(item.networks) ? item.networks : []).filter(Boolean))]
        .sort((a, b) => String(a).localeCompare(String(b), 'vi', { sensitivity: 'base' }));
    [...affiliateSelectedNetworks].forEach(value => { if (value !== '__none__' && !networks.includes(value)) affiliateSelectedNetworks.delete(value); });
    const chips = affiliateUi.networkFilter.querySelector('.affiliate-network-chips');
    const options = affiliateUi.networkFilter.querySelector('.affiliate-network-options');
    if (chips) chips.innerHTML = affiliateSelectedNetworks.size
        ? [...affiliateSelectedNetworks].map(value => `<span class="affiliate-network-chip">${escapeMonitorHtml(value === '__none__' ? 'Không tìm thấy NET' : value)}<button type="button" data-affiliate-network-remove="${escapeMonitorHtml(value)}" aria-label="Bỏ ${escapeMonitorHtml(value)}">×</button></span>`).join('')
        : '<span class="affiliate-network-placeholder">Tất cả nền tảng</span>';
    const values = ['__none__', ...networks];
    if (options) options.innerHTML = values.map(value => `<button type="button" class="affiliate-network-option${affiliateSelectedNetworks.has(value) ? ' selected' : ''}" data-affiliate-network-value="${escapeMonitorHtml(value)}"><span>${escapeMonitorHtml(value === '__none__' ? 'Không tìm thấy nền tảng' : value)}</span><i class="fa-solid fa-check"></i></button>`).join('');
}

function affiliateFilteredResults() {
    const filters = [...affiliateSelectedNetworks];
    const historyRows = affiliateHistoryResults();
    if (!filters.length) return historyRows;
    const includeNone = filters.includes('__none__');
    const selectedNetworks = filters.filter(value => value !== '__none__');
    return historyRows.filter(item => {
        const itemNetworks = Array.isArray(item.networks) ? item.networks : [];
        return (includeNone && !item.pending && !itemNetworks.length) || selectedNetworks.some(network => itemNetworks.includes(network));
    });
}

function affiliateRender() {
    if (!affiliateUi.body) return;
    affiliateSyncHistoryOptions();
    affiliateSyncNetworkFilterOptions();
    const networkFilters = [...affiliateSelectedNetworks];
    const filteredResults = affiliateFilteredResults();
    const historyResults = affiliateHistoryResults();
    const pageSize = Math.max(10, Number(affiliateUi.pageSize?.value) || 100);
    const total = filteredResults.length;
    const totalPages = Math.max(1, Math.ceil(total / pageSize));
    if (affiliatePage > totalPages) affiliatePage = totalPages;
    const startIdx = (affiliatePage - 1) * pageSize;
    const pageItems = filteredResults.slice(startIdx, startIdx + pageSize);

    if (!pageItems.length) {
        affiliateUi.body.innerHTML = `<tr class="empty-row"><td colspan="6" class="text-center">${affiliateResults.length ? 'Không có kết quả phù hợp bộ lọc hoặc phiên đã chọn.' : 'Nhập tên dự án và bắt đầu quét.'}</td></tr>`;
    } else {
        affiliateUi.body.innerHTML = pageItems.map((item, idx) => {
            const stt = startIdx + idx + 1;
            const project = escapeMonitorHtml(item.project || '');
            const networks = Array.isArray(item.networks) && item.networks.length
                ? item.networks.map(n => `<span class="badge" style="${affiliateTagStyle(n)};margin-right:4px;padding:2px 7px;border-radius:4px;font-weight:600">${escapeMonitorHtml(n)}</span>`).join('')
                : (item.pending ? `<span class="muted">${affiliateRun.running ? 'Đang tìm...' : 'Chờ tiếp tục...'}</span>` : '<span class="muted">Không tìm thấy NET nào</span>');

            const evidenceMatches = Array.isArray(item.matches) ? item.matches : [];
            const primaryEvidence = Array.isArray(item.networks) ? item.networks.map(network => evidenceMatches.find(match => match.network === network)).filter(Boolean) : [];
            const primaryEvidenceKeys = new Set(primaryEvidence.map(match => `${match.network}:${match.url}`));
            const visibleEvidence = [...primaryEvidence, ...evidenceMatches.filter(match => !primaryEvidenceKeys.has(`${match.network}:${match.url}`))].slice(0, 10);
            const matchesHtml = visibleEvidence.length
                ? visibleEvidence.map(m => `<div><a href="${escapeMonitorHtml(m.url)}" target="_blank" rel="noopener" style="color:var(--primary);text-decoration:underline;font-weight:500">[${escapeMonitorHtml(m.network)}] ${escapeMonitorHtml(m.title || m.url)}</a>${m.sourceLevel ? ` <span style="color:var(--text-muted);font-size:.7rem;font-weight:700">${escapeMonitorHtml(m.sourceLevel)} · ${Number(m.confidence || 0)}%</span>` : ''}${m.source === 'official-homepage' ? ' <span style="color:var(--emerald);font-size:.72rem;font-weight:700">WEBSITE CHÍNH THỨC</span>' : ''}</div>`).join('')
                : '-';

            const queries = Array.isArray(item.queriesUsed) ? item.queriesUsed.map(q => `<code>${escapeMonitorHtml(q)}</code>`).join('<br/>') : '-';
            const status = item.pending
                ? affiliateRun.running
                    ? '<span style="color:var(--text-muted)"><i class="fa-solid fa-spinner fa-spin"></i> Đang tìm</span>'
                    : '<span style="color:#fbbf24"><i class="fa-solid fa-pause"></i> Chờ tiếp tục</span>'
                : item.error
                ? `<span style="color:var(--red)" title="${escapeMonitorHtml(item.error)}">Lỗi</span>`
                : '<span style="color:var(--emerald)"><i class="fa-solid fa-check"></i> Xong</span>';

            return `<tr><td>${stt}</td><td><strong>${project}</strong></td><td>${networks}</td><td style="max-width:280px;font-size:0.85rem">${matchesHtml}</td><td style="font-size:0.8rem">${queries}</td><td>${status}</td></tr>`;
        }).join('');
    }

    if (affiliateUi.pageSummary) affiliateUi.pageSummary.textContent = networkFilters.length ? `${total.toLocaleString('vi-VN')} / ${historyResults.length.toLocaleString('vi-VN')} kết quả trong phiên` : `${total.toLocaleString('vi-VN')} kết quả`;
    if (affiliateUi.pageIndicator) affiliateUi.pageIndicator.textContent = `Trang ${affiliatePage} / ${totalPages}`;
    if (affiliateUi.pagePrev) affiliateUi.pagePrev.disabled = affiliatePage <= 1;
    if (affiliateUi.pageNext) affiliateUi.pageNext.disabled = affiliatePage >= totalPages;
    if (affiliateUi.summary) affiliateUi.summary.textContent = `Tổng local: ${affiliateResults.length.toLocaleString('vi-VN')} dự án${affiliateSelectedBatchId ? ` · Phiên đang xem: ${historyResults.length.toLocaleString('vi-VN')}` : ''}${networkFilters.length ? ` · Đang lọc ${networkFilters.length} NET · Hiển thị: ${total.toLocaleString('vi-VN')}` : ''} · Tìm thấy NET: ${historyResults.filter(r => r.networks && r.networks.length).length.toLocaleString('vi-VN')}`;
    if (affiliateUi.export) { affiliateUi.export.disabled = total === 0; affiliateUi.export.innerHTML = `<i class="fa-solid fa-file-excel"></i> ${networkFilters.length ? `Xuất theo lọc (${total.toLocaleString('vi-VN')})` : `Xuất tất cả (${total.toLocaleString('vi-VN')})`}`; }
    const pendingCount = affiliateResults.filter(item => item.pending).length;
    const errorCount = affiliateResults.filter(item => !item.pending && item.error).length;
    if (affiliateUi.resume) {
        affiliateUi.resume.hidden = pendingCount === 0 || affiliateRun.running;
        affiliateUi.resume.disabled = affiliateRun.running;
        affiliateUi.resume.innerHTML = `<i class="fa-solid fa-forward-step"></i> Tiếp tục phiên dở (${pendingCount.toLocaleString('vi-VN')})`;
    }
    if (affiliateUi.retry) {
        affiliateUi.retry.hidden = errorCount === 0 || affiliateRun.running;
        affiliateUi.retry.disabled = affiliateRun.running;
        affiliateUi.retry.innerHTML = `<i class="fa-solid fa-rotate-right"></i> Quét lại lỗi (${errorCount.toLocaleString('vi-VN')})`;
    }
    if (!affiliateRun.running && pendingCount && affiliateUi.state) affiliateUi.state.textContent = `Có phiên dở · ${pendingCount.toLocaleString('vi-VN')} dự án chưa chạy`;
}

async function affiliateLoadHistory() {
    try {
        affiliateResults = await affiliateReadDb();
        const raw = localStorage.getItem(AFFILIATE_LOCAL_KEY);
        if (raw) {
            const legacyRows = JSON.parse(raw);
            if (Array.isArray(legacyRows) && legacyRows.length) {
                const migratedAt = new Date().toISOString();
                const migrated = legacyRows.map((row, index) => ({ ...row, pending: false, historyId: row.historyId || affiliateNewId(`legacy-${index}`), batchId: row.batchId || 'legacy-localStorage', ownerKey: affiliateOwnerKey(), updatedAt: row.updatedAt || migratedAt }));
                await affiliateWriteDb(migrated);
                const existingIds = new Set(affiliateResults.map(row => row.historyId));
                affiliateResults.push(...migrated.filter(row => !existingIds.has(row.historyId)));
                affiliateResults.sort((a, b) => new Date(b.updatedAt || b.createdAt || 0) - new Date(a.updatedAt || a.createdAt || 0));
            }
            localStorage.removeItem(AFFILIATE_LOCAL_KEY);
        }
    } catch (error) {
        console.warn('Không tải được lịch sử Affiliate từ IndexedDB:', error);
        showMonitorToast(`Không tải được lịch sử Affiliate: ${error.message}`, 'error');
    }
    affiliateRender();
}

async function affiliateStart(resumePending = false, selectedRows = null) {
    try { await loadAffiliateFirecrawlSettings(); }
    catch (error) { return showMonitorToast(`Không xác định được nguồn Firecrawl: ${error.message}`, 'error'); }
    let projects, batchRows, batchId;
    if (resumePending) {
        batchRows = Array.isArray(selectedRows) ? selectedRows : affiliateResults.filter(item => item.pending);
        if (!batchRows.length) return showMonitorToast('Không còn dự án Affiliate nào đang chờ.', 'warning');
        projects = batchRows.map(item => item.project);
        batchId = batchRows[0]?.batchId || affiliateNewId('resume');
        batchRows.forEach((item, index) => { item.inputOrder = index; item.batchId = batchId; item.error = ''; item.updatedAt = new Date().toISOString(); });
    } else {
        const raw = String(affiliateUi.projects?.value || '').split(/[\r\n]+/);
        projects = [...new Set(raw.map(p => p.trim().replace(/^https?:\/\//i, '').replace(/\/.*$/, '')).filter(Boolean))];
        if (!projects.length) return alert('Nhập ít nhất một tên dự án hoặc domain hợp lệ.');
        batchId = affiliateNewId('batch');
        affiliateSelectedBatchId = batchId;
        const batchStartedAt = new Date().toISOString();
        batchRows = projects.map((p, idx) => ({ historyId: affiliateNewId('result'), batchId, ownerKey: affiliateOwnerKey(), project: p, inputOrder: idx, pending: true, createdAt: batchStartedAt, updatedAt: batchStartedAt }));
        affiliateResults = [...batchRows, ...affiliateResults];
    }
    affiliatePage = 1;
    affiliateRender();
    try { await affiliateWriteDb(batchRows); } catch (error) { showMonitorToast(`Không lưu được lô Affiliate vào IndexedDB: ${error.message}`, 'error'); }

    affiliateRun = { running: true, stopped: false, jobId: null };
    affiliateUi.start.disabled = true;
    affiliateUi.stop.disabled = false;
    const sourceLabel = { user: 'key cá nhân', admin: 'key Admin', both: 'kết hợp cá nhân + Admin' }[affiliateFirecrawlSettings.source] || affiliateFirecrawlSettings.source;
    affiliateUi.state.textContent = `Đang tìm 0/${projects.length} · ${sourceLabel}`;

    try {
        const res = await fetch('/api/affiliate/jobs', {
            method: 'POST',
            headers: { 'content-type': 'application/json' },
            body: JSON.stringify({ projects, concurrency: Number(affiliateUi.concurrency?.value) || 2, firecrawlSource: affiliateFirecrawlSettings.source })
        });
        const started = await res.json().catch(() => ({}));
        if (!res.ok) throw new Error(started.error || `HTTP ${res.status}`);
        affiliateRun.jobId = started.id;
        if (affiliateRun.stopped) {
            await fetch(`/api/affiliate/jobs/${encodeURIComponent(affiliateRun.jobId)}/stop`, { method: 'POST' });
        }

        let resultCursor = 0, completed = 0;
        while (affiliateRun.running) {
            const statusRes = await fetch(`/api/affiliate/jobs/${encodeURIComponent(affiliateRun.jobId)}?since=${resultCursor}`, { cache: 'no-store' });
            const status = await statusRes.json().catch(() => ({}));
            if (!statusRes.ok) throw new Error(status.error || `HTTP ${statusRes.status}`);

            (status.results || []).forEach(item => {
                const idx = item.inputOrder;
                if (idx !== undefined && batchRows[idx]) {
                    Object.assign(batchRows[idx], item.row, { historyId: batchRows[idx].historyId, batchId, ownerKey: affiliateOwnerKey(), pending: false, updatedAt: item.row?.updatedAt || new Date().toISOString() });
                }
            });

            if ((status.results || []).length) {
                try { await affiliateWriteDb((status.results || []).map(item => batchRows[item.inputOrder]).filter(Boolean)); }
                catch (error) { console.warn('Không lưu được kết quả Affiliate vào IndexedDB:', error); }
            }

            resultCursor = Number(status.nextCursor || resultCursor);
            completed = Number(status.completed || 0);
            affiliateUi.state.textContent = `Đang chạy ${completed}/${projects.length} · ${sourceLabel}`;
            affiliateRender();

            if (status.status !== 'running') {
                affiliateRun.stopped = status.status === 'stopped';
                break;
            }
            await new Promise(r => setTimeout(r, 1000));
        }
    } catch (error) {
        if (!affiliatePageUnloading) {
            batchRows.forEach(item => { if (item.pending) Object.assign(item, { pending: false, error: error.message || 'Không thể khởi tạo phiên quét Affiliate.', updatedAt: new Date().toISOString() }); });
            showMonitorToast(`Lỗi quét Affiliate: ${error.message}`, 'error');
        }
    } finally {
        if (!affiliatePageUnloading) batchRows.forEach(item => { if (item.pending) Object.assign(item, { pending: false, error: affiliateRun.stopped ? 'Đã dừng trước khi xử lý.' : 'Phiên kết thúc trước khi nhận được kết quả.', updatedAt: new Date().toISOString() }); });
        affiliateRun.running = false;
        affiliateUi.start.disabled = false;
        affiliateUi.stop.disabled = true;
        const hasError = batchRows.some(item => item.error);
        affiliateUi.state.textContent = affiliateRun.stopped ? 'Đã dừng' : hasError ? 'Có lỗi' : 'Hoàn tất';
        try { await affiliateWriteDb(batchRows); } catch (error) { showMonitorToast(`Không lưu được lịch sử Affiliate: ${error.message}`, 'error'); }
        affiliateRender();
    }
}

if (affiliateUi.start) affiliateUi.start.onclick = () => affiliateStart();
if (affiliateUi.resume) affiliateUi.resume.onclick = () => affiliateStart(true);
if (affiliateUi.retry) affiliateUi.retry.onclick = async () => {
    if (affiliateRun.running) return;
    const failedRows = affiliateResults.filter(item => !item.pending && item.error);
    if (!failedRows.length) return showMonitorToast('Không còn dự án Affiliate nào bị lỗi.', 'warning');
    const retriedAt = new Date().toISOString();
    failedRows.forEach(item => Object.assign(item, {
        pending: true,
        error: '',
        networks: [],
        matches: [],
        queriesUsed: [],
        updatedAt: retriedAt
    }));
    affiliatePage = 1;
    try { await affiliateWriteDb(failedRows); }
    catch (error) { return showMonitorToast(`Không chuẩn bị được dữ liệu quét lại: ${error.message}`, 'error'); }
    affiliateRender();
    affiliateStart(true, failedRows);
};
if (affiliateUi.networkFilter) affiliateUi.networkFilter.onclick = event => {
    const remove = event.target.closest('[data-affiliate-network-remove]');
    const option = event.target.closest('[data-affiliate-network-value]');
    const trigger = event.target.closest('.affiliate-network-trigger');
    const menu = affiliateUi.networkFilter.querySelector('.affiliate-network-options');
    if (remove) { event.stopPropagation(); affiliateSelectedNetworks.delete(remove.dataset.affiliateNetworkRemove); affiliatePage = 1; affiliateRender(); return; }
    if (option) { const value = option.dataset.affiliateNetworkValue; affiliateSelectedNetworks.has(value) ? affiliateSelectedNetworks.delete(value) : affiliateSelectedNetworks.add(value); affiliatePage = 1; affiliateRender(); if (menu) menu.hidden = false; affiliateUi.networkFilter.querySelector('.affiliate-network-trigger')?.setAttribute('aria-expanded', 'true'); return; }
    if (trigger) { const opening = Boolean(menu?.hidden); if (menu) menu.hidden = !opening; trigger.setAttribute('aria-expanded', String(opening)); }
};
if (affiliateUi.historyFilter) affiliateUi.historyFilter.onchange = () => {
    affiliateSelectedBatchId = affiliateUi.historyFilter.value || '';
    affiliateSelectedNetworks.clear();
    affiliatePage = 1;
    affiliateRender();
};
document.addEventListener('click', event => {
    if (!affiliateUi.networkFilter || affiliateUi.networkFilter.contains(event.target)) return;
    const menu = affiliateUi.networkFilter.querySelector('.affiliate-network-options');
    if (menu) menu.hidden = true;
    affiliateUi.networkFilter.querySelector('.affiliate-network-trigger')?.setAttribute('aria-expanded', 'false');
});
if (affiliateUi.clearFilter) affiliateUi.clearFilter.onclick = () => {
    affiliateSelectedNetworks.clear();
    affiliatePage = 1;
    affiliateRender();
};
if (affiliateUi.stop) affiliateUi.stop.onclick = async () => {
    affiliateRun.stopped = true;
    affiliateUi.state.textContent = 'Đang dừng...';
    if (!affiliateRun.jobId) return;
    try {
        const response = await fetch(`/api/affiliate/jobs/${encodeURIComponent(affiliateRun.jobId)}/stop`, { method: 'POST' });
        const result = await response.json().catch(() => ({}));
        if (!response.ok) throw new Error(result.error || `HTTP ${response.status}`);
    } catch (error) {
        showMonitorToast(`Không thể dừng phiên Affiliate: ${error.message}`, 'error');
    }
};
if (affiliateUi.clear) affiliateUi.clear.onclick = async () => {
    if (affiliateRun.running) return showMonitorToast('Hãy dừng phiên Affiliate đang chạy trước khi xóa lịch sử.', 'warning');
    if (!await showAppConfirm('Xóa toàn bộ lịch sử Affiliate đã lưu trong IndexedDB trên trình duyệt này?', { danger: true, confirmText: 'Xóa lịch sử' })) return;
    try {
        await affiliateClearDb();
        localStorage.removeItem(AFFILIATE_LOCAL_KEY);
        affiliateResults = [];
        affiliateSelectedBatchId = '';
        affiliateSelectedNetworks.clear();
        affiliatePage = 1;
        affiliateRender();
        showMonitorToast('Đã xóa toàn bộ lịch sử Affiliate trên trình duyệt này.');
    } catch (error) { showMonitorToast(`Không xóa được lịch sử Affiliate: ${error.message}`, 'error'); }
};
if (affiliateUi.pageSize) affiliateUi.pageSize.onchange = () => { affiliatePage = 1; affiliateRender(); };
if (affiliateUi.pagePrev) affiliateUi.pagePrev.onclick = () => { if (affiliatePage > 1) { affiliatePage--; affiliateRender(); } };
if (affiliateUi.pageNext) affiliateUi.pageNext.onclick = () => { affiliatePage++; affiliateRender(); };
if (affiliateUi.export) affiliateUi.export.onclick = () => {
    const rows = affiliateFilteredResults();
    if (!rows.length) return showMonitorToast('Không có kết quả Affiliate nào khớp bộ lọc để xuất.', 'warning');
    const historySlug = affiliateSelectedBatchId ? '_Mot_phien' : '';
    const filterSlug = affiliateSelectedNetworks.size ? `_${[...affiliateSelectedNetworks].map(value => value === '__none__' ? 'Khong_co_NET' : value).join('_').normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9_-]+/gi, '_').slice(0, 100)}` : '_Tat_ca';
    exportRowsToExcel(`Quet_Affiliate_NETs${historySlug}${filterSlug}_${new Date().toISOString().slice(0, 10)}.xls`, ['STT','Dự án','Nền tảng Affiliate (NETs)','Evidence Links','Từ khóa đã dùng','Thời gian','Trạng thái'], rows.map((item, idx) => [idx + 1, item.project, (item.networks || []).join(', '), (item.matches || []).map(m => `${m.network}: ${m.url}`).join(' | '), (item.queriesUsed || []).join(' | '), item.updatedAt || '', item.pending ? 'Đang xử lý' : item.error || 'Hoàn tất']));
    showMonitorToast(`Đã xuất ${rows.length.toLocaleString('vi-VN')} kết quả Affiliate${affiliateSelectedNetworks.size ? ' theo bộ lọc NET' : ''}.`);
};

// Workspace checkpoint: preserves the remaining in-memory result tables across reloads.
const WORKSPACE_DB_NAME = 'check_gg_ads_workspace_v1', WORKSPACE_DB_STORE = 'snapshots';
const workspaceSnapshotId = () => `workspace:${authenticatedUser?.id || 'anonymous'}`;
function workspaceOpenDb() {
    if (!('indexedDB' in window)) return Promise.reject(new Error('Trình duyệt không hỗ trợ IndexedDB.'));
    if (workspaceDbPromise) return workspaceDbPromise;
    workspaceDbPromise = new Promise((resolve, reject) => {
        const request = indexedDB.open(WORKSPACE_DB_NAME, 1);
        request.onupgradeneeded = () => { if (!request.result.objectStoreNames.contains(WORKSPACE_DB_STORE)) request.result.createObjectStore(WORKSPACE_DB_STORE, { keyPath: 'id' }); };
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
    });
    return workspaceDbPromise;
}
function workspaceClean(value) {
    return JSON.parse(JSON.stringify(value, (_, item) => item instanceof Set ? [...item] : item));
}
function workspaceCurrentSnapshot() {
    return workspaceClean({
        id: workspaceSnapshotId(),
        updatedAt: new Date().toISOString(),
        domain: {
            target: elTargetDomain?.value || '',
            rows: scrapedAdvertisers,
            partialRows: partialDomainAdvertisers
        },
        advertiser: {
            target: elTargetAdvId?.value || '',
            creatives: advertiserCreatives,
            filteredCreatives: filteredAdvertiserCreatives,
            domains: serializeDetectedDomainsForHistory(),
            sheetExportRecords
        },
        bulkTraffic: {
            input: elBulkTrafficInput?.value || '',
            provider: elBulkTrafficProvider?.value || '',
            rows: bulkTrafficRows.map(row => ({ ...row, loading: false }))
        }
    });
}
async function workspaceWrite() {
    if (!authenticatedUser || !workspaceRestored) return;
    const db = await workspaceOpenDb();
    const snapshot = workspaceCurrentSnapshot();
    await new Promise((resolve, reject) => {
        const transaction = db.transaction(WORKSPACE_DB_STORE, 'readwrite');
        transaction.objectStore(WORKSPACE_DB_STORE).put(snapshot);
        transaction.oncomplete = resolve;
        transaction.onerror = () => reject(transaction.error);
        transaction.onabort = () => reject(transaction.error || new Error('Giao dịch IndexedDB bị hủy.'));
    });
}
function workspaceScheduleSave(delay = 350) {
    if (!workspaceRestored) return;
    clearTimeout(workspaceSaveTimer);
    workspaceSaveTimer = setTimeout(() => workspaceWrite().catch(error => console.warn('Không lưu được workspace vào IndexedDB:', error)), delay);
}
async function workspaceRestore() {
    if (!authenticatedUser) return;
    try {
        const db = await workspaceOpenDb();
        const saved = await new Promise((resolve, reject) => {
            const request = db.transaction(WORKSPACE_DB_STORE, 'readonly').objectStore(WORKSPACE_DB_STORE).get(workspaceSnapshotId());
            request.onsuccess = () => resolve(request.result || null);
            request.onerror = () => reject(request.error);
        });
        if (!saved) return;
        const domain = saved.domain || {};
        scrapedAdvertisers = Array.isArray(domain.rows) ? domain.rows : [];
        partialDomainAdvertisers = Array.isArray(domain.partialRows) ? domain.partialRows : scrapedAdvertisers.slice();
        if (elTargetDomain && domain.target && !elTargetDomain.value) elTargetDomain.value = domain.target;
        if (scrapedAdvertisers.length) {
            renderTable(scrapedAdvertisers);
            if (elStatAdvertisers) elStatAdvertisers.textContent = scrapedAdvertisers.length;
            if (elStatAds) elStatAds.textContent = scrapedAdvertisers.reduce((sum, row) => sum + Number(row.creativeCount || 0), 0);
            if (elTableSearch) elTableSearch.disabled = false;
            if (elFormatFilter) elFormatFilter.disabled = false;
            if (elDateRangeFilter) elDateRangeFilter.disabled = false;
            if (elBtnExport) elBtnExport.disabled = false;
        }

        const advertiser = saved.advertiser || {};
        advertiserCreatives = Array.isArray(advertiser.creatives) ? advertiser.creatives : [];
        filteredAdvertiserCreatives = Array.isArray(advertiser.filteredCreatives) ? advertiser.filteredCreatives : advertiserCreatives.slice();
        detectedDomainsMap = {};
        Object.entries(advertiser.domains || {}).forEach(([domainName, info]) => {
            detectedDomainsMap[domainName] = { ...info, sources: new Set(Array.isArray(info.sources) ? info.sources : []), ads: Array.isArray(info.ads) ? info.ads : [], metricsLoading: false, summaryLoading: false, trafficLoading: false };
        });
        sheetExportRecords = Array.isArray(advertiser.sheetExportRecords) ? advertiser.sheetExportRecords : [];
        if (elTargetAdvId && advertiser.target && !elTargetAdvId.value) elTargetAdvId.value = advertiser.target;
        if (advertiserCreatives.length || Object.keys(detectedDomainsMap).length) {
            renderCreativeGrid(advertiserCreatives);
            renderFilteredAdsTable();
            renderAdvDomainTable();
            updateMetricRetryButtons();
            if (elStatAdvTotal) elStatAdvTotal.textContent = advertiserCreatives.length;
            if (elStatAdvDomains) elStatAdvDomains.textContent = Object.keys(detectedDomainsMap).length;
            if (elBtnPushGoogleSheet) elBtnPushGoogleSheet.disabled = sheetExportRecords.length === 0;
        }

        const traffic = saved.bulkTraffic || {};
        bulkTrafficRows = Array.isArray(traffic.rows) ? traffic.rows.map(row => ({ ...row, loading: false })) : [];
        if (elBulkTrafficInput && traffic.input && !elBulkTrafficInput.value) elBulkTrafficInput.value = traffic.input;
        if (elBulkTrafficProvider && traffic.provider) elBulkTrafficProvider.value = traffic.provider;
        if (bulkTrafficRows.length) {
            renderBulkTrafficRows();
            if (elBulkTrafficStatus) elBulkTrafficStatus.textContent = `Đã khôi phục ${bulkTrafficRows.length.toLocaleString('vi-VN')} kết quả từ IndexedDB`;
        }
    } catch (error) {
        console.warn('Không khôi phục được workspace từ IndexedDB:', error);
        showMonitorToast(`Không khôi phục được dữ liệu phiên trước: ${error.message}`, 'error');
    } finally {
        workspaceRestored = true;
    }
}
function workspaceStartAutosave() {
    clearInterval(workspaceAutosaveTimer);
    workspaceAutosaveTimer = setInterval(() => workspaceScheduleSave(0), 3000);
    workspaceScheduleSave(0);
}
