///////////////////////////////////////////////////////////////////////////////
// Constants
///////////////////////////////////////////////////////////////////////////////

const LOOKUP_REFRESH_INTERVAL      = 300;     // seconds — ri param for lookup queue
const LOOKUP_POLL_INTERVAL         = 5000;    // ms — base poll interval
const LOOKUP_POLL_BACKOFF_MAX      = 60000;   // ms — max backed-off poll interval
const LOOKUP_QUEUE_ABANDON_SIZE    = 500;     // queue size at which we give up
const LOOKUP_QUEUE_BACKOFF_TRIGGER = 10;      // queue size at which backoff starts
const LOOKUP_POLL_TIMEOUT          = 120000;  // ms — before giving up on a pending lookup
var grid_auto_search_delay = 800;

///////////////////////////////////////////////////////////////////////////////
// Pending lookup state
///////////////////////////////////////////////////////////////////////////////

const pendingLookups = new Map(); // domainId -> { cell, queuedAt }
let lookupPollTimer = null;

///////////////////////////////////////////////////////////////////////////////
// Domains of Interest — resolved aqid from config name
///////////////////////////////////////////////////////////////////////////////

let _domainsOfInterestAqid = 0;

///////////////////////////////////////////////////////////////////////////////
// App init
///////////////////////////////////////////////////////////////////////////////

document.addEventListener("DOMContentLoaded", async function() {
	await initApp();
});

async function initApp() {
	var loggedIn = await checkAuthSession();
	if (!loggedIn) return;
	showBody();
	startAuthSessionMonitor();
	initEventButtons();
	await loadDomainsOfInterest();
	loadPortfolioButtons();
	loadDomainColumns();

	// Attach lookup click handler here so #domain-portfolio-table exists
	document.getElementById("domain-portfolio-table").addEventListener("click", function(e) {
		const cell = e.target.closest(".btn-lookup-domain-row");
		if (!cell) return;

		const row = cell.closest("tr[data-id]");
		if (!row) return;

		const domainId = row.getAttribute("data-id");
		triggerDomainLookup(domainId, cell);
	});

	// Member Of pill — remove domain from category
	document.getElementById("domain-portfolio-table").addEventListener("click", async function(e) {
		const btn = e.target.closest(".member-of-remove");
		if (!btn) return;

		e.stopPropagation(); // don't select the row

		const row      = btn.closest("tr[data-id]");
		const domainId = row ? row.getAttribute("data-id") : null;
		const cid      = btn.getAttribute("data-cid");
		const catName  = btn.getAttribute("data-cat-name") || cid;
		if (!domainId || !cid) return;

		const domainCell = row.querySelector(".domain-cell");
		const domain     = domainCell ? domainCell.textContent.trim() : `ID ${domainId}`;

		const ok = await SedUI.confirm({
			title        : `Remove from "${catName}"`,
			message      : `Domain: ${domain}`,
			confirmLabel : 'Remove',
			confirmClass : 'btn-danger',
			cancelLabel  : 'Cancel',
		});
		if (!ok) return;

		_removeDomainFromCategory(domainId, cid);
	});

	initColumnPanel();
	initFilterToggleButton();
	initCategoryToggleButton();
	initLayoutToggleButton();
	initDomainActions();
	_watchThemeChanges();
}

///////////////////////////////////////////////////////////////////////////////
// Domain columns + table init
///////////////////////////////////////////////////////////////////////////////

// Global variable to hold column details
let domainColumns = {};

async function loadDomainColumns() {
	try {
		const response = await fetch(
			`${API_BASE_URL}?c=list&t=domain&oper=columns&debug`
		);
		const data = await response.json();

		if (data.status === "ok" && data.columns) {
			domainColumns = data.columns;
			console.log("Domain columns loaded:", domainColumns);

			// Load saved column config from server.
			const savedColumns = await loadColumnConfig();

			if (savedColumns) {
				// Saved config is already a fully-formed requestedColumns array
				// (e.g. ["ctrl", "d.registry_expiry", ...]) — pass directly to
				// buildColumnDefsFromDomainColumns, bypassing initPortfolioTable's
				// internal filtering which would double the "d." prefix.
				const colDefs = buildColumnDefsFromDomainColumns(domainColumns, savedColumns);
				initPortfolioTable(domainColumns, null, colDefs);
			} else {
				// No saved config — let initPortfolioTable use DEFAULT_REQUESTED_COLUMNS
				// as it does today, with no columns override.
				initPortfolioTable(domainColumns);
			}
		} else {
			console.error("Failed to load columns:", data.error);
			initPortfolioTable();
		}
	} catch (error) {
		console.error("Error fetching domain columns:", error);
		initPortfolioTable();
	}
	
	// Always init filters — domainColumns may be empty on error but
    // initPortfolioFilters handles that gracefully by flagging missing columns
    initPortfolioFilters(domainColumns);
}

