// Capa Supabase: cliente, adapters y API CRUD/realtime.
//
// Diseño:
//  - El cliente UMD `window.supabase` lo carga el <script> de unpkg en index.html.
//  - Aquí construimos `window.SUPABASE` (instancia) y un namespace `window.SLDb`
//    con las operaciones que useAppState/sync usan.
//  - Si las credenciales no están (window.__ENV__ vacío), todo queda como no-op
//    y `SUPABASE === null`, por lo que la app funciona 100% offline.
//  - Los adapters traducen entre el shape local (`{ es, en }`, ids cortos, epoch)
//    y el shape de la BD (TEXT, UUID, ISO).

const __ENV = (typeof window !== 'undefined' && window.__ENV__) || {};
const SUPABASE_URL = __ENV.PUBLIC_SUPABASE_URL || '';
const SUPABASE_ANON_KEY = __ENV.PUBLIC_SUPABASE_ANON_KEY || '';

const SUPABASE = (() => {
  if (!SUPABASE_URL || !SUPABASE_ANON_KEY) {
    console.warn('[shoplist] Supabase no configurado — modo offline.');
    return null;
  }
  if (typeof window === 'undefined' || !window.supabase || !window.supabase.createClient) {
    console.warn('[shoplist] supabase-js UMD no se cargó — modo offline.');
    return null;
  }
  return window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
    auth: { persistSession: true, autoRefreshToken: true, detectSessionInUrl: true, flowType: 'pkce' },
  });
})();

// Deep-link OAuth callback: cuando el custom scheme `com.jars.komprapp://`
// abre la app después del login Google, recibimos la URL con `?code=...` (PKCE)
// o `#access_token=...` (implicit). Pasamos a supabase-js para que cree la
// sesión y cerramos el Safari/Chrome embebido.
(async () => {
  if (!SUPABASE) return;
  const App = typeof window !== 'undefined' ? window.Capacitor?.Plugins?.App : null;
  if (!App?.addListener) return;
  App.addListener('appUrlOpen', async ({ url }) => {
    if (!url || !url.startsWith('com.jars.komprapp://')) return;
    try {
      const u = new URL(url);
      const code = u.searchParams.get('code');
      if (code) {
        const { error } = await SUPABASE.auth.exchangeCodeForSession(code);
        if (error) console.error('[shoplist] exchangeCodeForSession', error);
      } else if (u.hash && u.hash.length > 1) {
        // Implicit flow legacy: tokens en el fragment
        const params = new URLSearchParams(u.hash.slice(1));
        const access_token  = params.get('access_token');
        const refresh_token = params.get('refresh_token');
        if (access_token && refresh_token) {
          const { error } = await SUPABASE.auth.setSession({ access_token, refresh_token });
          if (error) console.error('[shoplist] setSession', error);
        }
      }
    } catch (e) { console.error('[shoplist] appUrlOpen parse', e); }
    try {
      const Browser = window.Capacitor?.Plugins?.Browser;
      if (Browser?.close) await Browser.close();
    } catch (_) {}
  });
})();

// ─── Adapters ────────────────────────────────────────────────────────────────

// nombre TEXT en BD <-> { es, en } en local. La BD puede contener:
//  1) JSON `{"es":"Casa","en":"Home"}`
//  2) string plano (legacy / shared) → se usa para ambos idiomas.
function nameDbToLocal(raw) {
  if (raw == null) return { es: '', en: '' };
  if (typeof raw === 'string') {
    try {
      const p = JSON.parse(raw);
      if (p && typeof p === 'object' && ('es' in p || 'en' in p)) {
        return { es: String(p.es ?? p.en ?? ''), en: String(p.en ?? p.es ?? '') };
      }
    } catch (_) { /* string plano */ }
    return { es: raw, en: raw };
  }
  return { es: '', en: '' };
}
function nameLocalToDb(name) {
  if (typeof name === 'string') return name;
  if (name && typeof name === 'object') return JSON.stringify({ es: name.es || '', en: name.en || '' });
  return '';
}

// timestamps
const toIso = (ms) => (typeof ms === 'number' ? new Date(ms).toISOString() : (ms || new Date().toISOString()));
const toMs  = (iso) => (iso ? Date.parse(iso) : Date.now());

