// assetBrowser.js — asset tree sidebar (clients → assets) with search + filters

import { getCustomers, getCustomerAssets, getContacts } from '../api/syncro.js';
import { normalizeUsername, usernameFuzzyMatch, usernameFirstNameMatch, usernameNameInitialMatch, usernameInitialLastNameMatch } from './usernameUtils.js';

// ── localStorage keys ─────────────────────────────────────────────────────────

const LS_EXPANDED      = 'assetBrowser_expanded';       // JSON number[]
const LS_ASSET_CACHE   = 'assetBrowser_assetCache';     // JSON { [customerId]: asset[] }
const LS_CACHE_TS      = 'assetBrowser_assetCacheTs';   // ms timestamp
const LS_BADGE_VIS     = 'assetBrowser_badgeVis';       // JSON { possession, user, lifecycle, infra }
const LS_CONTACT_CACHE    = 'assetBrowser_contactCache';    // JSON { [customerId]: contact[] }
const LS_SORT_PREFS       = 'assetBrowser_sortPrefs';       // JSON { clients, assets }
const LS_DISPLAY_PREFS    = 'assetBrowser_displayPrefs';    // JSON { showCount, showBillable, hideEmpty }
const LS_REMEMBER_MENU    = 'assetBrowser_rememberMenu';    // 'true'|'false', default true
const LS_REMEMBER_FILTERS = 'assetBrowser_rememberFilters'; // 'true'|'false', default false
const LS_FILTER_STATE     = 'assetBrowser_filterState';     // JSON saved filter state

const POLL_INTERVAL_MS = 5 * 60_000; // re-fetch all assets every 5 min

// ── Module state ──────────────────────────────────────────────────────────────

let _onAssetSelect = null;
let _onAssetClose  = null;
let _customers     = [];
let _assetCache    = new Map();  // customerId → asset[]
let _contactCache  = new Map();  // customerId → contact[]
let _expandedIds   = new Set();  // customer IDs whose subtrees are open
let _activeAssetId = null;
let _filterText    = '';
let _filterTimer   = null;
let _preSearchExpandedIds = null;  // snapshot saved when search begins
let _badgeVis    = { possession: true, user: true, lifecycle: true, infra: true };
let _sortPrefs    = { clients: 'alpha', assets: 'default' };
let _displayPrefs = { showCount: true, showBillable: false, hideEmpty: 'none' };

// ── Filter state ──────────────────────────────────────────────────────────────

const _filters = {
  lifecycle:  new Set(),  // empty = show all lifecycle stages
  possession: null,       // null | 'IT' | 'Deployed'
  infra:      null,       // null | true | false
};
let _filterPanelOpen = false;

// ── DOM refs (set in initAssetBrowser) ───────────────────────────────────────

let _sidebar     = null;
let _tree        = null;
let _searchInput = null;
let _filterBtn   = null;
let _filterPanel = null;

// ── Public API ────────────────────────────────────────────────────────────────

export function initAssetBrowser({ onAssetSelect, onAssetClose }) {
  _onAssetSelect = onAssetSelect;
  _onAssetClose  = onAssetClose ?? null;

  _sidebar     = document.getElementById('asset-sidebar');
  _tree        = document.getElementById('sidebar-tree');
  _searchInput = document.getElementById('sidebar-search');
  _filterBtn   = document.getElementById('sidebar-filter-btn');
  _filterPanel = document.getElementById('sidebar-filter-panel');
  const _searchClearBtn = document.getElementById('sidebar-search-clear');

  if (!_sidebar || !_tree) return;

  // Restore persisted state (expanded customers + asset cache) before loading
  _restoreState();
  _syncFilterChips(); // update chip UI to match any restored filter state

  // Wire manual refresh button
  document.getElementById('sidebar-refresh')?.addEventListener('click', _manualRefresh);

  // Wire menu button
  document.getElementById('sidebar-menu-btn')?.addEventListener('click', e => {
    e.stopPropagation();
    const isOpen = document.getElementById('sidebar-menu-panel')?.classList.contains('open');
    _openSidePanel(isOpen ? null : 'menu');
  });

  // Wire all menu panel controls
  _initMenuPanel();

  // Close panels when clicking outside the sidebar header
  document.addEventListener('click', e => {
    if (!e.target.closest('.sidebar-header') &&
        !e.target.closest('.sidebar-filter-panel') &&
        !e.target.closest('.sidebar-menu-panel')) {
      _openSidePanel(null);
    }
  });

  // Wire search filter (debounced) + clear button
  _searchInput?.addEventListener('input', () => {
    if (_searchClearBtn) _searchClearBtn.hidden = !_searchInput.value.trim();
    clearTimeout(_filterTimer);
    _filterTimer = setTimeout(() => {
      const newFilter = _searchInput.value.trim().toLowerCase();
      if (newFilter && _preSearchExpandedIds === null) {
        // Starting a search — snapshot current expansion state
        _preSearchExpandedIds = new Set(_expandedIds);
      } else if (!newFilter && _preSearchExpandedIds !== null) {
        // Search cleared — restore pre-search expansion state
        _expandedIds = new Set(_preSearchExpandedIds);
        _preSearchExpandedIds = null;
      }
      _filterText = newFilter;
      _renderTree();
    }, 200);
  });

  _searchClearBtn?.addEventListener('click', () => {
    _searchInput.value = '';
    _searchClearBtn.hidden = true;
    clearTimeout(_filterTimer);
    if (_preSearchExpandedIds !== null) {
      _expandedIds = new Set(_preSearchExpandedIds);
      _preSearchExpandedIds = null;
    }
    _filterText = '';
    _renderTree();
    _searchInput.focus();
  });

  // Wire filter button + panel
  _filterBtn?.addEventListener('click', () => {
    _openSidePanel(_filterPanelOpen ? null : 'filter');
  });

  _filterPanel?.addEventListener('click', _handleFilterClick);
  document.getElementById('sf-clear-btn')?.addEventListener('click', _clearFilters);

  // Wire "remember filters" checkbox
  const filterRememberCb = document.getElementById('filter-remember');
  if (filterRememberCb) {
    filterRememberCb.checked = localStorage.getItem(LS_REMEMBER_FILTERS) === 'true';
    filterRememberCb.addEventListener('change', () => {
      localStorage.setItem(LS_REMEMBER_FILTERS, String(filterRememberCb.checked));
      if (filterRememberCb.checked) {
        _saveFilterState(); // save current filters immediately when enabling
      } else {
        localStorage.removeItem(LS_FILTER_STATE); // clear saved state when disabling
      }
    });
  }

  // Event delegation for tree clicks
  _tree.addEventListener('click', _handleTreeClick);

  // Show loading state then fetch customers
  _tree.innerHTML = _loadingHTML('Loading clients…');
  _loadCustomers();
}

