// background/parsers.js — interpreta respuestas de la API interna de Skool
//
// Skool es una SPA React. Sus endpoints internos varían, pero siguen patrones
// comunes. Este parser detecta el tipo de dato y normaliza al esquema de Skoop.
//
// ESTRATEGIA: buscar patrones en el cuerpo de respuesta, no hardcodear rutas.
// Así funciona aunque Skool cambie sus URLs.

/**
 * @param {string} url - URL del endpoint interceptado
 * @param {any} data - cuerpo JSON de la respuesta
 * @param {string} method - GET, POST, etc.
 * @returns {{ type: string, ...parsed } | null}
 */
export function parseSkoolData(url, data, method) {
  if (!data || typeof data !== 'object') return null;

  // Diagnóstico: loguear cada intercepción para ver qué devuelve la API de Skool
  if (typeof data === 'object' && !Array.isArray(data)) {
    const keys = Object.keys(data).slice(0, 10);
    console.log(`[Skoop parser] ${method} ${url} → keys: ${keys.join(', ')}`);
  }

  // ── Patrones de URL conocidos ─────────────────────────────
  const urlLower = url.toLowerCase();

  // Miembros
  if (_matchesAny(urlLower, ['/members', '/group-members', '/community-members'])) {
    const members = _extractMembers(data);
    if (members.length > 0) return { type: 'members', members };
  }

  // Posts / feed
  if (_matchesAny(urlLower, ['/posts', '/feed', '/activities'])) {
    const posts = _extractPosts(data);
    if (posts.length > 0) return { type: 'posts', posts };
  }

  // Comentarios
  if (_matchesAny(urlLower, ['/comments', '/replies'])) {
    const comments = _extractComments(data);
    const postId = _extractPostId(url);
    if (comments.length > 0) return { type: 'comments', comments, postId };
  }

  // Perfil de miembro individual
  if (_matchesAny(urlLower, ['/profile', '/member/', '/user/'])) {
    const member = _extractSingleMember(data);
    if (member) return { type: 'profile', member };
  }

  // Info de la comunidad/grupo
  if (_matchesAny(urlLower, ['/group', '/community', '/classroom'])) {
    const community = _extractCommunity(data);
    if (community) return { type: 'community', community };
  }

  // ── Detección por contenido (cuando la URL no es clara) ───
  // Si tiene lista de objetos con campos de miembro
  const asMembersFromContent = _tryExtractMembersFromAny(data);
  if (asMembersFromContent.length > 0) {
    return { type: 'members', members: asMembersFromContent };
  }

  // Si tiene lista de posts
  const asPostsFromContent = _tryExtractPostsFromAny(data);
  if (asPostsFromContent.length > 0) {
    return { type: 'posts', posts: asPostsFromContent };
  }

  return null;
}

// ── EXTRACTORES ───────────────────────────────────────────

function _extractMembers(data) {
  // data.members, data.data, data.results, data.items
  const list = _findArray(data, ['members', 'groupMembers', 'communityMembers', 'data', 'results', 'items', 'users']);
  return list.map(_normalizeMember).filter(Boolean);
}

function _extractSingleMember(data) {
  const candidate = data.member || data.user || data.profile || data.data || data;
  if (!candidate || Array.isArray(candidate)) return null;
  return _normalizeMember(candidate);
}

function _extractPosts(data) {
  const list = _findArray(data, ['posts', 'feed', 'activities', 'data', 'results', 'items']);
  return list.map(_normalizePost).filter(Boolean);
}

function _extractComments(data) {
  const list = _findArray(data, ['comments', 'replies', 'data', 'results', 'items']);
  return list.map(_normalizeComment).filter(Boolean);
}

function _extractCommunity(data) {
  const candidate = data.group || data.community || data.data || data;
  if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) return null;
  const name = candidate.name || candidate.groupName || candidate.title;
  if (!name) return null;
  return {
    name,
    slug: candidate.slug || candidate.groupSlug,
    memberCount: candidate.memberCount || candidate.totalMembers,
    description: candidate.description,
    _raw: candidate,
  };
}

// ── NORMALIZADORES ────────────────────────────────────────

