// Tempo — News briefing (live RSS via allorigins.win CORS proxy)
// Free, no API key, parses XML directly. Falls back to last good cache on error.
// Sources FR (Le Monde, BFM Business) + US marché en direct (MarketWatch, CNBC,
// Reuters business, Yahoo Finance, Investing). Plusieurs titres par source,
// dédupliqués, triés par récence. Refresh auto périodique dans la journée.

// Le Monde + BFM (que l'utilisateur aime) + marchés/forex/macro en temps réel.
// Le briefing ne montre QUE les titres du JOUR (filtre plus bas), au fil de la journée.
const NEWS_FEEDS = [
  { source: 'Le Monde',          url: 'https://www.lemonde.fr/economie/rss_full.xml' },
  { source: 'BFM Business',      url: 'https://www.bfmtv.com/rss/economie/' },
  { source: 'Investing · Forex',    url: 'https://www.investing.com/rss/news_1.rss' },
  { source: 'Investing · Économie', url: 'https://www.investing.com/rss/news_14.rss' },
  { source: 'MarketWatch',       url: 'https://feeds.content.dowjones.io/public/rss/mw_topstories' },
  { source: 'CNBC',              url: 'https://search.cnbc.com/rs/search/combinedcms/view.xml?partnerId=wrss01&id=20910258' },
  { source: 'Reuters Business',  url: 'https://www.investing.com/rss/news_285.rss' },
];
const NEWS_CACHE_KEY = 'tempo:news:v6';
const NEWS_TTL_MS = 12 * 60 * 1000;          // 12 min — cohérent avec le refresh auto
const NEWS_REFRESH_MS = 12 * 60 * 1000;      // re-fetch périodique pendant la journée
const NEWS_MAX_ITEMS = 6;                    // 6 titres max, pas un fil fouillis
const PER_FEED_ITEMS = 3;                    // 3 titres par source avant filtre du jour + tri

// Plusieurs relais CORS essayés dans l'ordre : si l'un est down (allorigins l'a souvent
// été), on bascule au suivant. Chaque relais renvoie soit le XML brut, soit un JSON
// { contents: "<xml>" } (allorigins/get). C'est ce qui fiabilise le briefing.
const NEWS_PROXIES = [
  { make: (u) => 'https://api.allorigins.win/raw?url=' + encodeURIComponent(u), json: false },
  { make: (u) => 'https://corsproxy.io/?url=' + encodeURIComponent(u),          json: false },
  { make: (u) => 'https://api.allorigins.win/get?url=' + encodeURIComponent(u), json: true  },
  { make: (u) => 'https://thingproxy.freeboard.io/fetch/' + u,                  json: false },
];

function decodeCdata(s) {
  return (s || '').replace(/<!\[CDATA\[/g, '').replace(/\]\]>/g, '').trim();
}
function stripTags(s) {
  return (s || '').replace(/<[^>]*>/g, '').replace(/&nbsp;/g, ' ').replace(/\s+/g, ' ').trim();
}
function decodeEntities(s) {
  if (!s) return '';
  const map = { '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"', '&#39;': "'", '&apos;': "'", '&rsquo;': '’', '&lsquo;': '‘', '&laquo;': '«', '&raquo;': '»', '&hellip;': '…', '&nbsp;': ' ' };
  // 1) Named entities from map
  let out = s.replace(/&[a-z]+;/gi, m => map[m.toLowerCase()] || m);
  // 2) Hex numeric entities: &#xE9; → 'é'
  out = out.replace(/&#x([0-9a-f]+);/gi, (_, hex) => {
    try { return String.fromCodePoint(parseInt(hex, 16)); } catch (e) { return _; }
  });
  // 3) Decimal numeric entities: &#233; → 'é'
  out = out.replace(/&#(\d+);/g, (_, dec) => {
    try { return String.fromCodePoint(parseInt(dec, 10)); } catch (e) { return _; }
  });
  return out;
}

// Parse jusqu'à `max` <item> (ou <entry> Atom) du flux, les plus récents d'abord.
function parseRSSItems(xml, max = PER_FEED_ITEMS) {
  const out = [];
  const isAtom = /<entry[\s>]/i.test(xml) && !/<item[\s>]/i.test(xml);
  const re = isAtom
    ? /<entry[^>]*>([\s\S]*?)<\/entry>/gi
    : /<item[^>]*>([\s\S]*?)<\/item>/gi;
  let m;
  while ((m = re.exec(xml)) !== null && out.length < max) {
    const body = m[1];
    const titleM = body.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
    const dateM  = body.match(/<(?:pubDate|published|updated|dc:date)[^>]*>([\s\S]*?)<\/(?:pubDate|published|updated|dc:date)>/i);
    let link = '';
    // RSS: <link>url</link> ; Atom: <link href="url"/>
    const linkText = body.match(/<link[^>]*>([\s\S]*?)<\/link>/i);
    if (linkText && stripTags(decodeCdata(linkText[1]))) {
      link = stripTags(decodeCdata(linkText[1]));
    } else {
      const linkHref = body.match(/<link[^>]*href=["']([^"']+)["']/i);
      if (linkHref) link = decodeEntities(linkHref[1]);
    }
    const title = decodeEntities(stripTags(decodeCdata(titleM ? titleM[1] : '')));
    if (!title) continue;
    const pubStr = decodeCdata(dateM ? dateM[1] : '');
    const pub = pubStr ? new Date(pubStr) : null;
    out.push({ title, link, pub });
  }
  return out;
}

