// ============ AcademIA8 · Consola — Módulo Campañas · CAPA DE API (REAL) ============
// Adaptador contra el backend a8mail (AWS · SES). TODO el acceso a datos vive acá.
// Las FIRMAS de cada función se mantienen idénticas al mock: el resto del front no
// distingue mock de real. La traducción de claves y los mapeos que no son 1:1 viven
// exclusivamente en este archivo (ver README de cableado: console/CABLEADO-BACKEND.md).
//
//   Fuente de verdad: docs/API_CONTRACT.md del repo majul-ai/academia8-ses.
//   Base URL y API key: se configuran fuera del código versionado (ver getBaseUrl/getApiKey).
//
//   Contrato (rutas admin, header x-api-key):
//     POST   /admin/contacts/import           -> importarContactos()
//     GET    /admin/contacts/summary          -> resumenContactos()
//     GET    /admin/contacts                   -> listarContactos()  (hidrata /{id})
//     GET    /admin/contacts/{id}              -> (hidratación tags+estado)
//     PATCH  /admin/contacts/{id}/tags         -> actualizarContacto()
//     DELETE /admin/contacts/{id}              -> eliminarContacto()
//     GET    /admin/tags                        -> listarEtiquetas()
//     POST   /admin/campaigns                   -> crearCampania()
//     GET    /admin/campaigns                   -> listarCampanias() (pagina todo)
//     GET    /admin/campaigns/{id}              -> obtenerCampania()
//     PATCH  /admin/campaigns/{id}              -> actualizarCampania()
//     DELETE /admin/campaigns/{id}              -> eliminarCampania()
//     GET    /admin/campaigns/{id}/preview      -> previewCampania() / contarDestinatarios()
//     POST   /admin/campaigns/{id}/test         -> enviarPrueba()
//     POST   /admin/campaigns/{id}/send         -> enviarAhora() / programarCampania()
//     POST   /admin/campaigns/{id}/cancel       -> cancelarProgramacion()
//     GET    /admin/config/sender               -> obtenerConfig()
//     PUT    /admin/config/sender               -> guardarRemitente()