export function updateCachedAsset(asset) {
  for (const [customerId, assets] of _assetCache) {
    const idx = assets.findIndex(a => a.id === asset.id);
    if (idx !== -1) {
      assets[idx] = asset;
      if (_expandedIds.has(customerId)) _renderAssetList(customerId);
      break;
    }
  }
}

export function getCustomerContactNames(customerId) {
  const assets = _assetCache.get(customerId) ?? [];
  const names  = new Set();
  for (const a of assets) {
    const name = a.contact_fullname ?? a.contact?.name;
    if (name) names.add(name);
  }
  return [...names];
}

// Returns null when not yet loaded (callers can distinguish "loading" from "empty")
export function getCustomerContacts(customerId) {
  return _contactCache.has(customerId) ? _contactCache.get(customerId) : null;
}

export function searchLocal(query) {
  const q = query.trim().toLowerCase();
  if (q.length < 2) return [];

  const customerMap = new Map(_customers.map(c => [c.id, c.business_name ?? c.name ?? '']));
  const results = [];

  for (const [customerId, assets] of _assetCache) {
    for (const asset of assets) {
      const serial   = asset.asset_serial ?? asset.serial ?? asset.serial_number ?? '';
      const lastUser = asset.properties?.kabuto_information?.last_user ?? '';
      const contact  = asset.contact_fullname ?? asset.contact?.name ?? '';
      if (
        asset.name?.toLowerCase().includes(q) ||
        serial.toLowerCase().includes(q)      ||
        contact.toLowerCase().includes(q)     ||
        lastUser.toLowerCase().includes(q)    ||
        String(asset.id) === q
      ) {
        results.push({ asset, serial, contact, customerName: customerMap.get(customerId) ?? '' });
        if (results.length >= 8) return results;
      }
    }
  }
  return results;
}

export function setActiveAsset(assetId) {
  _activeAssetId = assetId;

  if (assetId === null) {
    _tree.querySelectorAll('.sb-asset').forEach(el => el.classList.remove('active'));
    return;
  }

  // Find which customer owns this asset and auto-expand if needed
  for (const [customerId, assets] of _assetCache) {
    const found = assets.find(a => a.id === assetId);
    if (found && !_expandedIds.has(customerId)) {
      _expandedIds.add(customerId);
      _saveExpandedState();
    }
  }

  // Update highlight in DOM
  _tree.querySelectorAll('.sb-asset').forEach(el => {
    el.classList.toggle('active', Number(el.dataset.assetId) === assetId);
  });

  // If the customer row needs to be expanded in the DOM but isn't yet rendered, re-render
  const activeRow = _tree.querySelector(`.sb-asset[data-asset-id="${assetId}"]`);
  if (!activeRow && _customers.length > 0) {
    _renderTree();
  }
}

// Programmatically set filters and re-render the tree (used by Quick View click-through)
export function setFiltersAndRender({ lifecycle = [], possession = null } = {}) {
  _filters.lifecycle  = new Set(lifecycle);
  _filters.possession = possession;
  // Auto-expand any customer that has at least one matching asset
  for (const [customerId, assets] of _assetCache) {
    if (assets.some(a => _assetMatchesFilters(a))) {
      _expandedIds.add(customerId);
    }
  }
  _renderTree();
}

// ── Internal: data loading ────────────────────────────────────────────────────

async function _loadCustomers() {
  try {
    _customers = await getCustomers();
    // Sort alphabetically
    _customers.sort((a, b) => (a.business_name ?? a.name ?? '').localeCompare(b.business_name ?? b.name ?? ''));
    // Auto-expand when there's only one customer (e.g. client role)
    if (_customers.length === 1) {
      _expandedIds.add(_customers[0].id);
    }
    _renderTree();
    // Re-expand any customers that were open before the page loaded —
    // if already in cache they render instantly, otherwise lazy-load with spinner
    for (const id of _expandedIds) {
      if (!_assetCache.has(id)) {
        _loadCustomerAssets(id);
      }
    }
    // Background: fetch all asset lists silently, then contacts, then start polling
    _preloadAllAssets().then(() => {
      _preloadAllContacts(); // fire-and-forget
      _startPolling();
    });
  } catch (err) {
    _tree.innerHTML = `<div class="sb-loading" style="color:var(--red-500)">Failed to load clients</div>`;
    console.error('[assetBrowser] getCustomers error:', err);
  }
}