// Lista BD → local (preserva campos cosméticos al hacer merge con la lista actual)
function listDbToLocal(row, prev) {
  const colors = ['cyan', 'pink', 'violet', 'orange', 'lime'];
  return {
    id: row.id,
    name: nameDbToLocal(row.name),
    color: prev?.color || colors[Math.floor(Math.random() * colors.length)],
    members: prev?.members ?? 1,
    createdAt: toMs(row.created_at),
    token: row.token || prev?.token || '',
    owner_id: row.owner_id || null,
    stores: row.stores || null,
  };
}
function listLocalToDb(local) {
  const out = { id: local.id, name: nameLocalToDb(local.name), token: local.token || null };
  if (local.owner_id) out.owner_id = local.owner_id;
  return out;
}

function productDbToLocal(row) {
  const norm = (typeof window.normalizeQty === 'function') ? window.normalizeQty : (v => v);
  return {
    id: row.id,
    name: row.name,
    quantity: norm(row.quantity),
    category: row.category || 'otros',
    is_purchased: !!row.is_purchased,
    is_archived: !!row.is_archived,
    created_at: toMs(row.added_at),
  };
}
function productLocalToDb(local, listId) {
  const norm = (typeof window.normalizeQty === 'function') ? window.normalizeQty : (v => v);
  const q = norm(local.quantity);
  return {
    id: local.id,
    list_id: listId,
    name: local.name,
    quantity: q === '' ? null : String(q),
    category: local.category || null,
    is_purchased: !!local.is_purchased,
    is_archived: !!local.is_archived,
    added_at: toIso(local.created_at),
  };
}

function historyDbToLocal(row) {
  return {
    id: row.id,
    action: row.action,
    item_name: row.item_name,
    who: row.performed_by_name || null,
    avatar: row.performed_by_avatar || '🟢',
    at: toMs(row.created_at),
  };
}
function historyLocalToDb(local, listId) {
  return {
    id: local.id,
    list_id: listId,
    action: local.action,
    item_name: local.item_name,
    performed_by_name: local.who || null,
    performed_by_avatar: local.avatar || null,
    created_at: toIso(local.at),
  };
}

// ─── DB API ──────────────────────────────────────────────────────────────────

// ─── Tokens locales ───────────────────────────────────────────────────────────
// En modo invitado (sin sesión), filtramos las listas por tokens conocidos
// guardados en localStorage. Esto evita que un guest vea las listas anónimas
// de otros usuarios que comparten la base de datos.
const TOKEN_STORE = 'shoplist.tokens';
function readLocalTokens() {
  try { return JSON.parse(localStorage.getItem(TOKEN_STORE) || '[]'); } catch (_) { return []; }
}
function rememberToken(token) {
  if (!token) return;
  const set = new Set(readLocalTokens());
  if (set.has(token)) return;
  set.add(token);
  try { localStorage.setItem(TOKEN_STORE, JSON.stringify([...set])); } catch (_) {}
}

