// Tempo — account & data management (GDPR: reset journal, delete account)
// All Supabase calls go through window.sb. Exposes window.resetMyData / window.deleteMyAccount.
//
// Storage layout (see state.jsx → uploadDayAttachment): files live at
//   {uid}/{date}/{timestamp}_{filename}
// Chaque fichier a une ligne dans day_attachments (file_path). L'effacement lit
// donc d'abord les chemins dans la table (une requête par 1 000 captures), puis
// parcourt le stockage en complément pour les fichiers sans ligne.

const BUCKET = 'day-attachments';
// Le service renvoie au plus 1 000 lignes (table) ou entrées (stockage) par appel.
const LIST_PAGE = 1000;
// Garde-fou des boucles de pagination (200 000 entrées) : au-delà, la lecture
// est déclarée incomplète plutôt que de tourner sans fin.
const LIST_MAX_PAGES = 200;

// Table absente du projet (migration pas encore appliquée) : elle ne contient
// aucune donnée, il n'y a rien à y effacer.
function _isMissingTable(error) {
  const code = String((error && error.code) || '');
  return code === 'PGRST205' || code === '42P01';
}

// Espace de stockage absent du projet (jamais créé) : il ne contient aucun
// fichier, il n'y a rien à y effacer — même règle que pour une table absente.
function _isMissingBucket(error) {
  if (!error) return false;
  return /bucket not found/i.test(String(error.message || '') + ' ' + String(error.error || ''));
}

// Liste complète d'un dossier du stockage, page par page, triée par nom (ordre
// stable d'une page à l'autre). Renvoie null si une page échoue.
async function _listAll(prefix) {
  const out = [];
  for (let page = 0; page < LIST_MAX_PAGES; page++) {
    let res;
    try {
      res = await window.sb.storage.from(BUCKET)
        .list(prefix, { limit: LIST_PAGE, offset: page * LIST_PAGE, sortBy: { column: 'name', order: 'asc' } });
    } catch (e) { return null; }
    if (res && _isMissingBucket(res.error)) return out;
    if (!res || res.error || !Array.isArray(res.data)) return null;
    for (const e of res.data) if (e && e.name) out.push(e);
    if (res.data.length < LIST_PAGE) return out;
  }
  return null;
}

// Chemins connus de la table day_attachments, lus par pages de 1 000 (boucle
// locale : ce fichier est chargé avant state.jsx). Renvoie { paths, ok } ;
// ok=false si une page n'a pas pu être lue (les chemins déjà lus sont gardés).
async function _attachmentPathsFromTable(uid) {
  const paths = [];
  for (let page = 0; page < LIST_MAX_PAGES; page++) {
    const from = page * LIST_PAGE;
    let res;
    try {
      res = await window.sb.from('day_attachments').select('id, file_path').eq('user_id', uid)
        .order('id').range(from, from + LIST_PAGE - 1);
    } catch (e) { return { paths, ok: false }; }
    if (!res || res.error) return { paths, ok: _isMissingTable(res && res.error) };
    const rows = Array.isArray(res.data) ? res.data : [];
    for (const r of rows) if (r && r.file_path) paths.push(r.file_path);
    if (rows.length < LIST_PAGE) return { paths, ok: true };
  }
  return { paths, ok: false };
}

// Parcours du stockage ({uid}/{date}/{fichier}) : tout ce qui s'y trouve encore.
// Les dossiers de date sont listés par lots de 8 en parallèle. Renvoie
// { paths, ok } ; ok=false si une liste a échoué.
async function _collectAttachmentPaths(uid) {
  const paths = [];
  const top = await _listAll(uid);
  if (!top) return { paths, ok: false };
  let ok = true;
  const folders = [];
  for (const entry of top) {
    // Un « dossier » n'a ni id ni metadata ; un vrai fichier en a.
    if (entry.id || entry.metadata) paths.push(`${uid}/${entry.name}`);
    else folders.push(`${uid}/${entry.name}`);
  }
  for (let i = 0; i < folders.length; i += 8) {
    const lot = folders.slice(i, i + 8);
    const lists = await Promise.all(lot.map(f => _listAll(f)));
    lists.forEach((inner, k) => {
      if (!inner) { ok = false; return; }
      for (const f of inner) paths.push(`${lot[k]}/${f.name}`);
    });
  }
  return { paths, ok };
}