// Third optional parameter `colDefs` — if provided, skip column building
// entirely and use it directly. This avoids the double "d." prefix problem when
// passing a pre-built column definition array from loadDomainColumns.
function initPortfolioTable(domainColumns, columns = null, colDefs = null) {
	const mandatoryColumns = ["dt_ser_num", "cb", "d.r_h_disp", "ctrl", "d.domain"];

	let domainGridColumns;

	if (colDefs) {
		// Pre-built column defs passed in directly — use as-is, skip all filtering
		domainGridColumns = colDefs;
	} else {
		domainGridColumns = getDefaultDomainGridColumns(domainColumns);

		// If columns parameter is provided, filter domainGridColumns
		if (columns && Array.isArray(columns)) {
			const allowed = new Set(
				mandatoryColumns.concat(columns.map(col => `d.${col}`))
			);
			domainGridColumns = domainGridColumns.filter(col => allowed.has(col.name));
		}

		// Remove d.* columns not present in domainColumns (if domainColumns is valid)
		if (domainColumns && Object.keys(domainColumns).length > 0) {
			domainGridColumns = domainGridColumns.filter(col => {
				if (!col.name.startsWith("d.")) return true;
				const colName = col.name.substring(2);
				return colName in domainColumns;
			});
		}

		// Apply widths from domainColumns
		if (domainColumns) {
			domainGridColumns.forEach(col => {
				const colName = col.name.startsWith("d.") ? col.name.substring(2) : col.name;
				if (domainColumns[colName] && typeof col.width === "number") {
					col.width = domainColumns[colName].width;
				} else if (domainColumns[colName] && col.width && typeof col.width.default === "number") {
					col.width.default = domainColumns[colName].width;
				}
			});

			// Reset control column width
			const controlCol = domainGridColumns.find(col => col.classes?.includes("control-cell"));
			if (controlCol) controlCol.width = 70;
		}
	}

	var source = { type: 'ajax', params: { url: API_BASE_URL, data: 'c=grid&t=domain&columns=domain,tld', post: ['cid', 'aqid'] } };
	var footer = [{ role: 'separator' }, { role: 'copy' }, { role: 'copy-domains', icon: 'copy-selected', tooltip: 'Copy Domain Names' }, { role: 'download' }, { role: 'separator' }, { role: 'refresh' }];

	// Restore any saved widths for virtual columns from localStorage
	const virtualWidths = getVirtualColumnWidths();
	if (Object.keys(virtualWidths).length > 0) {
		domainGridColumns.forEach(col => {
			if (col.virtual && virtualWidths[col.name] !== undefined) {
				col.width = virtualWidths[col.name];
			}
		});
	}

	// Mark editable columns based on PORTFOLIO_EDITABLE_FIELDS config
	if (typeof applyEditableFields === 'function') {
		applyEditableFields(domainGridColumns, domainColumns);
	}

	new DataTableView('dp-table', {
		columns: domainGridColumns,
		source: source,
		parent: 'domain-portfolio-table',
		footer: footer,
		sort: 'added_on',
		sortorder: 'desc',
		columnsort: false,
		count: 100,
		autosize: false,
		cid: _domainsOfInterestAqid > 0 ? 0 : 1,
		aqid: _domainsOfInterestAqid > 0 ? _domainsOfInterestAqid : 0,
		uidelay: grid_auto_search_delay ? grid_auto_search_delay : 600,
		handlers: {
			ajaxData: function(response) {
				onDomainGridAjaxData(response);
			},
			colwidth: function(column, width) {
				saveColumnWidth(column, width);
			}
		}
	});
}

///////////////////////////////////////////////////////////////////////////////
// Column width persistence
// Virtual columns → localStorage (no API backing)
// Real API columns → server via API
///////////////////////////////////////////////////////////////////////////////

const VIRTUAL_COL_WIDTHS_KEY = "portfolio_virtual_col_widths";

async function saveColumnWidth(name, width) {
	if (!name || !width) return;

	// Check if this column is virtual by looking at the live table column defs
	const dt   = DataTableView.getTable("dp-table");
	const col  = dt ? dt.options.columns.find(c => c.name === name) : null;
	const isVirtual = col && col.virtual === true;

	if (isVirtual) {
		// Save to localStorage — virtual columns have no API backing
		try {
			const stored = localStorage.getItem(VIRTUAL_COL_WIDTHS_KEY);
			const widths = stored ? JSON.parse(stored) : {};
			widths[name] = width;
			localStorage.setItem(VIRTUAL_COL_WIDTHS_KEY, JSON.stringify(widths));
		} catch (err) {
			console.error("[colwidth] Failed to save virtual column width to localStorage:", err);
		}
		return;
	}

	// Real API column — persist to server
	const params = new URLSearchParams({
		c    : "admin",
		t    : "domain",
		oper : "set",
		name : name,
		width: width
	});

	try {
		const res  = await fetch(API_BASE_URL, {
			method     : "POST",
			credentials: "same-origin",
			headers    : { "Content-Type": "application/x-www-form-urlencoded" },
			body       : params.toString()
		});
		const json = await res.json();
		if (json.status !== "ok") {
			console.warn("[colwidth] Failed to save width for", name, json.error);
		}
	} catch (err) {
		console.error("[colwidth] Error saving column width:", err);
	}
}

/**
 * Returns saved widths for virtual columns from localStorage.
 * Called from initPortfolioTable to restore widths on page load.
 * Returns an object keyed by column name: { "member_of": 220, ... }
 */
function getVirtualColumnWidths() {
	try {
		const stored = localStorage.getItem(VIRTUAL_COL_WIDTHS_KEY);
		return stored ? JSON.parse(stored) : {};
	} catch (err) {
		return {};
	}
}

///////////////////////////////////////////////////////////////////////////////
// Cell formatters
///////////////////////////////////////////////////////////////////////////////