async function _loadCustomerAssets(customerId, silent = false) {
  if (!silent) {
    // Show spinner only when the user explicitly expands a customer
    const list = _tree.querySelector(`.sb-asset-list[data-customer-id="${customerId}"]`);
    if (list) {
      list.innerHTML = `<div class="sb-asset-loading"><div class="sb-spinner"></div> Loading…</div>`;
      list.classList.add('visible');
    }
  }

  try {
    const assets = await getCustomerAssets(customerId);
    _assetCache.set(customerId, assets);
    if (!silent) {
      _renderAssetList(customerId);
    }
    _updateCustomerCount(customerId);
  } catch (err) {
    if (!silent) {
      const list = _tree.querySelector(`.sb-asset-list[data-customer-id="${customerId}"]`);
      if (list) {
        list.innerHTML = `<div class="sb-asset-empty" style="color:var(--red-500)">Failed to load assets</div>`;
      }
    }
    console.error(`[assetBrowser] getCustomerAssets(${customerId}) error:`, err);
  }
}

// ── Internal: background preload + polling ────────────────────────────────────

async function _preloadAllAssets() {
  // Fetch every customer's assets silently in parallel
  await Promise.allSettled(_customers.map(c => _loadCustomerAssets(c.id, true)));
  _saveAssetCache();
  // Refresh any expanded customer lists with fresh data
  for (const customerId of _expandedIds) {
    if (_assetCache.has(customerId)) _renderAssetList(customerId);
  }
}

async function _preloadAllContacts() {
  await Promise.allSettled(_customers.map(async c => {
    try {
      const contacts = await getContacts(c.id);
      contacts.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''));
      _contactCache.set(c.id, contacts);
    } catch (_) {}
  }));
  _saveContactCache();
}

function _saveContactCache() {
  try {
    const obj = {};
    for (const [id, contacts] of _contactCache) obj[id] = contacts;
    localStorage.setItem(LS_CONTACT_CACHE, JSON.stringify(obj));
  } catch (_) {}
}

function _startPolling() {
  setInterval(_preloadAllAssets, POLL_INTERVAL_MS);
}

function _manualRefresh() {
  const btn = document.getElementById('sidebar-refresh');
  if (btn) { btn.classList.add('spinning'); btn.disabled = true; }

  // Tell the server to wipe its cache, then clear our own localStorage caches
  fetch('/api/cache/refresh', { method: 'POST', credentials: 'same-origin' })
    .catch(() => {}) // best-effort — proceed regardless
    .finally(() => {
      _assetCache.clear();
      _contactCache.clear();
      localStorage.removeItem(LS_ASSET_CACHE);
      localStorage.removeItem(LS_CACHE_TS);
      localStorage.removeItem(LS_CONTACT_CACHE);
      Promise.all([_preloadAllAssets(), _preloadAllContacts()]).finally(() => {
        if (btn) { btn.classList.remove('spinning'); btn.disabled = false; }
      });
    });
}

function _saveAssetCache() {
  try {
    const obj = {};
    for (const [id, assets] of _assetCache) obj[id] = assets;
    localStorage.setItem(LS_ASSET_CACHE, JSON.stringify(obj));
    localStorage.setItem(LS_CACHE_TS, Date.now().toString());
  } catch (_) { /* storage full — ignore */ }
}

// ── Internal: rendering ───────────────────────────────────────────────────────

function _renderTree() {
  if (!_customers.length) {
    _tree.innerHTML = `<div class="sb-empty">No clients found</div>`;
    return;
  }

  const filter = _filterText;

  const hasFilter = filter || _filters.lifecycle.size > 0 || _filters.possession !== null || _filters.infra !== null;

  const visible = _customers.filter(c => {
    // hideEmpty filtering (skip when a search/filter is active or assets not loaded yet)
    if (_displayPrefs.hideEmpty !== 'none' && !hasFilter && _assetCache.has(c.id)) {
      const assets = _assetCache.get(c.id);
      if (_displayPrefs.hideEmpty === 'zero-assets'   && assets.length === 0)          return false;
      if (_displayPrefs.hideEmpty === 'zero-billable'  && _billableCount(assets) === 0) return false;
    }
    if (!hasFilter) return true;
    const name = (c.business_name ?? c.name ?? '').toLowerCase();
    if (filter && name.includes(filter) && _filters.lifecycle.size === 0 && _filters.possession === null && _filters.infra === null) return true;
    // Show customer only if at least one of its cached assets passes all filters
    const assets = _assetCache.get(c.id) ?? [];
    return assets.some(a => _assetMatchesSearch(a, filter) && _assetMatchesFilters(a));
  });

  const rows = _sortedCustomers(visible)
    .map(c => _customerRowHTML(c))
    .join('');

  _tree.innerHTML = rows || `<div class="sb-empty">No matches</div>`;

  // When a search is active, auto-expand every customer that has matching assets
  if (_filterText) {
    for (const [customerId, assets] of _assetCache) {
      if (assets.some(a => _assetMatchesSearch(a, _filterText) && _assetMatchesFilters(a))) {
        _expandedIds.add(customerId);
      }
    }
  }

  // Re-render asset lists for expanded customers
  for (const customerId of _expandedIds) {
    if (_assetCache.has(customerId)) {
      _renderAssetList(customerId);
    }
    // Mark customer row and asset list as expanded/visible
    const customerEl = _tree.querySelector(`.sb-customer[data-customer-id="${customerId}"]`);
    const listEl     = _tree.querySelector(`.sb-asset-list[data-customer-id="${customerId}"]`);
    if (customerEl) customerEl.classList.add('expanded');
    if (listEl)     listEl.classList.add('visible');
  }

  // Re-apply active asset highlight
  if (_activeAssetId) {
    const el = _tree.querySelector(`.sb-asset[data-asset-id="${_activeAssetId}"]`);
    if (el) el.classList.add('active');
  }
}