// Supprime des fichiers par lots de 100 (deux essais par lot, à un quart de
// seconde d'écart). Renvoie { removed, failed } en nombre de fichiers.
async function _removeFiles(paths) {
  let removed = 0, failed = 0;
  for (let i = 0; i < paths.length; i += 100) {
    const batch = paths.slice(i, i + 100);
    let done = false;
    for (let attempt = 0; attempt < 2 && !done; attempt++) {
      if (attempt) await new Promise(r => setTimeout(r, 250));
      try {
        const res = await window.sb.storage.from(BUCKET).remove(batch);
        done = !!res && !res.error;
      } catch (e) { done = false; }
    }
    if (done) removed += batch.length; else failed += batch.length;
  }
  return { removed, failed };
}

// Efface toutes les captures du compte. Renvoie { erased, complete } :
//   erased   : au moins un fichier a été supprimé ;
//   complete : il ne reste aucun fichier (aucune liste ni suppression en échec).
// Tant que complete est faux, l'appelant ne doit PAS effacer les lignes de
// day_attachments : ce sont elles qui permettent de retrouver les fichiers
// restants à la relance.
async function _eraseAttachmentFiles(uid) {
  let erased = false, complete = true;
  const known = await _attachmentPathsFromTable(uid);
  if (known.paths.length) {
    const r = await _removeFiles(Array.from(new Set(known.paths)));
    if (r.removed) erased = true;
    if (r.failed) complete = false;
  }
  // Complément : fichiers sans ligne (envoi interrompu, table illisible).
  // Les dossiers vidés ci-dessus n'existent plus : ce parcours est court.
  const rest = await _collectAttachmentPaths(uid);
  if (!rest.ok) complete = false;
  if (rest.paths.length) {
    const r = await _removeFiles(rest.paths);
    if (r.removed) erased = true;
    if (r.failed) complete = false;
  }
  return { erased, complete };
}

// Captures restées dans le stockage : rien d'autre n'est effacé tant qu'elles
// n'ont pas toutes disparu. Sert aussi à « Supprimer le compte ».
const RESET_FILES_MESSAGE = 'Tes captures d’écran n’ont pas toutes pu être supprimées. Vérifie ta connexion, puis relance l’opération.';

// Message d'une réinitialisation arrêtée en route. Formulation neutre : il sert
// aussi à « Supprimer le compte », qui passe par resetMyData.
const RESET_PARTIAL_MESSAGE = 'L’effacement s’est interrompu en cours de route\u00A0: une partie de tes données est déjà effacée. Relance l’opération pour terminer.';

// Efface les lignes du compte dans une table. Deux essais, à un quart de seconde
// d'écart : une micro-coupure ne doit pas laisser le journal à moitié effacé.
// Renvoie null (table vidée ou inexistante) ou la dernière erreur.
async function _deleteUserRows(table, uid) {
  let error = null;
  for (let attempt = 0; attempt < 2; attempt++) {
    if (attempt) await new Promise(r => setTimeout(r, 250));
    try {
      ({ error } = await window.sb.from(table).delete().eq('user_id', uid));
    } catch (e) {
      error = e || { message: 'Failed to fetch' };
    }
    if (!error) return null;
    if (_isMissingTable(error)) {
      console.warn('[Tempo] réinitialisation : table absente, ignorée :', table);
      return null;
    }
  }
  return error;
}