function domainStatusFormatter(cellvalue) {
	if (cellvalue == null)
		return "";
	let sparts = cellvalue.split(",");
	return "<span class=\"domain-status-span\"><span>" + sparts.join("</span><span>") + "</span></span>";
}

function controlFormatter(cellvalue, cd, rowData) {
	const mode = (typeof ColorMode !== "undefined") ? ColorMode.getMode() : "light";
	return `<img src="lib/layouts/colors/${mode}/images/64/toolbar/refresh.png">`;
}

function highlightFormatter(cellvalue) {
	if (cellvalue == 15 && cellvalue != null)
		return '<img title="Nearing expiry" class="dt-icon-alert" src="lib/layouts/images/icons/alerts/24/alert-yellow.png" alt="' + cellvalue + '">';
	else if (cellvalue == 17 && cellvalue != null)
		return '<img title="Past expiry" class="dt-icon-alert"  src="lib/layouts/images/icons/alerts/24/alert-red.png" alt="' + cellvalue + '">';
	else if (cellvalue == 20 && cellvalue != null)
		return '<img title="Name server discrepancy" class="dt-icon-alert"  src="lib/layouts/images/icons/alerts/24/tri-alert-orange.png" alt="' + cellvalue + '">';
	else if (cellvalue == 18 && cellvalue != null)
		return '<img title="Domain status alert" class="dt-icon-alert" src="lib/layouts/images/icons/alerts/24/tri-alert-red.png" alt="' + cellvalue + '">';
	else if (cellvalue == 16 && cellvalue != null)
		return '<img title="Nearing or past expiry" class="dt-icon-alert" src="lib/layouts/images/icons/alerts/24/alert-red.png" alt="' + cellvalue + '">';
	else if (cellvalue == 14 && cellvalue != null)
		return '<img title="Unknown expiry date" class="dt-icon-alert" src="lib/layouts/images/icons/alerts/24/unknown-blue.png" alt="' + cellvalue + '">';
	else if (cellvalue == 13 && cellvalue != null)
		return '<img title="No recent whois" class="dt-icon-alert" src="lib/layouts/images/icons/alerts/24/search-alert.png" alt="' + cellvalue + '">';
	else if (cellvalue == 12 && cellvalue != null)
		return '<img title="Available domain" class="dt-icon-alert" src="lib/layouts/images/icons/alerts/24/ok.png" alt="' + cellvalue + '">';
	else if (cellvalue == 11 && cellvalue != null)
		return '<img title="Requires whois search token setup" class="dt-icon-alert" src="lib/layouts/images/icons/alerts/24/tri-alert-yellow.png" alt="' + cellvalue + '">';
	else if (cellvalue == 10 && cellvalue != null)
		return '<img title="Manually edited" class="dt-icon-alert" src="lib/layouts/images/icons/alerts/24/pencil.png" alt="' + cellvalue + '">';
	else if (cellvalue == 19 && cellvalue != null)
		return '<img title="Write Protected" class="dt-icon-alert" src="lib/layouts/images/icons/alerts/24/lock.png" alt="' + cellvalue + '">';
	else if (cellvalue == null)
		return '<img class="dt-icon-alert" src="lib/layouts/images/icons/alerts/24/question.png" alt="' + cellvalue + '">';
	else
		return '<img class="dt-icon-alert" src="lib/layouts/images/icons/alerts/24/empty.png" alt="' + cellvalue + '">';
}

///////////////////////////////////////////////////////////////////////////////
// Member Of — category membership data
//
// _domainMembershipMap holds the category memberships for all rows currently
// loaded in the grid. Structure: Map<domainId, cid[]>
//
// Populated by onDomainGridAjaxData() after each grid reload.
// Used by memberOfFormatter() to render the member_of virtual column.
///////////////////////////////////////////////////////////////////////////////

let _domainMembershipMap = new Map(); // domainId (string) -> [cid, cid, ...]

/**
 * Called by the DataTableView ajaxData handler after each grid data load.
 * Extracts domain IDs from the loaded rows, fetches category memberships
 * from the API, populates _domainMembershipMap, then refreshes affected rows.
 */
async function onDomainGridAjaxData(response) {
	_domainMembershipMap.clear();

	if (!response || !Array.isArray(response.rows) || response.rows.length === 0)
		return;

	// Extract all domain IDs from the loaded rows
	const ids = response.rows
		.map(row => row.id)
		.filter(id => id != null)
		.join(",");

	if (!ids) return;

	try {
		const params = new URLSearchParams({
			c    : "get",
			t    : "domain",
			oper : "catmap",
			id   : ids
		});

		const res  = await fetch(API_BASE_URL + "?" + params.toString(), {
			credentials: "same-origin"
		});
		const json = await res.json();

		if (json.status !== "ok" || !json.data) {
			console.warn("[memberOf] catmap API returned no data:", json);
			return;
		}

		// Populate the map — API returns { domainId: [cid, ...], ... }
		Object.entries(json.data).forEach(([domainId, cids]) => {
			_domainMembershipMap.set(String(domainId), cids);
		});

		// Refresh all loaded rows so member_of cells re-render with real data
		const dt = DataTableView.getTable("dp-table");
		if (dt && dt.options.data) {
			dt.options.data.forEach((row, index) => {
				if (_domainMembershipMap.has(String(row.id))) {
					dt.refreshTableRow(index);
				}
			});
		}

	} catch (err) {
		console.error("[memberOf] Failed to fetch category map:", err);
	}
}

