// Tempo — Supabase client (initialized from CDN UMD bundle loaded in index.html)

const SUPABASE_URL = 'https://twaldzmjryjyrzwjckpe.supabase.co';
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InR3YWxkem1qcnlqeXJ6d2pja3BlIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzk3MzY5NDAsImV4cCI6MjA5NTMxMjk0MH0.s51IaUC7nuLb-PmRVNpKZ-uVlllCPH8zpPD-V04otWY';

// The supabase UMD bundle exposes `window.supabase` with a `createClient` factory.
// We expose our configured client as `window.sb`.
if (typeof window.supabase !== 'object' || typeof window.supabase.createClient !== 'function') {
  console.error('[Tempo] Supabase UMD SDK is not loaded. Check that index.html loads supabase-js BEFORE the JSX scripts.');
} else if (!window.sb) {
  window.sb = window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
    auth: {
      persistSession: true,
      autoRefreshToken: true,
      detectSessionInUrl: true,
    },
  });
}

// « Aujourd'hui » → 'YYYY-MM-DD' dans le fuseau du profil (window.__appTz,
// repli Europe/Paris). Charte §4 : « aujourd'hui » est le jour civil vécu par
// l'utilisateur dans le fuseau choisi dans Réglages, jamais celui du navigateur.
function todayISO() {
  return dateISO(new Date());
}