const SLDb = {
  enabled: !!SUPABASE,

  async getLists() {
    if (!SUPABASE) return [];
    const { data: { user } } = await SUPABASE.auth.getUser();
    if (user) {
      // Autenticado: cargo las listas que poseo + cualquier lista anónima cuyo
      // token tenga guardado localmente (lista compartida vía URL/QR).
      const { data: owned, error: e1 } = await SUPABASE.from('lists').select('*')
        .eq('owner_id', user.id).order('created_at', { ascending: false });
      if (e1) { console.error('[shoplist] getLists owned', e1); return []; }
      const tokens = readLocalTokens();
      let extras = [];
      if (tokens.length) {
        const ownedTokens = new Set((owned || []).map(l => l.token));
        const remaining = tokens.filter(t => !ownedTokens.has(t));
        if (remaining.length) {
          const { data: joined } = await SUPABASE.from('lists').select('*')
            .in('token', remaining).order('created_at', { ascending: false });
          extras = joined || [];
        }
      }
      return [...(owned || []), ...extras];
    }
    // Invitado: solo las listas cuyos tokens conozco localmente.
    const tokens = readLocalTokens();
    if (!tokens.length) return [];
    const { data, error } = await SUPABASE.from('lists').select('*')
      .in('token', tokens).order('created_at', { ascending: false });
    if (error) { console.error('[shoplist] getLists guest', error); return []; }
    return data || [];
  },

  async createList(local) {
    if (!SUPABASE) return null;
    const { data: { user } } = await SUPABASE.auth.getUser();
    const insert = listLocalToDb(local);
    if (user) insert.owner_id = user.id;
    const { data, error } = await SUPABASE.from('lists').insert(insert).select().single();
    if (error) { console.error('[shoplist] createList', error); return null; }
    if (data?.token) rememberToken(data.token);
    return data;
  },

  // Tras un login: las listas anónimas cuyos tokens conozco localmente pasan
  // a tener owner_id = el del usuario actual (mismo patrón que el proyecto
  // antiguo `migrateLocalListsToUser`). RLS permite el update porque la fila
  // origina con owner_id NULL.
  async claimAnonymousLists() {
    if (!SUPABASE) return;
    const { data: { user } } = await SUPABASE.auth.getUser();
    if (!user) return;
    const tokens = readLocalTokens();
    if (!tokens.length) return;
    const { error } = await SUPABASE.from('lists')
      .update({ owner_id: user.id })
      .in('token', tokens)
      .is('owner_id', null);
    if (error) console.error('[shoplist] claimAnonymousLists', error);
  },

  async updateList(id, patch) {
    if (!SUPABASE) return;
    const dbPatch = {};
    if (patch.name !== undefined) dbPatch.name = nameLocalToDb(patch.name);
    if (patch.token !== undefined) dbPatch.token = patch.token;
    if (patch.stores !== undefined) dbPatch.stores = patch.stores;
    if (Object.keys(dbPatch).length === 0) return;
    const { error } = await SUPABASE.from('lists').update(dbPatch).eq('id', id);
    if (error) console.error('[shoplist] updateList', error);
  },

  async deleteList(id) {
    if (!SUPABASE) return;
    const { error } = await SUPABASE.from('lists').delete().eq('id', id);
    if (error) console.error('[shoplist] deleteList', error);
  },

  async getProducts(listId) {
    if (!SUPABASE) return [];
    const { data, error } = await SUPABASE.from('products')
      .select('*').eq('list_id', listId).order('added_at', { ascending: false });
    if (error) { console.error('[shoplist] getProducts', error); return []; }
    return data || [];
  },

  async createProduct(listId, local) {
    if (!SUPABASE) return;
    const insert = productLocalToDb(local, listId);
    const { error } = await SUPABASE.from('products').insert(insert);
    if (error) console.error('[shoplist] createProduct', error);
  },

  async updateProduct(id, patch) {
    if (!SUPABASE) return;
    const dbPatch = {};
    if (patch.name !== undefined) dbPatch.name = patch.name;
    if (patch.quantity !== undefined) {
      const q = (typeof window.normalizeQty === 'function') ? window.normalizeQty(patch.quantity) : patch.quantity;
      dbPatch.quantity = (q === '' || q == null) ? null : String(q);
    }
    if (patch.category !== undefined) dbPatch.category = patch.category;
    if (patch.is_purchased !== undefined) {
      dbPatch.is_purchased = !!patch.is_purchased;
      dbPatch.purchased_at = patch.is_purchased ? new Date().toISOString() : null;
    }
    if (patch.is_archived !== undefined) dbPatch.is_archived = !!patch.is_archived;
    if (Object.keys(dbPatch).length === 0) return;
    const { error } = await SUPABASE.from('products').update(dbPatch).eq('id', id);
    if (error) console.error('[shoplist] updateProduct', error);
  },

  async deleteProduct(id) {
    if (!SUPABASE) return;
    const { error } = await SUPABASE.from('products').delete().eq('id', id);
    if (error) console.error('[shoplist] deleteProduct', error);
  },

  async deleteProductsByIds(ids) {
    if (!SUPABASE || !ids?.length) return;
    const { error } = await SUPABASE.from('products').delete().in('id', ids);
    if (error) console.error('[shoplist] deleteProductsByIds', error);
  },

  async getHistory(listId) {
    if (!SUPABASE) return [];
    const { data, error } = await SUPABASE.from('history_logs')
      .select('*').eq('list_id', listId).order('created_at', { ascending: false }).limit(200);
    if (error) { console.error('[shoplist] getHistory', error); return []; }
    return data || [];
  },

  async addHistory(listId, local) {
    if (!SUPABASE) return;
    const insert = historyLocalToDb(local, listId);
    // Adjuntar usuario autenticado si existe
    try {
      const { data: { user } } = await SUPABASE.auth.getUser();
      if (user) {
        insert.performed_by_name = insert.performed_by_name || user.user_metadata?.full_name || user.email || null;
        insert.performed_by_avatar = insert.performed_by_avatar || user.user_metadata?.avatar_url || null;
        insert.performed_by_user_id = user.id;
      }
    } catch (_) { /* ignore */ }
    const { error } = await SUPABASE.from('history_logs').insert(insert);
    if (error) console.error('[shoplist] addHistory', error);
  },

  async findListByToken(token) {
    if (!SUPABASE) return null;
    const { data, error } = await SUPABASE.from('lists').select('*').eq('token', token).single();
    if (error) return null;
    if (data?.token) rememberToken(data.token);
    return data;
  },

  // Helpers expuestos para sync.jsx
  rememberToken,
  readLocalTokens,
};

