/**
 * MGT2 – SectorMapApp
 *
 * Application interactive affichant une carte Traveller Map dans un IFRAME.
 * Les clics sur la carte affichent les détails du monde dans le chat.
 */

import { searchWorlds } from './travellerMapApi.js';
import { TravelDialog } from './travelDialog.js';

const { ApplicationV2 } = foundry.applications.api;
const MODULE_ID = 'mgt2-compendium-amiral-denisov';

export class SectorMapApp extends ApplicationV2 {
  static DEFAULT_OPTIONS = {
    id: 'mgt2-sector-map',
    classes: ['mgt2-sector-map'],
    position: { width: 960, height: 720 },
    window: { icon: 'fas fa-map', resizable: true, controls: [] },
  };

  constructor(sector, subsector, { readOnly = false } = {}) {
    super();
    this._sector = sector;
    this._subsector = subsector;
    this._readOnly = readOnly;
    this._handler = null;
    this._mapHex = null;
    this._mapCenter = null;
    this._mapScale = null;
    this._searchTimeout = null;
  }

  get title() {
    if (!this._sector) return 'Carte stellaire — Traveller Map';
    return this._subsector
      ? `Sous-secteur ${this._subsector} — ${this._sector}`
      : `Secteur ${this._sector}`;
  }

  get _mapUrl() {
    const base = 'https://travellermap.com';
    const cb = this._readOnly ? '' : '&callback=postmessage';
    if (!this._sector) return `${base}/?style=mongoose&hideui=1`;
    if (this._subsector) {
      return `${base}/?sector=${encodeURIComponent(this._sector)}&subsector=${encodeURIComponent(this._subsector)}&style=mongoose&hideui=1${cb}`;
    }
    let url = `${base}/?sector=${encodeURIComponent(this._sector)}&style=mongoose&hideui=1${cb}`;
    if (this._mapHex) {
      url += `&hex=${this._mapHex}&scale=512`;
    } else if (this._mapCenter && this._mapScale) {
      url += `&center=${this._mapCenter}&scale=${this._mapScale}`;
    }
    return url;
  }

  /* ───── Rendu ───── */

  _replaceHTML(result, config) {
    const content = this.element?.querySelector('.window-content');
    if (!content) return;
    const html = typeof result === 'string' ? result : this._lastHTML;
    content.innerHTML = typeof html === 'string' ? html : '';
  }

  _renderHTML() {
    if (this._readOnly) {
      return `<div class="mgt2-sector-map-outer">
        <div class="mgt2-sector-map-toolbar mgt2-sector-map-toolbar--ro">
          <span class="mgt2-sector-map-label">${SectorMapApp._escapeAttr(this.title)}</span>
          <span class="mgt2-sector-map-hint">Carte partagée par le MJ</span>
        </div>
        <iframe
          src="${this._mapUrl}"
          class="mgt2-sector-map-frame"
          allow="clipboard-write"
          referrerpolicy="no-referrer">
        </iframe>
      </div>`;
    }
    return `<div class="mgt2-sector-map-outer">
      <div class="mgt2-sector-map-toolbar">
        <div class="mgt2-sector-map-search">
          <input type="text" class="mgt2-sector-map-input" placeholder="Rechercher un monde…" autocomplete="off">
          <ul class="mgt2-sector-map-results"></ul>
        </div>
        <span class="mgt2-sector-map-label">${this._sector ? `${this._sector}${this._subsector ? ` — ss.${this._subsector}` : ''}` : 'Toute la carte'}</span>
        <span class="mgt2-sector-map-hint">Cliquez sur un hex pour voir les détails du monde</span>
        <button type="button" class="mgt2-sector-map-sync" disabled title="Cliquez d'abord sur un hex pour définir la position à synchroniser"><i class="fas fa-users"></i> Synchroniser</button>
        <button type="button" class="mgt2-sector-map-travel" title="Planifier un voyage"><i class="fas fa-route"></i> Voyage</button>
      </div>
      <iframe
        src="${this._mapUrl}"
        class="mgt2-sector-map-frame"
        allow="clipboard-write"
        referrerpolicy="no-referrer">
      </iframe>
    </div>`;
  }