function _customerRowHTML(customer) {
  const id   = customer.id;
  const name = customer.business_name ?? customer.name ?? `Customer ${id}`;
  const isExpanded = _expandedIds.has(id);

  let countBadge = '';
  if (_assetCache.has(id)) {
    const assets = _assetCache.get(id);
    if (_displayPrefs.showCount)    countBadge += `<span class="sb-customer-count">${_filteredAssetCount(assets)}</span>`;
    if (_displayPrefs.showBillable) countBadge += `<span class="sb-customer-billable">${_filteredBillableCount(assets)}</span>`;
  }

  // Building block row
  const row = `
    <div class="sb-customer${isExpanded ? ' expanded' : ''}" data-customer-id="${id}">
      <svg class="sb-customer-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
        <polyline points="9 18 15 12 9 6"/>
      </svg>
      <svg class="sb-customer-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/>
        <polyline points="9 22 9 12 15 12 15 22"/>
      </svg>
      <span class="sb-customer-name">${_esc(name)}</span>
      ${countBadge}
    </div>
    <div class="sb-asset-list${isExpanded ? ' visible' : ''}" data-customer-id="${id}">
      ${isExpanded && !_assetCache.has(id) ? `<div class="sb-asset-loading"><div class="sb-spinner"></div> Loading…</div>` : ''}
    </div>`;

  return row;
}

function _renderAssetList(customerId) {
  const list = _tree.querySelector(`.sb-asset-list[data-customer-id="${customerId}"]`);
  if (!list) return;

  const assets = _assetCache.get(customerId) ?? [];
  const filter = _filterText;

  const hasActiveFilter = filter || _filters.lifecycle.size > 0 || _filters.possession !== null || _filters.infra !== null;
  let filtered = hasActiveFilter
    ? assets.filter(a => _assetMatchesSearch(a, filter) && _assetMatchesFilters(a))
    : assets.slice();

  if (!filtered.length) {
    list.innerHTML = `<div class="sb-asset-empty">${hasActiveFilter ? 'No matches' : 'No assets'}</div>`;
    return;
  }

  _sortAssets(filtered);

  list.innerHTML = filtered.map(a => {
    const isActive  = a.id === _activeAssetId;
    const assetType = _assetDisplayType(a);
    const badges    = _assetBadgesHTML(a);
    return `
      <div class="sb-asset${isActive ? ' active' : ''}" data-asset-id="${a.id}" data-customer-id="${customerId}">
        <span class="sb-asset-name">${_esc(a.name ?? `Asset ${a.id}`)}</span>
        <div class="sb-asset-meta-row">
          ${badges}${assetType ? `<span class="sb-asset-type">${_esc(assetType)}</span>` : ''}
        </div>
      </div>`;
  }).join('');
}

// ── Billable helper ───────────────────────────────────────────────────────────

const _DEAD_STAGES = new Set(['For Parts', 'Decommissioned', 'Disposed of']);

function _billableCount(assets) {
  return assets.filter(a => !_DEAD_STAGES.has(a.properties?.['Lifecycle Stage'])).length;
}

// Returns count of assets passing the currently active filters (falls back to total when no filter)
function _filteredAssetCount(assets) {
  const hasFilter = _filterText || _filters.lifecycle.size > 0 || _filters.possession !== null || _filters.infra !== null;
  if (!hasFilter) return assets.length;
  return assets.filter(a => _assetMatchesSearch(a, _filterText) && _assetMatchesFilters(a)).length;
}

function _filteredBillableCount(assets) {
  const hasFilter = _filterText || _filters.lifecycle.size > 0 || _filters.possession !== null || _filters.infra !== null;
  if (!hasFilter) return _billableCount(assets);
  return assets.filter(a =>
    _assetMatchesSearch(a, _filterText) &&
    _assetMatchesFilters(a) &&
    !_DEAD_STAGES.has(a.properties?.['Lifecycle Stage'])
  ).length;
}

// ── Sort + badge helpers ───────────────────────────────────────────────────────

const _POSSESSION_ORDER = {
  'In IT Possession':   0,
  'Deployed':           1,
  'In User Possession': 1, // legacy value
};

const _LIFECYCLE_ORDER = {
  'Active':           0,
  'Inventory':        1,
  'Pre-Deployment':   2,
  'For Repair':       3,
  'For Upgrade':      4,
  'For Parts':        5,
  'Decommissioned':   6,
  'Disposed of':      7,
};

const _LIFECYCLE_SHORT = {
  'Pre-Deployment': 'Pre-Deploy',
  'Active':         'Active',
  'Inventory':      'Inventory',
  'For Repair':     'Repair',
  'For Upgrade':    'Upgrade',
  'For Parts':      'Parts',
  'Decommissioned': 'Decomm.',
  'Disposed of':    'Disposed',
};

const _LIFECYCLE_CSS = {
  'Pre-Deployment': 'sb-lc-predeployment',
  'Active':         'sb-lc-active',
  'Inventory':      'sb-lc-inventory',
  'For Repair':     'sb-lc-repair',
  'For Upgrade':    'sb-lc-upgrade',
  'For Parts':      'sb-lc-parts',
  'Decommissioned': 'sb-lc-decommissioned',
  'Disposed of':    'sb-lc-disposed',
};

