/*! trentpower.fr · /verify/verify.js · authored · signed via /integrity.json */
(function () {
  'use strict';

  var MAP = (typeof window !== 'undefined' && window.TP_VERIFICATION_MAP) || {};

  function getLang() {
    var t = document.documentElement.lang || 'en';
    return (typeof window.I18N === 'object' && window.I18N[t]) ? t : 'en';
  }
  function t(key, fallback) {
    var lang = getLang();
    var ref = (window.I18N && window.I18N[lang]) || {};
    var parts = key.split('.');
    for (var i = 0; i < parts.length; i++) {
      if (ref == null || typeof ref !== 'object') return fallback || '';
      ref = ref[parts[i]];
    }
    return (typeof ref === 'string') ? ref : (fallback || '');
  }

  function normalisePath(raw) {
    if (!raw) return '/';
    raw = String(raw).trim();
    if (!raw) return '/';
    if (raw.indexOf('//') !== -1) {
      try {
        var u = new URL(raw, location.origin);
        if (u.origin !== location.origin) return null;
        raw = u.pathname || '/';
      } catch (_) { return null; }
    }
    if (raw.charAt(0) !== '/') raw = '/' + raw;
    raw = raw.replace(/\/index\.html$/, '/');
    if (raw.indexOf('.') === -1 && raw.charAt(raw.length - 1) !== '/') raw += '/';
    return raw;
  }

  function el(tag, attrs, text) {
    var n = document.createElement(tag);
    if (attrs) for (var k in attrs) {
      if (Object.prototype.hasOwnProperty.call(attrs, k)) n.setAttribute(k, attrs[k]);
    }
    if (text != null) n.textContent = text;
    return n;
  }

  function safeHref(path) {
    if (!path) return '#';
    if (path.charAt(0) === '/') return path;
    try {
      var u = new URL(path, location.origin);
      if (u.origin !== location.origin) return '#';
      return u.pathname + (u.search || '');
    } catch (_) { return '#'; }
  }

  function copy(text, button) {
    if (!navigator.clipboard) return;
    navigator.clipboard.writeText(text).then(function () {
      var prev = button.textContent;
      button.textContent = t('verify.action.copied', 'Copied');
      button.setAttribute('data-state', 'copied');
      setTimeout(function () {
        button.textContent = prev;
        button.removeAttribute('data-state');
      }, 1400);
    }, function () { /* noop */ });
  }

  // The "Verify locally" command blocks were removed by design: command-
  // line release verification belongs on /integrity/, where the full
  // signed-manifest workflow is already documented. /verify/ stays
  // purely a per-page record. The Connected records strip below points
  // visitors at /integrity.json and the release archive when they want
  // to drop into the command-line flow.

  // ─── Localised archive label ──────────────────────────────
  // Mirrors cite.template.js's buildArchiveLabel , derives a human
  // form from the trailing YYYY-MM in /integrity/releases/YYYY-MM/.

  var LOCALE_MONTHS_VERIFY = {
    en: ['January','February','March','April','May','June','July','August','September','October','November','December'],
    fr: ['janvier','février','mars','avril','mai','juin','juillet','août','septembre','octobre','novembre','décembre'],
    it: ['gennaio','febbraio','marzo','aprile','maggio','giugno','luglio','agosto','settembre','ottobre','novembre','dicembre'],
    es: ['enero','febrero','marzo','abril','mayo','junio','julio','agosto','septiembre','octubre','noviembre','diciembre'],
    de: ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember']
  };
  function archiveLabel(routePath) {
    if (!routePath) return '';
    var m = /(\d{4})-(\d{2})\/?$/.exec(routePath);
    if (!m) return routePath;
    var year = parseInt(m[1], 10), month = parseInt(m[2], 10);
    if (!month || month < 1 || month > 12) return routePath;
    var lang = getLang();
    var months = LOCALE_MONTHS_VERIFY[lang] || LOCALE_MONTHS_VERIFY.en;
    var monthName = months[month - 1];
    if (lang === 'fr') return monthName.charAt(0).toUpperCase() + monthName.slice(1) + ' ' + year;
    if (lang === 'it') return monthName.charAt(0).toUpperCase() + monthName.slice(1) + ' ' + year;
    if (lang === 'es') return monthName.charAt(0).toUpperCase() + monthName.slice(1) + ' de ' + year;
    if (lang === 'de') return monthName + ' ' + year;
    return monthName + ' ' + year;
  }

  // ─── Section: Page record ──────────────────────────────
  // Editorial dossier, not a metadata table. Each group is a small
  // mono label + value: Citation (serif body, the emotional centre)
  // followed by Location, Evidence, Fingerprint, Archive in mono.
  // Hairlines separate groups, not rows. Actions sit at the bottom
  // as quiet utility-link mono.

  // ─── micro-grid row helper (phase 33 rewrite) ──────────────────────────
  // emits one row of the record-grid:
  //   <div class="record-grid__row">
  //     <dt>LABEL</dt>
  //     <dd>VALUE</dd>
  //   </div>
  // value can be a string, a single dom node, or an array of nodes/strings
  // (joined with <br> for the multi-line evidence row). all paths/hashes
  // wrap cleanly via the css `overflow-wrap: anywhere; word-break: break-
  // word` rules on `.record-grid code`.
  function buildRecordRow(labelKey, labelFallback, valueNode) {
    var row = el('div', { 'class': 'record-grid__row' });
    row.appendChild(el('dt', null, t(labelKey, labelFallback)));
    var dd = el('dd');
    if (typeof valueNode === 'string') {
      dd.textContent = valueNode;
    } else if (Array.isArray(valueNode)) {
      valueNode.forEach(function (n, i) {
        if (i > 0) dd.appendChild(el('br'));
        if (typeof n === 'string') dd.appendChild(document.createTextNode(n));
        else                       dd.appendChild(n);
      });
    } else if (valueNode) {
      dd.appendChild(valueNode);
    }
    row.appendChild(dd);
    return row;
  }
  function recordLink(href, label) {
    var a = el('a', { href: safeHref(href) });
    a.appendChild(el('code', null, label));
    return a;
  }
  function recordCode(text) {
    return el('code', null, text);
  }

  function buildThisPage(record) {
    // phase 33 · micro-grid record system. the card now reads as a
    // signed technical certificate: header (eyebrow + title +
    // status) above a definition-list of metadata rows (citation,
    // location, evidence, fingerprint, archive) below a hairline
    // actions strip. inspired by swiss archival records, museum
    // object labels, and printed release ledgers.
    // phase 51 · semantic upgrades — <section> → <article>, inner
    // <div class="verify-card__header"> → <header>. local var names
    // ('section', 'header') keep the diff minimal.
    var section = el('article', {
      'class': 'verify-card',
      'aria-labelledby': 'verify-record-title',
    });

    // ── header ── eyebrow + title + status line ─────────────────────
    var header = el('header', { 'class': 'verify-card__header' });
    header.appendChild(el('p', { 'class': 'eyebrow' },
      t('verify.thispage.kicker', 'Page record')));
    header.appendChild(el('h2', { 'id': 'verify-record-title' },
      record.title || ''));

    var statusBits = [];
    if (record.manifest_status === 'found') {
      statusBits.push(t('verify.thispage.status.short.signed', 'Signed'));
    }
    if (record.source) {
      statusBits.push(t('verify.thispage.status.short.source', 'Source'));
    }
    if (record.release) {
      statusBits.push(t('verify.thispage.status.short.archived', 'Archived'));
    }
    if (statusBits.length) {
      header.appendChild(el('p', { 'class': 'verify-status' },
        statusBits.join(' · ')));
    }
    section.appendChild(header);

    // ── record-grid ── one row per field ────────────────────────────
    var grid = el('dl', { 'class': 'record-grid' });

    if (record.citation) {
      grid.appendChild(buildRecordRow(
        'verify.thispage.group.citation', 'Citation',
        record.citation));
    }

    // location · canonical url (with route as quiet second line on
    // sub-pages where the canonical and route differ).
    if (record.canonical || record.route || record.path) {
      var locParts = [];
      if (record.canonical) {
        locParts.push(recordLink(record.canonical, record.canonical));
      }
      var routeStr = record.route || record.path;
      if (routeStr && routeStr !== '/') {
        locParts.push(recordCode(routeStr));
      }
      if (locParts.length) {
        grid.appendChild(buildRecordRow(
          'verify.thispage.group.location', 'Location',
          locParts));
      }
    }

    // evidence · `<a><code>/source/…</code></a>` on line 1; meta
    // ("HTML · 26 KB · Validated 2026-05-11") as a `<span class=
    // "record-meta">` block below. `.record-meta` carries the
    // smaller mono / fg3 styling so the supporting bytes/date sit
    // visibly under the primary file path.
    if (record.source || record.file_type || record.size_label || record.validated) {
      var evDd = el('div');
      if (record.source) {
        evDd.appendChild(recordLink(record.reader || record.source, record.source));
      }
      var metaBits = [];
      if (record.file_type)  metaBits.push(record.file_type);
      if (record.size_label) metaBits.push(record.size_label);
      if (record.validated) {
        var validatedPrefix = t('verify.thispage.validated_prefix', 'Validated');
        metaBits.push(validatedPrefix + ' ' + record.validated);
      }
      if (metaBits.length) {
        evDd.appendChild(el('span', { 'class': 'record-meta' },
          metaBits.join(' · ')));
      }
      grid.appendChild(buildRecordRow(
        'verify.thispage.group.evidence', 'Evidence',
        evDd));
    }

    // fingerprint · `<code>` with the full hash visible (record-grid
    // code wraps cleanly via `overflow-wrap: anywhere`). title +
    // aria-label still carry the full string for assistive tech and
    // for the copy-fingerprint action below.
    if (record.sha256 && record.sha256 !== '(missing)') {
      // phase 51 · <code> → <samp class="record-fingerprint"> with
      // <bdi> for directional isolation. title + aria-label keep
      // the full hash exposed for assistive tech and copy-fingerprint.
      var fpCode = el('samp', {
        'class': 'record-fingerprint page-hash',
        'title': record.sha256,
        'aria-label': record.sha256,
      });
      fpCode.appendChild(el('bdi', {}, record.sha256));
      grid.appendChild(buildRecordRow(
        'verify.thispage.group.fingerprint', 'Fingerprint',
        fpCode));
    }

    if (record.release) {
      grid.appendChild(buildRecordRow(
        'verify.thispage.group.archive', 'Archive',
        recordLink(record.release, record.release)));
    }

    section.appendChild(grid);

    // ── actions strip ── copy citation / copy fingerprint /
    //    view source code (reader) / raw source mirror.
    var hasCitation    = !!record.citation;
    var hasFingerprint = !!(record.sha256 && record.sha256 !== '(missing)');
    var hasReader      = !!record.reader;
    var hasSource      = !!record.source;
    if (hasCitation || hasFingerprint || hasReader || hasSource) {
      var actions = el('nav', {
        'class': 'record-tools',
        'aria-label': t('verify.thispage.actions_label', 'Page record actions'),
      });
      if (hasCitation) {
        var copyCit = el('button', { type: 'button', 'class': 'record-inline-action' },
          t('cite.overlay.action.copy_citation', 'Copy citation'));
        copyCit.addEventListener('click', function () { copy(record.citation, copyCit); });
        actions.appendChild(copyCit);
      }
      if (hasFingerprint) {
        var copyFp = el('button', { type: 'button', 'class': 'record-inline-action' },
          t('verify.action.copy_fingerprint', 'Copy fingerprint'));
        copyFp.addEventListener('click', function () { copy(record.sha256, copyFp); });
        actions.appendChild(copyFp);
      }
      if (hasReader) {
        actions.appendChild(el('a', { 'href': safeHref(record.reader), 'class': 'record-inline-action' },
          t('verify.action.view_source_code', 'View source code')));
      }
      if (hasSource && !hasReader) {
        // fallback for routes without a reader (e.g. /source/ itself has no .txt mirror)
        actions.appendChild(el('a', { 'href': safeHref(record.source), 'class': 'record-inline-action' },
          t('verify.action.open_source_mirror', 'Plain text')));
      }
      section.appendChild(actions);
    }

    return section;
  }

  // ─── Section: Other records ────────────────────────────────
  // editorial index of sibling routes. small h2 heading, two-column
  // grid of mono links. reads as a quiet appendix; the page-record
  // card stays the unambiguous primary object. current path renders
  // as a muted, non-linked span carrying aria-current="page".
  function buildChooser(currentPath) {
    var label = t('verify.chooser.heading', 'Other records');
    var labelId = 'other-records-title';
    var section = el('section', {
      'class': 'verify-chooser other-records',
      'aria-labelledby': labelId,
    });
    section.appendChild(el('h2', {
      'class': 'other-records-title',
      'id': labelId,
    }, label));
    var list = el('ul', { 'class': 'other-records-grid' });
    var routes = [
      { path: '/',                    key: 'verify.chooser.label.home',      fb: 'Homepage' },
      { path: '/privacy/',            key: 'verify.chooser.label.privacy',   fb: 'Privacy' },
      { path: '/integrity/',          key: 'verify.chooser.label.integrity', fb: 'Integrity' },
      { path: '/verify/',             key: 'verify.chooser.label.verify',    fb: 'Verify' },
      { path: '/source/',             key: 'verify.chooser.label.source',    fb: 'Source' },
      { path: '/security/',           key: 'verify.chooser.label.security',  fb: 'Security' },
      { path: '/integrity/releases/', key: 'verify.chooser.label.releases',  fb: 'Releases' },
    ];
    routes.forEach(function (r) {
      var rlabel = t(r.key, r.fb);
      var li = el('li');
      if (r.path === currentPath) {
        li.appendChild(el('span', { 'aria-current': 'page' }, rlabel));
      } else {
        li.appendChild(el('a', {
          'href': '/verify/?path=' + encodeURIComponent(r.path),
        }, rlabel));
      }
      list.appendChild(li);
    });
    section.appendChild(list);
    return section;
  }

  // Renderers
  // The h1 ("Verify this page") is i18n-bound and stays static. The
  // selected page is identified inside the page record card; the
  // hero never carries a contextual title.

  function renderSelected(record) {
    var root = document.getElementById('verify-root');
    if (!root) return;
    while (root.firstChild) root.removeChild(root.firstChild);

    // phase 33 · buildThisPage now emits its own `.verify-card`
    // section (the micro-grid certificate). no outer wrapper
    // needed. chooser sits below as quiet secondary nav.
    root.appendChild(buildThisPage(record));
    root.appendChild(buildChooser(record.path || record.route));
  }

  // renderGeneral was used when /verify/ was visited with no path. The
  // page now defaults to the homepage record instead, so a fresh visit
  // to /verify/ feels useful rather than empty. init() handles the
  // fallback chain , if the homepage record were ever missing from the
  // map (it shouldn't be), renderUnknown takes over.

  function renderUnknown(rawPath) {
    var root = document.getElementById('verify-root');
    if (!root) return;
    while (root.firstChild) root.removeChild(root.firstChild);

    // Calm notice for routes not in the verification map.
    var notice = el('section', { 'class': 'verify-unknown' });
    notice.appendChild(el('h2', { 'class': 'verify-section-heading' },
      t('verify.unknown.title', 'Route not in the verification map')));
    if (rawPath) {
      notice.appendChild(el('p', { 'class': 'verify-unknown-path' }, rawPath));
    }
    notice.appendChild(el('p', { 'class': 'verify-section-intro' },
      t('verify.unknown.body',
        'The published manifest only lists the canonical pages of trentpower.fr. Use the records below to inspect the manifest, signature and release archives directly.')));

    // Compact actions strip , Source · Integrity manifest · Release archive.
    // Mirrors .verify-thispage-actions so the fallback never reads as broken.
    var actions = el('p', { 'class': 'verify-unknown-actions' });
    function unknownLink(href, key, fallback) {
      var a = el('a', { 'class': 'verify-unknown-action', 'href': href },
        t(key, fallback));
      return a;
    }
    function unknownSep() {
      return el('span', { 'class': 'verify-unknown-actions-sep', 'aria-hidden': 'true' }, ' · ');
    }
    actions.appendChild(unknownLink('/source/',
      'verify.unknown.action.source', 'Source'));
    actions.appendChild(unknownSep());
    actions.appendChild(unknownLink('/integrity.json',
      'verify.unknown.action.manifest', 'Integrity manifest'));
    actions.appendChild(unknownSep());
    actions.appendChild(unknownLink('/integrity/releases/',
      'verify.unknown.action.releases', 'Release archive'));
    notice.appendChild(actions);

    root.appendChild(notice);
    root.appendChild(buildChooser(null));
  }

  function init() {
    var qs = new URLSearchParams(location.search);
    var raw = qs.get('path');
    // Default to the homepage record when no ?path= is supplied , a
    // fresh visit to /verify/ should land on a real public record, not
    // an empty page. The homepage is the canonical default.
    var norm = raw ? normalisePath(raw) : '/';
    if (norm === null) { renderUnknown(String(raw).slice(0, 80)); return; }
    var record = MAP[norm];
    if (record) renderSelected(record);
    else        renderUnknown(norm);
  }

  // Re-render when the language switcher fires (lang attribute changes).
  var observer = new MutationObserver(function (mutations) {
    for (var i = 0; i < mutations.length; i++) {
      if (mutations[i].attributeName === 'lang') { init(); break; }
    }
  });
  observer.observe(document.documentElement, { attributes: true });

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();