(function(){

  // ---------------------------------------------------------------------------
  // Config de conexión — NO se hardcodea la key en el código versionado.
  //   1) window.A8MAIL_CONFIG (definido en console/campaigns-config.local.js, ignorado por git)
  //   2) sessionStorage 'a8mail_api_key' (persiste durante la sesión del navegador)
  //   3) prompt() la primera vez y se guarda en sessionStorage
  // La Base URL NO es secreta (está en el contrato); la key la provee Nahir (RUNBOOK §4).
  // ---------------------------------------------------------------------------
  const DEFAULT_BASE = 'https://61zemntvb5.execute-api.us-east-1.amazonaws.com';
  const KEY_LS = 'a8mail_api_key';

  function getBaseUrl(){
    const cfg = (typeof window!=='undefined' && window.A8MAIL_CONFIG) || {};
    return (cfg.baseUrl || DEFAULT_BASE).replace(/\/+$/,'');
  }
  function getApiKey(){
    const cfg = (typeof window!=='undefined' && window.A8MAIL_CONFIG) || {};
    if (cfg.apiKey) return String(cfg.apiKey).trim();
    let k = '';
    try{ k = sessionStorage.getItem(KEY_LS) || ''; }catch(e){}
    if (!k){
      k = (window.prompt('Pegá la API key de a8mail (te la da Nahir · RUNBOOK §4):') || '').trim();
      if (k){ try{ sessionStorage.setItem(KEY_LS, k); }catch(e){} }
    }
    return k;
  }
  function clearApiKey(){ try{ sessionStorage.removeItem(KEY_LS); }catch(e){} }

  const delay = (ms)=> new Promise(r=>setTimeout(r, ms));

  // ---------- fetch wrapper (auth + errores del contrato) ----------
  // El stage tiene throttling (20 req/s, burst 40) y el contrato incorpora
  // 429 too_many_requests. Ante 429 reintentamos con backoff exponencial simple
  // (respetando Retry-After si viene). El 422 se eliminó del contrato (nunca se emitía).
  const MAX_429_RETRIES = 5;
  async function req(method, path, body){
    const headers = { 'content-type':'application/json', 'x-api-key': getApiKey() };
    const payload = body!=null ? JSON.stringify(body) : undefined;
    let attempt = 0;
    while (true){
      let res;
      try{
        res = await fetch(getBaseUrl()+path, { method, headers, body: payload });
      }catch(e){
        throw new Error('No se pudo conectar con el servidor. Revisá tu conexión a internet.');
      }
      // 429: backoff exponencial simple y reintento (hasta MAX_429_RETRIES).
      if (res.status===429 && attempt<MAX_429_RETRIES){
        const ra = parseFloat(res.headers.get('retry-after'));
        const wait = Number.isFinite(ra) ? ra*1000
                   : Math.min(8000, 400*Math.pow(2, attempt)) + Math.floor(Math.random()*200);
        attempt++;
        await delay(wait);
        continue;
      }
      if (res.status===204) return null;
      let data = null;
      const txt = await res.text();
      if (txt){ try{ data = JSON.parse(txt); }catch(e){ /* no-json */ } }
      if (!res.ok){
        if (res.status===401){ clearApiKey(); const e=new Error('La API key es inválida o falta. Volvé a intentar la acción y pegá la key correcta.'); e.status=401; e.code='unauthorized'; throw e; }
        if (res.status===429){ const e=new Error('El servidor está recibiendo demasiadas solicitudes. Esperá unos segundos y reintentá.'); e.status=429; e.code='too_many_requests'; throw e; }
        const msg = (data && data.error && data.error.message) || `Error ${res.status}`;
        const err = new Error(msg);
        err.status = res.status;
        err.code = (data && data.error && data.error.code) || String(res.status);
        throw err;
      }
      return data;
    }
  }

  // ---------------------------------------------------------------------------
  // Traducción de claves — vive SOLO acá.
  // ---------------------------------------------------------------------------
  // backend -> front
  const ESTC_FROM = { active:'activo', unsubscribed:'dado_de_baja', bounced:'rebotado', complained:'dado_de_baja' };
  const ESTCAMP_FROM = { draft:'borrador', scheduled:'programada', sending:'enviando', sent:'enviada', failed:'fallida' };

  // labels que muestra el front (claves = estados en español)
  const ESTADO_CONTACTO = { activo:'Activo', dado_de_baja:'Dado de baja', rebotado:'Rebotado' };
  const ESTADO_CAMPANIA = { borrador:'Borrador', programada:'Programada', enviando:'Enviando', enviada:'Enviada', fallida:'Fallida' };

  const DOMINIO_ENVIO = 'mail.academia8.com';
  const MAX_PRUEBA = 5;
  const TODAY = new Date().toISOString().slice(0,10);

  // ---------- mapeos de forma ----------
  function isoDate(s){ return (s||'').slice(0,10); }
  function isoMin(s){ if(!s) return null; const t=String(s); return t.length>=16 ? t.slice(0,16) : (t.slice(0,10)+'T00:00'); }

  function recipientsFromApi(r){
    if (!r) return { modo:'todos', etiquetas:[] };
    if (r.mode==='all') return { modo:'todos', etiquetas:[] };
    return { modo:'etiquetas', etiquetas: r.tags || [] };
  }
  // Convierte destinatarios del front al contrato. Devuelve null si es "por etiqueta"
  // sin ninguna etiqueta (para poder OMITIR recipients en un PATCH y no pisar lo guardado).
  function recipientsToApi(dest){
    if (!dest) return null;
    if (dest.modo==='todos') return { mode:'all' };
    const tags = (dest.etiquetas||[]).filter(Boolean);
    if (!tags.length) return null;
    return { mode:'tags', tags };
  }

  function netFrom(counters, status){
    if (!counters) return 0;
    if (status==='sent' && typeof counters.sent==='number') return counters.sent;
    const r = counters.recipients||0, s = counters.suppressedFiltered||0;
    return Math.max(0, r - s);
  }

  // Campaña backend -> campaña que consume el front.
  //   extra.html    : cuerpo HTML (solo lo trae el detalle, no la lista)
  //   extra.remitente: se adjunta desde config/sender (el backend no guarda remitente por campaña)
  function campFromApi(m, extra){
    extra = extra || {};
    const estado = ESTCAMP_FROM[m.status] || 'borrador';
    const counters = m.counters || null;
    const out = {
      id: m.campaignId,
      // El backend NO tiene "nombre interno"; usamos el asunto como nombre visible en la lista.
      nombre: m.subject || '(sin asunto)',
      asunto: m.subject || '',
      destinatarios: recipientsFromApi(m.recipients),
      html: (extra.html!=null) ? extra.html : (m.html!=null ? m.html : ''),
      estado,
      programadaPara: m.scheduleAt || null,
      enviadaEn: (estado==='enviada' || estado==='fallida') ? isoMin(m.updatedAt) : null,
      totalDestinatarios: netFrom(counters, m.status),
      excluidos: counters ? (counters.suppressedFiltered||0) : 0,
      // El backend no persiste a qué direcciones se mandó prueba.
      pruebasEnviadas: [],
      remitente: extra.remitente || null,
      creadoEn: isoDate(m.createdAt) || TODAY,
      actualizadoEn: isoDate(m.updatedAt) || TODAY,
      counters,
      // Cantidad de destinatarios con error (para badge/alerta). >0 también en un
      // envío 'enviada' con fallo PARCIAL (sent>0 y failed>0), que si no pasa inadvertido.
      fallidos: counters ? (counters.failed || 0) : 0,
    };
    if (estado==='fallida'){
      out.error = failureReasonText(m.failureReason)
        || ('El envío falló.' + (counters && counters.failed ? ` ${counters.failed} destinatario${counters.failed===1?'':'s'} con error.` : '')
            + ' Reintentá el envío; el sistema no reenvía a quienes ya recibieron.');
      out.failureReason = m.failureReason || null;
    }
    if (m.warnings) out.warnings = m.warnings;
    return out;
  }

  // El detalle de una campaña fallida trae failureReason (string). Lo traducimos a un
  // mensaje accionable para el operador; si no lo reconocemos, mostramos el texto crudo.
  const FALLA_ES = {
    throttling: 'Amazon limitó el ritmo de envío (throttling). Esperá unos minutos y reintentá.',
    rate_exceeded: 'Amazon limitó el ritmo de envío (throttling). Esperá unos minutos y reintentá.',
    quota_exceeded: 'Se alcanzó el límite diario de envíos de Amazon. Reintentá más tarde.',
    daily_quota_exceeded: 'Se alcanzó el límite diario de envíos de Amazon. Reintentá más tarde.',
    account_paused: 'El envío está pausado en la cuenta de Amazon. Contactá a soporte antes de reintentar.',
    account_suspended: 'La cuenta de envío de Amazon está suspendida. Contactá a soporte.',
    message_rejected: 'Amazon rechazó el contenido del correo. Revisá el HTML y reintentá.',
    sender_not_verified: 'El remitente no está verificado en Amazon. Verificalo y reintentá.',
    configuration_error: 'Error de configuración del envío. Avisá al equipo técnico antes de reintentar.',
    internal_error: 'Hubo un error interno durante el envío. Reintentá en unos minutos.',
  };
  function failureReasonText(reason){
    if (!reason) return null;
    const key = String(reason).trim().toLowerCase().replace(/[^a-z0-9]+/g,'_').replace(/^_+|_+$/g,'');
    if (FALLA_ES[key]) return FALLA_ES[key] + ' El sistema no reenvía a quienes ya recibieron.';
    const r = String(reason).toLowerCase();
    let base = null;
    if (r.includes('throttl') || r.includes('rate')) base = FALLA_ES.throttling;
    else if (r.includes('quota') || r.includes('limit')) base = FALLA_ES.quota_exceeded;
    else if (r.includes('paus')) base = FALLA_ES.account_paused;
    else if (r.includes('suspend')) base = FALLA_ES.account_suspended;
    else if (r.includes('reject')) base = FALLA_ES.message_rejected;
    else if (r.includes('verif')) base = FALLA_ES.sender_not_verified;
    if (base) return base + ' El sistema no reenvía a quienes ya recibieron.';
    // Motivo desconocido: mostramos el texto crudo del backend, envuelto.
    return 'El envío falló: ' + String(reason).trim() + '. Reintentá el envío; el sistema no reenvía a quienes ya recibieron.';
  }

  // Advertencia informativa (no bloqueante) -> texto legible para el operador.
  function warningText(w){
    if (w && w.message) return w.message;
    if (w && w.code==='foreign_unsubscribe') return 'El HTML trae un link de baja de otro sistema (ej. Mailchimp). No suprime en Academia 8; igual se agrega el link de baja propio.';
    return 'Advertencia en el contenido.';
  }

  // caché liviana de la config de remitente (para adjuntar remitente a las campañas sin refetch)
  let _cfgCache = null;
  async function obtenerConfigRaw(){
    const s = await req('GET','/admin/config/sender');
    _cfgCache = {
      remitente: { nombre: s.fromName || 'Academia 8', email: s.fromAddress || ('cursos@'+DOMINIO_ENVIO) },
      replyTo: s.replyTo || null,
      sandbox: !!s.sandbox,
      verificadas: s.verifiedIdentities || [],
    };
    return _cfgCache;
  }

  // ================= API PÚBLICA =================
  const CampAPI = {
    ESTADO_CONTACTO, ESTADO_CAMPANIA, TODAY, DOMINIO_ENVIO, MAX_PRUEBA,

    // ---------------- config / remitente ----------------
    async obtenerConfig(){ return await obtenerConfigRaw(); },
    async guardarRemitente(remitente){
      const s = await req('PUT','/admin/config/sender', { fromName: remitente.nombre, fromAddress: remitente.email });
      const out = { nombre: (s&&s.fromName)||remitente.nombre, email: (s&&s.fromAddress)||remitente.email };
      if (_cfgCache) _cfgCache.remitente = out;
      return out;
    },

    // ---------------- etiquetas ----------------
    // El backend no tiene entidad "etiqueta": las etiquetas son strings que nacen al usarse.
    async listarEtiquetas(){
      const r = await req('GET','/admin/tags');
      return (r.tags||[]).map(t=>({ id: t.tag, nombre: t.tag, count: t.count }));
    },
    // No pega a ningún endpoint: nace al usarse (import o campaña). Devuelve etiqueta local.
    async crearEtiqueta(nombre){ const n=(nombre||'').trim(); return { id:n, nombre:n }; },

    // ---------------- contactos ----------------
    // La lista del backend ahora trae tags (array) y status en cada ítem: una sola
    // request por página (se eliminó la hidratación N+1 con GET /{id}).
    async listarContactos({ etiqueta='todas', cursor=null, pageSize=25 }={}){
      const qs = new URLSearchParams();
      qs.set('limit', String(pageSize));
      if (etiqueta && etiqueta!=='todas') qs.set('tag', etiqueta);
      if (cursor) qs.set('cursor', cursor);
      const res = await req('GET','/admin/contacts?'+qs.toString());
      const items = (res.items||[]).map(it=>({
        id: it.contactId,
        email: it.email,
        nombre: it.nombre||'',
        apellido: it.apellido||'',
        etiquetas: it.tags||[],
        estado: ESTC_FROM[it.status]||'activo',
      }));
      // prevCursor no existe en el backend: el componente mantiene la pila (ver ContactsView).
      return { items, nextCursor: res.nextCursor || null, prevCursor:null, count: items.length, pageSize };
    },
    async resumenContactos(){
      const s = await req('GET','/admin/contacts/summary');
      const activos = s.active||0;
      const baja = (s.unsubscribed||0) + (s.complained||0);
      const rebote = s.bounced||0;
      const total = (typeof s.total==='number') ? s.total : (activos+baja+rebote);
      return { total, activos, baja, rebote };
    },
    // Solo soporta cambio de etiquetas (único caso que usa el front). Diff local -> add/remove.
    async actualizarContacto(id, patch){
      if (patch && patch.etiquetas){
        const d = await req('GET','/admin/contacts/'+encodeURIComponent(id));
        const old = d.tags || [];
        const next = patch.etiquetas || [];
        const add = next.filter(t=>!old.includes(t));
        const remove = old.filter(t=>!next.includes(t));
        if (add.length || remove.length){
          const body = {};
          if (add.length) body.add = add;
          if (remove.length) body.remove = remove;
          await req('PATCH','/admin/contacts/'+encodeURIComponent(id)+'/tags', body);
        }
        const d2 = await req('GET','/admin/contacts/'+encodeURIComponent(id));
        return { id:d2.contactId, email:d2.email, nombre:d2.nombre||'', apellido:d2.apellido||'', etiquetas:d2.tags||[], estado: ESTC_FROM[d2.status]||'activo' };
      }
      return { id, ...(patch||{}) };
    },
    async eliminarContacto(id){ await req('DELETE','/admin/contacts/'+encodeURIComponent(id)); return { ok:true }; },

    // importar: rows JSON + tags del lote. El backend exige >=1 etiqueta.
    async importarContactos(filas, etiquetaIds){
      const tags = (etiquetaIds||[]).filter(Boolean);
      if (!tags.length){ const e=new Error('Elegí al menos una etiqueta para el lote (el backend la exige para importar).'); e.code='needs_tag'; throw e; }
      const rows = (filas||[]).map(f=>({ email:f.email, nombre:f.nombre||'', apellido:f.apellido||'' }));
      const res = await req('POST','/admin/contacts/import', { tags, rows });
      return { creados: res.created||0, actualizados: res.updated||0, skipped: res.skipped||[], totalRows: res.totalRows };
    },

    // ---------------- destinatarios ----------------
    // Antes de guardar la campaña no hay endpoint para contar un set arbitrario:
    //   - "todos": usamos el resumen (active ~ net).
    //   - "por etiqueta": sumamos los counts de /admin/tags -> APROXIMADO (no dedup ni supresión).
    // El conteo EXACTO (net) sale de previewCampania(id), ya guardada la campaña.
    async contarDestinatarios(modo, etiquetaIds){
      if (modo==='todos'){
        const s = await req('GET','/admin/contacts/summary');
        const activos = s.active||0;
        const total = (typeof s.total==='number') ? s.total : activos;
        return { total: activos, excluidos: total-activos, alcanzables: total, aprox:false };
      }
      const t = await req('GET','/admin/tags');
      const map = {}; (t.tags||[]).forEach(x=>map[x.tag]=x.count);
      const total = (etiquetaIds||[]).reduce((a,id)=> a + (map[id]||0), 0);
      return { total, excluidos:0, alcanzables: total, aprox:true };
    },
    async previewCampania(id){
      const p = await req('GET','/admin/campaigns/'+encodeURIComponent(id)+'/preview');
      return { total: (typeof p.net==='number')?p.net:(p.recipients||0), excluidos: p.suppressedFiltered||0, alcanzables: p.recipients||0, aprox:false };
    },

    // ---------------- campañas ----------------
    // El front espera un array completo; el backend pagina. Recorremos todas las páginas.
    async listarCampanias(){
      let out = [], cursor = null, guard = 0;
      do{
        const qs = new URLSearchParams(); qs.set('limit','200'); if (cursor) qs.set('cursor', cursor);
        const res = await req('GET','/admin/campaigns?'+qs.toString());
        out = out.concat((res.items||[]).map(m=>campFromApi(m)));
        cursor = res.nextCursor || null; guard++;
      } while (cursor && guard<50);
      return out;
    },
    async obtenerCampania(id){
      let remitente = null;
      try{ const cfg = _cfgCache || await obtenerConfigRaw(); remitente = cfg.remitente; }catch(e){}
      const m = await req('GET','/admin/campaigns/'+encodeURIComponent(id));
      return campFromApi(m, { remitente });
    },
    async crearCampania(data){
      const rec = recipientsToApi(data.destinatarios);
      if (!rec){ const e=new Error('Elegí al menos una etiqueta para poder guardar esta campaña.'); e.code='no_recipients'; throw e; }
      const subject = (data.asunto || data.nombre || '').trim() || '(sin asunto)';
      const m = await req('POST','/admin/campaigns', { subject, html: data.html||'', recipients: rec });
      return campFromApi(m);
    },
    async actualizarCampania(id, data){
      const body = {};
      const subject = (data.asunto || data.nombre || '').trim();
      if (subject) body.subject = subject;
      if ('html' in data) body.html = data.html || '';
      const rec = recipientsToApi(data.destinatarios);
      if (rec) body.recipients = rec; // si es "etiqueta sin tags", se OMITE (no se toca en el server)
      const m = await req('PATCH','/admin/campaigns/'+encodeURIComponent(id), body);
      return campFromApi(m);
    },
    async eliminarCampania(id){ await req('DELETE','/admin/campaigns/'+encodeURIComponent(id)); return { ok:true }; },
    // No hay endpoint de duplicado: se crea una campaña nueva con el asunto+HTML+destinatarios del origen.
    async duplicarCampania(id){
      const src = await req('GET','/admin/campaigns/'+encodeURIComponent(id));
      const rec = (src.recipients && src.recipients.mode==='all') ? { mode:'all' } : { mode:'tags', tags:(src.recipients&&src.recipients.tags)||[] };
      const body = { subject: (src.subject||'(sin asunto)')+' (copia)', html: src.html||'', recipients: rec };
      const m = await req('POST','/admin/campaigns', body);
      return campFromApi(m);
    },

    // ---------------- prueba / envío / programación ----------------
    async enviarPrueba(idOrData, direcciones){
      const id = (typeof idOrData==='string') ? idOrData : (idOrData && idOrData.id);
      const emails = (direcciones||[]).map(e=>e.trim()).filter(Boolean);
      if (!emails.length) throw new Error('Ingresá al menos una dirección');
      if (emails.length>MAX_PRUEBA) throw new Error(`El máximo es ${MAX_PRUEBA} direcciones de prueba`);
      const res = await req('POST','/admin/campaigns/'+encodeURIComponent(id)+'/test', { emails });
      const resultados = res.results || [];
      return { ok: resultados.every(r=>r.ok), enviadas: resultados.filter(r=>r.ok).map(r=>r.email), resultados };
    },
    async enviarAhora(id){
      const res = await req('POST','/admin/campaigns/'+encodeURIComponent(id)+'/send', {});
      return { id, estado: ESTCAMP_FROM[res && res.status] || 'enviando' };
    },
    async programarCampania(id, isoLocal){
      // El front manda 'YYYY-MM-DDTHH:MM' (hora de Córdoba). El contrato pide segundos.
      const scheduleAt = /T\d\d:\d\d:\d\d$/.test(isoLocal) ? isoLocal : (isoLocal+':00');
      const res = await req('POST','/admin/campaigns/'+encodeURIComponent(id)+'/send', { scheduleAt });
      return { id, estado: ESTCAMP_FROM[res && res.status] || 'programada', programadaPara: (res && res.scheduleAt) || scheduleAt };
    },
    async cancelarProgramacion(id){
      const res = await req('POST','/admin/campaigns/'+encodeURIComponent(id)+'/cancel', {});
      return { id, estado: ESTCAMP_FROM[res && res.status] || 'borrador' };
    },

    // El fin del envío lo determina el backend. Polling suave del detalle mientras esté "enviando".
    async pollCampania(id, { onTick, intervalMs=3000, maxTries=20 }={}){
      for (let i=0;i<maxTries;i++){
        const c = await this.obtenerCampania(id);
        if (onTick) { try{ onTick(c); }catch(e){} }
        if (c.estado!=='enviando') return c;
        await delay(intervalMs);
      }
      return await this.obtenerCampania(id);
    },

    // helpers de conexión (para diagnóstico / cambiar la key en runtime)
    _setApiKey(k){ try{ sessionStorage.setItem(KEY_LS, String(k||'').trim()); }catch(e){} },
    _clearApiKey: clearApiKey,
    _baseUrl: getBaseUrl,
    async _ping(){ return await req('GET','/admin/ping'); },

    // helper para el warning legible (lo usa el editor)
    _warningText: warningText,
  };

  window.CampAPI = CampAPI;
})();
