// ===== DASHBOARD CONSTANTS =====
const DC = {
  NAME: 'DashCaddy',
  POLL: {
    DASHBOARD:  10000,   // 10s — main refreshAll interval
    LOGS:       3000,    // 3s  — log viewer updates
    STATS:      5000,    // 5s  — resource monitor refresh
    WEATHER:    600000,  // 10m — weather widget refresh
    HEALTH:     1000,    // 1s  — card health badge refresh
    DEPLOY_SSL: 5000,    // 5s  — SSL cert check during deploy
  },
  DELAYS: {
    BTN_RESET:  2000,    // Button text reset after action
    RELOAD:     5000,    // Page reload after restart
    MODAL_CLOSE: 500,    // Modal close animation
    PORT_CHECK: 500,     // Debounce for port availability check
    DEPLOY_INIT: 3000,   // Initial deploy cert check delay
  },
  DEFAULTS: {
    DNS_PORT:    '5380',
    SERVICE_PORT: '8080',
    TTL:         300,
    CADDYFILE:   'C:\\caddy\\Caddyfile',
  },
};

// Error handler for tracking issues
const errorHandler = new ErrorHandler();

// ===== GLOBAL SITE CONFIG (loaded from server, cached in localStorage) =====
// Only non-sensitive display preferences are cached; DNS IPs/topology are fetched from API
const _cachedCfg = JSON.parse(localStorage.getItem('dashcaddy_site_config') || 'null');
const SITE = {
  tld: (_cachedCfg && _cachedCfg.tld) || '.home',
  dnsIp: '',
  dnsPort: DC.DEFAULTS.DNS_PORT,
  dnsServers: {},
  configurationType: (_cachedCfg && _cachedCfg.configurationType) || 'homelab',
  domain: (_cachedCfg && _cachedCfg.domain) || '',
  defaults: (_cachedCfg && _cachedCfg.defaults) || {},
  routingMode: (_cachedCfg && _cachedCfg.routingMode) || 'subdomain',
  onboardingCompleted: false
};
window.__dashcaddySiteConfigLoaded = (async function loadSiteConfig() {
  try {
    const r = await fetch('/api/v1/config');
    if (r.ok) {
      const c = await r.json();
      if (c.tld) SITE.tld = c.tld.startsWith('.') ? c.tld : '.' + c.tld;
      if (c.dns) {
        SITE.dnsIp = c.dns.ip || '';
        SITE.dnsPort = c.dns.port || DC.DEFAULTS.DNS_PORT;
      }
      if (c.dnsServers && typeof c.dnsServers === 'object') {
        for (const [k, v] of Object.entries(c.dnsServers)) {
          if (k !== '__proto__' && k !== 'constructor' && k !== 'prototype') SITE.dnsServers[k] = v;
        }
      }
      if (c.configurationType) SITE.configurationType = c.configurationType;
      if (c.domain) SITE.domain = c.domain;
      if (c.defaults) SITE.defaults = c.defaults;
      if (c.routingMode) SITE.routingMode = c.routingMode;
      SITE.onboardingCompleted = c.onboardingCompleted === true;
      // Cache only non-sensitive display config (TLD, domain, routing mode)
      // DNS IPs and server topology are NOT cached — fetched from API each load
      localStorage.setItem('dashcaddy_site_config', JSON.stringify({
        tld: SITE.tld, configurationType: SITE.configurationType,
        domain: SITE.domain, routingMode: SITE.routingMode
      }));
      // Render DNS cards dynamically based on configured servers
      renderDnsCards();
      // Hide Tokens button if no DNS servers configured
      const tokensBtn = document.getElementById('manage-tokens');
      if (tokensBtn) tokensBtn.style.display = Object.keys(SITE.dnsServers).length ? '' : 'none';
    }
  } catch (_) {}
  // Update static HTML elements with configured TLD
  document.querySelectorAll('[data-tld]').forEach(el => el.textContent = SITE.tld);
  const tldSuffix = document.getElementById('edit-tld-suffix');
  if (tldSuffix) tldSuffix.textContent = SITE.tld;
  const proxyIpInput = document.getElementById('external-proxy-ip');
  if (proxyIpInput && SITE.dnsIp) { proxyIpInput.value = SITE.dnsIp; proxyIpInput.placeholder = SITE.dnsIp; }
})();
function buildDomain(sub) { return sub + SITE.tld; }
function buildServiceUrl(sub) {
  if (SITE.routingMode === 'subdirectory' && SITE.domain) return 'https://' + SITE.domain + '/' + sub;
  if (SITE.configurationType === 'public' && SITE.domain) return 'https://' + sub + '.' + SITE.domain;
  return 'https://' + buildDomain(sub);
}
function getDnsServerAddr(dnsId) {
  const s = SITE.dnsServers[dnsId];
  return s ? `${s.ip}:${s.port}` : buildDomain(dnsId);
}
/** Get the DNS ID whose IP matches the primary DNS config (dns.ip) */
function getPrimaryDnsId() {
  if (!SITE.dnsIp) return null;
  for (const [id, info] of Object.entries(SITE.dnsServers)) {
    if (info.ip === SITE.dnsIp) return id;
  }
  return null;
}