// Renvoie { ok: true }, ou { error } si rien n'a été effacé, ou
// { error, partial: true } si l'effacement s'est arrêté après avoir déjà
// supprimé des données (l'appelant doit alors relire le compte : l'écran ne
// peut plus se fier à ce qu'il a en mémoire).
async function resetMyData() {
  const { data: u } = await window.sb.auth.getUser();
  const uid = u?.user?.id;
  if (!uid) return { error: { message: 'Non connecté.' } };

  // Quelque chose a-t-il déjà été effacé (fichier ou table) ?
  let erased = false;

  // 1) Les fichiers d'abord. S'il en reste (liste ou suppression en échec), on
  //    s'arrête AVANT les tables : les lignes day_attachments pointent encore
  //    vers eux, une relance les retrouvera. Annoncer un succès en laissant des
  //    captures orphelines dans le stockage serait un effacement incomplet.
  let files;
  try {
    files = await _eraseAttachmentFiles(uid);
  } catch (e) {
    files = { erased: false, complete: false };
  }
  erased = files.erased;
  if (!files.complete) {
    console.warn('[Tempo] réinitialisation : captures non supprimées en totalité');
    return erased
      ? { partial: true, error: { message: RESET_FILES_MESSAGE } }
      : { error: { message: RESET_FILES_MESSAGE } };
  }

  // 2) Delete DB rows. Order matters for FKs: child rows before parents.
  //    (completions reference habits; trades may reference strategies;
  //     backtest_trades reference backtest_sessions.)
  const tables = [
    'habit_completions', 'day_attachments', 'day_journals', 'agenda_events',
    'trades', 'habits',
    'backtest_trades', 'backtest_sessions', 'strategies',
    'fondamental_snapshots',
  ];
  for (const t of tables) {
    const error = await _deleteUserRows(t, uid);
    if (error) {
      // Rien d'effacé : l'erreur telle quelle (« Problème réseau » reste exact).
      if (!erased) return { error };
      console.warn('[Tempo] réinitialisation interrompue sur', t, ':', error.message || error);
      return { partial: true, error: { message: RESET_PARTIAL_MESSAGE } };
    }
    erased = true;
  }
  // 3) Notes « Plan trade » du calendrier éco : stockées uniquement en
  //    localStorage (econ.jsx), jamais en base — on les efface aussi.
  try {
    localStorage.removeItem('tempo:econNotes');
    localStorage.removeItem('tempo:econNotesSeen');
  } catch (e) { /* stockage indisponible : rien à purger */ }
  return { ok: true };
}

// Deprovision every MetaApi connection via the broker worker before wiping
// broker_connections. Returns the ids of the connections whose MetaApi account
// could NOT be confirmed deleted (worker unreachable, MetaApi error…) so the
// caller keeps their rows: without the row, the MetaApi account_id is lost and
// nobody can ever purge the broker credentials held by the sub-processor.
async function _disconnectBrokers(uid) {
  const failed = [];
  try {
    const { data: conns } = await window.sb.from('broker_connections').select('id').eq('user_id', uid);
    if (!conns || !conns.length) return failed;
    const { data: s } = await window.sb.auth.getSession();
    const token = s?.session?.access_token;
    const base = String(window.BROKER_WORKER_DEFAULT || '').replace(/\/+$/, '');
    if (!token || !base) return conns.map(c => c.id);
    for (const c of conns) {
      let ok = false;
      try {
        const res = await fetch(base + '/disconnect', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
          body: JSON.stringify({ connectionId: c.id }),
        });
        ok = res.ok;
        try { const j = await res.json(); ok = ok && j.metaapiDeleted !== false; } catch (e) { ok = false; }
      } catch (e) { ok = false; }
      if (!ok) failed.push(c.id);
    }
  } catch (e) { /* best-effort — connexions non lues : rien à conserver */ }
  return failed;
}