  async _onRender(context, options) {
    if (this._readOnly) {
      this._listen();
      return;
    }
    this._listen();
    this.element?.querySelector('.mgt2-sector-map-sync')?.addEventListener('click', () => {
      this._syncAll();
    });
    this.element?.querySelector('.mgt2-sector-map-travel')?.addEventListener('click', () => {
      this._openTravelDialog();
    });

    const input = this.element?.querySelector('.mgt2-sector-map-input');
    const results = this.element?.querySelector('.mgt2-sector-map-results');
    if (input && results) {
      input.addEventListener('input', () => {
        if (this._searchTimeout) clearTimeout(this._searchTimeout);
        this._searchTimeout = setTimeout(() => this._doSearch(input, results), 300);
      });
      input.addEventListener('blur', () => {
        setTimeout(() => { results.innerHTML = ''; }, 200);
      });
      input.addEventListener('focus', () => {
        if (input.value.trim().length >= 2) this._doSearch(input, results);
      });
    }
  }

  /* ───── Écoute des clics IFRAME ───── */

  _listen() {
    if (this._handler) return;
    this._handler = (event) => {
      const d = event.data || {};
      if (d.type === 'viewport') {
        this._sector = d.sector || this._sector;
        this._subsector = d.subsector || this._subsector;
        this._mapHex = d.hex || null;
        this._mapCenter = (d.x != null && d.y != null) ? `${d.x},${d.y}` : null;
        this._mapScale = d.scale || null;
        return;
      }

      // Click on map
      const wx = d.x ?? d.location?.x;
      const wy = d.y ?? d.location?.y;
      if (wx == null || wy == null) return;
      const x = Number(wx);
      const y = Number(wy);
      if (isNaN(x) || isNaN(y)) return;
      this._onMapClick({ x, y }).catch(err => {
        console.error('SectorMapApp | click handler failed:', err);
      });
    };
    window.addEventListener('message', this._handler);
  }

  async _onMapClick(loc) {
    const wx = loc?.x;
    const wy = loc?.y;
    if (wx == null || wy == null) return;

    const coordResp = await fetch(
      `https://travellermap.com/api/coordinates?x=${wx}&y=${wy}`
    );
    if (!coordResp.ok) { console.error('SectorMapApp | /api/coordinates failed', coordResp.status); return; }
    const coord = await coordResp.json();
    const { sx, sy, hx, hy } = coord;
    if (sx == null || hx == null || hy == null) { console.error('SectorMapApp | no sx/hx/hy in', coord); return; }

    const metaResp = await fetch(
      `https://travellermap.com/api/metadata?sx=${sx}&sy=${sy}`
    );
    if (!metaResp.ok) { console.error('SectorMapApp | /api/metadata failed', metaResp.status); return; }
    const meta = await metaResp.json();
    const sectorName = meta.Names?.[0]?.Text;
    if (!sectorName) { console.error('SectorMapApp | no Names[0].Text in metadata', meta); return; }

    // Store click position for sync
    this._sector = sectorName;
    this._subsector = null;
    this._mapHex = String(hx).padStart(2, '0') + String(hy).padStart(2, '0');
    this._mapCenter = `${wx},${wy}`;
    this._mapScale = loc.scale || 512;

    const hex = String(hx).padStart(2, '0') + String(hy).padStart(2, '0');
    const resp = await fetch(
      `https://travellermap.com/data/${encodeURIComponent(sectorName)}/${hex}`
    );
    if (!resp.ok) { console.error('SectorMapApp | /data failed', resp.status, sectorName, hex); return; }

    const data = await resp.json();
    const world = data.Worlds?.[0];
    if (!world) { console.error('SectorMapApp | no Worlds in data', data); return; }

    this._enableSyncButton(world.Name || hex);

    this._postWorldCard(world);
  }

  _enableSyncButton(worldName) {
    const btn = this.element?.querySelector('.mgt2-sector-map-sync');
    if (btn) {
      btn.disabled = false;
      btn.title = worldName ? `Synchroniser la vue sur ${worldName}` : 'Ouvrir la carte interactive chez tous les joueurs';
    }
  }

  /* ───── Carte de chat ───── */