// ─── Realtime ────────────────────────────────────────────────────────────────

const SLRealtime = {
  subscribeProducts(listId, onChange) {
    if (!SUPABASE || !listId) return () => {};
    const channel = SUPABASE
      .channel(`products:${listId}`)
      .on('postgres_changes',
        { event: '*', schema: 'public', table: 'products', filter: `list_id=eq.${listId}` },
        (payload) => onChange({ eventType: payload.eventType, new: payload.new, old: payload.old }))
      .subscribe();
    return () => SUPABASE.removeChannel(channel);
  },

  subscribeHistory(listId, onChange) {
    if (!SUPABASE || !listId) return () => {};
    const channel = SUPABASE
      .channel(`history_logs:${listId}`)
      .on('postgres_changes',
        { event: 'INSERT', schema: 'public', table: 'history_logs', filter: `list_id=eq.${listId}` },
        (payload) => onChange({ eventType: payload.eventType, new: payload.new, old: payload.old }))
      .subscribe();
    return () => SUPABASE.removeChannel(channel);
  },

  subscribeLists(onChange) {
    if (!SUPABASE) return () => {};
    const channel = SUPABASE
      .channel('lists')
      .on('postgres_changes',
        { event: '*', schema: 'public', table: 'lists' },
        (payload) => onChange({ eventType: payload.eventType, new: payload.new, old: payload.old }))
      .subscribe();
    return () => SUPABASE.removeChannel(channel);
  },
};

// ─── Auth (mínima — disponible para futuras pantallas) ───────────────────────