function _sortAssets(arr) {
  if (_sortPrefs.assets === 'alpha') {
    arr.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''));
    return;
  }
  if (_sortPrefs.assets === 'user') {
    arr.sort((a, b) => {
      const ua = (a.contact_fullname ?? a.contact?.name ?? '').toLowerCase();
      const ub = (b.contact_fullname ?? b.contact?.name ?? '').toLowerCase();
      if (!ua && ub) return 1;
      if (ua && !ub) return -1;
      if (ua !== ub) return ua.localeCompare(ub);
      return (a.name ?? '').localeCompare(b.name ?? '');
    });
    return;
  }
  if (_sortPrefs.assets === 'last-sync') {
    arr.sort((a, b) => {
      const ta = a.properties?.kabuto_information?.last_synced_at;
      const tb = b.properties?.kabuto_information?.last_synced_at;
      if (!ta && !tb) return (a.name ?? '').localeCompare(b.name ?? '');
      if (!ta) return 1;
      if (!tb) return -1;
      return new Date(tb) - new Date(ta); // most recent first
    });
    return;
  }
  // default: possession → lifecycle → name
  arr.sort((a, b) => {
    const pa = _POSSESSION_ORDER[a.properties?.['Possession Status']] ?? 2;
    const pb = _POSSESSION_ORDER[b.properties?.['Possession Status']] ?? 2;
    if (pa !== pb) return pa - pb;
    const la = _LIFECYCLE_ORDER[a.properties?.['Lifecycle Stage']] ?? 7;
    const lb = _LIFECYCLE_ORDER[b.properties?.['Lifecycle Stage']] ?? 7;
    if (la !== lb) return la - lb;
    return (a.name ?? '').localeCompare(b.name ?? '');
  });
}

function _sortedCustomers(arr) {
  if (_sortPrefs.clients === 'alpha') return arr;
  const copy = arr.slice();
  if (_sortPrefs.clients === 'most-assets') {
    copy.sort((a, b) => {
      const d = (_assetCache.get(b.id)?.length ?? 0) - (_assetCache.get(a.id)?.length ?? 0);
      return d !== 0 ? d : (a.business_name ?? a.name ?? '').localeCompare(b.business_name ?? b.name ?? '');
    });
  } else if (_sortPrefs.clients === 'most-billable') {
    copy.sort((a, b) => {
      const d = _billableCount(_assetCache.get(b.id) ?? []) - _billableCount(_assetCache.get(a.id) ?? []);
      return d !== 0 ? d : (a.business_name ?? a.name ?? '').localeCompare(b.business_name ?? b.name ?? '');
    });
  } else if (_sortPrefs.clients === 'most-users') {
    copy.sort((a, b) => {
      const ua = new Set((_assetCache.get(a.id) ?? []).map(x => x.contact_fullname ?? x.contact?.name).filter(Boolean)).size;
      const ub = new Set((_assetCache.get(b.id) ?? []).map(x => x.contact_fullname ?? x.contact?.name).filter(Boolean)).size;
      return ub !== ua ? ub - ua : (a.business_name ?? a.name ?? '').localeCompare(b.business_name ?? b.name ?? '');
    });
  }
  return copy;
}

function _assetBadgesHTML(asset) {
  const possession   = asset.properties?.['Possession Status'];
  const lifecycle    = asset.properties?.['Lifecycle Stage'];
  const isInfra      = asset.properties?.['Infrastructure'] === 'Yes';
  const rawLastUser      = asset.properties?.kabuto_information?.last_user ?? '';
  const lastUser         = normalizeUsername(rawLastUser);
  const assignedUser     = asset.contact_fullname ?? null;
  const allContactNames  = getCustomerContactNames(asset.customer_id);
  const sameUser         = !!(lastUser && assignedUser && (
    usernameFuzzyMatch(rawLastUser, assignedUser) ||
    usernameFirstNameMatch(rawLastUser, assignedUser, allContactNames) ||
    usernameNameInitialMatch(rawLastUser, assignedUser, allContactNames)    ||
    usernameInitialLastNameMatch(rawLastUser, assignedUser, allContactNames)
  ));

  let html = '';

  if (_badgeVis.infra && isInfra) {
    html += `<span class="sb-badge sb-infra">Infra</span>`;
  }

  if (_badgeVis.possession) {
    if (possession === 'In IT Possession') {
      html += `<span class="sb-badge sb-poss-it">IT</span>`;
    } else if (possession === 'Deployed' || possession === 'In User Possession') {
      html += `<span class="sb-badge sb-poss-user">Deployed</span>`;
    }
  }

  if (_badgeVis.user && !isInfra) {
    if (sameUser) {
      // Show contact name (properly formatted from Syncro) with tooltip for long names
      html += `<span class="sb-badge sb-user-same" title="${_esc(assignedUser)}">${_esc(assignedUser)}</span>`;
    } else {
      if (lastUser)     html += `<span class="sb-badge sb-user-last" title="${_esc(lastUser)}">${_esc(lastUser)}</span>`;
      if (assignedUser) html += `<span class="sb-badge sb-user-assigned" title="${_esc(assignedUser)}">${_esc(assignedUser)}</span>`;
    }
  }

  if (_badgeVis.lifecycle && lifecycle && _LIFECYCLE_SHORT[lifecycle]) {
    const cls   = _LIFECYCLE_CSS[lifecycle] ?? 'sb-lc-unknown';
    const label = _LIFECYCLE_SHORT[lifecycle];
    html += `<span class="sb-badge ${cls}">${label}</span>`;
  }

  return html;
}