/**
 * Formatter for the member_of virtual column.
 * Receives (cellvalue, cd, rowData) — cellvalue is always undefined for virtual columns.
 * Looks up rowData.id in _domainMembershipMap and resolves cids to category names
 * using the globally cached _categories array from loadPortfolioButtons().
 *
 * TODO: replace dummy return with real lookup once onDomainGridAjaxData is wired.
 */
function memberOfFormatter(cellvalue, cd, rowData) {
	const domainId = rowData ? String(rowData.id) : null;

	if (!domainId || !_domainMembershipMap.has(domainId)) {
		// TODO: remove dummy value once real data is available
		return '<span class="member-of-span"></span>';
	}

	const cids  = _domainMembershipMap.get(domainId) || [];
	const cats  = window._categories || [];
	const pairs = cids
		.map(cid => {
			const cat = cats.find(c => String(c.id) === String(cid));
			return cat ? { cid, name: cat.name } : null;
		})
		.filter(Boolean);

	if (!pairs.length) return '<span class="member-of-span"></span>';

	return "<span class=\"member-of-span\">" +
		pairs.map(({ cid, name }) =>
			`<span class="member-of-pill">` +
			`${name}` +
			`<button class="member-of-remove" data-cid="${cid}" data-cat-name="${name.replace(/"/g, '&quot;')}">&times;</button>` +
			`</span>`
		).join("") +
		"</span>";
}

///////////////////////////////////////////////////////////////////////////////
// Nameservers summary virtual column formatter
///////////////////////////////////////////////////////////////////////////////

