/**
 * Serviços — Orquestração multi-produto (Spec 018)
 * Fase 1: clientes derivados VM112 + tiles cPanel + purge Spec 017.
 */
const DeskServices = (() => {
  const API_BASE = '/api';
  let _domains = [];
  let _clients = [];
  let _query = '';
  let _selectedClientId = null;
  let _selectedServiceId = 'email_tenant';
  let _billingByDomain = {};

  const OPS_SCOPES = [
    { id: 'carbonio', label: 'Carbonio', desc: 'Domínio e contas de e-mail (zmprov)' },
    { id: 'site', label: 'ligbox-sites', desc: 'Pasta do tenant em /opt/ligbox-sites/domains/' },
    { id: 'portal', label: 'Portal users', desc: 'Contas Self-Service ligadas ao domínio' },
    { id: 'cloudflare', label: 'Cloudflare', desc: 'Zona DNS na conta Ibytera' },
    { id: 'traefik', label: 'Traefik / SNI', desc: 'Routers mail.* no CT114' },
    { id: 'desk', label: 'Desk OPS', desc: 'Webhooks, tickets e audit_domains' },
  ];

  const SERVICE_CATALOG = [
    {
      id: 'email_tenant',
      label: 'E-mail Tenant',
      desc: 'Domínio partilhado no Carbonio VM112',
      icon: '✉',
      wizard: 'vm112-mail',
      wizardHost: 'VM112',
      deliveryModel: 'saas',
      category: 'apps',
      phase: 'active',
    },
    {
      id: 'site_cms',
      label: 'Site / CMS',
      desc: 'Pasta ligbox-sites do domínio',
      icon: '🌐',
      wizard: 'vm112-mail',
      wizardHost: 'VM112',
      deliveryModel: 'saas',
      category: 'apps',
      phase: 'active',
    },
    {
      id: 'mail_dedicated',
      label: 'Servidor E-mail Dedicado',
      desc: 'VM ou CT exclusivo — wizard próprio (Proxmox)',
      icon: '🖥',
      wizard: null,
      wizardHost: 'futuro',
      deliveryModel: 'saas',
      category: 'apps',
      phase: 'planned',
    },
    {
      id: 'firewall',
      label: 'Firewall',
      desc: 'pfSense — NAT, regras, WAN',
      icon: '🛡',
      wizard: 'wizard-iaas-fw',
      wizardHost: 'futuro',
      deliveryModel: 'iaas',
      category: 'security',
      phase: 'planned',
    },
    {
      id: 'cloud',
      label: 'Cloud / IaaS',
      desc: 'VPS gerenciado no Proxmox',
      icon: '☁',
      wizard: 'wizard-iaas-vps',
      wizardHost: 'futuro',
      deliveryModel: 'iaas',
      category: 'infra',
      phase: 'planned',
    },
    {
      id: 'wazuh_domain',
      label: 'Wazuh por domínio',
      desc: 'SOC e agentes — wizard próprio',
      icon: '👁',
      wizard: 'wizard-soc-wazuh',
      wizardHost: 'futuro',
      deliveryModel: 'saas',
      category: 'security',
      phase: 'planned',
    },
  ];

  const CATEGORY_LABELS = {
    apps: 'Aplicações (MOSP)',
    infra: 'Infraestrutura',
    security: 'Segurança',
  };

  const DELIVERY_LABELS = {
    saas: 'SaaS',
    iaas: 'IaaS',
    paas: 'PaaS',
    traditional: 'Suporte',
  };

  function canAccess() {
    if (window.DeskModules?.loaded && !window.DeskModules.isEnabled('overview-home')) return false;
    return typeof canManageVm112Domains === 'function' && canManageVm112Domains();
  }

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

  function formatFetchError(err) {
    const msg = String(err?.message || err || '');
    if (err?.name === 'AbortError' || msg.includes('aborted') || msg.includes('Failed to fetch')) {
      return 'VM112 não respondeu a tempo — o wizard pode estar sobrecarregado ou a reiniciar. Aguarde 1–2 min e clique «Tentar de novo».';
    }
    return msg;
  }

  async function apiFetch(path, options = {}, timeoutMs = 60000) {
    let res;
    try {
      res = await fetchWithTimeout(`${API_BASE}${path}`, {
        headers: authHeaders({ 'Content-Type': 'application/json', ...(options.headers || {}) }),
        ...options,
      }, timeoutMs);
    } catch (err) {
      throw new Error(formatFetchError(err));
    }
    if (res.status === 401) { logout(); throw new Error('sessão expirada'); }
    if (!res.ok) {
      const data = await res.json().catch(() => ({}));
      const detail = data.detail;
      let errText = typeof detail === 'string' ? detail : JSON.stringify(detail || `${res.status}`);
      if (res.status === 504) {
        errText = '504 Gateway Timeout — o purge pode demorar vários minutos. Verifique na VM112 se concluiu antes de repetir.';
      }
      throw new Error(errText);
    }
    return res.json();
  }

  function buildClients(domains) {
    return domains.map((d) => ({
      id: d.domain,
      domain: d.domain,
      displayName: d.domain,
      subtitle: d.portal_admin_email || 'sem admin portal',
      health: d.carbonio_exists ? 'ok' : 'warn',
      raw: d,
    }));
  }

  async function loadBillingMap() {
    if (!window.DeskModules?.isEnabled('billing-recurrence')) return;
    try {
      const data = await apiFetch('/v1/billing/accounts?limit=500');
      _billingByDomain = {};
      for (const a of data.accounts || []) _billingByDomain[a.domain] = a;
    } catch { _billingByDomain = {}; }
  }

  async function loadDomains() {
    const data = await apiFetch('/v1/vm112/domains');
    _domains = data.domains || [];
    await loadBillingMap();
    _clients = buildClients(_domains);
    if (_selectedClientId && !_clients.some((c) => c.id === _selectedClientId)) {
      _selectedClientId = null;
    }
    return _domains;
  }

  function filteredClients() {
    const q = _query.trim().toLowerCase();
    if (!q) return _clients;
    return _clients.filter((c) => {
      const blob = [
        c.domain,
        c.subtitle,
        c.raw.mail_host,
        (c.raw.portal_users || []).map((u) => u.login_id).join(' '),
      ].join(' ').toLowerCase();
      return blob.includes(q);
    });
  }

  function selectedClient() {
    return _clients.find((c) => c.id === _selectedClientId) || null;
  }

  function serviceStatus(serviceId, client) {
    if (!client) return 'inactive';
    const d = client.raw;
    if (serviceId === 'email_tenant') {
      if (d.carbonio_exists || d.site_folder_exists || (d.portal_users || []).length) return 'active';
      return 'inactive';
    }
    if (serviceId === 'site_cms') {
      return d.site_folder_exists ? 'active' : 'inactive';
    }
    const cat = SERVICE_CATALOG.find((s) => s.id === serviceId);
    return cat?.phase === 'planned' ? 'planned' : 'inactive';
  }

  function statusLabel(status) {
    if (status === 'active') return 'Activo';
    if (status === 'planned') return 'Em breve';
    return 'Não contratado';
  }

  function activeOfferCount(client) {
    return SERVICE_CATALOG.filter((s) => serviceStatus(s.id, client) === 'active').length;
  }

  function statsHtml() {
    const billingActive = Object.values(_billingByDomain).filter((a) => a.recurrence_active).length;
    const total = _clients.length;
    const emailActive = _clients.filter((c) => serviceStatus('email_tenant', c) === 'active').length;
    const sites = _clients.filter((c) => c.raw.site_folder_exists).length;
    const logins = _clients.reduce((n, c) => n + (c.raw.portal_users || []).length, 0);
    return `
      <div class="servicos-stat"><strong>${total}</strong><span>clientes</span></div>
      <div class="servicos-stat"><strong>${emailActive}</strong><span>tenant e-mail ativo</span></div>
      <div class="servicos-stat"><strong>${sites}</strong><span>tenant site CMS</span></div>
      <div class="servicos-stat"><strong>${logins}</strong><span>logins portal</span></div>
      <div class="servicos-stat"><strong>${billingActive}</strong><span>recorrências</span></div>`;
  }

  function clientRow(c) {
    const sel = _selectedClientId === c.id ? ' servicos-client-row--selected' : '';
    const emailSt = serviceStatus('email_tenant', c);
    const offers = activeOfferCount(c);
    return `
      <button type="button" class="servicos-client-row${sel}" data-client-id="${esc(c.id)}">
        <span class="servicos-client-dot ${c.health}"></span>
        <div class="servicos-client-main">
          <strong>${esc(c.displayName)}</strong>
          <span>${esc(c.subtitle)} · ${offers} tenant(s) de oferta</span>
        </div>
        <span class="servicos-client-badge servicos-client-badge--${emailSt}">${statusLabel(emailSt)}</span>
        ${_billingByDomain[c.domain]?.recurrence_active ? `<span class="servicos-billing-badge servicos-billing-badge--active" data-billing-domain="${esc(c.domain)}" title="Recorrência ativa">💳</span>` : (_billingByDomain[c.domain] ? `<span class="servicos-billing-badge" data-billing-domain="${esc(c.domain)}" title="Cobrança">💳</span>` : '')}
      </button>`;
  }

  function clientsListHtml() {
    const rows = filteredClients();
    return rows.length
      ? rows.map(clientRow).join('')
      : '<p class="servicos-empty">Nenhum cliente encontrado.</p>';
  }

  function serviceTile(service, client) {
    const status = client ? serviceStatus(service.id, client) : 'inactive';
    const clickable = status === 'active' && service.id === 'email_tenant';
    const sel = _selectedServiceId === service.id ? ' servicos-tile--selected' : '';
    const tag = statusLabel(status);
    const dm = DELIVERY_LABELS[service.deliveryModel] || service.deliveryModel;
    const wizNote = service.wizard
      ? `wizard: ${service.wizard} (${service.wizardHost})`
      : (service.phase === 'planned' ? 'wizard próprio — planeamento MOSP' : '');
    return `
      <article class="servicos-tile servicos-tile--${status}${sel}${clickable ? ' servicos-tile--clickable' : ''}"
        data-service-id="${esc(service.id)}"
        data-clickable="${clickable ? '1' : '0'}">
        <span class="servicos-tile-tier servicos-tile-tier--${esc(service.deliveryModel)}">${esc(dm)}</span>
        <div class="servicos-tile-icon" aria-hidden="true">${service.icon}</div>
        <h4 class="servicos-tile-title">${esc(service.label)}</h4>
        <p class="servicos-tile-desc">${esc(service.desc)}</p>
        <span class="servicos-tile-status servicos-tile-status--${status}">${tag}</span>
        ${wizNote ? `<span class="servicos-tile-wizard">${esc(wizNote)}</span>` : ''}
        ${clickable ? '<span class="servicos-tile-action">Gerir / Purge →</span>' : ''}
      </article>`;
  }

  function servicesGridHtml() {
    const client = selectedClient();
    if (!client) {
      return '<div class="servicos-empty servicos-empty--center">Seleccione um cliente à esquerda para ver os tenants de oferta.</div>';
    }
    const byCat = {};
    for (const s of SERVICE_CATALOG) {
      const cat = s.category || 'apps';
      if (!byCat[cat]) byCat[cat] = [];
      byCat[cat].push(s);
    }
    const sections = Object.keys(CATEGORY_LABELS).map((cat) => {
      const items = byCat[cat];
      if (!items?.length) return '';
      return `
        <div class="servicos-tiles-section">
          <h4 class="servicos-tiles-section-title">${esc(CATEGORY_LABELS[cat])}</h4>
          <div class="servicos-tiles-grid">
            ${items.map((s) => serviceTile(s, client)).join('')}
          </div>
        </div>`;
    }).join('');
    return `
      <div class="servicos-client-banner">
        <strong>${esc(client.displayName)}</strong>
        <span>${esc(client.subtitle)} · ${esc(client.raw.mail_host || '')}</span>
      </div>
      ${sections}
      <p class="servicos-wizard-note">Cada oferta MOSP terá <strong>wizard próprio</strong> (VM112 = só e-mail). Orquestração aqui no Desk VM122 — sem alterar a Hero do portal.</p>`;
  }

  function scopesHtml() {
    const client = selectedClient();
    if (!client) {
      return '<p class="servicos-scope-hint">Escolha um cliente e o serviço <strong>E-mail Tenant</strong> para ver o escopo de purge OPS.</p>';
    }
    if (_selectedServiceId !== 'email_tenant') {
      return `<p class="servicos-scope-hint">Escopo OPS detalhado disponível para <strong>E-mail Tenant</strong>. Serviço actual: ${esc(SERVICE_CATALOG.find((s) => s.id === _selectedServiceId)?.label || _selectedServiceId)}.</p>`;
    }
    return OPS_SCOPES.map((s) => `
      <div class="servicos-scope-item">
        <strong>${esc(s.label)}</strong>
        <span>${esc(s.desc)}</span>
      </div>`).join('');
  }

  function pageHtml() {
    return `
      <div class="servicos-page">
        <header class="servicos-header">
          <div>
            <h2 class="servicos-title">Orquestração de Serviços</h2>
            <p class="servicos-sub">Desk VM122 · Orquestração MOSP</p>
            <p class="servicos-lead">Clientes e <strong>tenants de oferta</strong> — cada produto com wizard próprio; aqui gere estado OPS e purge de teste.</p>
          </div>
          <button type="button" class="btn btn-ghost" id="servicos-refresh">Actualizar</button>
        </header>
        <div class="servicos-stats-row" id="servicos-stats">${statsHtml()}</div>
        <div class="servicos-layout">
          <section class="servicos-panel servicos-panel--clients">
            <div class="servicos-panel-head">
              <h3>Clientes <span class="servicos-count" id="servicos-client-count">${filteredClients().length}</span></h3>
              <input type="search" id="servicos-search" class="servicos-search" placeholder="Pesquisar cliente…" value="${esc(_query)}"/>
            </div>
            <div class="servicos-panel-body" id="servicos-client-list">${clientsListHtml()}</div>
          </section>
          <section class="servicos-panel servicos-panel--services">
            <div class="servicos-panel-head"><h3>Tenants de Oferta</h3></div>
            <div class="servicos-panel-body" id="servicos-services">${servicesGridHtml()}</div>
          </section>
          <aside class="servicos-panel servicos-panel--scopes">
            <div class="servicos-panel-head"><h3>Escopo OPS</h3></div>
            <div class="servicos-panel-body servicos-scopes" id="servicos-scopes">${scopesHtml()}</div>
            <p class="servicos-scope-note">Purge (Spec 017): tile E-mail Tenant → modal → senha Root + confirmação do domínio.</p>
          </aside>
        </div>
      </div>`;
  }

  function refreshPanels(root) {
    const list = root.querySelector('#servicos-client-list');
    const count = root.querySelector('#servicos-client-count');
    const services = root.querySelector('#servicos-services');
    const scopes = root.querySelector('#servicos-scopes');
    const stats = root.querySelector('#servicos-stats');
    if (list) list.innerHTML = clientsListHtml();
    if (count) count.textContent = String(filteredClients().length);
    if (services) services.innerHTML = servicesGridHtml();
    if (scopes) scopes.innerHTML = scopesHtml();
    if (stats) stats.innerHTML = statsHtml();
    bindClientClicks(root);
    bindServiceClicks(root);
  }

  function bindPage(root) {
    root.querySelector('#servicos-refresh')?.addEventListener('click', async () => {
      root.querySelector('#servicos-services').innerHTML = '<p class="loading">A actualizar…</p>';
      await loadDomains();
      refreshPanels(root);
    });
    root.querySelector('#servicos-search')?.addEventListener('input', (e) => {
      _query = e.target.value;
      refreshPanels(root);
    });
    bindClientClicks(root);
    bindServiceClicks(root);
  }

  function bindClientClicks(root) {
    root.querySelectorAll('[data-client-id]').forEach((btn) => {
      btn.addEventListener('click', () => {
        _selectedClientId = btn.dataset.clientId;
        _selectedServiceId = 'email_tenant';
        refreshPanels(root);
      });
    });
  }

  function bindServiceClicks(root) {
    root.querySelectorAll('[data-service-id]').forEach((tile) => {
      tile.addEventListener('click', () => {
        _selectedServiceId = tile.dataset.serviceId;
        const client = selectedClient();
        if (tile.dataset.clickable === '1' && client) {
          openModal(client.domain);
        }
        refreshPanels(root);
      });
    });
  }

  function formatTs(iso) {
    if (!iso) return '—';
    try {
      const d = new Date(iso);
      return d.toLocaleTimeString('pt-PT', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
    } catch {
      return String(iso);
    }
  }

  const PURGE_WAIT_STEPS = [
    ...OPS_SCOPES.map((s) => s.label),
    'Desk — webhook_events',
    'Desk — tickets',
    'Desk — audit_domains',
    'Desk — assist_sessions',
    'Desk — audit_checks',
    'Purge concluído',
  ];

  let _purgeElapsedTimer = null;
  let _purgeLiveSteps = [];

  function upsertPurgeStep(step) {
    if (!step) return;
    const runVm112 = _purgeLiveSteps.findIndex(
      (s) => s.status === 'running' && String(s.label).includes('VM112')
    );
    if (runVm112 >= 0 && step.status === 'ok' && String(step.label).includes('VM112')) {
      _purgeLiveSteps.splice(runVm112, 1);
    }
    const sameLabel = _purgeLiveSteps.findIndex((s) => s.label === step.label);
    if (sameLabel >= 0) {
      _purgeLiveSteps[sameLabel] = step;
    } else {
      _purgeLiveSteps.push(step);
    }
    renderPurgeTimeline(_purgeLiveSteps, {
      running: _purgeLiveSteps.some((s) => s.status === 'running'),
    });
  }

  function onPurgeHeartbeat(ev) {
    const idx = _purgeLiveSteps.findIndex(
      (s) => s.status === 'running' && String(s.label).includes('VM112')
    );
    if (idx < 0) return;
    _purgeLiveSteps[idx] = {
      ..._purgeLiveSteps[idx],
      detail: `Em curso na VM112 (${ev.elapsed || 0}s)`,
    };
    renderPurgeTimeline(_purgeLiveSteps, { running: true });
  }

  function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  function isNetworkFetchError(err) {
    const msg = String(err?.message || err || '');
    return msg === 'Failed to fetch'
      || err?.name === 'AbortError'
      || msg.includes('NetworkError')
      || msg.includes('network');
  }

  async function recoverPurgeJob(domain, jobId) {
    const q = domain ? `?domain=${encodeURIComponent(domain)}` : '';
    return apiFetch(`/v1/vm112/purge/jobs/${encodeURIComponent(jobId)}/recover${q}`, {
      method: 'POST',
      body: '{}',
    }, 60000);
  }

  function applyPurgeJobToTimeline(job) {
    if (!job) return;
    const steps = Array.isArray(job.timeline) ? job.timeline : [];
    if (!steps.length) {
      if (job.status === 'running') {
        upsertPurgeStep({
          at: new Date().toISOString(),
          label: 'Purge em execução',
          status: 'running',
          detail: 'A aguardar passos da VM112…',
        });
      }
      return;
    }
    for (const step of steps) upsertPurgeStep(step);
    renderPurgeTimeline(_purgeLiveSteps, { running: job.status === 'running' });
  }

  async function showPurgeSuccess(done, domain) {
    applyPurgeJobToTimeline(done);
    const deskTotal = Object.values(done?.desk || {}).reduce((a, b) => a + Number(b || 0), 0);
    upsertPurgeStep({
      at: new Date().toISOString(),
      label: 'Purge concluído',
      status: 'ok',
      detail: deskTotal ? `Desk: ${deskTotal} registo(s) removido(s)` : (done?.recovered ? 'Recuperação automática' : 'Concluído'),
    });
    renderPurgeTimeline(_purgeLiveSteps, { running: false });
    const msg = document.getElementById('vm112-purge-msg');
    const btn = document.getElementById('vm112-purge-btn');
    if (msg) {
      msg.textContent = `✓ Purge concluído com sucesso.${deskTotal ? ` (${deskTotal} registo(s) Desk)` : ''}`;
      msg.classList.add('vm112-purge-success');
    }
    if (btn) { btn.textContent = 'Concluído ✓'; btn.disabled = true; }
    _domains = _domains.filter((d) => d.domain !== domain);
    await loadBillingMap();
    _clients = buildClients(_domains);
    if (_selectedClientId === domain) _selectedClientId = null;
    setTimeout(() => {
      const el = document.getElementById('overview-home-content');
      if (el) renderPage(el);
      closeModal();
    }, 8000);
  }

  async function tryRecoverPurge(domain, jobId) {
    try {
      const job = await recoverPurgeJob(domain, jobId);
      applyPurgeJobToTimeline(job);
      return job?.status === 'done' ? job : null;
    } catch {
      return null;
    }
  }

  async function pollPurgeJob(domain, confirmDomain, rootPassword) {
    const start = await apiFetch(`/v1/vm112/domains/${encodeURIComponent(domain)}/purge/jobs`, {
      method: 'POST',
      body: JSON.stringify({ confirm_domain: confirmDomain, root_password: rootPassword }),
    }, 60000);
    const jobId = start.job_id;
    if (!jobId) throw new Error('Job purge não iniciado');
    _lastPurgeJobId = jobId;
    let networkErrors = 0;
    while (true) {
      let job;
      try {
        job = await apiFetch(`/v1/vm112/purge/jobs/${encodeURIComponent(jobId)}`, {}, 60000);
        networkErrors = 0;
      } catch (e) {
        const msg = String(e.message || '');
        if (msg.includes('não encontrado') || msg.includes('404') || msg === '500' || msg.includes('502') || msg.includes('503')) {
          const recovered = await tryRecoverPurge(domain, jobId);
          if (recovered) return recovered;
        }
        if (isNetworkFetchError(e)) {
          networkErrors += 1;
          upsertPurgeStep({
            at: new Date().toISOString(),
            label: 'Ligação ao servidor',
            status: 'running',
            detail: `Reconectando… (tentativa ${networkErrors})`,
          });
          if (networkErrors >= 2) {
            const recovered = await tryRecoverPurge(domain, jobId);
            if (recovered) return recovered;
          }
          await sleep(2500);
          continue;
        }
        throw e;
      }
      applyPurgeJobToTimeline(job);
      if (job.elapsed_vm112) {
        const el = document.getElementById('vm112-purge-elapsed');
        if (el) el.textContent = `${job.elapsed_vm112}s`;
      }
      if (job.status === 'done') {
        return job;
      }
      if (job.status === 'error') {
        throw new Error(job.error || job.timeline?.find((s) => s.status === 'fail')?.detail || 'Purge falhou');
      }
      await sleep(2000);
    }
  }

  async function consumePurgeStream(domain, confirmDomain, rootPassword) {
    const res = await fetchWithTimeout(
      `${API_BASE}/v1/vm112/domains/${encodeURIComponent(domain)}/purge/stream`,
      {
        method: 'POST',
        headers: authHeaders({
          'Content-Type': 'application/json',
          Accept: 'text/event-stream',
        }),
        body: JSON.stringify({
          confirm_domain: confirmDomain,
          root_password: rootPassword,
        }),
      },
      600000
    );
    if (res.status === 401) { logout(); throw new Error('sessão expirada'); }
    if (!res.ok) {
      const data = await res.json().catch(() => ({}));
      const detail = data.detail;
      throw new Error(typeof detail === 'string' ? detail : `HTTP ${res.status}`);
    }
    const reader = res.body?.getReader();
    if (!reader) throw new Error('Stream indisponível no browser');
    const decoder = new TextDecoder();
    let buf = '';
    let donePayload = null;
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      buf += decoder.decode(value, { stream: true });
      const parts = buf.split('\n\n');
      buf = parts.pop() || '';
      for (const part of parts) {
        const line = part.split('\n').find((l) => l.startsWith('data: '));
        if (!line) continue;
        let ev;
        try {
          ev = JSON.parse(line.slice(6));
        } catch {
          continue;
        }
        if (ev.type === 'step') upsertPurgeStep(ev.step);
        else if (ev.type === 'heartbeat') onPurgeHeartbeat(ev);
        else if (ev.type === 'error') {
          upsertPurgeStep(ev.step || { at: new Date().toISOString(), label: 'Purge falhou', status: 'fail', detail: 'erro' });
          throw new Error(ev.step?.detail || 'Purge falhou');
        } else if (ev.type === 'done') {
          upsertPurgeStep(ev.step);
          donePayload = ev;
        }
      }
    }
    return donePayload;
  }

  function stopPurgeElapsed() {
    if (_purgeElapsedTimer) {
      clearInterval(_purgeElapsedTimer);
      _purgeElapsedTimer = null;
    }
  }

  function openPurgeDrawer() {
    const shell = document.getElementById('vm112-modal-shell');
    const drawer = document.getElementById('vm112-purge-drawer');
    if (shell) shell.classList.add('vm112-modal-shell--purge-open');
    if (drawer) drawer.setAttribute('aria-hidden', 'false');
  }

  function closePurgeDrawer() {
    stopPurgeElapsed();
    const shell = document.getElementById('vm112-modal-shell');
    const drawer = document.getElementById('vm112-purge-drawer');
    const list = document.getElementById('vm112-purge-timeline');
    const elapsed = document.getElementById('vm112-purge-elapsed');
    if (shell) shell.classList.remove('vm112-modal-shell--purge-open');
    if (drawer) drawer.setAttribute('aria-hidden', 'true');
    if (list) list.innerHTML = '';
    if (elapsed) elapsed.textContent = '—';
  }

  function renderPurgeTimeline(steps, { running = false } = {}) {
    const list = document.getElementById('vm112-purge-timeline');
    if (!list) return;
    list.innerHTML = (steps || []).map((step, i) => {
      const status = step.status || 'pending';
      const isRun = running && status === 'running';
      return `
        <li class="vm112-purge-step vm112-purge-step--${esc(status)}${isRun ? ' vm112-purge-step--pulse' : ''}">
          <span class="vm112-purge-step-time">${esc(formatTs(step.at))}</span>
          <div class="vm112-purge-step-body">
            <strong>${esc(step.label)}</strong>
            ${step.detail ? `<span>${esc(step.detail)}</span>` : ''}
          </div>
        </li>`;
    }).join('');
    list.scrollTop = list.scrollHeight;
  }

  function startPurgeElapsed() {
    const el = document.getElementById('vm112-purge-elapsed');
    const t0 = Date.now();
    stopPurgeElapsed();
    const tick = () => {
      const sec = Math.floor((Date.now() - t0) / 1000);
      if (el) el.textContent = `${sec}s`;
    };
    tick();
    _purgeElapsedTimer = setInterval(tick, 1000);
  }

  function initPurgeTimelineRunning() {
    _purgeLiveSteps = [];
    _lastPurgeJobId = null;
    openPurgeDrawer();
    startPurgeElapsed();
    upsertPurgeStep({
      at: new Date().toISOString(),
      label: 'A iniciar purge',
      status: 'running',
      detail: 'A validar credenciais…',
    });
  }

  async function runPurge(domain) {
    const msg = document.getElementById('vm112-purge-msg');
    const confirmDomain = document.getElementById('vm112-purge-confirm')?.value?.trim() || '';
    const rootPassword = document.getElementById('vm112-purge-root-pwd')?.value || '';
    if (!confirmDomain || !rootPassword) {
      if (msg) msg.textContent = 'Preencha domínio e senha Root.';
      return;
    }
    if (!window.confirm(`PURGE definitivo de ${domain}?`)) return;
    const btn = document.getElementById('vm112-purge-btn');
    if (btn) { btn.disabled = true; btn.textContent = 'A apagar…'; }
    if (msg) { msg.textContent = 'A executar purge…'; msg.classList.remove('vm112-purge-success'); }
    initPurgeTimelineRunning();
    try {
      const done = await pollPurgeJob(domain, confirmDomain, rootPassword);
      stopPurgeElapsed();
      showPurgeSuccess(done, domain);
      return;
    } catch (e) {
      stopPurgeElapsed();
      if (isNetworkFetchError(e) && _purgeLiveSteps.some((s) => s.status === 'ok')) {
        const recovered = _lastPurgeJobId
          ? await tryRecoverPurge(domain, _lastPurgeJobId).catch(() => null)
          : null;
        if (recovered) {
          showPurgeSuccess(recovered, domain);
          return;
        }
        showPurgeSuccess({ status: 'done', desk: {}, timeline: _purgeLiveSteps, recovered: true }, domain);
        return;
      }
      if (!_purgeLiveSteps.length) {
        const failAt = new Date().toISOString();
        renderPurgeTimeline([
          { at: failAt, label: 'Purge falhou', status: 'fail', detail: e.message || 'erro' },
        ]);
      }
      const errMsg = isNetworkFetchError(e)
        ? 'Ligação interrompida durante o purge — verifique se o domínio já saiu da lista'
        : (e.message || 'Purge falhou — verifique se o domínio já foi removido na lista');
      if (msg) msg.textContent = errMsg;
      if (btn) { btn.disabled = false; btn.textContent = 'Apagar domínio e todos os dados'; }
    }
  }

  function closeModal() {
    closePurgeDrawer();
    const modal = document.getElementById('vm112-domain-modal');
    if (!modal) return;
    modal.classList.add('hidden');
    modal.setAttribute('aria-hidden', 'true');
  }

  function openModal(domain) {
    const modal = document.getElementById('vm112-domain-modal');
    const body = document.getElementById('vm112-domain-modal-body');
    const title = document.getElementById('vm112-domain-modal-title');
    const sub = document.getElementById('vm112-domain-modal-sub');
    if (!modal || !body) return;
    modal.classList.remove('hidden');
    modal.setAttribute('aria-hidden', 'false');
    title.textContent = domain;
    sub.textContent = 'E-mail Tenant · a carregar…';
    body.innerHTML = '<p class="loading">A carregar detalhe VM112…</p>';
    loadModal(domain);
    modal.querySelectorAll('[data-close-vm112-modal]').forEach((el) => {
      el.onclick = closeModal;
    });
  }

  async function loadModal(domain) {
    const body = document.getElementById('vm112-domain-modal-body');
    const sub = document.getElementById('vm112-domain-modal-sub');
    try {
      const d = await apiFetch(`/v1/vm112/domains/${encodeURIComponent(domain)}`, {}, 180000);
      const accN = (d.accounts || []).length;
      sub.textContent = `E-mail Tenant · ${accN} conta(s) · ${d.mail_host || ''}`;
      const steps = (d.infra_status?.steps || [])
        .map((s) => `<li class="${s.ok ? 'ok' : 'fail'}"><strong>${esc(s.label)}</strong> — ${esc(s.message)}</li>`)
        .join('');
      const accounts = (d.accounts || [])
        .map((a) => `<li>${esc(a)}</li>`).join('') || '<li class="muted">Nenhuma</li>';
      const cf = d.cloudflare_zone;
      const cfTxt = cf?.name ? `Zona ${cf.name} (${cf.status || '—'})` : 'Sem zona Cloudflare Ibytera';
      body.innerHTML = `
        <div class="modal-section">
          <h4>Serviço: E-mail Tenant</h4>
          <p><strong>Cliente / domínio:</strong> ${esc(domain)}</p>
          <p><strong>Admin portal:</strong> ${esc(d.portal_admin_email || '—')}</p>
          <p><strong>Carbonio:</strong> ${d.carbonio_exists ? 'ativo' : 'ausente'}</p>
          <p><strong>Pasta site:</strong> ${d.site_folder_exists ? 'sim' : 'não'}</p>
          <p><strong>Cloudflare:</strong> ${esc(cfTxt)}</p>
        </div>
        <div class="modal-section">
          <h4>Infraestrutura VM112</h4>
          <ul class="vm112-infra-steps">${steps || '<li>—</li>'}</ul>
        </div>
        <div class="modal-section">
          <h4>Contas Carbonio</h4>
          <ul>${accounts}</ul>
        </div>
        <div class="modal-section vm112-purge-zone">
          <h4>Purge completo (testes — Spec 017)</h4>
          <p class="vm112-purge-warn">Remove Carbonio, site, portal users, Cloudflare, Traefik e registos Desk. <strong>Irreversível.</strong> Requer senha Root.</p>
          <label>Confirmar domínio</label>
          <input type="text" id="vm112-purge-confirm" class="vm112-purge-input" placeholder="${esc(domain)}" autocomplete="off"/>
          <label>Senha Root</label>
          <input type="password" id="vm112-purge-root-pwd" class="vm112-purge-input" autocomplete="current-password"/>
          <button type="button" class="btn btn-danger" id="vm112-purge-btn">Apagar domínio e todos os dados</button>
          <p id="vm112-purge-msg" class="ticket-meta"></p>
        </div>`;
      body.querySelector('#vm112-purge-btn')?.addEventListener('click', () => runPurge(domain));
    } catch (e) {
      body.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`;
    }
  }

  function setPollStatus(root, message, isError = false) {
    if (!root) return;
    let el = root.querySelector('#servicos-poll-status');
    if (!message) {
      el?.remove();
      return;
    }
    if (!el) {
      el = document.createElement('p');
      el.id = 'servicos-poll-status';
      el.className = 'servicos-poll-status';
      root.querySelector('.servicos-header')?.appendChild(el);
    }
    el.className = `servicos-poll-status${isError ? ' servicos-poll-status--err' : ''}`;
    el.textContent = message;
  }

  async function renderPage(container, options = {}) {
    const { poll = false } = options;
    if (!container) return;
    if (!canAccess()) {
      container.innerHTML = '<p class="loading">Sem permissão — perfil Admin (super_admin ou ops_lead) necessário.</p>';
      return;
    }
    const hasPage = Boolean(container.querySelector('.servicos-page'));
    if (poll && hasPage) {
      try {
        await loadDomains();
        refreshPanels(container);
        setPollStatus(container, '');
      } catch (e) {
        setPollStatus(container, `Actualização falhou: ${e.message}`, true);
      }
      return;
    }
    if (!hasPage) {
      container.innerHTML = '<p class="loading">A carregar clientes e serviços VM112…</p>';
    }
    try {
      await loadDomains();
      if (_clients.length && !_selectedClientId) {
        _selectedClientId = _clients[0].id;
      }
      container.innerHTML = pageHtml();
      bindPage(container);
      setPollStatus(container, '');
    } catch (e) {
      if (hasPage) {
        setPollStatus(container, `Erro ao carregar VM112: ${e.message}`, true);
        return;
      }
      container.innerHTML = `<div class="servicos-page"><p class="servicos-empty">Erro ao carregar VM112: ${esc(e.message)}</p><button type="button" class="btn" id="servicos-retry">Tentar de novo</button></div>`;
      container.querySelector('#servicos-retry')?.addEventListener('click', () => renderPage(container));
    }
  }

  return { renderPage, loadDomains, openModal, canAccess, SERVICE_CATALOG };
})();

window.DeskServices = DeskServices;
window.DeskAccounts = DeskServices;
