// services/auth/src/ak.js
// ------------------------------------------------------------
// Cliente mínimo y robusto para la API Admin de Authentik (v3)
// - Sin dependencias externas (usa fetch nativo de Node >=18)
// - ESM compatible
// - Timeouts, reintentos opcionales y mensajes de error claros
// - Compatible con services/auth/src/index.js actual
// ------------------------------------------------------------

/**
 * Lee configuración desde process.env en cada llamada (para evitar problemas
 * de orden de imports con dotenv). No falla en import-time.
 */
function getConfig() {
  const BASE = (process.env.AUTHENTIK_BASE_URL || '').replace(/\/+$/, '');
  const TOKEN = process.env.AUTHENTIK_TOKEN || '';
  if (!BASE) throw new Error('AK_CONFIG: Falta AUTHENTIK_BASE_URL');
  if (!TOKEN) throw new Error('AK_CONFIG: Falta AUTHENTIK_TOKEN');
  return { BASE, TOKEN };
}


// ------------------------------------------------------------
// Helpers de sincronización
// ------------------------------------------------------------

// -- util GET contra la API admin (ajusta si ya tenés un helper igual)
async function akGET(path) {
  const base = (process.env.AUTHENTIK_BASE_URL || '').replace(/\/+$/, '');
  const url = `${base}${path}`;
  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.AUTHENTIK_TOKEN}` },
  });
  if (!res.ok) {
    const body = await res.text().catch(() => '');
    throw new Error(`AK GET ${path} -> ${res.status}: ${body}`);
  }
  return res.json();
}

// -- listar grupos con búsqueda por nombre/slug
export async function akListGroups(search = '') {
  const q = search ? `?search=${encodeURIComponent(search)}` : '';
  const data = await akGET(`/api/v3/core/groups/${q}`);
  // algunas versiones devuelven {results:[]}, otras un array directo
  return Array.isArray(data) ? data : (data.results || []);
}

export async function akPatchUserAttributes(userPk, partialAttrs = {}) {
  // PATCH del usuario para asegurar attributes.tenant_uuid
  return akRequest('patch', `/api/v3/core/users/${userPk}/`, {
    data: { attributes: partialAttrs },
  });
}

export async function akEnsureGroupForTenant(tenantHex) {
  const groupName = `tenant_${tenantHex}`;

  // buscar por nombre
  const data = await akRequest('get', '/api/v3/core/groups/', { params: { name: groupName }});
  const g = (data?.results || [])[0];
  if (g) return g.pk;

  // crear si no existe
  const created = await akRequest('post', '/api/v3/core/groups/', {
    data: { name: groupName, attributes: { tenant_uuid: tenantHex } },
  });
  return created.pk;
}

export async function akAddUserToGroup(userPk, groupPk) {
  // Endpoint de membership (en versiones recientes, POST users/<pk>/groups/)
  return akRequest('post', `/api/v3/core/users/${userPk}/groups/`, { data: { group: groupPk } });
}

// Utilidad de espera
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

/**
 * Llamada HTTP genérica con fetch + timeout + manejo de errores.
 * @param {('GET'|'POST'|'PUT'|'PATCH'|'DELETE')} method
 * @param {string} path - Ruta a partir de /api/v3 (por ej. "/core/users/")
 * @param {{qs?:Record<string,string|number|boolean>, body?:any, timeoutMs?:number, retries?:number}} [opts]
 */
async function request(method, path, opts = {}) {
  const { BASE, TOKEN } = getConfig();
  const {
    qs = undefined,
    body = undefined,
    timeoutMs = 10000,
    retries = 0,
  } = opts;

  const url = new URL(`${BASE}/api/v3${path}`);
  if (qs) Object.entries(qs).forEach(([k, v]) => url.searchParams.set(k, String(v)));

  let lastErr;
  for (let attempt = 1; attempt <= Math.max(1, retries + 1); attempt++) {
    const controller = new AbortController();
    const t = setTimeout(() => controller.abort(new Error('AK_TIMEOUT')), timeoutMs);
    try {
      const res = await fetch(url, {
        method,
        signal: controller.signal,
        headers: {
          'Authorization': `Bearer ${TOKEN}`,
          'Content-Type': 'application/json',
          'Accept': 'application/json',
        },
        body: body !== undefined ? JSON.stringify(body) : undefined,
      });
      clearTimeout(t);

      if (res.status === 204) return null; // sin contenido

      // intenta parsear JSON; si no es JSON, devuelve texto
      const ctype = res.headers.get('content-type') || '';
      const payload = ctype.includes('application/json') ? await res.json().catch(() => ({})) : await res.text();

      if (!res.ok) {
        const detail = typeof payload === 'string' ? payload : payload?.detail || payload?.error || JSON.stringify(payload);
        const err = new Error(`AK ${method} ${url.pathname} → HTTP ${res.status}: ${detail}`);
        err.status = res.status; // @ts-ignore
        throw err;
      }

      return payload;
    } catch (e) {
      clearTimeout(t);
      lastErr = e;
      // Reintentos sólo en ECONNREFUSED/timeout/5xx
      const msg = String(e?.message || e);
      const retriable = msg.includes('ECONNREFUSED') || msg.includes('AK_TIMEOUT') || /\b5\d\d\b/.test(e?.status?.toString?.() || '');
      if (!retriable || attempt > retries) throw e;
      await sleep(500 * attempt); // backoff lineal suave
    }
  }
  throw lastErr;
}

// ------------------------------------------------------------
// Funciones públicas
// ------------------------------------------------------------

/**
 * Busca un usuario por email en Authentik (case-insensitive) usando ?search=
 * Devuelve el usuario exacto o null si no existe.
 */
export async function akFindUserByEmail(email) {
  if (!email) throw new Error('akFindUserByEmail: email requerido');
  const data = await request('GET', '/core/users/', { qs: { search: email, page_size: 50 }, retries: 3 });
  const list = Array.isArray(data?.results) ? data.results : [];
  const lower = String(email).toLowerCase();
  return list.find((u) => (u.email || '').toLowerCase() === lower) || null;
}

/**
 * Crea un usuario en Authentik con atributos de tenant y opcionalmente lo
 * agrega a un grupo existente.
 * @param {{email:string, displayName?:string, tenantUuid?:string, addToGroupId?: number|string, isActive?: boolean}} p
 * @returns {Promise<any>} el objeto usuario creado
 */
export async function akCreateUser(p) {
  const email = p?.email;
  if (!email) throw new Error('akCreateUser: email requerido');
  const name = p?.displayName || email;
  const tenantUuid = (p?.tenantUuid || '').replace(/-/g, '');
  const isActive = p?.isActive ?? true;

  // 1) crear usuario
  const user = await request('POST', '/core/users/', {
    body: {
      username: email,
      name,
      email,
      is_active: isActive,
      attributes: tenantUuid ? { tenant_uuid: tenantUuid } : {},
    },
    retries: 3,
  });

  // 2) agregar a grupo (opcional)
  if (p?.addToGroupId) {
    try {
      await request('POST', `/core/users/${user.pk}/groups/`, { body: { group: p.addToGroupId }, retries: 2 });
    } catch (e) {
      // No rompas todo por el grupo; deja registro del error para que el caller decida.
      console.warn(`akCreateUser: no se pudo agregar al grupo ${p.addToGroupId}:`, e?.message || e);
    }
  }

  return user;
}

/**
 * Establece/forza una contraseña a un usuario (si tu política lo permite).
 * @param {number|string} userPk
 * @param {string} password
 * @param {boolean} requireChange - si el usuario debe cambiarla al siguiente login
 */
export async function akSetPassword(userPk, password, requireChange = true) {
  if (!userPk) throw new Error('akSetPassword: userPk requerido');
  if (!password) throw new Error('akSetPassword: password requerida');
  try {
    await request('POST', `/core/users/${userPk}/set_password/`, {
      body: { password, require_change: !!requireChange },
      retries: 1,
    });
    return true;
  } catch (e) {
    // Algunas instalaciones no permiten setear password por API (políticas). Propaga un error legible.
    const err = new Error(`akSetPassword: no se pudo establecer la contraseña: ${e?.message || e}`);
    err.cause = e;
    throw err;
  }
}

/**
 * Helper opcional para obtener grupos por nombre/slug si en el futuro lo necesitas
 * (no usado por index.js; se deja por conveniencia).
 */

export async function akResolveGroupIdByName(name) {
  const data = await akListGroups(name);
  const lower = String(name || '').toLowerCase();
  const found = data.find(g =>
    String(g.name || '').toLowerCase() === lower ||
    String(g.slug || '').toLowerCase() === lower
  );
  return found?.pk ?? null;
}

export async function akResolveGroupId({ id, pk, uuid, name, slug } = {}) {
  // si te pasan pk/id directo, devolvelo
  if (pk != null) return Number(pk);
  if (id != null) return Number(id);

  // por UUID (devuelve objeto con pk)
  if (uuid) {
    try {
      const g = await akGET(`/api/v3/core/groups/${encodeURIComponent(uuid)}/`);
      if (g?.pk != null) return Number(g.pk);
    } catch (e) {
      // sigue intentando por nombre/slug
    }
  }

  // por nombre/slug
  if (name || slug) {
    const needle = (name || slug);
    const list = await akListGroups(needle);
    const lower = String(needle || '').toLowerCase();
    const found = list.find(g =>
      String(g.name || '').toLowerCase() === lower ||
      String(g.slug || '').toLowerCase() === lower
    );
    if (found?.pk != null) return Number(found.pk);
  }

  return null;
}

// ------------------------------------------------------------
// Fin
// ------------------------------------------------------------