  static _STARPORT = { A:'Excellent', B:'Bon', C:'Routinier', D:'Médiocre', E:'Frontière', X:'Aucun' };
  static _SIZE = ['Aucun (Astéroïde)','1 600 km','3 200 km','4 800 km','6 400 km','8 000 km','9 600 km','11 200 km','12 800 km','14 400 km','16 000 km'];
  static _ATMO = [
    'Aucune (vide)','Trace','Très ténue (polluée)','Très ténue','Ténue (polluée)','Ténue','Standard','Standard (polluée)','Dense','Dense (polluée)',
    'Exotique','Corrosive','Insidieuse','','',''];
  static _HYDRO = [ '0–5% (désert)','6–15%','16–25%','26–35%','36–45%','46–55%','56–65%','66–75%','76–85%','86–95%','96–100%' ];
  static _POP = ['','Dizaines','Centaines','Milliers','Dizaines de milliers','Centaines de milliers','Millions','Dizaines de millions','Centaines de millions','Milliards','Dizaines de milliards','','','','','',''];
  static _GOV = [
    'Aucun','Compagnie / Corporation','Démocratie participative','Oligarchie auto-perpétuée',
    'Démocratie représentative','Technocratie féodale','Gouvernement captif / Colonie',
    'Balkanisation','Bureaucratie de service civil','Bureaucratie impersonnelle',
    'Dictature charismatique','Dictature non-charismatique','Oligarchie charismatique',
    'Dictature religieuse','Oligarchie religieuse','Gouvernement tribal'];
  static _LAW = [
    'Aucune', 'Armes de poing, explosifs, poison','Armes à énergie portatives','Mitrailleuses, armes auto',
    'Armes d\'assaut légères, PM','Armes de poing individuelles','Toutes les armes à feu sauf neutralisateur',
    'Fusils, neutralisateur','Armes blanches, neutralisateur','Armes hors du domicile','Armes interdites',
    'Contrôle rigide','Aucune arme','Contrôle militariste sévère'];
  static _TL = [
    'Âge de pierre','Âge du bronze/fer','Médiéval','Grandes découvertes','Révolution industrielle',
    'Production mécanisée','Ère nucléaire','Pré-stellaire (ère de l\'information)','Propulsion à saut (1re gen)',
    'Propulsion à saut-2','Propulsion à saut-3','Propulsion à saut-4','Propulsion à saut-5',
    'Propulsion à saut-6','Transporteur','Moyenne stellaire'];

  static _hexVal(ch) {
    const n = parseInt(ch, 36);
    if (isNaN(n)) return -1;
    return n;
  }

  static _uwpDigit(desc, val) {
    return `<span class="uwp-dig" title="${desc}">${val}</span>`;
  }

  static _uwpBreakdown(uwp) {
    if (!uwp || uwp.length < 2) return '';
    const d = SectorMapApp._hexVal;
    const s = d(uwp[0]), sz = d(uwp[1]), a = d(uwp[2]), h = d(uwp[3]);
    const p = d(uwp[4]), g = d(uwp[5]), l = d(uwp[6]);
    const t = uwp.length > 8 ? d(uwp[8]) : -1;

    const lines = [];

    const starport = SectorMapApp._STARPORT[uwp[0]];
    lines.push(`<tr><td>${uwp[0]}</td><td>Starport</td><td>${starport ?? '—'}</td></tr>`);

    if (uwp[1] === 'F' || uwp[1] === 'f') {
      lines.push(`<tr><td>${uwp[1]}</td><td>Taille</td><td>Gaz géant</td></tr>`);
    } else if (sz >= 0 && sz <= 10) {
      const km = SectorMapApp._SIZE[sz];
      const grav = sz === 0 ? '0g' : (sz < 10 ? `0.${sz}g` : '1.0g+');
      lines.push(`<tr><td>${uwp[1]}</td><td>Taille</td><td>${km} (${grav})</td></tr>`);
    }

    if (a >= 0 && a <= 15) {
      const atmo = SectorMapApp._ATMO[a] ?? '—';
      lines.push(`<tr><td>${uwp[2]}</td><td>Atmosphère</td><td>${atmo}</td></tr>`);
    }

    if (h >= 0 && h <= 10) {
      lines.push(`<tr><td>${uwp[3]}</td><td>Hydrosphère</td><td>${SectorMapApp._HYDRO[h]}</td></tr>`);
    }

    if (p >= 0 && p <= 15) {
      lines.push(`<tr><td>${uwp[4]}</td><td>Population</td><td>${SectorMapApp._POP[p] ?? '—'}</td></tr>`);
    }

    if (g >= 0 && g <= 15) {
      lines.push(`<tr><td>${uwp[5]}</td><td>Gouvernement</td><td>${SectorMapApp._GOV[g] ?? '—'}</td></tr>`);
    }

    if (l >= 0 && l <= 15) {
      lines.push(`<tr><td>${uwp[6]}</td><td>Niveau légal</td><td>${SectorMapApp._LAW[l] ?? '—'}</td></tr>`);
    }

    if (t >= 0 && t <= 15) {
      lines.push(`<tr><td>${uwp[8]}</td><td>Technologie</td><td>${SectorMapApp._TL[t] ?? '—'}</td></tr>`);
    }

    return `<table class="uwp-breakdown"><tbody>${lines.join('')}</tbody></table>`;
  }