// dateISO(d) → 'YYYY-MM-DD'. Une chaîne est déjà un jour civil : on la garde
// telle quelle. Un instant (Date ou horodatage) donne son jour dans le fuseau
// du profil, via localDayInTz (format-prefs.jsx, chargé juste après ce
// fichier) ; le repli Intl ne sert que si ce module manque. Jamais
// getFullYear/getDate : ce serait le jour du navigateur.
function dateISO(d) {
  if (d === undefined || d === null || d === '') d = new Date();
  if (typeof d === 'string') return d.slice(0, 10);
  const inst = d instanceof Date ? d : new Date(d);
  if (isNaN(inst.getTime())) return '';
  const tz = (typeof window !== 'undefined' && window.__appTz) || 'Europe/Paris';
  if (typeof localDayInTz === 'function') {
    try { return localDayInTz(inst, tz); } catch (e) { /* repli ci-dessous */ }
  }
  try {
    const parts = new Intl.DateTimeFormat('en-CA', { timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit' }).formatToParts(inst);
    const part = (t) => (parts.find((p) => p.type === t) || {}).value;
    if (part('year') && part('month') && part('day')) return part('year') + '-' + part('month') + '-' + part('day');
  } catch (e) { /* fuseau inconnu : repli UTC ci-dessous */ }
  return inst.toISOString().slice(0, 10);
}

// Traduit toute erreur (authentification, base, réseau, Worker, SDK) en phrase
// française lisible. Charte §7 : le message brut part dans
// console.warn('[Tempo] …') et n'apparaît jamais à l'écran. Ordre : code
// structuré (GoTrue, SQLSTATE Postgres, PGRST, statut HTTP) ou motif connu,
// puis filet. Un message déjà rédigé en français par nos modules (Error sans
// code) passe tel quel ; la fonction est idempotente (un message déjà traduit
// ressort à l'identique).
//
// « Rédigé en français par nos modules » se reconnaît ainsi (sbIsOwnFrench) :
// ni code, ni statut HTTP, ni nom d'erreur du SDK ou du navigateur, et un mot
// français sans équivalent anglais (article, préposition, élision, vocabulaire
// d'erreur) HORS des passages cités entre guillemets. Les citations sont
// écartées parce qu'elles reprennent une saisie de l'utilisateur : une
// stratégie « Break of structure » ou « Network effect » ne doit ni faire
// passer la phrase pour de l'anglais, ni déclencher un motif (« réseau »,
// « mot de passe trop courant »), et une valeur accentuée citée dans un message
// anglais ne doit pas le faire passer pour du français. Ce contrôle précède
// donc tous les motifs. Un mot anglais hors citation l'emporte sur les petits
// mots communs aux deux langues (SB_EN_VETO) : le message n'est alors pas tenu
// pour français.
const SB_FR_LETTERS = 'a-zà-öø-ÿœ';
// Un mot compte s'il est isolé : ni lettre, ni chiffre, ni tiret collé (le
// « de » de « de-serialize » n'est pas la préposition).
const SB_FR_GLUE = SB_FR_LETTERS + '0-9_\\-';
const SB_FR_EVIDENCE = new RegExp(
  '(?:^|[^' + SB_FR_GLUE + '])(?:'
  + '(?:le|la|les|un|une|des|du|de|au|aux|et|ou|où|à|est|sont|ce|cette|ces|il|elle|tu|ta|tes|te|ne|pas|que|qui|pour|avec|dans|sur|trop|déjà|aucun|aucune'
  + '|erreur|échec|invalide|introuvable|indisponible|obligatoire|réessaie|vérifie)(?![' + SB_FR_GLUE + '])'
  + '|(?:[cdjlmnst]|qu)[’\'](?=[' + SB_FR_LETTERS + '])'
  + ')', 'i');
// Passages cités : « … », “ … ”, " … " et ' … ' (l'apostrophe ouvrante n'est
// pas collée à une lettre, ce qui laisse les élisions « l'habitude » en place).
const SB_QUOTED = new RegExp('«[^»]*»|“[^”]*”|"[^"]*"|(?:^|[^' + SB_FR_LETTERS + '])\'[^\']*\'(?![' + SB_FR_LETTERS + '])', 'gi');
// Mots anglais sans homographe français courant (« relation » ou « service »
// en sont exclus). Liste unique, employée deux fois : par le filet final de
// sbErrorMessage (message anglais → phrase neutre) et par le veto ci-dessous.
const SB_EN_WORDS = 'error|failed|failure|invalid|null|constraint|violates|schema|column|does not exist|denied|jwt|token|expired|unauthori[sz]ed|forbidden|not found|policy|syntax|unexpected|could not|cannot|unable|missing|not allowed|overflow|coerce|unavailable|bad request|server|timeout|went wrong|deadlock|required|must|should|please|already|unknown|unreachable|upstream|refused|rejected|disabled|exceeded|unsupported|malformed|internal|gateway|request|response|connection|locked|the|is|are|was|were|has|have|been|not|of|for|with|to|from|this|that|rows?|returned|requested';
const SB_EN_FILET = new RegExp('PGRST|\\b(' + SB_EN_WORDS + ')\\b', 'i');
// Veto : un message qui contient un mot anglais HORS citation n'est pas « déjà
// en français », même s'il contient aussi un petit mot commun aux deux langues
// (« Failed to fetch de la page », « Quota exceeded pour ce compte ») ; il suit
// alors les motifs puis le filet, comme tout message anglais. La liste est
// celle du filet, moins « token » et « timeout », emprunts courants en français
// (« Le token du calendrier est invalide. »), plus les mots des motifs placés
// avant le filet. Bornes de mot : celles de SB_FR_EVIDENCE et non \b, qui en
// JavaScript voit une frontière avant une lettre accentuée (« noté »
// contiendrait « not », « forêt » « for »).
const SB_EN_VETO = new RegExp(
  'PGRST|(?:^|[^' + SB_FR_LETTERS + '0-9_])(?:'
  + SB_EN_WORDS.split('|').filter(function (w) { return w !== 'token' && w !== 'timeout'; }).join('|')
  + '|network|fetch|load failed|aborted|duplicate|permission|password|credentials|characters|rate limit|too many|weak|leaked|pwned|reach|session missing|registered|confirmed|signups?'
  + ')(?![' + SB_FR_LETTERS + '0-9_])', 'i');
function sbIsOwnFrench(message, code, status, name) {
  if (code || status || (name !== '' && name !== 'Error')) return false;
  if (!/\s/.test(message)) return false; // un code nu (« not_configured ») n'est pas une phrase
  const t = message.replace(SB_QUOTED, ' ');
  if (SB_EN_VETO.test(t)) return false;
  return SB_FR_EVIDENCE.test(t);
}

function sbErrorMessage(err) {
  if (!err) return '';
  const GENERIC = 'Une erreur est survenue. Réessaie dans un instant.';
  const TIMEOUT = 'Le serveur met trop de temps à répondre. Réessaie dans un instant.';
  const UNAVAILABLE = 'Le service est momentanément indisponible. Réessaie dans un instant.';
  const SESSION = 'Ta session a expiré. Reconnecte-toi.';
  const o = typeof err === 'object' ? err : {};
  // Réponse d'un Worker ou d'une fonction : { error: 'texte' } ou { error: { message, code } }.
  const inner = o.error && typeof o.error === 'object' ? o.error : {};
  const m = String(typeof err === 'string' ? err
    : (o.message || o.error_description || o.msg || (typeof o.error === 'string' ? o.error : '') || inner.message || '')).trim();
  const code = String(o.code || o.error_code || inner.code || '');
  const name = String(o.name || '');
  // Statut HTTP : porté par l'erreur (AuthError, Response : status ; stockage
  // Supabase : statusCode, en chaîne) ou par nos « HTTP 502 ».
  const httpInMsg = m.match(/^HTTP (\d{3})\b/);
  const sc = String(o.statusCode || inner.statusCode || '');
  const status = Number(o.status) || (/^\d{3}$/.test(sc) ? Number(sc) : 0) || (httpInMsg ? Number(httpInMsg[1]) : 0);
  const fr = (() => {
    // Message de nos modules, déjà en français : tel quel, avant tout motif.
    if (sbIsOwnFrench(m, code, status, name)) return m;
    // Authentification
    if (code === 'invalid_credentials' || /Invalid login credentials/i.test(m)) return 'Email ou mot de passe incorrect.';
    if (code === 'email_not_confirmed' || /Email not confirmed/i.test(m)) return 'Tu dois confirmer ton email avant de te connecter.';
    // Message neutre : ne confirme pas l'existence d'un compte (anti-énumération d'emails).
    if (code === 'user_already_exists' || code === 'email_exists' || /User already registered|email address has already been registered/i.test(m)) return 'Impossible de créer ce compte. Si tu as déjà un compte, connecte-toi ou utilise « Mot de passe oublié ».';
    const wait = m.match(/after (\d+) seconds?/i);
    if (wait) { const n = Number(wait[1]); return 'Par sécurité, attends ' + n + (n > 1 ? ' secondes' : ' seconde') + ' avant de refaire une demande.'; }
    if (status === 429 || /^over_\w*rate_limit$/.test(code) || /rate limit|too many requests/i.test(m)) return 'Trop de tentatives — réessaie dans quelques minutes.';
    if (code === 'same_password' || /different from the old password/i.test(m)) return 'Ton nouveau mot de passe doit être différent de l’ancien.';
    // Politique de mot de passe GoTrue (longueur minimale, protection mots de passe fuités).
    if (/at least (\d+) characters/i.test(m)) return 'Le mot de passe doit faire au moins ' + m.match(/(\d+) characters/i)[1] + ' caractères.';
    if (code === 'weak_password' || /weak|easy to guess|pwned|leaked/i.test(m)) return 'Ce mot de passe est trop courant ou a fuité dans une base publique. Choisis-en un autre.';
    if (/should contain at least one character of each/i.test(m)) return 'Le mot de passe doit combiner plusieurs types de caractères (lettres, chiffres, symboles).';
    if (/requires a valid password|password is required/i.test(m)) return 'Saisis un mot de passe.';
    if (code === 'email_address_invalid' || /email address.*invalid|validate email/i.test(m)) return 'Adresse email invalide. Vérifie ta saisie.';
    if (code === 'signup_disabled' || /signups? not allowed/i.test(m)) return 'Les inscriptions sont momentanément fermées. Réessaie plus tard.';
    if (code === 'otp_expired' || /token has expired|link is invalid or has expired/i.test(m)) return 'Ce lien a expiré ou a déjà servi. Demandes-en un nouveau.';
    if (/^(session_not_found|session_expired|refresh_token_not_found|refresh_token_already_used|user_not_found|bad_jwt|no_authorization|PGRST30\d)$/.test(code)
      || status === 401 || /auth session missing|jwt|refresh token|non authentifié/i.test(m)) return SESSION;
    // Envoi d'email par GoTrue (confirmation, récupération, lien magique).
    if (/error sending .*email/i.test(m)) return 'L’envoi de l’email a échoué. Réessaie dans quelques minutes.';
    // Erreur côté base à l'inscription (souvent un trigger sur auth.users).
    if (/database error|saving new user/i.test(m)) return 'Création du compte momentanément indisponible. Réessaie dans un instant.';
    // Requête interrompue (délai dépassé via AbortController). Avant le motif
    // réseau : Safari écrit « Fetch is aborted ».
    if (name === 'AbortError' || name === 'TimeoutError' || /\baborted\b/i.test(m)) return TIMEOUT;
    // Réseau (Chrome « Failed to fetch », Firefox « NetworkError », Safari « Load failed » ;
    // le SDK lève AuthRetryableFetchError sans statut ou FunctionsFetchError).
    if (/network|fetch|load failed/i.test(m) || name === 'FunctionsFetchError'
      || (name === 'AuthRetryableFetchError' && !(status >= 500))) return 'Problème réseau — vérifie ta connexion.';
    // Base de données (SQLSTATE Postgres)
    if (code === '23505' || /duplicate key/i.test(m)) return 'Cet élément existe déjà.';
    if (code === '23503' || /foreign key constraint/i.test(m)) return 'Cet élément est lié à d’autres données ou n’existe plus. Actualise la page puis réessaie.';
    if (code === '23502' || /violates not-null constraint/i.test(m)) return 'Un champ obligatoire est vide. Vérifie ta saisie.';
    if (code === '42501' || status === 403 || /permission denied|row-level security/i.test(m)) return 'Accès refusé à cette donnée. Reconnecte-toi puis réessaie.';
    if (code === '22001' || code === '23514' || /value too long|check constraint/i.test(m)) return 'Une valeur saisie est trop longue ou invalide. Vérifie ta saisie.';
    if (code === '22003' || /out of range|numeric field overflow/i.test(m)) return 'Un nombre saisi est trop grand. Vérifie ta saisie.';
    if (code === '22P02' || code === '22007' || code === '22008' || /invalid input syntax|date\/time field value/i.test(m)) return 'Une valeur saisie n’a pas le bon format. Vérifie ta saisie.';
    if (code === '57014' || status === 408 || status === 504 || /statement timeout|timed? ?out\b/i.test(m)) return TIMEOUT;
    // Serveur ou base indisponible (5xx, SQLSTATE 08 connexion, 53 ressources, 57P arrêt).
    if (status >= 500 || /^(08|53|57P)[0-9A-Z]{3}$/.test(code)) return UNAVAILABLE;
    // Filet : tout autre code technique (schéma absent 42703 / 42P01 / 42883,
    // PGRST116 / 202 / 204 / 205…), toute erreur du SDK (AuthApiError,
    // PostgrestError, FunctionsHttpError, StorageApiError… toujours en anglais),
    // un code nu renvoyé par un Worker (« not_configured », « locked ») ou tout
    // message anglais → phrase neutre, jamais l'anglais brut. Les mots retenus
    // n'existent pas en français (« relation » ou « service » en sont exclus).
    if (/^(\d{2}[0-9A-Z]{3}|PGRST\d+)$/.test(code)
      || /^(Auth|Postgrest|Functions|Storage)\w*Error$/.test(name)
      || /^HTTP \d{3}$/.test(m)
      || /^[a-z][a-z0-9]*(_[a-z0-9]+)+$/i.test(m)
      || SB_EN_FILET.test(m)) return GENERIC;
    // Dernier cas : aucun motif ni mot anglais reconnu, mais l'erreur porte un
    // code, un statut ou un nom d'erreur technique (« boom » code 500,
    // « Conflict » code 409, « Unprocessable Entity » statut 422, réponse du
    // stockage). Elle vient d'un serveur ou du navigateur, jamais de nos
    // modules (leurs messages n'ont ni code ni statut) : phrase neutre, et
    // « indisponible » pour un code 5xx donné en chaîne (les statuts ≥ 500
    // sont traités plus haut).
    if (code || status || (name && name !== 'Error')) return /^5\d\d$/.test(code) ? UNAVAILABLE : GENERIC;
    return m || GENERIC;
  })();
  if (fr !== m) console.warn('[Tempo] erreur' + (code || status ? ' (' + (code || status) + ')' : '') + ' :', m || err);
  return fr;
}

Object.assign(window, { SUPABASE_URL, SUPABASE_ANON_KEY, todayISO, dateISO, sbErrorMessage });
