/*! 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 '#'; }
  }

  // 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', 'Canonical 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) {
        // source mirror row points at the raw .txt mirror directly.
        // the polished source viewer is reached via the "view source
        // code" action below the card.
        evDd.appendChild(recordLink(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', 'Source mirror',
        evDd));
    }

    // fingerprint · quiet two-part rendering. a small mono kicker
    // ("sha256") sits above the hash itself in a <samp> with a bdi
    // wrapper for directional isolation and natural wrap rules from
    // the .page-hash + .record-grid samp rules. title + aria-label
    // keep the full string exposed for assistive tech and for the
    // copy-fingerprint action below. the algorithm prefix is no
    // longer crammed into the hash string — it reads as an archival
    // attribution, not a forensic blob. phase 67.
    if (record.sha256 && record.sha256 !== '(missing)') {
      var fpWrap = el('div', { 'class': 'record-fingerprint-block' });
      var rawHash = String(record.sha256);
      var algoMatch = rawHash.match(/^([a-z0-9]+)[-:]/i);
      var algo = algoMatch ? algoMatch[1].toLowerCase() : 'sha256';
      var hashBody = algoMatch ? rawHash.slice(algoMatch[0].length) : rawHash;
      // wrap the algo prefix in <abbr> so screen readers and hover
      // expand the acronym to its full meaning.
      var algoSpan = el('span', { 'class': 'record-fingerprint-algo' });
      algoSpan.appendChild(el('abbr', { 'title': 'Secure Hash Algorithm, 256-bit' }, algo));
      fpWrap.appendChild(algoSpan);
      var fpCode = el('samp', {
        'class': 'record-fingerprint page-hash',
        'title': rawHash,
        'aria-label': rawHash,
      });
      fpCode.appendChild(el('bdi', {}, hashBody));
      fpWrap.appendChild(fpCode);
      grid.appendChild(buildRecordRow(
        'verify.thispage.group.fingerprint', 'Page fingerprint',
        fpWrap));
    }

    if (record.release) {
      grid.appendChild(buildRecordRow(
        'verify.thispage.group.archive', 'Release 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) {
        // the shared /copy.js delegated listener handles the click via
        // these data-copy-* attributes. citation copies opt into the
        // in-place confirmation (data-copy-mode="cite") — the button's
        // own label becomes "Cited · Edition YYYY-MM-DD" for ~1 s on
        // successful copy, then restores. aria-live still announces
        // "Citation copied" via the cite.overlay translation key.
        actions.appendChild(el('button', {
          type: 'button', 'class': 'record-inline-action',
          'data-copy-text': record.citation,
          'data-copy-feedback': t('cite.overlay.toast.citation_copied', 'Citation copied'),
          'data-copy-mode': 'cite'
        }, t('cite.overlay.action.copy_citation', 'Copy citation')));
      }
      if (hasFingerprint) {
        // fingerprint copy keeps the legacy textContent swap — the
        // button label is a verb ("Copy fingerprint") that reads well
        // as "Copied". the in-place inscription is reserved for
        // citation copies (where the edition matters).
        actions.appendChild(el('button', {
          type: 'button', 'class': 'record-inline-action',
          'aria-live': 'polite', 'aria-atomic': 'true',
          'data-copy-text': record.sha256,
          'data-copy-feedback': t('verify.action.copied', 'Copied')
        }, t('verify.action.copy_fingerprint', 'Copy fingerprint')));
      }
      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;
  }

  // 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 —
    // the page-record card is the only object on the page.
    root.appendChild(buildThisPage(record));
  }

  // 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);
  }

  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();
  }
})();