// Récupère le XML d'un flux en essayant chaque relais jusqu'au premier qui répond
// avec des items. Renvoie le XML (string) ou null si tous les relais échouent.
async function fetchFeedXml(feedUrl) {
  const TIMEOUT_MS = 8000;
  for (const proxy of NEWS_PROXIES) {
    try {
      const controller = new AbortController();
      const t = setTimeout(() => controller.abort(), TIMEOUT_MS);
      const r = await fetch(proxy.make(feedUrl), { mode: 'cors', signal: controller.signal });
      clearTimeout(t);
      if (!r.ok) continue;
      let xml;
      if (proxy.json) { const j = await r.json(); xml = j && j.contents; }
      else { xml = await r.text(); }
      if (xml && (xml.includes('<item') || xml.includes('<entry'))) return xml;
    } catch (e) { /* relais down → on tente le suivant */ }
  }
  return null;
}

async function fetchRSSItem(feed) {
  try {
    const xml = await fetchFeedXml(feed.url);
    if (!xml) throw new Error('tous les relais ont échoué');
    const raw = parseRSSItems(xml, PER_FEED_ITEMS);
    if (!raw.length) throw new Error('aucun titre');
    const now = Date.now();
    return raw.map(it => {
      const valid = it.pub && !isNaN(it.pub.getTime());
      return {
        source: feed.source,
        headline: it.title.slice(0, 220),
        link: it.link || '#',
        // timestamp ms pour tri/dédup ; on borne au présent pour éviter les dates futures aberrantes
        ts: valid ? Math.min(it.pub.getTime(), now) : null,
        time: valid
          ? it.pub.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })
          : '',
      };
    });
  } catch (e) {
    console.warn('[Tempo] news ' + feed.source + ' failed:', e?.message || e);
    return null;
  }
}

// Aplatit les résultats par source, dédoublonne (titre normalisé), trie par récence,
// limite à NEWS_MAX_ITEMS. Les items sans date passent après les datés.
function mergeNewsResults(results) {
  const flat = [];
  results.forEach(r => { if (Array.isArray(r)) r.forEach(it => flat.push(it)); });
  const seen = new Set();
  const deduped = [];
  for (const it of flat) {
    const key = (it.headline || '').toLowerCase().replace(/[^a-z0-9àâäéèêëïîôöùûüç]+/gi, ' ').trim();
    if (!key || seen.has(key)) continue;
    seen.add(key);
    deduped.push(it);
  }
  deduped.sort((a, b) => {
    if (a.ts == null && b.ts == null) return 0;
    if (a.ts == null) return 1;
    if (b.ts == null) return -1;
    return b.ts - a.ts;
  });
  // Ne garder que les titres du JOUR (date locale) : au fil de la journée, pas un
  // backlog des jours passés. Repli sur les plus récents si rien n'est encore tombé
  // aujourd'hui (très tôt le matin), pour ne jamais afficher un briefing vide.
  const tz = (typeof window !== 'undefined' && window.__appTz) || 'Europe/Paris';
  const dayOf = (ts) => {
    if (ts == null) return null;
    if (typeof window !== 'undefined' && typeof window.localDayInTz === 'function') {
      try { return window.localDayInTz(new Date(ts), tz); } catch (e) {}
    }
    return new Date(ts).toISOString().slice(0, 10);
  };
  const todayKey = dayOf(Date.now());
  const todays = deduped.filter(it => dayOf(it.ts) === todayKey);
  const chosen = todays.length ? todays : deduped;
  return chosen.slice(0, NEWS_MAX_ITEMS);
}

// Titre adaptatif selon l'heure locale (Europe/Paris via getHours du device).
// < 12h → "Briefing du matin" ; 12h-18h → "Point marché" ;
// >= 18h (clôture US en cours / after-hours) → "Clôture & after-hours" ; nuit → "Marchés overnight".
function briefingTitle(hour) {
  const h = (hour == null) ? new Date().getHours() : hour;
  if (h >= 0 && h < 5)  return 'Marchés overnight';
  if (h < 12)           return 'Briefing du matin';
  if (h < 18)           return 'Point marché';
  return 'Clôture & after-hours';
}

function readNewsCache() {
  try {
    const c = JSON.parse(localStorage.getItem(NEWS_CACHE_KEY) || 'null');
    if (!c || !Array.isArray(c.items)) return null;
    return c;
  } catch (e) { return null; }
}
function writeNewsCache(items) {
  try { localStorage.setItem(NEWS_CACHE_KEY, JSON.stringify({ ts: Date.now(), items })); } catch (e) {}
}

// force=true → ignore le cache frais (utilisé par le refresh périodique).
async function loadNews(force) {
  const c = readNewsCache();
  if (!force && c && Date.now() - c.ts < NEWS_TTL_MS && c.items.length) {
    return c.items;
  }
  const results = await Promise.all(NEWS_FEEDS.map(f => fetchRSSItem(f)));
  const fresh = mergeNewsResults(results);
  if (fresh.length) {
    writeNewsCache(fresh);
    return fresh;
  }
  // Tout a échoué → on garde le cache (même périmé) plutôt que vide.
  if (c && c.items.length) return c.items;
  return [];
}

function useNewsBriefing() {
  const initial = readNewsCache();
  const [items, setItems] = React.useState(initial ? initial.items : []);

  React.useEffect(() => {
    let live = true;
    const run = async (force) => {
      const next = await loadNews(force);
      if (live && next.length) setItems(next);
    };
    run(false);
    // Refresh auto pendant la journée (bypass du cache frais à chaque tick).
    const id = setInterval(() => run(true), NEWS_REFRESH_MS);
    return () => { live = false; clearInterval(id); };
  }, []);

  return items;
}

Object.assign(window, {
  NEWS_FEEDS, NEWS_CACHE_KEY, NEWS_TTL_MS, NEWS_REFRESH_MS,
  fetchRSSItem, parseRSSItems, mergeNewsResults, loadNews,
  useNewsBriefing, briefingTitle,
});