// Clé de sessionStorage posée quand l'identifiant de connexion n'a pas pu être
// supprimé : l'écran de connexion (App, app.jsx) la lit pour prévenir
// l'utilisateur, y compris après un rechargement de la page.
const DELETE_NOTICE_KEY = 'tempo:deleteNotice';

// Full account erasure: deprovision brokers, wipe all personal data, delete the
// profile row, delete the auth.users row via the security-definer RPC
// delete_my_account (supabase-migration-securite.sql — cascades finish the
// cleanup), then sign out. If the RPC is unavailable (migration not applied) or
// fails, the account is still emptied and signed out, and the caller is told
// (authLeft) so the user learns that the residual auth record remains and can
// be purged server-side on request.
// Renvoie { error } (rien de définitif n'a eu lieu, ou effacement partiel :
// voir resetMyData) ou { ok: true, authLeft, signedOut } :
//   authLeft  : l'identifiant de connexion (email, mot de passe) existe encore ;
//   signedOut : la déconnexion a abouti (l'écran de connexion est déjà affiché).
async function deleteMyAccount() {
  const { data: u0 } = await window.sb.auth.getUser();
  const uid0 = u0?.user?.id;
  // Deprovision MetaApi first (needs the session). If a MetaApi account could not
  // be deleted, stop BEFORE wiping anything: the broker_connections row (and the
  // auth account) must survive so the deprovisioning can be retried.
  if (uid0) {
    const failed = await _disconnectBrokers(uid0);
    if (failed.length) {
      return { error: { message: 'La déconnexion de ton compte broker a échoué (service temporairement indisponible). Réessaie dans quelques minutes, ou déconnecte tes brokers depuis Réglages avant de supprimer ton compte.' } };
    }
  }
  const r = await resetMyData();
  if (r.error) return r;
  const { data: u } = await window.sb.auth.getUser();
  const uid = u?.user?.id;
  try { if (uid) await window.sb.from('broker_connections').delete().eq('user_id', uid); } catch (e) {}
  try { if (uid) await window.sb.from('profiles').delete().eq('id', uid); } catch (e) {}
  // Delete the auth.users row itself (email, password hash, metadata).
  // supabase-js ne lève pas pour une RPC en erreur : il renvoie { error }.
  let authLeft = false;
  try {
    const { error } = await window.sb.rpc('delete_my_account');
    if (error) { authLeft = true; console.warn('[Tempo] delete_my_account :', error.message || error); }
  } catch (e) {
    authLeft = true;
    console.warn('[Tempo] delete_my_account :', (e && e.message) || e);
  }
  // Posé AVANT la déconnexion : l'écran de connexion s'affiche dès qu'elle aboutit.
  try {
    if (authLeft) sessionStorage.setItem(DELETE_NOTICE_KEY, '1');
    else sessionStorage.removeItem(DELETE_NOTICE_KEY);
  } catch (e) { /* stockage indisponible : l'avertissement ne survivra pas à un rechargement */ }
  // Déconnexion AVANT la purge locale : si l'identifiant existe encore, sa
  // session est révoquée côté serveur (la purge d'abord, et signOut n'aurait
  // plus de jeton à révoquer). Compte supprimé : signOut reçoit 401/403 et
  // supabase-js retire quand même la session locale, sans erreur.
  let signedOut = false;
  try {
    const { error } = await window.sb.auth.signOut();
    signedOut = !error;
  } catch (e) { /* hors ligne : la purge ci-dessous retire la session locale */ }
  // Tout le stockage local est vidé (données personnelles, et clé de session si
  // la déconnexion a échoué hors ligne) ; seul le thème, préférence d'appareil
  // conservée aussi à la déconnexion, est remis en place.
  try {
    const theme = localStorage.getItem('tempo:theme');
    localStorage.clear();
    if (theme) localStorage.setItem('tempo:theme', theme);
  } catch (e) {}
  return { ok: true, authLeft, signedOut };
}

Object.assign(window, { resetMyData, deleteMyAccount });
