// lib/giftbit.js — Client API Giftbit (cartes-cadeaux). VALIDÉ testbed 2026-06-10 (voir memory/reference_giftbit.md).
// Capacités : solde (funds), marketplace (produits), création de campagne (SHORTLINK), statut des cartes,
//             révocation (DELETE → GIVER_CANCELLED) pour remplacer un lien compromis.
// ⚠️ Cloudflare bannit les User-Agent non-navigateur (erreur 1010) → on envoie un UA Chrome.
// Secrets : GIFTBIT_TOKEN + GIFTBIT_BASE via ENV (jamais dans le repo). Défaut = TESTBED (aucune vraie carte).
const https = require('https')
const { json, parseBody } = require('./helpers')

const BASE = (process.env.GIFTBIT_BASE || 'https://testbedapp.giftbit.com/papi/v1').replace(/\/+$/, '')
const TOKEN = process.env.GIFTBIT_TOKEN || ''
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36'
const isTestbed = /testbed/i.test(BASE)
const enc = encodeURIComponent

function api (method, path, body) {
  return new Promise((resolve) => {
    let u; try { u = new URL(BASE + path) } catch (e) { return resolve({ ok: false, status: 0, error: 'bad url' }) }
    const data = body != null ? Buffer.from(JSON.stringify(body)) : null
    const headers = { Authorization: 'Bearer ' + TOKEN, Accept: 'application/json', 'User-Agent': UA }
    if (data) { headers['Content-Type'] = 'application/json'; headers['Content-Length'] = data.length }
    const req = https.request({ hostname: u.hostname, port: 443, path: u.pathname + u.search, method, headers }, (r) => {
      let b = ''; r.on('data', c => { b += c }); r.on('end', () => {
        let j = null; try { j = JSON.parse(b) } catch (e) {}
        resolve({ ok: r.statusCode >= 200 && r.statusCode < 300, status: r.statusCode, body: j, raw: j ? undefined : String(b).slice(0, 300) })
      })
    })
    req.on('error', e => resolve({ ok: false, status: 0, error: String((e && e.message) || e) }))
    req.setTimeout(30000, () => { req.destroy(); resolve({ ok: false, status: 0, error: 'timeout' }) })
    if (data) req.write(data); req.end()
  })
}

const ping = () => api('GET', '/ping')
const funds = () => api('GET', '/funds')
const marketplace = (q = {}) => api('GET', '/marketplace?' + new URLSearchParams({ region: String(q.region || '2'), limit: String(q.limit || '50'), offset: String(q.offset || '0') }).toString())
const brands = (q = {}) => api('GET', '/brands?' + new URLSearchParams({ region: String(q.region || '2'), limit: String(q.limit || '50'), offset: String(q.offset || '0') }).toString())
const listGifts = (q = {}) => { const p = new URLSearchParams(); for (const k of ['campaign_uuid', 'campaign_id', 'limit', 'offset', 'status']) if (q[k] != null && q[k] !== '') p.set(k, String(q[k])); return api('GET', '/gifts?' + p.toString()) }
const getGift = (uuid) => api('GET', '/gifts/' + enc(uuid))
const cancelGift = (uuid) => api('DELETE', '/gifts/' + enc(uuid)) // → GIVER_CANCELLED : révoque le shortlink gtbt.co + rend les fonds

// Construit la liste marketplace_gifts. 1 entrée = reward DIRECT (marque fixée) ; ≥2 = TEMPLATE (le destinataire choisit à la rédemption → brand_code null jusqu'au choix).
function buildMarketplaceGifts (o = {}) {
  if (Array.isArray(o.marketplace_gifts) && o.marketplace_gifts.length) return o.marketplace_gifts.map(g => ({ id: Number(g.id), price_in_cents: Number(g.price_in_cents != null ? g.price_in_cents : o.price_in_cents) }))
  if (Array.isArray(o.brand_ids) && o.brand_ids.length) return o.brand_ids.map(id => ({ id: Number(id), price_in_cents: Number(o.price_in_cents) }))
  return [{ id: Number(o.marketplace_gift_id), price_in_cents: Number(o.price_in_cents) }]
}
// Création de campagne (schéma VALIDÉ) : marketplace_gifts:[{id, price_in_cents}] (PAS "price"/brand_codes) ; message/subject à la racine.
function createCampaign (o = {}) {
  const body = {
    id: String(o.id),
    delivery_type: o.delivery_type || 'SHORTLINK',
    expiry: o.expiry || defaultExpiry(),
    message: o.message || '',
    subject: o.subject || '',
    suppress_default_greeting: !!o.suppress_default_greeting,
    marketplace_gifts: buildMarketplaceGifts(o),
    contacts: (o.contacts || []).map(c => ({ email: c.email, firstname: c.firstname || '', lastname: c.lastname || '' }))
  }
  return api('POST', '/campaign', body)
}
function defaultExpiry () { const d = new Date(); d.setFullYear(d.getFullYear() + 1); return d.toISOString().slice(0, 10) }