  static _REMARKS_HELP = {
    AB:'Anneau (ceinture)', AG:'Agricole', AN:'Site ancien', AS:'Astéroïde',
    BA:'Bande astéroïdale', CP:'Sous-secteur capitale', CS:'Colonie',
    CX:'Chasseur (Croiseur)', CY:'Colonie', DA:'Déchu', DE:'Désertique',
    DI:'Interdit (Diebar)', FL:'Fluides Lo', FO:'Interdit (Forbidden)',
    FR:'Gelé (Frozen)', GA:'Jardin (Garden)', HE:'Helios', HI:'Haute population',
    HT:'Haute technologie', IC:'Mondes gelés (Ice)', IN:'Industrialisé',
    LI:'Faible population', LO:'Faible population (Low)', LT:'Basse technologie (Low Tech)',
    MI:'Militaire', MR:'Mine (ressources)', NA:'Non-agricole',
    NI:'Non-industrialisé', OC:'Océanique', OX:'Oxydant',
    PA:'Pré-agricole (Pre-Agricultural)', PH:'Phosphore',
    PO:'Pauvre (Poor)', PR:'Pré-industriel (Pre-Industrial)',
    PX:'Prisonnier (exil)', PZ:'Puzzle (énigmatique)',
    RE:'Religieux (Religious)', RI:'Riche (Rich)',
    SA:'Bande d\'astéroïdes (Satellite)', SC:'Sainte (colonie)',
    SL:'Esclavage (Slave)', SO:'Soleil (Sol)', SP:'Désert (Despoiled)',
    SR:'Réserve (Reserve)', ST:'Base stellaire', SU:'Secteur capitale',
    TR:'Traces (Trace)', TU:'Tucannides', TZ:'Mondes Tz',
    UN:'Inhabité (Uninhabited)', VA:'Vide (Vacuum)',
    WA:'Monde aquatique (Water)', WT:'Monde d\'eau (Watery)',
  };

  static _STAR_TYPES = { O:'Bleu (hypergéante)', B:'Bleu-blanc', A:'Blanc', F:'Blanc-jaune', G:'Jaune (naine)', K:'Orange (naine)', M:'Rouge (naine)', L:'Brune', T:'Brune', Y:'Brune' };
  static _STAR_CLASS = { 'I':'Supergéante', 'II':'Géante brillante', 'III':'Géante', 'IV':'Sous-géante', 'V':'Naine (séquence principale)', 'VI':'Sous-naine', 'VII':'Naine blanche' };

  static _BASES_HELP = {
    N:'Base navale', S:'Base scout', W:'Relais', D:'Dépôt naval',
    T:'Base TAS', C:'Consulat', P:'Base pirate', R:'Base de réparation',
    K:'Base navale (K.)', X:'Relais Xboat',
  };

  static _NOBILITY = {
    B:'Chevalier (Baronet)', C:'Baron', D:'Marquis', E:'Comte', F:'Duc', G:'Archiduc', H:'Empereur',
  };

  static _IMPORTANCE = {
    '-5':'Très mineur', '-4':'Mineur', '-3':'Mineur', '-2':'Très secondaire', '-1':'Secondaire',
    '0':'Ordinaire', '1':'Important', '2':'Important', '3':'Très important', '4':'Majeure',
    '5':'Majeure', '6':'Capitale',
  };

  static _foldRow(label, value, detail, titleAttr) {
    const valAttr = titleAttr ? ` title="${titleAttr}"` : '';
    return `<tr><td colspan="2">
      <details>
        <summary><span class="fold-label">${label}</span><span class="fold-value"${valAttr}>${value}</span></summary>
        <div class="fold-content">${detail}</div>
      </details>
    </td></tr>`;
  }