function nameserversFormatter(cellvalue, cd, rowData) {
    if (!rowData) return "";

    const escapeHtml = value => String(value)
        .replace(/&/g, "&amp;")
        .replace(/"/g, "&quot;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;");

    const NS_BACKING_COLS = [
        "d.ns1","d.ns2","d.ns3","d.ns4",
        "d.ns5","d.ns6","d.ns7","d.ns8"
    ];

    const values = NS_BACKING_COLS
        .map(col => (rowData[col] || "").trim())
        .filter(v => v.length > 0);

    if (!values.length) return '<span class="ns-summary-span"></span>';

    const limit = Number(window.PORTFOLIO_NAMESERVER_DISPLAY_LIMIT || 0) || 0;
    const visibleValues = limit > 0 ? values.slice(0, limit) : values;
    const hiddenCount = values.length - visibleValues.length;
    const title = values.join("\n");

    let html = visibleValues.map(ns =>
        `<span class="ns-summary-pill">${escapeHtml(ns)}</span>`
    ).join("");

    if (hiddenCount > 0) {
        html += `<span class="ns-summary-pill" title="${escapeHtml(title)}">+${hiddenCount} more</span>`;
    }

    return `<span class="ns-summary-span" title="${escapeHtml(title)}">${html}</span>`;
}

///////////////////////////////////////////////////////////////////////////////
// Remove a single domain from a category via pill × click
///////////////////////////////////////////////////////////////////////////////

async function _removeDomainFromCategory(domainId, cid) {
	try {
		const params = new URLSearchParams({
			c    : "set",
			t    : "category",
			oper : "delfrom",
			cid  : cid,
			id   : domainId
		});

		const response = await fetch(API_BASE_URL, {
			method     : "POST",
			credentials: "same-origin",
			headers    : { "Content-Type": "application/x-www-form-urlencoded" },
			body       : params.toString()
		});

		const json = await response.json();

		if (json.status === "ok") {
			// Update membership map — remove this cid from the domain's entry
			const current = _domainMembershipMap.get(String(domainId)) || [];
			const updated = current.filter(c => String(c) !== String(cid));
			_domainMembershipMap.set(String(domainId), updated);

			// Refresh just this row
			const dt = DataTableView.getTable("dp-table");
			if (dt && dt.options.data) {
				const index = dt.options.data.findIndex(r => String(r.id) === String(domainId));
				if (index !== -1) dt.refreshTableRow(index);
			}
		} else {
			console.error("[memberOf] Remove failed:", json.error);
			SedUI.alert({ title: 'Remove Failed', message: json.error || 'Unknown error', type: 'error' });
		}

	} catch (err) {
		console.error("[memberOf] Remove error:", err);
		SedUI.alert({ title: 'Error', message: 'An error occurred. Please try again.', type: 'error' });
	}
}

///////////////////////////////////////////////////////////////////////////////
// Category buttons
///////////////////////////////////////////////////////////////////////////////

let _overflowCategories   = [];
let _allVisibleCategories = [];

// Returns the current category layout, preferring the user's localStorage choice
// over the PHP-emitted PORTFOLIO_CATEGORY_LAYOUT default.
function _getLayout() {
    const saved = localStorage.getItem("portfolio_category_layout");
    if (saved === "left" || saved === "top") return saved;
    const php = String(window.PORTFOLIO_CATEGORY_LAYOUT || "top").toLowerCase();
    return (php === "left") ? "left" : "top";
}

function _getMaxCategoryButtons() {
    if (_getLayout() === "left") {
        return Number.MAX_SAFE_INTEGER;
    }
    return Math.min(30, Math.max(4, Math.floor(window.innerWidth / 180) * 2));
}

async function loadPortfolioButtons(hidden_categories = []) {
	try {
		const response  = await fetch(`${API_BASE_URL}?c=grid&t=category&columns=name,sortindex,caticon&page=1&rows=50&sort=sortindex`);
		const data      = await response.json();
		const categories = data.rows || [];

		window._categories    = categories;
		_allVisibleCategories = categories.filter(item =>
			!hidden_categories.some(h => h.toLowerCase() === (item.name || "").toLowerCase())
		);

		_buildCategoryButtons(_allVisibleCategories);

		if (typeof initDomainActionCategories === "function")
			initDomainActionCategories(categories);

	} catch (error) {
		console.error("Error loading categories:", error);
	}
}

function applyPortfolioCategoryLayoutClass() {
    const header = document.getElementById("domain-portfolio-header");
    if (!header) return;

    const layout = _getLayout();

    if (layout === "left") {
        header.classList.add("domain-portfolio-header-left");
        header.classList.remove("domain-portfolio-header-top");
        _applySidebarPadding();
    } else {
        header.classList.remove("domain-portfolio-header-left");
        header.classList.add("domain-portfolio-header-top");
        const content = document.getElementById("content");
        if (content) content.style.paddingLeft = "";
    }
}

// Reads actual DOM width so it works correctly for left layout.
// Resets padding if header is hidden or layout is not left.
function _applySidebarPadding() {
    const header  = document.getElementById("domain-portfolio-header");
    const content = document.getElementById("content");
    if (!header || !content) return;
    const layout = _getLayout();
    if (layout !== "left" || header.style.display === "none") {
        content.style.paddingLeft = "";
        return;
    }
    requestAnimationFrame(() => {
        const width = Math.ceil(header.getBoundingClientRect().width);
        content.style.paddingLeft = (width + 10) + "px";
    });
}

///////////////////////////////////////////////////////////////////////////////
// Category bar toggle button — injected into footer, left of filter toggle
///////////////////////////////////////////////////////////////////////////////

function initCategoryToggleButton() {
    const footer = document.getElementById("domain-portfolio-footer");
    if (!footer) return;

    const mode = (typeof ColorMode !== "undefined") ? ColorMode.getMode() : "light";

    const btn         = document.createElement("button");
    btn.id            = "btn-toggle-categories";
    btn.className     = "btn btn-sm btn-secondary pf-category-toggle-btn";
    btn.title         = "Show / Hide Categories";

    const img     = document.createElement("img");
    img.src       = `lib/layouts/colors/${mode}/images/64/toolbar/folder.png`;
    img.className = "pf-filter-btn-icon";
    btn.appendChild(img);

    btn.addEventListener("click", _toggleCategoryBar);

    // Insert before filter toggle if present, else before Columns, else append
    const anchor = document.getElementById("btn-toggle-filters")
                || document.getElementById("cp-open-btn");
    if (anchor) footer.insertBefore(btn, anchor);
    else        footer.appendChild(btn);

    // Panels start visible — add active class by default, remove if restoring hidden state
    btn.classList.add("pf-category-toggle-active");
    if (localStorage.getItem("portfolio_categories_hidden") === "1") {
        const header = document.getElementById("domain-portfolio-header");
        if (header) header.style.display = "none";
        btn.classList.remove("pf-category-toggle-active");
        _applySidebarPadding();
    }
}

function _toggleCategoryBar() {
    const header = document.getElementById("domain-portfolio-header");
    const btn    = document.getElementById("btn-toggle-categories");
    if (!header) return;

    const isVisible = header.style.display !== "none";
    header.style.display = isVisible ? "none" : "";
    localStorage.setItem("portfolio_categories_hidden", isVisible ? "1" : "0");
    // active = now visible = !isVisible
    if (btn) btn.classList.toggle("pf-category-toggle-active", !isVisible);
    _applySidebarPadding();
}

///////////////////////////////////////////////////////////////////////////////
// Layout toggle button — switches category area between top and left layouts
///////////////////////////////////////////////////////////////////////////////

function initLayoutToggleButton() {
    const footer = document.getElementById("domain-portfolio-footer");
    if (!footer) return;

    const btn     = document.createElement("button");
    btn.id        = "btn-toggle-layout";
    btn.className = "btn btn-sm btn-secondary pf-layout-toggle-btn";
    btn.title     = "Toggle Category Layout";

    const img     = document.createElement("img");
    img.className = "pf-filter-btn-icon";
    btn.appendChild(img);

    _updateLayoutToggleIcon(btn);
    btn.addEventListener("click", _toggleCategoryLayout);

    // Insert before categories toggle (placing it to the left of it)
    const anchor = document.getElementById("btn-toggle-categories")
                || document.getElementById("btn-toggle-filters")
                || document.getElementById("cp-open-btn");
    if (anchor) footer.insertBefore(btn, anchor);
    else        footer.appendChild(btn);
}

function _toggleCategoryLayout() {
    const next = _getLayout() === "left" ? "top" : "left";
    localStorage.setItem("portfolio_category_layout", next);
    applyPortfolioCategoryLayoutClass();
    _updateLayoutToggleIcon(document.getElementById("btn-toggle-layout"));
    // Rebuild category buttons so overflow/left behaviour updates immediately
    if (_allVisibleCategories.length) _buildCategoryButtons(_allVisibleCategories);
}

function _updateLayoutToggleIcon(btn) {
    if (!btn) return;
    const img  = btn.querySelector("img");
    if (!img)  return;
    const mode   = (typeof ColorMode !== "undefined") ? ColorMode.getMode() : "light";
    const layout = _getLayout();
    img.src = `lib/layouts/colors/${mode}/images/64/toolbar/${layout}-toolbar.png`;
}

// Watches for theme changes and refreshes all footer toggle button icons.
// Needed because theme.js only updates .control img and .header img, not
// our footer buttons.
function _watchThemeChanges() {
    const observer = new MutationObserver(function() {
        const mode = (typeof ColorMode !== "undefined") ? ColorMode.getMode() : "light";
        // Layout toggle uses layout-specific icons — delegate to its own helper
        _updateLayoutToggleIcon(document.getElementById("btn-toggle-layout"));
        // Category and filter toggles have fixed icon names — just swap the mode path
        ["btn-toggle-categories", "btn-toggle-filters", "btn-download-all", "btn-scheduled-reports"].forEach(function(id) {
            const img = document.querySelector("#" + id + " img");
            if (img && img.src) {
                img.src = img.src.replace(/\/(light|dark)\//, "/" + mode + "/");
            }
        });
    });
    observer.observe(document.body, { attributes: true, attributeFilter: ["class"] });
}

function _getCategoryButtonWrap() {
    const header = document.getElementById("domain-portfolio-header");
    if (!header) return null;

    let wrap = header.querySelector(".domain-portfolio-header-buttons");
    if (!wrap) {
        wrap = document.createElement("div");
        wrap.className = "domain-portfolio-header-buttons";
        header.appendChild(wrap);
    }

    return wrap;
}

function _buildCategoryButtons(visible) {
    const container = document.getElementById("domain-portfolio-header");
    if (!container) return;

    applyPortfolioCategoryLayoutClass();

    const buttonWrap = _getCategoryButtonWrap();
    if (!buttonWrap) return;

    const prevActiveCid = buttonWrap.querySelector(".btn-cat-selector.btn-active")
		?.getAttribute("data-cid") ?? null;

    buttonWrap.innerHTML = "";
    document.getElementById("cat-overflow-dropdown")?.remove();
    _overflowCategories = [];

    const max   = _getMaxCategoryButtons();
    const shown = visible.slice(0, max);
    _overflowCategories = visible.slice(max);

    shown.forEach(item => {
        const btn   = _makeCatBtn(item, false);
        const isAll = (item.name || "").toLowerCase() === "all domains";
        if (prevActiveCid ? String(item.id) === String(prevActiveCid) : isAll)
            btn.classList.add("btn-active");
        buttonWrap.appendChild(btn);
        if (isAll) {
            buttonWrap.appendChild(_makeCuratedListBtn());
        }
    });

    if (_overflowCategories.length) {
        const moreBtn = document.createElement("button");
        moreBtn.id        = "cat-overflow-more";
        moreBtn.className = "btn btn-sm btn-pf-category btn-cat-selector cat-overflow-more-btn";
        moreBtn.textContent = `+${_overflowCategories.length} more`;
        if (prevActiveCid && _overflowCategories.some(c => String(c.id) === String(prevActiveCid)))
            moreBtn.classList.add("btn-active");
        moreBtn.addEventListener("click", e => { e.stopPropagation(); _toggleOverflowDropdown(moreBtn); });
        buttonWrap.appendChild(moreBtn);
        _buildOverflowDropdown();
    }

    if (!buttonWrap.querySelector(".btn-cat-selector.btn-active"))
        buttonWrap.querySelector(".btn-cat-selector")?.classList.add("btn-active");
	
	applyPortfolioCategoryLayoutClass();
}

function _makeCuratedListBtn() {
    const btn     = document.createElement("button");
    btn.id        = "btn-curated-list";
    btn.className = "btn btn-sm btn-pf-category btn-cat-selector btn-curated-list-btn";
    _updateCuratedListBtnLabel(btn);
    btn.addEventListener("click", function(e) {
        e.stopPropagation();
        if (typeof openCuratedListDialog === "function") openCuratedListDialog();
    });
    return btn;
}

function _updateCuratedListBtnLabel(btn) {
    const el    = btn || document.getElementById("btn-curated-list");
    if (!el) return;
    const count = typeof getCuratedDomainCount === "function" ? getCuratedDomainCount() : 0;
    el.style.cssText += `background-image:url('../lib/layouts/images/icons/categories/light/lightning.png');background-repeat:no-repeat;background-size:16px;background-position:5px center;padding-left:28px;`;
    if (count > 0) {
        el.textContent = `Curated List (${count})`;
        el.classList.add("btn-active", "btn-curated-active");
    } else {
        el.textContent = "Curated List";
        el.classList.remove("btn-active", "btn-curated-active");
    }
}

// Exposed so portfolio-filters.js can set cid/aqid to the All Domains base
function getDomainsOfInterestAqid() {
    return _domainsOfInterestAqid;
}

function _makeCatBtn(item, isDropdownItem) {
	const btn = document.createElement("button");
	btn.className = isDropdownItem ? "cat-overflow-item" : "btn btn-sm btn-pf-category btn-cat-selector";
	btn.textContent = item.name;
	btn.setAttribute("data-cid",        item.id || "");
	btn.setAttribute("data-query",      "");
	btn.setAttribute("data-parameters", "");
	if (item.caticon) {
		const pos = isDropdownItem ? "8px" : "5px";
		btn.style.cssText += `background-image:url('../lib/layouts/images/icons/categories/light/${item.caticon}');background-repeat:no-repeat;background-size:16px;background-position:${pos} center;padding-left:${isDropdownItem ? 32 : 28}px;`;
	}
	btn.addEventListener("click", () => _activateCategoryButton(btn));
	return btn;
}

function _activateCategoryButton(btn) {
	document.querySelectorAll(".btn-cat-selector").forEach(b => b.classList.remove("btn-active"));
	document.getElementById("cat-overflow-more")?.classList.remove("btn-active");
	btn.classList.add("btn-active");
	const cid = btn.getAttribute("data-cid");
	if (_overflowCategories.some(c => String(c.id) === String(cid)))
		document.getElementById("cat-overflow-more")?.classList.add("btn-active");
	categoryButtonClicked(cid, "", "");
	_closeOverflowDropdown();
}

function _buildOverflowDropdown() {
	const dropdown = document.createElement("div");
	dropdown.id        = "cat-overflow-dropdown";
	dropdown.className = "cat-overflow-dropdown";
	dropdown.style.display = "none";
	_overflowCategories.forEach(cat => dropdown.appendChild(_makeCatBtn(cat, true)));
	document.body.appendChild(dropdown);
}

function _toggleOverflowDropdown(anchorBtn) {
	const dropdown = document.getElementById("cat-overflow-dropdown");
	if (!dropdown) return;
	if (dropdown.style.display === "none") {
		const r  = anchorBtn.getBoundingClientRect();
		const dw = 220;
		let left = Math.min(r.right - dw, window.innerWidth - dw - 4);
		if (left < 4) left = 4;
		const maxH = window.innerHeight - r.bottom - 8;
		Object.assign(dropdown.style, {
			display   : "block",
			position  : "fixed",
			top       : (r.bottom + 4) + "px",
			left      : left + "px",
			width     : dw + "px",
			maxHeight : Math.max(maxH, 120) + "px",
			overflowY : "auto"
		});
	} else {
		dropdown.style.display = "none";
	}
}

function _closeOverflowDropdown() {
	const d = document.getElementById("cat-overflow-dropdown");
	if (d) d.style.display = "none";
}

document.addEventListener("click", e => {
	const d = document.getElementById("cat-overflow-dropdown");
	if (d && !d.contains(e.target) && e.target?.id !== "cat-overflow-more")
		_closeOverflowDropdown();
});

;(function () {
	let t = null;
	window.addEventListener("resize", () => {
		clearTimeout(t);
		t = setTimeout(() => { if (_allVisibleCategories.length) _buildCategoryButtons(_allVisibleCategories); }, 150);
	});
})();

async function categoryButtonClicked(cid, query, parameters) {
	// Clear curated list when user explicitly selects a category
	if (typeof clearCuratedList === "function") clearCuratedList();

	let dt = DataTableView.getTable('dp-table');
	if (dt) {
		if (cid > 1) {
			dt.options.cid  = cid;
			dt.options.aqid = 0;
		}
		else if (cid == 1) {
			if (_domainsOfInterestAqid > 0) {
				dt.options.cid  = 0;
				dt.options.aqid = _domainsOfInterestAqid;
			} else {
				dt.options.cid  = 1;
				dt.options.aqid = 0;
			}
		}
		dt.reload();
	}
}

///////////////////////////////////////////////////////////////////////////////
// Resolve PORTFOLIO_DOMAINS_OF_INTEREST_NAME → numeric aqid
// Fetches the auto-query list and finds the matching entry by name.
// Result stored in _domainsOfInterestAqid (0 if not found or not configured).
///////////////////////////////////////////////////////////////////////////////

async function loadDomainsOfInterest() {
	const name = (window.PORTFOLIO_DOMAINS_OF_INTEREST_NAME || '').trim();
	if (!name) return;

	try {
		const res  = await fetch(
			`${API_BASE_URL}?c=grid&t=query&columns=name&page=1&rows=500`,
			{ credentials: 'same-origin' }
		);
		const data = await res.json();
		const rows = data.rows || [];

		const match = rows.find(r =>
			(r.name || '').toLowerCase() === name.toLowerCase()
		);

		if (match) {
			_domainsOfInterestAqid = match.id;
			console.log(`[domainsOfInterest] Resolved "${name}" → aqid ${match.id}`);
		} else {
			console.warn(`[domainsOfInterest] Auto-query not found: "${name}"`);
		}
	} catch (err) {
		console.error('[domainsOfInterest] Failed to resolve auto-query name:', err);
	}
}

///////////////////////////////////////////////////////////////////////////////
// Domain data fetch
///////////////////////////////////////////////////////////////////////////////

async function getDomainRowData(domainId) {
	const params = new URLSearchParams({
		c: "get",
		t: "domain",
		id: domainId,
		alerts: 1
	});

	const response = await fetch(API_BASE_URL + "?" + params.toString(), {
		credentials: "same-origin"
	});

	const json = await response.json();

	if (json.status !== "ok" || !json.data) {
		throw new Error("Failed to fetch domain data for ID: " + domainId);
	}

	return json.data;
}

///////////////////////////////////////////////////////////////////////////////
// Lookup queue status fetch
// Empty luqinfo array means lookup is complete (success or fail)
///////////////////////////////////////////////////////////////////////////////

async function getLuqStatus(domainId) {
	const params = new URLSearchParams({
		c: "get",
		t: "luq",
		id: domainId
	});

	const response = await fetch(API_BASE_URL + "?" + params.toString(), {
		credentials: "same-origin"
	});

	const json = await response.json();

	if (json.status !== "ok") {
		throw new Error("Failed to fetch luq status for ID: " + domainId);
	}

	return json;
}

///////////////////////////////////////////////////////////////////////////////
// Domain lookup trigger
///////////////////////////////////////////////////////////////////////////////

function triggerDomainLookup(domainId, cell) {
	const img = cell ? cell.querySelector("img") : null;
	if (img) img.classList.add("lookup-in-progress");

	const params = new URLSearchParams({
		c: "set",
		t: "luq",
		oper: "add",
		what: 1,   // DOMAIN_RECORDS
		id: domainId,
		ri: LOOKUP_REFRESH_INTERVAL
	});

	fetch(API_BASE_URL, {
		method: "POST",
		credentials: "same-origin",
		headers: { "Content-Type": "application/x-www-form-urlencoded" },
		body: params.toString()
	})
	.then(r => r.json())
	.then(data => {
		console.log("Lookup queued for domain ID:", domainId, data);
		pendingLookups.set(domainId, { cell, queuedAt: Date.now() });
		startLookupPoller();
	})
	.catch(err => {
		console.error("Lookup queue error:", err);
		if (img) img.classList.remove("lookup-in-progress");
	});
}

///////////////////////////////////////////////////////////////////////////////
// Row data injection into DataTableView internal data array
///////////////////////////////////////////////////////////////////////////////

function injectDomainRowData(domainId, newData) {
	const dt = DataTableView.getTable('dp-table');
	if (!dt || !dt.options.data) return false;

	// row.id is integer, domainId is string — use == for coercion
	const foundIndex = dt.options.data.findIndex(row => row.id == domainId);
	if (foundIndex === -1) {
		console.warn("injectDomainRowData: row not found for domain ID:", domainId);
		return false;
	}

	// Only update keys that already exist in the current row.
	// Grid row keys often have a "d." prefix (e.g. d.registry_expiry) while
	// the API response uses plain keys (e.g. registry_expiry) — strip prefix to match.
	const existingRow = dt.options.data[foundIndex];
	Object.keys(existingRow).forEach(key => {
		if (key in newData) {
			existingRow[key] = newData[key];
		} else if (key.startsWith("d.") && key.substring(2) in newData) {
			existingRow[key] = newData[key.substring(2)];
		}
	});

	dt.refreshTableRow(foundIndex);

	return true;
}

///////////////////////////////////////////////////////////////////////////////
// Row update
///////////////////////////////////////////////////////////////////////////////

function updateDomainRow(domainId, data) {
	const injected = injectDomainRowData(domainId, data);
	console.log("Row update ready for domain ID:", domainId, "injected:", injected, data);
}

///////////////////////////////////////////////////////////////////////////////
// Lookup poller
///////////////////////////////////////////////////////////////////////////////

function startLookupPoller() {
	if (lookupPollTimer !== null) return;
	lookupPollTimer = setInterval(pollPendingLookups, LOOKUP_POLL_INTERVAL);
}

function stopLookupPoller() {
	if (lookupPollTimer === null) return;
	clearInterval(lookupPollTimer);
	lookupPollTimer = null;
}

function rescheduleLookupPoller(interval) {
	if (lookupPollTimer !== null) clearInterval(lookupPollTimer);
	lookupPollTimer = setInterval(pollPendingLookups, interval);
}

async function pollPendingLookups() {
	if (pendingLookups.size === 0) {
		stopLookupPoller();
		return;
	}

	const now = Date.now();

	for (const [domainId, meta] of pendingLookups) {

		// Drop if timed out
		if (now - meta.queuedAt > LOOKUP_POLL_TIMEOUT) {
			console.warn("Lookup timed out for domain ID:", domainId);
			if (meta.cell) meta.cell.querySelector("img")?.classList.remove("lookup-in-progress");
			pendingLookups.delete(domainId);
			continue;
		}

		try {
			const luqStatus = await getLuqStatus(domainId);
			const queueSize = parseInt(luqStatus.queue_size, 10) || 0;

			// Abandon if queue is too large to be practical
			if (queueSize >= LOOKUP_QUEUE_ABANDON_SIZE) {
				console.warn("Queue too large (" + queueSize + "), abandoning lookup for domain ID:", domainId);
				if (meta.cell) meta.cell.querySelector("img")?.classList.remove("lookup-in-progress");
				pendingLookups.delete(domainId);
				continue;
			}

			// Back off poll interval based on queue size.
			// A healthy server processes the queue every 60s, so back off proportionally.
			if (queueSize >= LOOKUP_QUEUE_BACKOFF_TRIGGER) {
				const backoff = Math.min(
					LOOKUP_POLL_INTERVAL * Math.ceil(queueSize / LOOKUP_QUEUE_BACKOFF_TRIGGER),
					LOOKUP_POLL_BACKOFF_MAX
				);
				rescheduleLookupPoller(backoff);
			} else {
				rescheduleLookupPoller(LOOKUP_POLL_INTERVAL);
			}

			// Empty luqinfo means lookup is done (success or fail)
			if (!luqStatus.luqinfo || luqStatus.luqinfo.length === 0) {
				console.log("Lookup complete for domain ID:", domainId, "— fetching row data");
				const data = await getDomainRowData(domainId);
				updateDomainRow(domainId, data);
				if (meta.cell) meta.cell.querySelector("img")?.classList.remove("lookup-in-progress");
				pendingLookups.delete(domainId);
			}

		} catch (err) {
			console.error("Poll error for domain ID:", domainId, err);
		}
	}
}