// Remplacer un lien (soupçon de piratage) : révoque l'ancien gift PUIS recrée pour le même contact.
// Renvoie { old:{uuid,shortlink,status}, created } → l'appelant conserve l'ancien en HISTORIQUE.
async function replaceGift (o = {}) {
  const cur = await getGift(o.uuid)
  const g = cur.body && cur.body.gift
  if (!g) return { ok: false, error: 'gift introuvable', detail: cur.body || cur.error }
  const cancel = await cancelGift(o.uuid)
  if (!cancel.ok) return { ok: false, error: 'révocation échouée', cancel: cancel.body || cancel.error }
  const created = await createCampaign({
    id: o.new_campaign_id || ('replace-' + String(o.uuid).slice(0, 10) + '-' + Date.now()),
    expiry: o.expiry || defaultExpiry(),
    message: o.message || 'Nouveau lien de remplacement.',
    subject: o.subject || 'Votre carte-cadeau (nouveau lien)',
    marketplace_gift_id: o.marketplace_gift_id || g.marketplacegift_id,
    price_in_cents: o.price_in_cents || g.price_in_cents,
    contacts: [{ email: o.email || g.recipient_email, firstname: (o.firstname || (g.recipient_name || '').split(' ')[0] || ''), lastname: o.lastname || '' }]
  })
  return {
    ok: !!created.ok,
    old: { uuid: g.uuid, shortlink: g.shortlink, status: (cancel.body && cancel.body.gift && cancel.body.gift.status) || 'GIVER_CANCELLED', price_in_cents: g.price_in_cents, brand_code: g.brand_code, recipient_email: g.recipient_email, recipient_name: g.recipient_name },
    created: created.body || null,
    created_status: created.status
  }
}