function _assetDisplayType(asset) {
  // Use form_factor (set by Kabuto/RMM agent) first; fall back to asset_type
  // but skip generic "Syncro Device" placeholder
  const formFactor = asset.properties?.form_factor
                  ?? asset.properties?.kabuto_information?.general?.form_factor;
  if (formFactor) return formFactor;
  const type = asset.asset_type ?? '';
  return type === 'Syncro Device' ? '' : type;
}

function _updateCustomerCount(customerId) {
  const row = _tree.querySelector(`.sb-customer[data-customer-id="${customerId}"]`);
  if (!row) return;
  const assets = _assetCache.get(customerId) ?? [];

  // Total count badge
  if (_displayPrefs.showCount) {
    let b = row.querySelector('.sb-customer-count');
    if (!b) { b = document.createElement('span'); b.className = 'sb-customer-count'; row.appendChild(b); }
    b.textContent = _filteredAssetCount(assets);
  } else {
    row.querySelector('.sb-customer-count')?.remove();
  }

  // Billable count badge
  if (_displayPrefs.showBillable) {
    let b = row.querySelector('.sb-customer-billable');
    if (!b) { b = document.createElement('span'); b.className = 'sb-customer-billable'; row.appendChild(b); }
    b.textContent = _filteredBillableCount(assets);
  } else {
    row.querySelector('.sb-customer-billable')?.remove();
  }
}

// ── Internal: interaction ─────────────────────────────────────────────────────

function _handleTreeClick(e) {
  const customerRow = e.target.closest('.sb-customer');
  const assetRow    = e.target.closest('.sb-asset');

  if (customerRow) {
    _toggleCustomer(Number(customerRow.dataset.customerId));
  } else if (assetRow) {
    const assetId    = Number(assetRow.dataset.assetId);
    const customerId = Number(assetRow.dataset.customerId);
    const asset      = _assetCache.get(customerId)?.find(a => a.id === assetId);
    if (asset) {
      if (asset.id === _activeAssetId && _onAssetClose) {
        _onAssetClose();
      } else if (_onAssetSelect) {
        _onAssetSelect(asset);
      }
    }
  }
}

function _toggleCustomer(customerId) {
  if (_expandedIds.has(customerId)) {
    _expandedIds.delete(customerId);
  } else {
    _expandedIds.add(customerId);
  }
  _saveExpandedState();

  // Toggle expanded class on customer row
  const customerEl = _tree.querySelector(`.sb-customer[data-customer-id="${customerId}"]`);
  const listEl     = _tree.querySelector(`.sb-asset-list[data-customer-id="${customerId}"]`);

  if (_expandedIds.has(customerId)) {
    customerEl?.classList.add('expanded');
    listEl?.classList.add('visible');
    if (_assetCache.has(customerId)) {
      // Already cached (preload or prior expand) — just render the list
      _renderAssetList(customerId);
    } else {
      // Not yet fetched — load with spinner
      _loadCustomerAssets(customerId);
    }
  } else {
    customerEl?.classList.remove('expanded');
    listEl?.classList.remove('visible');
  }
}

// ── Internal: panel open/close ────────────────────────────────────────────────

function _openSidePanel(which) {
  // which: 'filter' | 'menu' | null (close all)
  const filterPanel = _filterPanel;
  const menuPanel   = document.getElementById('sidebar-menu-panel');
  const menuBtn     = document.getElementById('sidebar-menu-btn');

  const openFilter = which === 'filter';
  const openMenu   = which === 'menu';

  filterPanel?.classList.toggle('open', openFilter);
  _filterBtn?.classList.toggle('active', openFilter);
  _filterPanelOpen = openFilter;

  menuPanel?.classList.toggle('open', openMenu);
  menuBtn?.classList.toggle('active', openMenu);

  // Add .ready after the panel open animation completes so subsection body
  // transitions don't fire spuriously while the outer panel is animating.
  if (menuPanel) {
    menuPanel.classList.remove('ready');
    if (openMenu) {
      // 230ms = panel max-height transition duration (0.22s) + small buffer
      setTimeout(() => menuPanel.classList.add('ready'), 230);
    }
  }
}

// ── Internal: filter handlers ─────────────────────────────────────────────────

function _handleFilterClick(e) {
  const chip = e.target.closest('.sf-chip');
  if (!chip) return;

  const section = chip.closest('[data-filter-type]');
  const type    = section?.dataset.filterType;

  if (type === 'lifecycle') {
    const val = chip.dataset.value;
    if (_filters.lifecycle.has(val)) {
      _filters.lifecycle.delete(val);
      chip.classList.remove('active');
    } else {
      _filters.lifecycle.add(val);
      chip.classList.add('active');
    }
  } else if (type === 'possession') {
    const val = chip.dataset.value;
    _filters.possession = val === '' ? null : val;
    section.querySelectorAll('.sf-chip').forEach(c =>
      c.classList.toggle('active', c.dataset.value === val)
    );
  } else if (type === 'infra') {
    const val = chip.dataset.value;
    _filters.infra = val === '' ? null : val === 'true';
    section.querySelectorAll('.sf-chip').forEach(c =>
      c.classList.toggle('active', c.dataset.value === val)
    );
  }

  _saveFilterState();
  _updateFilterBadge();
  _renderTree();
}