// ===== DYNAMIC DNS CARD RENDERER =====
function renderDnsCards() {
  const topRow = document.querySelector('.top');
  if (!topRow) return;
  const dnsIds = Object.keys(SITE.dnsServers);
  if (!dnsIds.length) return; // No DNS servers configured — show nothing

  const svgIcon = '<svg viewBox="0 0 24 24" class="service-icon">'
    + '<rect x="3" y="4" width="18" height="16" rx="2" fill="#34495e"/>'
    + '<rect x="5" y="6" width="14" height="2" rx="1" fill="#3498db"/>'
    + '<rect x="5" y="9" width="10" height="1" fill="#ecf0f1"/>'
    + '<rect x="5" y="11" width="12" height="1" fill="#ecf0f1"/>'
    + '<rect x="5" y="13" width="8" height="1" fill="#ecf0f1"/>'
    + '<rect x="5" y="15" width="14" height="1" fill="#ecf0f1"/>'
    + '<circle cx="17" cy="11" r="2" fill="#e74c3c"/>'
    + '<path d="M17 9v4M15 11h4" stroke="white" stroke-width="1"/></svg>';

  const firstChild = topRow.firstElementChild;
  dnsIds.forEach(id => {
    const safeId = escapeHtml(id);
    const label = escapeHtml((SITE.dnsServers[id].name || id).toUpperCase());
    const card = document.createElement('div');
    card.className = 'card';
    card.setAttribute('data-app', id);
    card.setAttribute('data-status', 'off');
    card.innerHTML =
      `<span id="${safeId}-dot" class="dot bad at-bl"></span>`
      + `<div class="row"><div class="logo-wrap">${svgIcon}</div>`
      + `<span class="name">${label}</span><span class="spacer"></span>`
      + `<span id="${safeId}-pill" class="badge off">OFF</span></div>`
      + `<div class="response-row"><span id="${safeId}-time" class="response-time">--</span></div>`
      + `<div class="health-row" id="health-${safeId}"><span id="uptime-${safeId}" class="uptime-chip">--</span><div class="uptime-mini-bar"><div class="fill" id="uptime-bar-${safeId}" style="width: 0%"></div></div></div>`
      + `<div class="btn-row">`
      + `<button id="${safeId}-restart" class="restart-btn">Restart</button>`
      + `<button id="${safeId}-update" class="update-btn" title="Update DNS server">⬆️</button>`
      + `<button id="${safeId}-open">Open</button>`
      + `<button id="${safeId}-logs" class="logs-btn">Logs</button>`
      + `<button id="${safeId}-settings" class="settings-btn">⚙️</button>`
      + `</div>`;
    topRow.insertBefore(card, firstChild);
  });
}
window.renderDnsCards = renderDnsCards;

// ===== CSRF PROTECTION =====
let csrfToken = null;

/**
 * Get CSRF token from server (cached)
 * @returns {Promise<string>} CSRF token
 */
async function getCSRFToken() {
  if (csrfToken) return csrfToken;

  try {
    const response = await fetch('/api/v1/csrf-token');
    if (!response.ok) {
      throw new Error('Failed to fetch CSRF token');
    }
    const data = await response.json();
    csrfToken = data.token;
    return csrfToken;
  } catch (error) {
    errorHandler.logError('[CSRF] Get Token', error, { function: 'getCSRFToken' });
    throw error;
  }
}

/**
 * Secure fetch wrapper that automatically adds CSRF token to state-changing requests.
 * If the server returns a CSRF error (403 with DC-100/DC-101), automatically
 * refreshes the token and retries once.
 * @param {string} url - URL to fetch
 * @param {Object} options - Fetch options
 * @returns {Promise<Response>} Fetch response
 */