  static _decodeImportance(ix) {
    if (!ix) return '';
    const m = String(ix).match(/\{?\s*(-?\d+)\s*\}?/);
    if (!m) return SectorMapApp._foldRow('Importance', ix, '');
    const val = m[1];
    const desc = SectorMapApp._IMPORTANCE[val] || '—';
    const detail = `<div class="fold-desc">Valeur d’importance économique et stratégique du monde.<br>${val} = ${desc}</div>`;
    return SectorMapApp._foldRow('Importance', `{ ${val} } ${desc}`, detail);
  }

  static _decodeEconomics(ex) {
    if (!ex) return '';
    let s = String(ex).replace(/[()\s]/g, '');
    const m = s.match(/^([\dA-F])([\dA-F])([\dA-F])([+-]\d+)$/i);
    if (!m) return SectorMapApp._foldRow('Économie', ex, '');
    const res = m[1], lab = m[2], inf = m[3], eff = m[4];
    const detail = `<table class="fold-subtable">
      <tr><td>Ressources</td><td>${res}</td></tr>
      <tr><td>Main-d’œuvre</td><td>${lab}</td></tr>
      <tr><td>Infrastructure</td><td>${inf}</td></tr>
      <tr><td>Efficacité</td><td>${eff}</td></tr>
    </table>`;
    return SectorMapApp._foldRow('Économie', `( ${res} ${lab} ${inf} ${eff} )`, detail);
  }

  static _decodeCulture(cx) {
    if (!cx) return '';
    const s = String(cx).replace(/[\[\]\s]/g, '');
    if (s.length < 4) return SectorMapApp._foldRow('Culture', cx, '');
    const h = s[0].toUpperCase(), t = s[1].toUpperCase(), p = s[2].toUpperCase(), a = s[3].toUpperCase();
    const detail = `<table class="fold-subtable">
      <tr><td>Hétérogénéité</td><td>${h}</td></tr>
      <tr><td>Traditionalisme</td><td>${t}</td></tr>
      <tr><td>Progressisme</td><td>${p}</td></tr>
      <tr><td>Agressivité</td><td>${a}</td></tr>
    </table>`;
    return SectorMapApp._foldRow('Culture', `[ ${h} ${t} ${p} ${a} ]`, detail);
  }

  static _decodePopulation(uwp, pbg) {
    if (!uwp || uwp.length < 5) return '';
    const popUwp = SectorMapApp._hexVal(uwp[4]);
    if (popUwp < 0) return '';
    const popPbg = pbg ? parseInt(pbg[0], 10) : null;
    const multiplier = popPbg != null && !isNaN(popPbg) ? popPbg : 1;
    const base = Math.pow(10, popUwp);
    const total = multiplier * base;
    const fmtBase = `10<sup>${popUwp}</sup>`;
    const fmtMult = multiplier;
    const fmtTotal = total >= 1e9 ? `${(total / 1e9).toFixed(1)}&nbsp;milliards`
      : total >= 1e6 ? `${(total / 1e6).toFixed(1)}&nbsp;millions`
      : total >= 1e3 ? `${(total / 1e3).toFixed(0)}&nbsp;000`
      : String(total);
    const belts = pbg ? parseInt(pbg[1], 10) : null;
    const gas = pbg ? parseInt(pbg[2], 10) : null;
    const detail = `<div class="fold-desc">Population = multiplicateur (PBG: <b>${popPbg}</b>) × 10<sup>chiffre UWP (${uwp[4]})</sup><br>
      Ceintures d’astéroïdes : <b>${belts ?? '?'}</b> &nbsp;|&nbsp; Géantes gazeuses : <b>${gas ?? '?'}</b></div>`;
    return SectorMapApp._foldRow('Population', `${fmtMult} × ${fmtBase} = ${fmtTotal}`, detail);
  }

  static _decodeNobility(nob) {
    if (!nob) return '';
    const titles = [];
    for (const ch of nob) {
      const desc = SectorMapApp._NOBILITY[ch.toUpperCase()];
      if (desc) titles.push(`${ch} (${desc})`);
    }
    if (!titles.length) return SectorMapApp._foldRow('Noblesse', nob, '');
    const detail = `<div class="fold-desc">Titres de noblesse impériale présents sur ce monde.</div>`;
    return SectorMapApp._foldRow('Noblesse', titles.join(', '), detail);
  }