function _clearFilters() {
  _filters.lifecycle.clear();
  _filters.possession = null;
  _filters.infra      = null;

  if (_filterPanel) {
    // Reset lifecycle: clear all active chips
    _filterPanel.querySelectorAll('[data-filter-type="lifecycle"] .sf-chip').forEach(c =>
      c.classList.remove('active')
    );
    // Reset single-select sections: re-activate "All" chip
    _filterPanel.querySelectorAll('[data-filter-type="possession"] .sf-chip, [data-filter-type="infra"] .sf-chip').forEach(c =>
      c.classList.toggle('active', c.dataset.value === '')
    );
  }

  _saveFilterState();
  _updateFilterBadge();
  _renderTree();
}

function _syncFilterChips() {
  if (!_filterPanel) return;
  _filterPanel.querySelectorAll('[data-filter-type="lifecycle"] .sf-chip').forEach(c =>
    c.classList.toggle('active', _filters.lifecycle.has(c.dataset.value))
  );
  const possStr  = _filters.possession === null ? '' : _filters.possession;
  _filterPanel.querySelectorAll('[data-filter-type="possession"] .sf-chip').forEach(c =>
    c.classList.toggle('active', c.dataset.value === possStr)
  );
  const infraStr = _filters.infra === null ? '' : String(_filters.infra);
  _filterPanel.querySelectorAll('[data-filter-type="infra"] .sf-chip').forEach(c =>
    c.classList.toggle('active', c.dataset.value === infraStr)
  );
  _updateFilterBadge();
}

function _updateFilterBadge() {
  const count = _filters.lifecycle.size
              + (_filters.possession !== null ? 1 : 0)
              + (_filters.infra      !== null ? 1 : 0);
  const badge = document.getElementById('sidebar-filter-badge');
  if (badge) {
    badge.textContent = count;
    badge.hidden = count === 0;
  }
}

// ── Internal: search + filter predicates ──────────────────────────────────────

function _assetMatchesSearch(asset, filter) {
  if (!filter) return true;
  const serial   = asset.asset_serial ?? asset.serial ?? asset.serial_number ?? '';
  const lastUser = asset.properties?.kabuto_information?.last_user ?? '';
  const contact  = asset.contact_fullname ?? asset.contact?.name ?? '';
  return (
    (asset.name ?? '').toLowerCase().includes(filter) ||
    serial.toLowerCase().includes(filter)             ||
    contact.toLowerCase().includes(filter)            ||
    lastUser.toLowerCase().includes(filter)           ||
    String(asset.id) === filter
  );
}

function _assetMatchesFilters(asset) {
  if (_filters.lifecycle.size > 0) {
    const lc = asset.properties?.['Lifecycle Stage'] ?? '';
    if (!_filters.lifecycle.has(lc)) return false;
  }
  if (_filters.possession !== null) {
    const poss = asset.properties?.['Possession Status'] ?? '';
    if (_filters.possession === 'IT'       && poss !== 'In IT Possession') return false;
    if (_filters.possession === 'Deployed' && poss !== 'Deployed' && poss !== 'In User Possession') return false;
  }
  if (_filters.infra !== null) {
    const isInfra = asset.properties?.['Infrastructure'] === 'Yes';
    if (_filters.infra !== isInfra) return false;
  }
  return true;
}

// ── Internal: persistence ─────────────────────────────────────────────────────

function _restoreState() {
  // Expanded customer IDs
  try {
    const saved = localStorage.getItem(LS_EXPANDED);
    if (saved) {
      const ids = JSON.parse(saved);
      if (Array.isArray(ids)) _expandedIds = new Set(ids);
    }
  } catch (_) { /* ignore corrupt localStorage */ }

  // Badge vis, sort, display prefs — only if "remember menu" is on (default: true)
  if (localStorage.getItem(LS_REMEMBER_MENU) !== 'false') {
    try {
      const raw = localStorage.getItem(LS_BADGE_VIS);
      if (raw) _badgeVis = { ..._badgeVis, ...JSON.parse(raw) };
    } catch (_) {}
    try { const r = localStorage.getItem(LS_SORT_PREFS);    if (r) _sortPrefs    = { ..._sortPrefs,    ...JSON.parse(r) }; } catch (_) {}
    try { const r = localStorage.getItem(LS_DISPLAY_PREFS); if (r) _displayPrefs = { ..._displayPrefs, ...JSON.parse(r) }; } catch (_) {}
  }

  // Asset cache — populate immediately so tree renders with counts + assets on first paint
  try {
    const raw = localStorage.getItem(LS_ASSET_CACHE);
    if (raw) {
      const data = JSON.parse(raw);
      for (const [idStr, assets] of Object.entries(data)) {
        _assetCache.set(Number(idStr), assets);
      }
    }
  } catch (_) { /* ignore corrupt localStorage */ }

  // Contact cache
  try {
    const raw = localStorage.getItem(LS_CONTACT_CACHE);
    if (raw) {
      const data = JSON.parse(raw);
      for (const [idStr, contacts] of Object.entries(data)) {
        _contactCache.set(Number(idStr), contacts);
      }
    }
  } catch (_) {}

  // Filter state — only if "remember filters" is on (default: false)
  if (localStorage.getItem(LS_REMEMBER_FILTERS) === 'true') {
    try {
      const raw = localStorage.getItem(LS_FILTER_STATE);
      if (raw) {
        const saved = JSON.parse(raw);
        if (Array.isArray(saved.lifecycle)) saved.lifecycle.forEach(v => _filters.lifecycle.add(v));
        if (saved.possession !== undefined) _filters.possession = saved.possession;
        if (saved.infra      !== undefined) _filters.infra      = saved.infra;
      }
    } catch (_) {}
  }
}