async function secureFetch(url, options = {}) {
  const method = (options.method || 'GET').toUpperCase();
  const needsCsrf = !['GET', 'HEAD', 'OPTIONS'].includes(method);

  if (needsCsrf) {
    try {
      const token = await getCSRFToken();
      options.headers = {
        ...options.headers,
        'X-CSRF-Token': token
      };
    } catch (error) {
      errorHandler.logError('[CSRF] Add to Request', error, { function: 'secureFetch' });
    }
  }

  // Default 15s timeout if no signal provided (prevents hanging requests)
  if (!options.signal) {
    options = { ...options, signal: AbortSignal.timeout(15000) };
  }

  const response = await fetch(url, options);

  // Auto-retry once on CSRF failure (token may have been rotated by TOTP re-auth)
  if (needsCsrf && response.status === 403) {
    try {
      const body = await response.clone().json();
      if (body.error && (body.error.includes('DC-100') || body.error.includes('DC-101'))) {
        csrfToken = null; // Clear cached token
        const freshToken = await getCSRFToken();
        options.headers = { ...options.headers, 'X-CSRF-Token': freshToken };
        // New signal for retry (old one may have been consumed)
        options.signal = AbortSignal.timeout(15000);
        return fetch(url, options);
      }
    } catch (_) {
      // If clone/json fails, return original response
    }
  }

  return response;
}

// ===== API CALL HELPERS =====

/** POST JSON and return parsed response. Throws on HTTP or API error. */
async function postJSON(url, data) {
  const resp = await secureFetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  });
  const result = await resp.json();
  if (!resp.ok || result.success === false) {
    throw new Error(result.error || `Request failed (${resp.status})`);
  }
  return result;
}

/** GET JSON and return parsed response. Throws on HTTP error with server message. */
async function getJSON(url) {
  const resp = await secureFetch(url);
  if (!resp.ok) {
    let msg = `Request failed (${resp.status})`;
    try { const body = await resp.json(); msg = body.error || msg; } catch (_) {}
    throw new Error(msg);
  }
  return resp.json();
}

/** DELETE request. Returns parsed JSON response. */
async function deleteAPI(url) {
  const resp = await secureFetch(url, { method: 'DELETE' });
  const result = await resp.json();
  if (!resp.ok || result.success === false) {
    throw new Error(result.error || `Delete failed (${resp.status})`);
  }
  return result;
}

/**
 * Run an async operation with button loading state.
 * Disables the button, shows loading text, restores on complete.
 * @param {HTMLElement} btn - Button element
 * @param {string} loadingText - Text to show while loading (e.g. 'Saving...')
 * @param {function} asyncFn - Async function to execute
 * @param {Object} opts - Options: { successText, resetDelay }
 */
async function withButton(btn, loadingText, asyncFn, opts = {}) {
  const original = btn.innerHTML;
  const { successText = '✅', resetDelay = DC.DELAYS.BTN_RESET } = opts;
  btn.disabled = true;
  btn.innerHTML = loadingText;
  try {
    const result = await asyncFn();
    btn.innerHTML = successText;
    setTimeout(() => { btn.innerHTML = original; btn.disabled = false; }, resetDelay);
    return result;
  } catch (e) {
    btn.innerHTML = original;
    btn.disabled = false;
    throw e;
  }
}

/** Show/hide a modal by ID */
function openModal(id) {
  document.getElementById(id)?.classList.add('show');
}
function closeModal(id) {
  document.getElementById(id)?.classList.remove('show');
}

/** Wire backdrop-click close + optional close buttons for a modal element */
function wireModal(modal, ...closeBtns) {
  if (!modal) return;
  modal.addEventListener('click', (e) => { if (e.target === modal) modal.classList.remove('show'); });
  // Skip anything that isn't a real listener target. Without this guard, a
  // single typo (e.g. passing a function or null instead of a button element)
  // throws and aborts the rest of the calling module's init — which can take
  // down license.js (FREE TIER badge), TOTP wiring, etc.
  closeBtns.forEach(btn => {
    if (btn && typeof btn.addEventListener === 'function') {
      btn.addEventListener('click', () => modal.classList.remove('show'));
    }
  });
}