  static _buildWorldCardHTML(w) {
    const sector = w.Sector || '';
    const hex = w.Hex || '';
    const name = w.Name || '—';
    const uwp = w.UWP || '???????-?';
    const bases = w.Bases || '';
    const remarks = w.Remarks || '';
    const allegiance = w.Allegiance || '';
    const stellar = w.Stellar || '';
    const zone = w.Zone || '';
    const pbg = w.PBG || '';

    const zoneLabel = zone === 'R' ? 'Rouge'
      : zone === 'A' ? 'Ambre'
      : 'Verte';

    const lines = [];

    const uwpDetail = SectorMapApp._uwpBreakdown(uwp);
    lines.push(SectorMapApp._foldRow('UWP', `<span class="mono">${uwp}</span>`, uwpDetail));

    const ixRow = SectorMapApp._decodeImportance(w.Ix);
    if (ixRow) lines.push(ixRow);
    const exRow = SectorMapApp._decodeEconomics(w.Ex);
    if (exRow) lines.push(exRow);
    const cxRow = SectorMapApp._decodeCulture(w.Cx);
    if (cxRow) lines.push(cxRow);
    const popRow = SectorMapApp._decodePopulation(uwp, pbg);
    if (popRow) lines.push(popRow);
    const nobRow = SectorMapApp._decodeNobility(w.Nobility);
    if (nobRow) lines.push(nobRow);

    if (bases) {
      const bCodes = bases.split(/[\s,;]+/);
      const bList = bCodes.map(c => {
        const desc = SectorMapApp._BASES_HELP[c.toUpperCase()];
        return `<tr><td class="mono">${c}</td><td>${desc || '—'}</td></tr>`;
      }).join('');
      const bDetail = `<table class="fold-subtable"><tbody>${bList}</tbody></table>`;
      lines.push(SectorMapApp._foldRow('Bases', bases, bDetail));
    }

    if (remarks) {
      const rCodes = remarks.split(/[\s,;]+/);
      const rList = rCodes.map(c => {
        const desc = SectorMapApp._REMARKS_HELP[c.toUpperCase()];
        return `<tr><td class="mono">${c}</td><td>${desc || '—'}</td></tr>`;
      }).join('');
      const rDetail = `<table class="fold-subtable"><tbody>${rList}</tbody></table>`;
      lines.push(SectorMapApp._foldRow('Remarques', remarks, rDetail));
    }

    if (allegiance) {
      const allegFull = w.AllegianceName || '';
      const aDetail = `<div class="fold-desc">${allegFull || allegiance}</div>`;
      lines.push(SectorMapApp._foldRow('Allégeance', allegFull ? `${allegiance} (${allegFull})` : allegiance, aDetail));
    }

    if (stellar) {
      const sList = [];
      let remaining = stellar.trim();
      const reStar = /^([OBAFGKMLTY])(\d)\s*(VII|VI|V|IV|III|II|I)\s*/i;
      while (remaining) {
        const m = remaining.match(reStar);
        if (m) {
          const type = SectorMapApp._STAR_TYPES[m[1].toUpperCase()] || m[1];
          const cls = SectorMapApp._STAR_CLASS[m[3]] || m[3];
          sList.push(`<tr><td class="mono">${m[1]}${m[2]} ${m[3]}</td><td>${type} · ${cls}</td></tr>`);
          remaining = remaining.slice(m[0].length).trimStart();
        } else {
          const next = remaining.indexOf(' ');
          if (next < 0) break;
          remaining = remaining.slice(next + 1).trimStart();
        }
      }
      const sDetail = sList.length ? `<table class="fold-subtable"><tbody>${sList.join('')}</tbody></table>` : `<div class="fold-desc">${stellar}</div>`;
      lines.push(SectorMapApp._foldRow('Étoile', `<span class="mono">${stellar}</span>`, sDetail));
    }

    return `<section class="mgt2-world-card">
      <div class="mgt2-world-card-header">
        <span class="mgt2-world-name">${name}</span>
        <span class="mgt2-world-hex">${sector} ${hex}</span>
        <span class="mgt2-world-zone zone-${zone.toLowerCase() || 'g'}">${zoneLabel}</span>
      </div>
      <table class="mgt2-world-card-body"><tbody>${lines.join('')}</tbody></table>
      <div class="mgt2-world-card-actions">
        <a class="mgt2-world-commerce" data-sector="${this._escapeAttr(sector)}" data-hex="${hex}" data-uwp="${uwp}" data-zone="${zone || 'normal'}" data-name="${this._escapeAttr(name)}">
          <i class="fas fa-balance-scale"></i> Commerce
        </a>
      </div>
    </section>`;
  }