// Normaliza el timestamp de Skool a milisegundos. Skool mezcla ISO strings con
// epochs en micro/nanosegundos (ej. metadata.lastOffline: 1781361450272101400).
function _toMs(v) {
  if (v == null) return null;
  if (typeof v === 'number') {
    if (v > 1e17) return Math.floor(v / 1e6); // nanosegundos → ms
    if (v > 1e14) return Math.floor(v / 1e3); // microsegundos → ms
    return v;                                 // ya en ms
  }
  const t = new Date(v).getTime();
  return Number.isNaN(t) ? null : t;
}

function _normalizeMember(raw) {
  if (!raw || typeof raw !== 'object') return null;

  // Skool puede anidar el user dentro de un wrapper { member: { user: {...} } }
  const user = raw.user || raw.profile || raw;
  const merged = { ...raw, ...user };
  const m = merged.member || {};       // sub-objeto "member" (rol, lastOffline, metadata)
  const md = merged.metadata || {};    // metadata (bio, pictureProfile, lastOffline)
  const mmd = m.metadata || {};        // member.metadata (email en mbme)

  // El HANDLE de Skool vive en `name` (ej. "shari-vazquez-6458"). Es la MISMA clave
  // que usa el DOM scanner como username → así API y DOM se fusionan en una sola ficha.
  const handle = merged.handle || merged.slug || merged.username || merged.name;

  // Nombre real para mostrar: firstName + lastName (no el handle)
  const displayName =
    [merged.firstName, merged.lastName].filter(Boolean).join(' ').trim() ||
    merged.displayName || merged.fullName || handle;

  const userId = merged.id || merged.userId || merged.uuid || m.userId;

  // Clave de fusión: handle primero (coincide con el DOM), luego userId/displayName
  const id = handle || userId || displayName;
  if (!id) return null;

  // Última actividad: member.lastOffline (ISO, lo más fiable) → metadata.lastOffline → otros
  const lastActivityRaw =
    m.lastOffline || md.lastOffline || merged.lastOffline ||
    merged.lastPostAt || merged.lastActiveAt || merged.lastActivityAt ||
    merged.lastSeenAt || merged.updatedAt;
  const lastPostTs = _toMs(lastActivityRaw);
  const lastSeenAt = lastPostTs ? new Date(lastPostTs).toISOString() : null;

  // Puntos / nivel guardados como JSON en metadata.spData ("{\"pts\":0,\"lv\":1...}")
  let points, level;
  try {
    const sp = typeof md.spData === 'string' ? JSON.parse(md.spData) : md.spData;
    if (sp) { points = sp.pts; level = sp.lv; }
  } catch (_) {}

  const email =
    merged.email || mmd.mbme || merged.emailAddress || merged.userEmail || null;

  // ── FASE 1: encuesta de ingreso + datos de billing ───────
  // member.metadata.survey → respuestas de ingreso (WhatsApp, Instagram, nicho).
  // Puede venir como JSON string, como { survey: [...] } o como array directo.
  const surveyAnswers = parseSurvey(mmd.survey || md.survey);
  const surveyFields = extractSurveyFields(surveyAnswers);

  // Billing: mbsltv (valor en centavos, 4700 = $47), mmbp (plan/precio), msbs (sub_id Stripe)
  const ltvCents = _toNum(mmd.mbsltv);
  // mmbp puede venir como número, como string "$47", o como objeto/JSON
  // {"currency":"usd","amount":3700,"recurring_interval":"month"}.
  let planObj = mmd.mmbp;
  if (typeof planObj === 'string' && /^\s*\{/.test(planObj)) {
    try { planObj = JSON.parse(planObj); } catch (_) { /* deja string */ }
  }
  let planCents, planInterval = null;
  if (planObj && typeof planObj === 'object') {
    planCents = _toNum(planObj.amount ?? planObj.price ?? planObj.value);
    planInterval = planObj.recurring_interval || planObj.interval || planObj.billing_interval || null;
  } else {
    planCents = typeof planObj === 'number' ? planObj : _toNum(planObj);
  }
  const planRaw = planObj;

  // FASE 2: estado de suscripción / cancelación en proceso.
  const sub = detectSubscription(mmd, md, merged);

  // Tier de membresía (Standard / Premium / VIP) — etiqueta por miembro.
  const tier = detectTier(mmd, md, merged);

  return {
    id: String(id),
    name: displayName,
    username: handle ? String(handle) : undefined,
    userId: userId ? String(userId) : undefined,
    avatarUrl: md.pictureProfile || md.pictureBubble ||
               merged.avatarUrl || merged.profileImageUrl || merged.photoUrl || merged.avatar || merged.image,
    role: m.role || merged.role || merged.memberRole,
    level: level ?? merged.level,
    bio: md.bio || merged.bio,
    joinedAt: merged.createdAt || merged.joinedAt || merged.memberSince || m.createdAt,
    lastSeenAt,
    lastPostTs,
    points: points ?? merged.points,
    email: email || null,
    // Fase 1: contacto + nicho de la encuesta de ingreso
    whatsapp: surveyFields.whatsapp,
    instagram: surveyFields.instagram,
    niche: surveyFields.niche,
    surveyAnswers: surveyAnswers.length ? surveyAnswers : undefined,
    // Fase 1: billing — valores en dólares (centavos / 100)
    ltv: ltvCents != null ? Math.round(ltvCents) / 100 : null,
    planPrice: planCents != null ? Math.round(planCents) / 100 : (typeof planRaw === 'string' ? planRaw : null),
    planInterval: planInterval ? String(planInterval).toLowerCase() : null, // 'month' | 'year'
    subId: mmd.msbs || null,
    // Fase 2: churn en proceso
    subStatus: sub.subStatus,   // 'active' | 'cancelling' | 'canceled' | null
    cancelAt: sub.cancelAt,     // ms — fin del acceso pagado (o cuándo se canceló)
    accessUntil: sub.periodEnd || sub.cancelAt || null, // ms — corte de acceso si se conoce
    tier,                       // 'Standard' | 'Premium' | 'VIP' | null
    _raw: raw,
  };
}

// ── ENCUESTA DE INGRESO (Fase 1) ──────────────────────────
// Normaliza member.metadata.survey a un array de { question, answer }.
// Acepta: JSON string, objeto { survey:[...] } / { answers:[...] }, o array directo.
export function parseSurvey(rawSurvey) {
  if (!rawSurvey) return [];
  let s = rawSurvey;
  if (typeof s === 'string') {
    try { s = JSON.parse(s); } catch (_) { return []; }
  }
  const arr = Array.isArray(s)
    ? s
    : (Array.isArray(s.survey) ? s.survey
      : (Array.isArray(s.answers) ? s.answers
        : (Array.isArray(s.questions) ? s.questions : [])));
  return arr.map((item) => {
    if (!item || typeof item !== 'object') return null;
    const question = item.question || item.q || item.label || item.prompt || item.title || '';
    let answer = item.answer ?? item.a ?? item.value ?? item.response ?? item.text ?? '';
    if (Array.isArray(answer)) answer = answer.join(', ');
    else if (answer && typeof answer === 'object') answer = JSON.stringify(answer);
    answer = String(answer || '').trim();
    if (!question && !answer) return null;
    return { question: String(question).trim(), answer };
  }).filter(Boolean);
}

// Detecta WhatsApp / Instagram / nicho por keyword en el texto de la pregunta.
export function extractSurveyFields(answers) {
  const out = { whatsapp: null, instagram: null, niche: null };
  for (const { question, answer } of (answers || [])) {
    if (!answer) continue;
    const q = (question || '').toLowerCase();
    if (out.whatsapp == null && /(whats|whatsapp|wsp|tel[eé]fono|telefono|n[uú]mero|numero|celular|m[oó]vil|movil|phone|cel\b)/.test(q)) {
      out.whatsapp = _cleanPhone(answer);
    } else if (out.instagram == null && /(instagram|insta\b|\big\b|@|usuario)/.test(q)) {
      out.instagram = _cleanHandle(answer);
    } else if (out.niche == null && /(nicho|niche|rubro|negocio|tema|industria|sector|dedicas|a qu[eé]|de qu[eé]|tu marca|emprend)/.test(q)) {
      out.niche = answer.slice(0, 140);
    }
  }
  return out;
}

function _cleanPhone(v) {
  const s = String(v).trim();
  const plus = s.trim().startsWith('+') ? '+' : '';
  const digits = s.replace(/[^\d]/g, '');
  if (digits.length < 6) return null; // no parece un número
  return plus + digits;
}

function _cleanHandle(v) {
  let s = String(v).trim();
  // Si pegó una URL de instagram, extraer el handle
  const urlMatch = s.match(/instagram\.com\/([A-Za-z0-9_.]+)/i);
  if (urlMatch) return '@' + urlMatch[1].replace(/\/$/, '');
  s = s.replace(/^@+/, '').replace(/\s+/g, '');
  if (!s || /^https?:/i.test(s)) return null;
  return '@' + s;
}

// ── ACTIVIDAD POR MIEMBRO (Fase 3) ────────────────────────
// Agrega los comentarios capturados a cada miembro: cuántos, último (texto+fecha).
// Empareja por cualquiera de las claves del miembro (id/handle, userId, username,
// name) contra authorId/authorName del comentario; deduplica por id de comentario.
// Pura → testeable. Devuelve { [memberId]: { commentCount, lastCommentAt, lastCommentText } }.
export function aggregateMemberComments(members, comments) {
  const keyToId = new Map();
  for (const m of (members || [])) {
    if (!m || m.id == null) continue;
    for (const k of [m.id, m.userId, m.username, m.name]) {
      if (k != null && String(k) !== '') keyToId.set(String(k), m.id);
    }
  }

  const acc = new Map(); // memberId -> { count, lastAt, lastText, ids:Set }
  for (const cm of (comments || [])) {
    if (!cm) continue;
    const candidateKeys = [cm.authorId, cm.authorName].filter((x) => x != null && String(x) !== '');
    const key = candidateKeys.map(String).find((x) => keyToId.has(x));
    if (!key) continue;
    const mid = keyToId.get(key);
    let s = acc.get(mid);
    if (!s) { s = { count: 0, lastAt: null, lastText: '', ids: new Set() }; acc.set(mid, s); }
    const cid = cm.id != null ? String(cm.id) : null;
    if (cid && s.ids.has(cid)) continue;
    if (cid) s.ids.add(cid);
    s.count++;
    const ts = cm.createdAt ? new Date(cm.createdAt).getTime() : null;
    if (ts && !Number.isNaN(ts) && (!s.lastAt || ts > s.lastAt)) {
      s.lastAt = ts;
      s.lastText = String(cm.content || '').slice(0, 160);
    }
  }

  const out = {};
  for (const [mid, s] of acc) {
    out[mid] = { commentCount: s.count, lastCommentAt: s.lastAt, lastCommentText: s.lastText };
  }
  return out;
}

// ── ESTADO DE SUSCRIPCIÓN / CANCELACIÓN (Fase 2) ──────────
// Skool guarda billing en member.metadata con prefijos cortos (mb*, mm*).
// NO conocemos el nombre EXACTO del campo de cancelación (varía por comunidad),
// así que detectamos por heurística sobre TODAS las claves de la metadata:
//   - una clave que mencione "cancel" → señal de cancelación (bool true o fecha)
//   - un campo de status textual ("cancelling"/"canceled"/"active"…)
//   - un timestamp de fin de periodo (period end / valid until) como fecha de corte
// Devuelve { subStatus, cancelAt }. Si la heurística falla, ambos null (sin daño).
// El panel "Diagnóstico de cancelación" del dashboard muestra las claves reales
// para afinar esta función con datos verídicos.
export function detectSubscription(mmd, md, merged) {
  const sources = [mmd, md, merged].filter((s) => s && typeof s === 'object');
  let cancelAt = null;
  let periodEnd = null;
  let statusStr = null;
  let cancelFlag = false;

  for (const src of sources) {
    for (const [k, v] of Object.entries(src)) {
      if (v == null || typeof v === 'object') continue;
      const key = k.toLowerCase();

      // Status textual de la suscripción
      if (statusStr == null && typeof v === 'string' &&
          /(substatus|subscriptionstatus|billingstatus|mbss|mbstatus|mbsubstatus|memberstatus)/.test(key)) {
        statusStr = v.toLowerCase();
      }
      // Campo de cancelación (bool, fecha o string). Cualquier valor "verdadero"
      // en una clave que mencione cancel es señal de cancelación.
      if (/cancel/.test(key)) {
        const t = _dateLike(v);
        if (t) {
          if (!cancelAt || t > cancelAt) cancelAt = t;
          cancelFlag = true;
        } else if (v === true || v === 'true' || v === 1) {
          cancelFlag = true;
        } else if (typeof v === 'string' && v.trim() && !/^(false|none|no|0|null|n\/a)$/i.test(v.trim())) {
          cancelFlag = true;
          if (statusStr == null && /cancel/.test(v.toLowerCase())) statusStr = v.toLowerCase();
        }
      }
      // Fin del periodo pagado / acceso hasta (no es cancelación por sí solo,
      // pero si hay cancelación es la fecha de corte del acceso)
      if (/(periodend|currentperiodend|accessuntil|expiresat|expireat|mbexp|validuntil|paiduntil|renewsat|nextbill)/.test(key)) {
        const t = _dateLike(v);
        if (t && (!periodEnd || t > periodEnd)) periodEnd = t;
      }
    }
  }

  const now = Date.now();
  const isCancel = cancelFlag || (statusStr && /cancel/.test(statusStr));
  // Fecha de corte: el cancelAt explícito, o el fin de periodo si hay cancelación.
  const cutoff = cancelAt || (isCancel ? periodEnd : null);

  let subStatus = null;
  if (statusStr) {
    if (/cancel/.test(statusStr)) subStatus = (cutoff && cutoff > now) ? 'cancelling' : 'canceled';
    else if (/active|trialing|paid|paying/.test(statusStr)) subStatus = 'active';
  }
  if (!subStatus && isCancel) subStatus = (cutoff && cutoff > now) ? 'cancelling' : 'canceled';

  // periodEnd se devuelve aparte: sirve como fecha de corte ("churns in X days")
  // incluso cuando la cancelación se detecta por la pestaña ?t=cancelling y no
  // por un campo de la metadata.
  return { subStatus, cancelAt: cutoff, periodEnd };
}

// Detecta el tier de membresía (Standard / Premium / VIP) por heurística sobre
// la metadata. Si el campo de Skool no se llama de forma reconocible, devuelve
// null (sin daño); el panel 🔍 Diagnóstico ayuda a confirmar el campo real.
export function detectTier(mmd, md, merged) {
  const sources = [mmd, md, merged].filter((s) => s && typeof s === 'object');
  for (const src of sources) {
    for (const [k, v] of Object.entries(src)) {
      if (typeof v !== 'string') continue;
      const val = v.trim().toLowerCase();
      const key = k.toLowerCase();
      // Valor exacto standard/premium/vip
      if (/^(standard|premium|vip)$/.test(val)) return _capTier(val);
      // Clave relacionada a tier/membership con el valor adentro
      if (/(tier|membership|plan|level)/.test(key)) {
        const m = val.match(/\b(standard|premium|vip)\b/);
        if (m) return _capTier(m[1]);
      }
    }
  }
  return null;
}

function _capTier(v) {
  if (v === 'vip') return 'VIP';
  return v.charAt(0).toUpperCase() + v.slice(1);
}

// Acepta un valor y devuelve ms SOLO si parece una fecha plausible (2015–2040).
function _dateLike(v) {
  const t = _toMs(v);
  if (t == null) return null;
  const year = new Date(t).getFullYear();
  return year >= 2015 && year <= 2040 ? t : null;
}

// Convierte a número desde number | "4700" | "$47.00" | "47,00". Null si no aplica.
function _toNum(v) {
  if (v == null) return null;
  if (typeof v === 'number') return Number.isFinite(v) ? v : null;
  const n = parseFloat(String(v).replace(/[^\d.-]/g, ''));
  return Number.isFinite(n) ? n : null;
}

function _normalizePost(raw) {
  if (!raw || typeof raw !== 'object') return null;

  // Skool (Next.js SSR) envuelve cada item del feed como { post: {...}, comments: [...] }
  // y el texto vive en post.metadata.content, no en el nivel superior.
  const p = (raw.post && typeof raw.post === 'object') ? raw.post : raw;
  const md = p.metadata || {};

  const id = p.id || p.postId || p.uuid || raw.id;
  if (!id) return null;

  const content =
    p.body || p.content || p.text || p.message ||
    md.content || md.body || md.text || md.description || '';

  const author = p.user || p.author || p.member || p.createdBy || raw.user || {};
  const amd = author.metadata || {};
  const authorName =
    author.name || author.displayName || author.username ||
    [author.firstName, author.lastName].filter(Boolean).join(' ').trim() || amd.name || '';

  return {
    id: String(id),
    content: typeof content === 'string' ? content.slice(0, 2000) : '',
    authorId: String(author.id || author.userId || p.userId || ''),
    authorName,
    authorAvatarUrl: author.avatarUrl || author.profileImageUrl || amd.pictureProfile || amd.pictureBubble || '',
    createdAt: p.createdAt || p.publishedAt || p.postedAt || md.createdAt,
    likesCount: p.likesCount || p.likes || md.upvotes || md.likesCount || 0,
    commentsCount: p.commentsCount || p.commentCount || p.replyCount || md.comments || md.commentsCount || 0,
    isPinned: p.isPinned || p.pinned || md.pinned || false,
    // Conserva el wrapper completo: el SW saca de aquí los comentarios embebidos.
    _raw: raw,
  };
}

function _normalizeComment(raw) {
  if (!raw || typeof raw !== 'object') return null;

  const c = (raw.comment && typeof raw.comment === 'object') ? raw.comment : raw;
  const md = c.metadata || {};

  const id = c.id || c.commentId || c.uuid || raw.id;
  if (!id) return null;

  const author = c.user || c.author || c.member || c.createdBy || raw.user || {};
  const amd = author.metadata || {};
  const authorName =
    author.name || author.displayName || author.username ||
    [author.firstName, author.lastName].filter(Boolean).join(' ').trim() || amd.name || '';

  return {
    id: String(id),
    content: (c.body || c.content || c.text || md.content || md.body || md.text || '').slice(0, 1000),
    authorId: String(author.id || author.userId || c.userId || ''),
    authorName,
    createdAt: c.createdAt || c.postedAt || md.createdAt,
    likesCount: c.likesCount || c.likes || 0,
    _raw: raw,
  };
}

// ── DETECCIÓN POR CONTENIDO ───────────────────────────────

function _tryExtractMembersFromAny(data) {
  const lists = _allArraysIn(data);
  for (const list of lists) {
    if (list.length < 2) continue;
    const sample = list[0];
    // Tiene campos típicos de miembro
    const hasMemberFields =
      'name' in sample || 'displayName' in sample || 'username' in sample;
    const hasId = 'id' in sample || 'userId' in sample || 'uuid' in sample;
    if (hasMemberFields && hasId) {
      const normalized = list.map(_normalizeMember).filter(Boolean);
      if (normalized.length > 0) return normalized;
    }
  }
  return [];
}

function _tryExtractPostsFromAny(data) {
  const lists = _allArraysIn(data);
  for (const list of lists) {
    if (list.length < 1) continue;
    const sample = list[0];
    const inner = (sample && sample.post && typeof sample.post === 'object') ? sample.post : sample;
    const imd = (inner && inner.metadata) || {};
    const hasPostFields =
      'body' in inner || 'content' in inner || 'text' in inner || 'message' in inner ||
      'content' in imd || 'body' in imd;
    const hasId = 'id' in inner || 'postId' in inner || 'uuid' in inner;
    if (hasPostFields && hasId) {
      const normalized = list.map(_normalizePost).filter(Boolean);
      if (normalized.length > 0) return normalized;
    }
  }
  return [];
}

// ── HELPERS ───────────────────────────────────────────────

function _findArray(data, keys) {
  for (const key of keys) {
    if (Array.isArray(data[key]) && data[key].length > 0) return data[key];
  }
  // Buscar en data.data.*
  if (data.data && typeof data.data === 'object') {
    return _findArray(data.data, keys);
  }
  return [];
}

function _allArraysIn(data, depth = 0) {
  if (depth > 2 || !data || typeof data !== 'object') return [];
  const results = [];
  for (const val of Object.values(data)) {
    if (Array.isArray(val) && val.length > 0 && typeof val[0] === 'object') {
      results.push(val);
    } else if (val && typeof val === 'object' && !Array.isArray(val)) {
      results.push(..._allArraysIn(val, depth + 1));
    }
  }
  return results;
}

function _matchesAny(url, patterns) {
  return patterns.some((p) => url.includes(p));
}

function _extractPostId(url) {
  const match = url.match(/posts?\/([^/?]+)/i);
  return match ? match[1] : null;
}