const DEMO_HTML = `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>TARGO &times; Giftbit &mdash; Integration Demo (Sandbox)</title>
<style>
*{box-sizing:border-box} body{margin:0;font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:#f4f6fa;color:#1c2533}
header{background:#0f1b2d;color:#fff;padding:18px 22px} header h1{margin:0;font-size:18px} header .sub{opacity:.7;font-size:12.5px;margin-top:3px}
.sandbox{background:#fff7e6;border-bottom:1px solid #ffe0a3;color:#7a5200;padding:9px 22px;font-size:12.5px}
main{max-width:860px;margin:18px auto;padding:0 16px}
.card{background:#fff;border:1px solid #e3e8ef;border-radius:12px;padding:16px 18px;margin-bottom:16px}
.card h2{font-size:14px;margin:0 0 10px;color:#0f1b2d}
.step{display:inline-block;width:22px;height:22px;border-radius:50%;background:#5b6ef5;color:#fff;text-align:center;line-height:22px;font-size:12px;margin-right:7px}
.modes label{display:inline-flex;align-items:center;gap:6px;margin-right:18px;font-size:13.5px;cursor:pointer}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px;margin-top:6px}
.brand{border:2px solid #e3e8ef;border-radius:10px;padding:10px;cursor:pointer;display:flex;align-items:center;gap:8px;font-size:12.5px;transition:.12s}
.brand:hover{border-color:#b9c2d6} .brand.sel{border-color:#5b6ef5;background:#eef0ff}
.brand img{width:34px;height:34px;object-fit:contain;border-radius:5px;background:#fff}
.row{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:8px}
input[type=email]{padding:8px 10px;border:1px solid #cfd6e0;border-radius:8px;font-size:13px;min-width:240px}
button{background:#5b6ef5;color:#fff;border:0;border-radius:9px;padding:10px 16px;font-size:13.5px;font-weight:600;cursor:pointer}
button:disabled{opacity:.5;cursor:not-allowed}
.result{background:#f0fff4;border:1px solid #b7e4c7;border-radius:10px;padding:14px;margin-top:10px;display:none}
.result.show{display:block} .result.err{background:#fff0f0;border-color:#f5c2c2}
.link{font-family:ui-monospace,Menlo,monospace;font-size:13px;background:#fff;border:1px dashed #b9c2d6;border-radius:7px;padding:8px 10px;word-break:break-all;margin:8px 0}
.muted{color:#6a7686;font-size:12px} .ok{color:#1b873f;font-weight:600}
.sec{font-size:12.5px;color:#44506a;line-height:1.6} .sec b{color:#0f1b2d}
.tag{display:inline-block;font-size:10.5px;background:#eef0ff;color:#3b46b5;border-radius:5px;padding:1px 6px;margin-left:6px}
</style></head><body>
<header><h1>TARGO Communications &times; Giftbit &mdash; Integration Demo</h1><div class="sub">Reward creation via Giftbit API &mdash; direct brand &amp; multi-brand link (recipient choice)</div></header>
<div class="sandbox">🧪 <b>SANDBOX / TESTBED</b> &mdash; Runs only against <b>testbedapp.giftbit.com</b>. No real cards are issued. The API token is held <b>server-side</b> and is <b>never</b> sent to this page. Amount fixed at <b>$5 CAD</b>, single recipient.</div>
<main>
 <div class="card" style="border-left:4px solid #5b6ef5">
  <h2>For Giftbit reviewers</h2>
  <div class="sec">This page demonstrates TARGO Communications' integration with the <b>Giftbit API (papi/v1)</b>. It is connected to the <b>testbed</b> account only. You can create a reward two ways and inspect the resulting shortlink redemption flow:
   <br>&bull; <b>Direct reward</b> &mdash; one specific brand, fixed at issuance.
   <br>&bull; <b>Reward link (multi-brand)</b> &mdash; several brands offered; the recipient selects their brand on the Giftbit redemption page (the gift's <code>brand_code</code> stays null until then).
   <br>Brands are loaded live from <code>GET /marketplace</code>. Rewards are created via <code>POST /campaign</code> with <code>marketplace_gifts</code>. Links can be cancelled and re-issued with <code>DELETE /gifts/{uuid}</code>. Contact: <b>louis@targo.ca</b>.</div>
 </div>
 <div class="card"><h2><span class="step">1</span>Reward type</h2>
  <div class="modes">
   <label><input type="radio" name="mode" value="direct" checked> <b>Direct reward</b> &mdash; one brand (fixed)</label>
   <label><input type="radio" name="mode" value="template"> <b>Reward link</b> &mdash; several brands, recipient chooses at redemption</label>
  </div>
  <div class="muted" id="modehint" style="margin-top:6px">Pick exactly one brand below.</div>
 </div>
 <div class="card"><h2><span class="step">2</span>Choose brand(s) <span class="tag">GET /marketplace</span></h2>
  <div id="brands" class="grid"><div class="muted">Loading options&hellip;</div></div>
 </div>
 <div class="card"><h2><span class="step">3</span>Recipient &amp; create</h2>
  <div class="row"><input type="email" id="email" placeholder="recipient@example.com (optional, sandbox)" value="demo@targo.ca">
   <button id="go" disabled>Create $5 reward (testbed)</button></div>
  <div class="result" id="result"></div>
 </div>
 <div class="card"><h2>Generated reward links <span class="tag">GET /gifts</span><button id="refresh" style="float:right;padding:5px 11px;font-size:12px;background:#eef0ff;color:#3b46b5">↻ Refresh</button></h2>
  <div id="history"><div class="muted">Loading&hellip;</div></div>
  <div class="muted" style="margin-top:6px">Live statuses from the testbed: <b>SENT_AND_REDEEMABLE</b> &rarr; <b>REDEEMED</b> (brand fills in once the recipient chooses) &rarr; <b>GIVER_CANCELLED</b> (revoked). &ldquo;Revoke&rdquo; cancels a reward link via <code>DELETE /gifts/{uuid}</code>.</div>
 </div>
 <div class="card"><h2>Activity by day <span class="tag">redemption trend</span></h2>
  <div id="stats"><div class="muted">Loading&hellip;</div></div>
  <div class="muted" style="margin-top:6px">Organic redemptions cluster right after delivery, then taper off. A steady daily trickle &mdash; or many <b>instant</b> redemptions (&lt; 15 min after issue) &mdash; can indicate automated / low-profile abuse.</div>
 </div>
 <div class="card"><h2>Security &amp; correct use</h2>
  <div class="sec">
   &bull; <b>Token server-side only</b> &mdash; the Giftbit Bearer token lives in the hub environment; this browser page never receives it.<br>
   &bull; <b>Sandboxed</b> &mdash; this endpoint refuses to run against production; amount, brand list and recipient count are capped.<br>
   &bull; <b>Direct</b> create sends one <code>marketplace_gifts</code> entry; <b>multi-brand</b> sends several &mdash; Giftbit returns a shortlink where the recipient picks their brand (<code>brand_code</code> is null until chosen).<br>
   &bull; <b>HTTPS</b> end-to-end; links are delivered via our own wrapper with independent expiry; links can be cancelled (<code>DELETE /gifts/{uuid}</code>) and re-issued at any time.
  </div>
 </div>
</main>
<script>
var BRANDS=[],SEL=[];
function mode(){return document.querySelector('input[name=mode]:checked').value}
function refreshHint(){document.getElementById('modehint').textContent = mode()==='direct' ? 'Pick exactly one brand below.' : 'Pick 2 or more brands; the recipient chooses at redemption.';syncGo()}
function syncGo(){var m=mode();var ok = m==='direct' ? SEL.length===1 : SEL.length>=2;document.getElementById('go').disabled=!ok}
function toggle(id,el){var i=SEL.indexOf(id);if(i>=0){SEL.splice(i,1);el.classList.remove('sel')}else{if(mode()==='direct'){SEL=[id];document.querySelectorAll('.brand').forEach(function(b){b.classList.remove('sel')})}else{SEL.push(id)}el.classList.add('sel')}syncGo()}
fetch('/giftbit-demo/marketplace').then(function(r){return r.json()}).then(function(d){
 BRANDS=d.brands||[];var c=document.getElementById('brands');c.innerHTML='';
 if(!BRANDS.length){c.innerHTML='<div class="muted">No options.</div>';return}
 BRANDS.forEach(function(b){var el=document.createElement('div');el.className='brand';
  el.innerHTML=(b.image_url?'<img src="'+b.image_url+'" alt="">':'')+'<span>'+b.name.replace('CAD ','').replace(/ \\|.*$/,'')+'</span>';
  el.onclick=function(){toggle(b.id,el)};c.appendChild(el)})
}).catch(function(){document.getElementById('brands').innerHTML='<div class="muted">Failed to load options.</div>'});
document.querySelectorAll('input[name=mode]').forEach(function(r){r.onchange=function(){SEL=[];document.querySelectorAll('.brand').forEach(function(b){b.classList.remove('sel')});refreshHint()}});
document.getElementById('go').onclick=function(){
 var btn=this;btn.disabled=true;btn.textContent='Creating…';
 var res=document.getElementById('result');res.className='result';
 fetch('/giftbit-demo/create',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({brand_ids:SEL,email:document.getElementById('email').value})})
 .then(function(r){return r.json()}).then(function(d){
  btn.textContent='Create $5 reward (testbed)';syncGo();
  if(!d.ok){res.className='result show err';res.innerHTML='<b>Error:</b> '+(d.error||'failed');return}
  var g=(d.gifts&&d.gifts[0])||{};
  if(g.shortlink){renderDemo(d,g);}else{res.className='result show';res.innerHTML='<div class="ok">✓ Reward created &mdash; generating secure link…</div><div class="muted">Campaign '+d.campaign_id+'</div>';pollDemo(d,0);}
 }).catch(function(){btn.textContent='Create $5 reward (testbed)';res.className='result show err';res.innerHTML='Request failed.'})
};
function renderDemo(d,g){var res=document.getElementById('result');
 var html='<div class="ok">✓ '+(d.mode==='template'?'Multi-brand reward link created':'Direct reward created')+' &mdash; $5 CAD (testbed)</div>';
 html+='<div class="link">'+g.shortlink+'</div><a href="'+g.shortlink+'" target="_blank"><button>Open redemption page ↗</button></a>';
 html+='<div class="muted" style="margin-top:8px">Campaign: '+d.campaign_id+' &middot; status: '+(g.status||'')+' &middot; brand: '+(g.brand_code||(d.mode==='template'?'(recipient chooses at redemption)':'set'))+'</div>';
 res.className='result show';res.innerHTML=html;loadHistory();loadStats();}
function pollDemo(d,n){if(n>14){document.getElementById('result').innerHTML+='<div class="muted">Link still generating (testbed latency) — refresh status shortly.</div>';return}
 setTimeout(function(){fetch('/giftbit-demo/status?campaign_id='+encodeURIComponent(d.campaign_id)).then(function(r){return r.json()}).then(function(s){var g=(s.gifts&&s.gifts[0])||{};if(g.shortlink){d.gifts=[g];renderDemo(d,g);}else{pollDemo(d,n+1);}}).catch(function(){pollDemo(d,n+1)})},1800);}
function statusBadge(s){var c=({REDEEMED:'#1b873f',SENT_AND_REDEEMABLE:'#3b46b5',GIVER_CANCELLED:'#b54343'})[s]||'#6a7686';return '<span style="font-size:10px;font-weight:700;color:#fff;background:'+c+';border-radius:5px;padding:1px 7px">'+(s||'—')+'</span>';}
var HIST=[],HPAGE=0,HSIZE=20,HALL=false;
function loadHistory(){fetch('/giftbit-demo/history').then(function(r){return r.json()}).then(function(d){HIST=d.gifts||[];renderHist()}).catch(function(){document.getElementById('history').innerHTML='<div class="muted">Failed to load history.</div>'})}
function renderHist(){var h=document.getElementById('history');if(!HIST.length){h.innerHTML='<div class="muted">No reward links generated yet.</div>';return}var total=HIST.length,pages=HALL?1:Math.max(1,Math.ceil(total/HSIZE));if(HPAGE>=pages)HPAGE=0;var slice=HALL?HIST:HIST.slice(HPAGE*HSIZE,HPAGE*HSIZE+HSIZE);var t='<table style="width:100%;border-collapse:collapse;font-size:12.5px"><tr style="text-align:left;color:#6a7686;border-bottom:1px solid #e3e8ef"><th style="padding:5px 4px">Created</th><th>Brand</th><th>Amount</th><th>Status</th><th>Redeemed</th><th>Link</th><th></th></tr>';slice.forEach(function(g){t+='<tr style="border-bottom:1px solid #f0f2f6"><td style="padding:6px 4px">'+(g.created_date||'')+'</td><td>'+(g.brand_code?g.brand_code:'<span class=muted>recipient chooses</span>')+'</td><td>$'+(((g.price_in_cents||0)/100).toFixed(2))+'</td><td>'+statusBadge(g.status)+'</td><td>'+(g.redeemed_date?g.redeemed_date:'<span class=muted>—</span>')+'</td><td>'+(g.shortlink?'<a href="'+g.shortlink+'" target="_blank">open ↗</a>':'<span class=muted>…</span>')+'</td><td style="text-align:right">'+(g.status==='SENT_AND_REDEEMABLE'?'<button class="rev" data-u="'+g.uuid+'" style="padding:3px 9px;font-size:11px;background:#fff0f0;color:#b54343;border:1px solid #f5c2c2">Revoke</button>':'<span class=muted>'+(g.status==='REDEEMED'?'redeemed':'revoked')+'</span>')+'</td></tr>';});t+='</table>';var pg='<div style="margin-top:9px;display:flex;gap:5px;align-items:center;flex-wrap:wrap"><span class="muted" style="margin-right:4px">'+total+' link(s)</span>';if(!HALL&&pages>1){for(var i=0;i<pages;i++){pg+='<button class="pg" data-p="'+i+'" style="padding:3px 9px;font-size:12px;border:1px solid #cfd6e0;border-radius:6px;cursor:pointer;background:'+(i===HPAGE?'#5b6ef5':'#fff')+';color:'+(i===HPAGE?'#fff':'#3b46b5')+'">'+(i+1)+'</button>';}}pg+='<button id="hall" style="padding:3px 10px;font-size:12px;border:1px solid #cfd6e0;border-radius:6px;cursor:pointer;background:#eef0ff;color:#3b46b5;margin-left:6px">'+(HALL?'Paginate (20/page)':'Show all')+'</button></div>';h.innerHTML=t+pg;bindHist()}
function bindHist(){Array.prototype.forEach.call(document.querySelectorAll('.rev'),function(b){b.onclick=function(){var u=b.getAttribute('data-u');b.disabled=true;b.textContent='…';fetch('/giftbit-demo/revoke',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({uuid:u})}).then(function(r){return r.json()}).then(function(){loadHistory()}).catch(function(){loadHistory()})}});Array.prototype.forEach.call(document.querySelectorAll('.pg'),function(b){b.onclick=function(){HPAGE=parseInt(b.getAttribute('data-p'),10);renderHist()}});var a=document.getElementById('hall');if(a)a.onclick=function(){HALL=!HALL;HPAGE=0;renderHist()}}
document.getElementById('refresh').onclick=loadHistory;
function loadStats(){fetch('/giftbit-demo/stats').then(function(r){return r.json()}).then(renderStats).catch(function(){document.getElementById('stats').innerHTML='<div class="muted">Failed to load.</div>'})}
function renderStats(d){var el=document.getElementById('stats');var days=(d&&d.days)||[];var t=(d&&d.totals)||{};if(!days.length){el.innerHTML='<div class="muted">No data yet.</div>';return}var max=1;days.forEach(function(x){max=Math.max(max,x.created,x.redeemed)});var h='<div style="display:flex;align-items:flex-end;gap:10px;height:118px;padding:4px 2px;overflow-x:auto">';days.forEach(function(x){var ch=Math.round(x.created/max*96),rh=Math.round(x.redeemed/max*96);h+='<div style="text-align:center;min-width:40px"><div style="display:flex;gap:2px;align-items:flex-end;height:98px;justify-content:center"><div title="issued '+x.created+'" style="width:13px;height:'+ch+'px;background:#cfd6e0;border-radius:3px 3px 0 0"></div><div title="redeemed '+x.redeemed+'" style="width:13px;height:'+rh+'px;background:#1b873f;border-radius:3px 3px 0 0"></div></div><div class="muted" style="font-size:10px;margin-top:3px">'+String(x.date).slice(5)+'</div></div>';});h+='</div>';h+='<div style="font-size:12px;margin-top:6px"><span style="display:inline-block;width:10px;height:10px;background:#cfd6e0;border-radius:2px"></span> issued&nbsp;&nbsp;<span style="display:inline-block;width:10px;height:10px;background:#1b873f;border-radius:2px"></span> redeemed&nbsp;·&nbsp;total '+(t.gifts||0)+' issued / '+(t.redeemed||0)+' redeemed';if(t.fast_redemptions)h+='&nbsp;·&nbsp;<b style="color:#b54343">⚠ '+t.fast_redemptions+' instant (&lt;15 min)</b>';if(t.late_redemptions)h+='&nbsp;·&nbsp;<b style="color:#b07a00">'+t.late_redemptions+' late (&gt;72 h)</b>';h+='</div>';el.innerHTML=h}
loadHistory();
loadStats();
refreshHint();
</script></body></html>`
// ─────────────────────────────────────────────────────────────────────────────
// DÉMO « JAIL » (sandbox testbed, publique) — pour la validation par Giftbit.
// Garanties : (1) testbed UNIQUEMENT (refus si prod), (2) montant FIXE 5 $, (3) marques d'une
// liste blanche, (4) 1 destinataire, (5) le token reste SERVEUR (jamais envoyé au navigateur).
// ─────────────────────────────────────────────────────────────────────────────
const DEMO_BRAND_IDS = [386, 574, 572, 617, 619, 620, 621, 622, 568] // CAD, montant variable (≥5 $) — region=1
const DEMO_PRICE = 500
const DEMO_GUARD = () => isTestbed // jamais en prod
async function demoBrands () {
  const r = await marketplace({ region: '1', limit: '50' })
  const list = (r.body && r.body.marketplace_gifts) || []
  return list.filter(g => DEMO_BRAND_IDS.includes(Number(g.id)))
    .map(g => ({ id: g.id, name: g.name, image_url: g.image_url, currency: g.currencyisocode }))
}
async function demoCreate (o = {}) {
  if (!DEMO_GUARD()) return { ok: false, error: 'Démo désactivée hors environnement testbed.' }
  const ids = (Array.isArray(o.brand_ids) ? o.brand_ids : []).map(Number).filter(x => DEMO_BRAND_IDS.includes(x))
  if (!ids.length) return { ok: false, error: 'Choisir au moins une marque de la liste.' }
  const email = /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(String(o.email || '')) ? o.email : 'demo@targo.ca'
  const mode = ids.length > 1 ? 'template' : 'direct'
  const cid = 'targo-demo-' + Date.now()
  const created = await createCampaign({
    id: cid, delivery_type: 'SHORTLINK', expiry: defaultExpiry(),
    subject: 'TARGO × Giftbit — Integration demo', message: mode === 'template' ? 'Choose your gift card.' : 'Your gift card.',
    marketplace_gifts: ids.map(id => ({ id, price_in_cents: DEMO_PRICE })),
    contacts: [{ email, firstname: 'Demo', lastname: 'Recipient' }]
  })
  if (!created.ok) return { ok: false, error: 'Création refusée', detail: created.body }
  // Le gift est asynchrone ET le shortlink se génère APRÈS la carte (~8 s) → on sonde jusqu'au shortlink (max ~12 s ; la page poll aussi en repli).
  let gifts = []
  for (let i = 0; i < 5; i++) { await new Promise(r => setTimeout(r, 800)); const g = await listGifts({ campaign_id: cid }); gifts = (g.body && g.body.gifts) || []; if (gifts.length && gifts[0].shortlink) break } // réponse rapide ; la page poll /status pour le shortlink si pas encore prêt
  return { ok: true, mode, campaign_id: cid, campaign_uuid: (created.body && created.body.campaign && created.body.campaign.uuid) || '', price_in_cents: DEMO_PRICE, brands: ids, gifts: gifts.map(g => ({ uuid: g.uuid, shortlink: g.shortlink, status: g.status, brand_code: g.brand_code })) }
}
// Historique des liens générés par la démo (campaign_id préfixé targo-demo-), trié du + récent au + ancien.
async function demoHistory () {
  const r = await listGifts({ limit: 500 })
  const gs = (r.body && r.body.gifts) || []
  return gs.filter(g => String(g.campaign_id || '').startsWith('targo-demo-'))
    .sort((a, b) => String(b.created_date || '').localeCompare(String(a.created_date || '')))
    .map(g => ({ uuid: g.uuid, shortlink: g.shortlink, status: g.status, brand_code: g.brand_code, price_in_cents: g.price_in_cents, created_date: g.created_date, redeemed_date: g.redeemed_date || '', recipient_email: g.recipient_email }))
}
// Stat « activité par jour » : créés vs rédemés/jour + signaux d'abus (rédemptions « instantanées » < 15 min = bot-like ;
// rédemptions tardives = filet régulier après la courbe organique). Révèle un pirate low-profile.
function dayOf (s) { return String(s || '').slice(0, 10) }
function _ts (s) { const t = Date.parse(String(s || '').replace(' ', 'T') + 'Z'); return isFinite(t) ? t : null }
function dailyAgg (gifts) {
  const map = {}; let redeemed = 0; let fast = 0; let lateDrip = 0
  for (const g of gifts) {
    const cd = dayOf(g.created_date); if (cd) { (map[cd] = map[cd] || { created: 0, redeemed: 0 }).created++ }
    if (g.status === 'REDEEMED' && g.redeemed_date) {
      const rd = dayOf(g.redeemed_date); (map[rd] = map[rd] || { created: 0, redeemed: 0 }).redeemed++; redeemed++
      const t1 = _ts(g.created_date); const t2 = _ts(g.redeemed_date)
      if (t1 != null && t2 != null) { const dh = (t2 - t1) / 3600000; if (dh < 0.25) fast++; if (dh > 72) lateDrip++ } // <15 min = instantané ; >72 h = tardif
    }
  }
  const days = Object.keys(map).sort().map(d => ({ date: d, created: map[d].created, redeemed: map[d].redeemed }))
  return { days, totals: { gifts: gifts.length, redeemed, fast_redemptions: fast, late_redemptions: lateDrip } }
}