  static _escapeAttr(str) {
    if (!str) return '';
    return String(str).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
  }

  _postWorldCard(w) {
    const html = SectorMapApp._buildWorldCardHTML(w);
    ChatMessage.create({
      content: html,
      whisper: [game.user.id],
    });
  }

  /* ───── Recherche de monde ───── */

  async _doSearch(input, results) {
    const query = input.value.trim();
    if (query.length < 2) { results.innerHTML = ''; return; }
    const worlds = await searchWorlds(query);
    if (!worlds.length) {
      results.innerHTML = '<li class="no-result">Aucun monde trouvé</li>';
      return;
    }
    results.innerHTML = worlds.slice(0, 12).map(w =>
      `<li data-sector="${w.sector}" data-hex="${w.hex}" data-name="${w.name}">
        <span class="world-name">${w.name}</span>
        <span class="world-uwp">${w.uwp}</span>
        <span class="world-sector">${w.sector}</span>
      </li>`
    ).join('');
    results.querySelectorAll('li[data-sector]').forEach(li => {
      li.addEventListener('click', () => this._selectWorld(li, input, results));
    });
  }

  async _selectWorld(li, input, results) {
    const sector = li.dataset.sector;
    const hex = li.dataset.hex;
    const name = li.dataset.name;
    results.innerHTML = '';
    input.value = name;

    this._sector = sector;
    this._subsector = null;
    this._mapHex = hex;
    this._mapCenter = null;
    this._mapScale = null;
    this._enableSyncButton(name);

    const iframe = this.element?.querySelector('.mgt2-sector-map-frame');
    if (iframe) iframe.src = this._mapUrl;

    ui.notifications.info(`Carte centrée sur ${name} (${sector} ${hex})`);
  }

  /* ───── Partage ───── */

  async _syncAll() {
    if (!game.socket?.emit) {
      ui.notifications.warn('Socket indisponible — impossible de synchroniser');
      return;
    }
    try {
      // Tentative de récupération du zoom via getViewport
      const viewport = await new Promise((resolve) => {
        const handler = (event) => {
          if (event.data?.type === 'viewport') {
            window.removeEventListener('message', handler);
            resolve(event.data);
          }
        };
        window.addEventListener('message', handler);
        const iframe = this.element?.querySelector('.mgt2-sector-map-frame');
        if (iframe?.contentWindow) {
          iframe.contentWindow.postMessage(
            { source: 'travellermap', type: 'getViewport' },
            '*'
          );
        }
        setTimeout(() => {
          window.removeEventListener('message', handler);
          resolve(null);
        }, 1000);
      });

      if (viewport) {
        this._sector = viewport.sector || this._sector;
        this._subsector = viewport.subsector || this._subsector;
        this._mapHex = viewport.hex || this._mapHex;
        this._mapCenter = (viewport.x != null && viewport.y != null) ? `${viewport.x},${viewport.y}` : this._mapCenter;
        this._mapScale = viewport.scale || this._mapScale;
      }

      const payload = {
        _type: 'sectorMapSync',
        sector: this._sector,
        subsector: this._subsector,
        mapHex: this._mapHex,
        mapCenter: this._mapCenter,
        mapScale: this._mapScale,
      };
      const players = game.users.filter(u => !u.isGM && u.active);
      for (const player of players) {
        game.socket.emit(`module.${MODULE_ID}`, payload);
      }
      ui.notifications.info(`Vue synchronisée chez ${players.length} joueur(s)`);
    } catch (err) {
      console.error(`${MODULE_ID} | Erreur socket sync:`, err);
      ui.notifications.error('Erreur lors de la synchronisation');
    }
  }

  updatePosition(sector, subsector, mapHex, mapUrl) {
    this._sector = sector;
    this._subsector = subsector;
    this._mapHex = mapHex || null;
    const iframe = this.element?.querySelector('.mgt2-sector-map-frame');
    if (iframe) iframe.src = mapUrl || this._mapUrl;
  }

  _openTravelDialog() {
    const existing = Object.values(ui.windows).find(w => w.id === 'mgt2-travel-dialog');
    if (existing) { existing.bringToTop(); return; }
    const dialog = new TravelDialog();
    dialog.render({ force: true });
  }

  /* ───── Nettoyage ───── */

  close() {
    if (this._handler) {
      window.removeEventListener('message', this._handler);
      this._handler = null;
    }
    return super.close();
  }
}