// Nonce + hash helpers para Sign in with Apple. Apple firma sha256(nonce) en
// el identityToken; Supabase verifica el nonce raw que le pasamos contra el
// claim del JWT. Sin nonce, signInWithIdToken rechaza el token.
function generateNonce() {
  const bytes = new Uint8Array(32);
  (window.crypto || window.msCrypto).getRandomValues(bytes);
  let bin = '';
  for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
  return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
async function sha256Hex(input) {
  const buf = new TextEncoder().encode(input);
  const hash = await crypto.subtle.digest('SHA-256', buf);
  return Array.from(new Uint8Array(hash))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
}

const SLAuth = {
  async getUser() {
    if (!SUPABASE) return null;
    const { data: { user } } = await SUPABASE.auth.getUser();
    return user;
  },
  async signInWithGoogle() {
    if (!SUPABASE) return { error: new Error('Supabase no configurado') };
    // En Capacitor (iOS/Android nativo) `window.location.origin` es
    // capacitor://localhost — un esquema que ni Supabase ni el SO saben enrutar
    // de vuelta a la app. Usamos un custom scheme registrado en el bundle
    // (CFBundleURLTypes en iOS, intent-filter en Android) para que el callback
    // OAuth abra la app y `appUrlOpen` capture el code+state.
    const isCapacitor = typeof window !== 'undefined' && (
      !!window.Capacitor?.isNativePlatform?.() || /^capacitor:/.test(window.location?.origin || '')
    );
    const redirectTo = isCapacitor
      ? 'com.jars.komprapp://oauth-redirect'
      : (typeof window !== 'undefined' ? window.location.origin : undefined);
    if (isCapacitor) {
      // Skip browser redirect — abrimos manualmente el authorize URL en el
      // browser nativo del SO para que Google muestre su pantalla de cuentas
      // sin secuestrar la WebView de Capacitor.
      const { data, error } = await SUPABASE.auth.signInWithOAuth({
        provider: 'google',
        options: { redirectTo, skipBrowserRedirect: true },
      });
      if (error) return { error };
      try {
        const Browser = window.Capacitor?.Plugins?.Browser;
        if (Browser?.open) await Browser.open({ url: data.url });
        else window.open(data.url, '_system');
      } catch (e) { return { error: e }; }
      return { data, error: null };
    }
    return SUPABASE.auth.signInWithOAuth({ provider: 'google', options: { redirectTo } });
  },
  async signInWithApple() {
    if (!SUPABASE) return { error: new Error('Supabase no configurado') };
    const isCapacitor = typeof window !== 'undefined' && (
      !!window.Capacitor?.isNativePlatform?.() || /^capacitor:/.test(window.location?.origin || '')
    );
    const platform = (typeof window !== 'undefined' && window.Capacitor?.getPlatform?.()) || 'web';
    const SignInWithApple = typeof window !== 'undefined' ? window.Capacitor?.Plugins?.SignInWithApple : null;
    // iOS nativo: usar AuthenticationServices (sin webview). El plugin devuelve
    // un JWT (identityToken) que entregamos a Supabase con signInWithIdToken.
    // Apple firma un nonce hasheado en el JWT; Supabase lo compara con el nonce
    // raw que pasamos, por eso enviamos sha256(nonce) al plugin y el nonce raw
    // a Supabase.
    if (isCapacitor && platform === 'ios' && SignInWithApple?.authorize) {
      try {
        const rawNonce = generateNonce();
        const hashedNonce = await sha256Hex(rawNonce);
        const res = await SignInWithApple.authorize({
          clientId: 'com.jars.komprapp',
          redirectURI: 'com.jars.komprapp://oauth-redirect',
          scopes: 'email name',
          nonce: hashedNonce,
        });
        const idToken = res?.response?.identityToken;
        if (!idToken) return { error: new Error('Apple: no identity token') };
        const { data, error } = await SUPABASE.auth.signInWithIdToken({
          provider: 'apple',
          token: idToken,
          nonce: rawNonce,
        });
        return { data, error };
      } catch (e) {
        return { error: e };
      }
    }
    // Android + web: OAuth redirect estándar (Apple muestra su consent page
    // en el system browser → callback al custom scheme → exchangeCodeForSession).
    const redirectTo = isCapacitor
      ? 'com.jars.komprapp://oauth-redirect'
      : (typeof window !== 'undefined' ? window.location.origin : undefined);
    if (isCapacitor) {
      const { data, error } = await SUPABASE.auth.signInWithOAuth({
        provider: 'apple',
        options: { redirectTo, skipBrowserRedirect: true },
      });
      if (error) return { error };
      try {
        const Browser = window.Capacitor?.Plugins?.Browser;
        if (Browser?.open) await Browser.open({ url: data.url });
        else window.open(data.url, '_system');
      } catch (e) { return { error: e }; }
      return { data, error: null };
    }
    return SUPABASE.auth.signInWithOAuth({ provider: 'apple', options: { redirectTo } });
  },
  async signOut() {
    if (!SUPABASE) return;
    // Limpiamos los tokens locales para que el siguiente modo invitado no
    // herede acceso a las listas del usuario que acaba de salir (shared device).
    try { localStorage.removeItem(TOKEN_STORE); } catch (_) {}
    return SUPABASE.auth.signOut();
  },
  onChange(cb) {
    if (!SUPABASE) return () => {};
    const { data } = SUPABASE.auth.onAuthStateChange((_e, session) => cb(session?.user ?? null));
    return () => data?.subscription?.unsubscribe?.();
  },
};

// ─── Helpers de id ───────────────────────────────────────────────────────────

const SLIds = {
  uuid() {
    if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID();
    // Fallback simple, compatible con uuid v4
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
      const r = (Math.random() * 16) | 0;
      const v = c === 'x' ? r : (r & 0x3) | 0x8;
      return v.toString(16);
    });
  },
  shortToken() {
    return Math.random().toString(36).slice(2, 5) + '-' +
           Math.random().toString(36).slice(2, 5) + '-' +
           Math.random().toString(36).slice(2, 5);
  },
};

// Stub por defecto — sync.jsx lo reemplaza con la implementación real. Esto
// evita un crash si sync.jsx no se carga por algún motivo.
if (typeof window.useSupabaseSync !== 'function') {
  window.useSupabaseSync = function () { /* no-op */ };
}

Object.assign(window, {
  SUPABASE,
  SLDb,
  SLRealtime,
  SLAuth,
  SLIds,
  __slAdapters: { listDbToLocal, listLocalToDb, productDbToLocal, productLocalToDb, historyDbToLocal, historyLocalToDb },
});