function _saveExpandedState() {
  localStorage.setItem(LS_EXPANDED, JSON.stringify([..._expandedIds]));
}

function _saveBadgeVis()     { if (localStorage.getItem(LS_REMEMBER_MENU) !== 'false') localStorage.setItem(LS_BADGE_VIS,     JSON.stringify(_badgeVis)); }
function _saveSortPrefs()    { if (localStorage.getItem(LS_REMEMBER_MENU) !== 'false') localStorage.setItem(LS_SORT_PREFS,    JSON.stringify(_sortPrefs)); }
function _saveDisplayPrefs() { if (localStorage.getItem(LS_REMEMBER_MENU) !== 'false') localStorage.setItem(LS_DISPLAY_PREFS, JSON.stringify(_displayPrefs)); }

function _saveFilterState() {
  if (localStorage.getItem(LS_REMEMBER_FILTERS) !== 'true') return;
  try {
    localStorage.setItem(LS_FILTER_STATE, JSON.stringify({
      lifecycle:  [..._filters.lifecycle],
      possession: _filters.possession,
      infra:      _filters.infra,
    }));
  } catch (_) {}
}

// ── Helpers ───────────────────────────────────────────────────────────────────

function _esc(str) {
  return String(str)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;');
}

function _loadingHTML(msg) {
  return `<div class="sb-loading"><div class="sb-spinner"></div>${_esc(msg)}</div>`;
}

// ── Menu panel wiring ─────────────────────────────────────────────────────────

function _initMenuPanel() {
  // ── Sub-section collapse/expand ──────────────────────────────────────────
  document.querySelectorAll('.sidebar-menu-subsection-header').forEach(header => {
    header.addEventListener('click', () => {
      header.closest('.sidebar-menu-subsection')?.classList.toggle('open');
    });
  });

  // ── Badge visibility checkboxes ──────────────────────────────────────────
  ['possession', 'user', 'lifecycle', 'infra'].forEach(key => {
    const cb = document.getElementById(`badge-vis-${key}`);
    if (!cb) return;
    cb.checked = _badgeVis[key];
    cb.addEventListener('change', () => {
      _badgeVis[key] = cb.checked;
      _saveBadgeVis();
      for (const customerId of _expandedIds) {
        if (_assetCache.has(customerId)) _renderAssetList(customerId);
      }
    });
  });

  // ── Display: show count ──────────────────────────────────────────────────
  const showCountCb = document.getElementById('disp-show-count');
  if (showCountCb) {
    showCountCb.checked = _displayPrefs.showCount;
    showCountCb.addEventListener('change', () => {
      _displayPrefs.showCount = showCountCb.checked;
      _saveDisplayPrefs();
      _renderTree();
    });
  }

  // ── Display: show billable ───────────────────────────────────────────────
  const showBillableCb = document.getElementById('disp-show-billable');
  if (showBillableCb) {
    showBillableCb.checked = _displayPrefs.showBillable;
    showBillableCb.addEventListener('change', () => {
      _displayPrefs.showBillable = showBillableCb.checked;
      _saveDisplayPrefs();
      _renderTree();
    });
  }

  // ── Display: hide empty ──────────────────────────────────────────────────
  const hideEmptySel = document.getElementById('disp-hide-empty');
  if (hideEmptySel) {
    hideEmptySel.value = _displayPrefs.hideEmpty;
    hideEmptySel.addEventListener('change', () => {
      _displayPrefs.hideEmpty = hideEmptySel.value;
      _saveDisplayPrefs();
      _renderTree();
    });
  }

  // ── Remember menu settings ───────────────────────────────────────────────
  const rememberMenuCb = document.getElementById('menu-remember');
  if (rememberMenuCb) {
    rememberMenuCb.checked = localStorage.getItem(LS_REMEMBER_MENU) !== 'false';
    rememberMenuCb.addEventListener('change', () => {
      localStorage.setItem(LS_REMEMBER_MENU, String(rememberMenuCb.checked));
      if (rememberMenuCb.checked) {
        // Save current state immediately so it'll be there on next reload
        _saveBadgeVis();
        _saveSortPrefs();
        _saveDisplayPrefs();
      } else {
        // Clear saved state so next reload uses defaults
        localStorage.removeItem(LS_BADGE_VIS);
        localStorage.removeItem(LS_SORT_PREFS);
        localStorage.removeItem(LS_DISPLAY_PREFS);
      }
    });
  }

  // ── Sort chips ───────────────────────────────────────────────────────────
  document.querySelectorAll('[data-menu-sort]').forEach(chip => {
    const sortType = chip.dataset.menuSort; // 'clients' or 'assets'
    const value    = chip.dataset.value;

    // Set initial active state
    const currentVal = sortType === 'clients' ? _sortPrefs.clients : _sortPrefs.assets;
    chip.classList.toggle('active', value === currentVal);

    chip.addEventListener('click', () => {
      if (sortType === 'clients') {
        _sortPrefs.clients = value;
        _saveSortPrefs();
        // Sync .active on sibling chips
        document.querySelectorAll('[data-menu-sort="clients"]').forEach(c =>
          c.classList.toggle('active', c.dataset.value === value)
        );
        _renderTree();
      } else if (sortType === 'assets') {
        _sortPrefs.assets = value;
        _saveSortPrefs();
        document.querySelectorAll('[data-menu-sort="assets"]').forEach(c =>
          c.classList.toggle('active', c.dataset.value === value)
        );
        // Re-render all currently expanded asset lists
        for (const customerId of _expandedIds) {
          if (_assetCache.has(customerId)) _renderAssetList(customerId);
        }
      }
    });
  });
}