// Révocation depuis la démo : SEULEMENT un gift démo (garde-fou) → DELETE → GIVER_CANCELLED (montre le traitement d'un lien compromis).
async function demoRevoke (uuid) {
  if (!DEMO_GUARD()) return { ok: false, error: 'testbed only' }
  const cur = await getGift(uuid); const g = cur.body && cur.body.gift
  if (!g || !String(g.campaign_id || '').startsWith('targo-demo-')) return { ok: false, error: 'Only demo links can be revoked here.' }
  if (g.status !== 'SENT_AND_REDEEMABLE') return { ok: false, error: 'Only unredeemed links can be revoked (current status: ' + g.status + ').' }
  const r = await cancelGift(uuid)
  return { ok: r.ok, status: (r.body && r.body.gift && r.body.gift.status) || 'GIVER_CANCELLED' }
}

async function handle (req, res, method, path, url) {
  const qs = (url && url.searchParams) || new URLSearchParams()
  // ── Démo publique (sandbox) : page HTML + données testbed. Sert AVANT le contrôle TOKEN pour que la page charge toujours. ──
  if (path === '/giftbit-demo' || path === '/giftbit-demo/' || path === '/giftbit-demo.html') { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' }); return res.end(DEMO_HTML) }
  if (path.startsWith('/giftbit-demo/')) {
    if (!TOKEN) return json(res, 503, { ok: false, error: 'token absent' })
    if (!isTestbed) return json(res, 403, { ok: false, error: 'Démo disponible en testbed seulement.' })
    if (path === '/giftbit-demo/marketplace') return json(res, 200, { ok: true, testbed: true, brands: await demoBrands() })
    if (path === '/giftbit-demo/create' && method === 'POST') { const b = await parseBody(req); return json(res, 200, await demoCreate(b)) }
    if (path === '/giftbit-demo/status') { const r = await listGifts({ campaign_id: qs.get('campaign_id') || '' }); return json(res, 200, r.body || { ok: false }) }
    if (path === '/giftbit-demo/history') return json(res, 200, { ok: true, gifts: await demoHistory() })
    if (path === '/giftbit-demo/stats') return json(res, 200, { ok: true, ...dailyAgg(await demoHistory()) })
    if (path === '/giftbit-demo/revoke' && method === 'POST') { const b = await parseBody(req); if (!b.uuid) return json(res, 400, { ok: false, error: 'uuid requis' }); return json(res, 200, await demoRevoke(b.uuid)) }
    return json(res, 404, { ok: false, error: 'route démo inconnue' })
  }
  if (!TOKEN) return json(res, 503, { ok: false, error: 'GIFTBIT_TOKEN absent de l\'env du hub' })
  try {
    if (path === '/giftbit/ping') { const r = await ping(); return json(res, 200, { ok: r.ok, testbed: isTestbed, ...(r.body || {}), _status: r.status }) }
    if (path === '/giftbit/funds') { const r = await funds(); return json(res, 200, { ok: r.ok, testbed: isTestbed, ...(r.body || {}) }) }
    if (path === '/giftbit/marketplace') { const r = await marketplace(Object.fromEntries(qs)); return json(res, 200, r.body || { ok: false, error: r.error }) }
    if (path === '/giftbit/brands') { const r = await brands(Object.fromEntries(qs)); return json(res, 200, r.body || { ok: false, error: r.error }) }
    if (path === '/giftbit/gifts' && method === 'GET') { const r = await listGifts(Object.fromEntries(qs)); return json(res, 200, r.body || { ok: false, error: r.error }) }
    if (path === '/giftbit/stats' && method === 'GET') { const q = Object.fromEntries(qs); const r = await listGifts({ campaign_id: q.campaign_id || '', campaign_uuid: q.campaign_uuid || '', limit: q.limit || 1000 }); const gifts = (r.body && r.body.gifts) || []; return json(res, 200, { ok: true, count: gifts.length, ...dailyAgg(gifts) }) }
    if (path.startsWith('/giftbit/gift/') && method === 'GET') { const r = await getGift(path.split('/').pop()); return json(res, 200, r.body || { ok: false, error: r.error }) }
    if (path === '/giftbit/cancel' && method === 'POST') { const b = await parseBody(req); if (!b.uuid) return json(res, 400, { ok: false, error: 'uuid requis' }); const r = await cancelGift(b.uuid); return json(res, 200, { ok: r.ok, ...(r.body || {}) }) }
    if (path === '/giftbit/campaign' && method === 'POST') { const b = await parseBody(req); if (!b.id || !b.marketplace_gift_id || !b.price_in_cents || !Array.isArray(b.contacts) || !b.contacts.length) return json(res, 400, { ok: false, error: 'id, marketplace_gift_id, price_in_cents, contacts[] requis' }); const r = await createCampaign(b); return json(res, 200, { ok: r.ok, ...(r.body || {}), _status: r.status }) }
    if (path === '/giftbit/replace' && method === 'POST') { const b = await parseBody(req); if (!b.uuid) return json(res, 400, { ok: false, error: 'uuid requis' }); const r = await replaceGift(b); return json(res, r.ok ? 200 : 500, r) }
    return json(res, 404, { ok: false, error: 'route giftbit inconnue' })
  } catch (e) { return json(res, 500, { ok: false, error: String((e && e.message) || e) }) }
}

module.exports = { handle, api, ping, funds, marketplace, brands, listGifts, getGift, cancelGift, createCampaign, replaceGift, isTestbed, BASE }