/** Toast-style notification (replaces all alert() usage) */
function showNotification(text, type = 'info', duration = 3000) {
  const existingNotif = document.querySelector('.deploy-notification');
  if (existingNotif) existingNotif.remove();

  const colors = {
    info:    { bg: '#2196F3',       fg: '#fff' },
    success: { bg: 'var(--ok-bg)',  fg: 'var(--ok-fg)' },
    error:   { bg: '#f44336',       fg: '#fff' },
    warning: { bg: '#ff9800',       fg: '#fff' }
  };

  const c = colors[type] || colors.info;
  const msg = document.createElement('div');
  msg.className = 'deploy-notification';
  msg.textContent = text;
  msg.style.cssText = `
    position: fixed; top: 20px; right: 20px;
    background: ${c.bg}; color: ${c.fg};
    padding: 16px 24px; border-radius: 8px;
    box-shadow: 0 4px 12px rgba(0,0,0,.3);
    z-index: 10000; animation: slideIn 0.3s ease-out;
    max-width: 400px; white-space: pre-line; font-size: 14px;
  `;
  document.body.appendChild(msg);
  if (duration > 0) setTimeout(() => msg.remove(), duration);
}

/** Relative time display (e.g. "5m ago", "2h ago") */
function timeAgo(ts) {
  const diff = Date.now() - new Date(ts).getTime();
  if (diff < 60000) return 'just now';
  if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
  if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago';
  return Math.floor(diff / 86400000) + 'd ago';
}

// ===== SAFE STORAGE WRAPPERS =====
// Prevents crashes in Safari private browsing, quota exceeded, or restricted environments
function safeGet(key, fallback = null) {
  try { const v = localStorage.getItem(key); return v !== null ? v : fallback; } catch (_) { return fallback; }
}
function safeSet(key, value) {
  try { localStorage.setItem(key, value); } catch (_) { /* quota exceeded or private mode */ }
}
function safeRemove(key) {
  try { localStorage.removeItem(key); } catch (_) {}
}
function safeSessionGet(key, fallback = null) {
  try { const v = sessionStorage.getItem(key); return v !== null ? v : fallback; } catch (_) { return fallback; }
}
function safeSessionSet(key, value) {
  try { sessionStorage.setItem(key, value); } catch (_) {}
}
/** Parse JSON from localStorage with fallback — avoids try/catch at every call site */
function safeGetJSON(key, fallback = null) {
  try { const v = localStorage.getItem(key); return v ? JSON.parse(v) : fallback; } catch (_) { return fallback; }
}

/** Escape HTML entities for safe innerHTML insertion (handles both content and attributes) */
function escapeHtml(text) {
  return String(text ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}

/** Inject modal HTML into DOM (idempotent — skips if element already exists) */
function injectModal(id, html) {
  if (document.getElementById(id)) return;
  document.body.insertAdjacentHTML('beforeend', html);
}

// ===== EVENT BUS =====
// Lightweight pub/sub for cross-module communication without window globals.
// Usage: DC_BUS.on('services:loaded', handler); DC_BUS.emit('services:loaded', data);
const DC_BUS = {
  _handlers: {},
  on(event, fn) { (this._handlers[event] ||= []).push(fn); },
  off(event, fn) { this._handlers[event] = this._handlers[event]?.filter(h => h !== fn); },
  emit(event, data) { this._handlers[event]?.forEach(fn => fn(data)); }
};

// ===== CENTRALIZED APP STATE =====
// Single source of truth for the services array. Modules should use AppState
// instead of mutating window.APPS directly. Emits 'apps:changed' on updates.
const AppState = {
  _apps: [],
  getApps() { return this._apps; },
  setApps(apps) { this._apps = apps; window.APPS = apps; DC_BUS.emit('apps:changed', apps); },
  findApp(id) { return this._apps.find(a => a.id === id); },
  addApp(app) { this._apps.push(app); window.APPS = this._apps; DC_BUS.emit('apps:changed', this._apps); },
  removeApp(id) {
    const idx = this._apps.findIndex(a => a.id === id);
    if (idx > -1) { this._apps.splice(idx, 1); window.APPS = this._apps; DC_BUS.emit('apps:changed', this._apps); }
    return idx > -1;
  },
  updateApp(id, changes) {
    const app = this._apps.find(a => a.id === id);
    if (app) {
      for (const [k, v] of Object.entries(changes)) {
        if (k !== '__proto__' && k !== 'constructor' && k !== 'prototype') app[k] = v;
      }
      DC_BUS.emit('apps:changed', this._apps);
    }
    return app;
  }
};
