// services/auth/src/index.js
// ------------------------------------------------------------
// SuiteCoffee — Servicio de Autenticación (Express + OIDC)
// - ESM compatible (Node >=18)
// - Sesiones con Redis (compartibles con otros servicios)
// - Vistas EJS (login)
// - Rutas OIDC: /auth/login, /auth/callback, /auth/logout
// - Registro de usuario: /api/users/register (DB + Authentik)
// ------------------------------------------------------------

import 'dotenv/config';
import chalk from 'chalk';
import express from 'express';
import cors from 'cors';
import path from 'node:path';
import fs from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { Pool } from 'pg';
import session from 'express-session';
import { createClient as createRedisClient } from 'redis';
import * as connectRedis from 'connect-redis';
import expressLayouts from 'express-ejs-layouts';
import { Issuer, generators } from 'openid-client';
import crypto from 'node:crypto';
import { readFile } from 'node:fs/promises';

// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------

const SESSION_COOKIE_NAME = 'sc.sid';

// Normaliza UUID (acepta con/sin guiones) → "hex" sin guiones
const cleanUuid = (u) => (u ? String(u).toLowerCase().replace(/[^0-9a-f]/g, '') : '');

// Nombre de schema/rol a partir de uuid limpio
const schemaNameFor = (uuidHex) => `schema_tenant_${uuidHex}`;
const roleNameFor   = (uuidHex) => `tenant_${uuidHex}`;

// Helpers de Authentik (admin API)
const {
  akFindUserByEmail,
  akCreateUser,
  akSetPassword,
  akResolveGroupId
} = await import('./ak.js');

// Quoter seguro de identificadores SQL (roles, schemas, tablas)
// Identificador SQL (schema, role, table, …)
const qi = (ident) => `"${String(ident).replace(/"/g, '""')}"`;

// Literal de texto SQL (valores: contraseñas, strings, …)
const qs = (val) => `'${String(val).replace(/'/g, "''")}'`;


const VALID_IDENT = /^[a-zA-Z_][a-zA-Z0-9_:$-]*$/;

// --- Resolver y cachear el grupo por ID/UUID/NOMBRE una sola vez ---
let DEFAULT_GROUP_ID = process.env.AUTHENTIK_DEFAULT_GROUP_ID
  ? Number(process.env.AUTHENTIK_DEFAULT_GROUP_ID)
  : null;

if (!DEFAULT_GROUP_ID) {
  (async () => {
    try {
      // Si tenés akResolveGroupIdByName, usalo:
      // DEFAULT_GROUP_ID = await akResolveGroupIdByName(process.env.AUTHENTIK_DEFAULT_GROUP_NAME);

      // Con el helper genérico que te dejé en ak.js:
      DEFAULT_GROUP_ID = await akResolveGroupId({
        uuid: process.env.AUTHENTIK_DEFAULT_GROUP_UUID,
        name: process.env.AUTHENTIK_DEFAULT_GROUP_NAME,
      });
      console.log('[AK] DEFAULT_GROUP_ID resuelto:', DEFAULT_GROUP_ID);
    } catch (e) {
      console.warn('[AK] No se pudo resolver DEFAULT_GROUP_ID:', e?.message || e);
    }
  })();
}

function nukeSession(req, res, redirectTo = '/auth/login', reason = 'reset') {
  try {
    // Destruye la sesión en el store (Redis)
    req.session?.destroy(() => {
      // Limpia la cookie en el navegador
      res.clearCookie(SESSION_COOKIE_NAME, {
        path: '/',
        httpOnly: true,
        sameSite: 'lax',
        secure: process.env.NODE_ENV === 'production',
      });
      // Reinicia el flujo
      return res.redirect(303, `${redirectTo}?reason=${encodeURIComponent(reason)}`);
    });
  } catch {
    // Si algo falla, al menos intentamos redirigir
    return res.redirect(303, `${redirectTo}?reason=${encodeURIComponent(reason)}`);
  }
}

// Verificar existencia del tenant sin crear (en la DB de tenants)
async function tenantExists(uuidHex) {
  if (!uuidHex) return false;
  const schema = schemaNameFor(uuidHex);
  const client = await tenantsPool.connect();
  try {
    const q = await client.query(
      'SELECT 1 FROM information_schema.schemata WHERE schema_name=$1',
      [schema]
    );
    return q.rowCount > 0;
  } finally {
    client.release();
  }
}

// Intenta obtener el tenant por orden:
// 1) DB principal (app_user por email)
// 2) Authentik (attributes.tenant_uuid del usuario)
// 3) valor provisto en el request (si viene)
async function resolveExistingTenantUuid({ email, requestedTenantUuid }) {
  const normEmail = String(email).trim().toLowerCase();

  // 1) DB principal
  const dbRes = await pool.query(
    'SELECT tenant_uuid FROM app_user WHERE LOWER(email)=LOWER($1) LIMIT 1',
    [normEmail]
  );
  if (dbRes.rowCount) {
    const fromDb = cleanUuid(dbRes.rows[0].tenant_uuid);
    if (fromDb) return fromDb;
  }

  // 2) Authentik
  const akUser = await akFindUserByEmail(normEmail).catch(() => null);
  const fromAk = cleanUuid(akUser?.attributes?.tenant_uuid);
  if (fromAk) return fromAk;

  // 3) Pedido del request
  const fromReq = cleanUuid(requestedTenantUuid);
  if (fromReq) return fromReq;

  return null; // no hay tenant conocido
}

// Helper para crear tenant si falta
async function ensureTenant({ tenant_uuid }) {
  const admin = await tenantsPool.connect();
  try {
    await admin.query('BEGIN');

    // uuid y nombres
    const uuid = (tenant_uuid || crypto.randomUUID()).toLowerCase();
    const hex  = uuid.replace(/-/g, '');
    if (!/^[a-f0-9]{32}$/.test(hex)) throw new Error('tenant_uuid inválido');

    const schema = `schema_tenant_${hex}`;
    const role   = `tenant_${hex}`;
    const pwd    = crypto.randomBytes(18).toString('base64url');

    if (!VALID_IDENT.test(schema) || !VALID_IDENT.test(role)) {
      throw new Error('Identificador de schema/rol inválido');
    }

    // 1) Crear ROL si no existe  (PASSWORD debe ser LITERAL, no parámetro)
    const r = await admin.query('SELECT 1 FROM pg_roles WHERE rolname=$1', [role]);
    if (!r.rowCount) {
      await admin.query(`CREATE ROLE ${qi(role)} LOGIN PASSWORD ${qs(pwd)}`);
      // Si quisieras rotarla luego:
      // await admin.query(`ALTER ROLE ${qi(role)} PASSWORD ${qs(pwd)}`);
    }

    // 2) Crear SCHEMA si no existe y asignar owner
    const s = await admin.query(
      'SELECT 1 FROM information_schema.schemata WHERE schema_name=$1',
      [schema]
    );
    if (!s.rowCount) {
      await admin.query(`CREATE SCHEMA ${qi(schema)} AUTHORIZATION ${qi(role)}`);
    } else {
      await admin.query(`ALTER SCHEMA ${qi(schema)} OWNER TO ${qi(role)}`);
    }

    // 3) Permisos por defecto
    await admin.query(`GRANT USAGE ON SCHEMA ${qi(schema)} TO ${qi(role)}`);
    await admin.query(
      `ALTER DEFAULT PRIVILEGES IN SCHEMA ${qi(schema)}
         GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${qi(role)}`
    );
    await admin.query(
      `ALTER DEFAULT PRIVILEGES IN SCHEMA ${qi(schema)}
         GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO ${qi(role)}`
    );

    // 4) Aplicar 01_init.sql en la misma transacción
    const initSql = await loadInitSql();           // tu caché/loader actual
    if (initSql && initSql.trim()) {
      await admin.query(`SET LOCAL search_path TO ${qi(schema)}, public`);
      await admin.query(initSql);
    }

    await admin.query('COMMIT');
    return { tenant_uuid: uuid, schema, role, role_password: pwd };
  } catch (e) {
    try { await admin.query('ROLLBACK'); } catch {}
    throw e;
  } finally {
    admin.release();
  }
}

// async function ensureTenant({ tenant_uuid }) {
//   const client = await tenantsPool.connect();
//   try {
//     await client.query('BEGIN');

//     // Si no vino UUID, generamos uno
//     let uuid = (tenant_uuid || crypto.randomUUID()).toLowerCase();
//     const uuidNoHyphen = uuid.replace(/-/g, '');

//     const schema = `schema_tenant_${uuidNoHyphen}`;
//     const role   = `tenant_${uuidNoHyphen}`;
//     const pwd    = crypto.randomBytes(18).toString('base64url'); // password del rol

//     if (!VALID_IDENT.test(schema) || !VALID_IDENT.test(role)) {
//       throw new Error('Identificador de schema/rol inválido');
//     }

//     // 1) Crear ROL si no existe
//     const { rowCount: hasRole } = await client.query(
//       'SELECT 1 FROM pg_roles WHERE rolname=$1',
//       [role]
//     );
//     if (!hasRole) {
//       // Para el identificador usamos qi(); el password sí va parametrizado
//       await client.query(`CREATE ROLE ${qi(role)} LOGIN PASSWORD ${qs(pwd)}`);
//     }

//     // 2) Crear SCHEMA si no existe y asignar owner al rol del tenant
//     const { rowCount: hasSchema } = await client.query(
//       'SELECT 1 FROM information_schema.schemata WHERE schema_name=$1',
//       [schema]
//     );
//     if (!hasSchema) {
//       await client.query(`CREATE SCHEMA ${qi(schema)} AUTHORIZATION ${qi(role)}`);
//     }

//     // 3) Permisos mínimos para el rol del tenant en su schema
//     await client.query(`GRANT USAGE ON SCHEMA ${qi(schema)} TO ${qi(role)}`);
//     await client.query(
//       `ALTER DEFAULT PRIVILEGES IN SCHEMA ${qi(schema)}
//          GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${qi(role)}`
//     );
//     await client.query(
//       `ALTER DEFAULT PRIVILEGES IN SCHEMA ${qi(schema)}
//          GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO ${qi(role)}`
//     );

//     await client.query('COMMIT');

//     // 4) Inicialización del esquema con 01_init.sql (solo si está vacío)
//     try {
//       await initializeTenantSchemaIfEmpty(client, schema);
//     } catch (e) {
//       // Podés decidir si esto es fatal o "best-effort".
//       // Si querés cortar el alta cuando falla la init, usa: throw e;
//       console.warn(`[TENANT INIT] Falló inicialización de ${schema}:`, e?.message || e);
//     }

//     return { tenant_uuid: uuid, schema, role, role_password: pwd };
//   } catch (e) {
//     try { await client.query('ROLLBACK'); } catch {}
//     throw e;
//   } finally {
//     client.release();
//   }
// }

// Carga el 01_init.sql del disco, elimina BEGIN/COMMIT y sustituye el schema.

let _cachedInitSql = null;
async function loadInitSql() {
  if (_cachedInitSql !== null) return _cachedInitSql;
  const candidates = [
    process.env.TENANT_INIT_SQL,                                     // recomendado via .env
    path.resolve(__dirname, 'db', 'initTenant.sql'),
    path.resolve(__dirname, '..', 'src', 'db', 'initTenant.sql'),
  ].filter(Boolean);
  for (const p of candidates) {
    try {
      await fs.promises.access(p, fs.constants.R_OK);
      const txt = await readFile(p, 'utf8');
      _cachedInitSql = String(txt || '');
      console.log(`[TENANT INIT] initTenant.sql: ${p} (${_cachedInitSql.length} bytes)`);
      return _cachedInitSql;
    } catch {}
  }
  console.warn('[TENANT INIT] initTenant.sql no encontrado (se omitirá).');
  _cachedInitSql = '';
  return _cachedInitSql;
}

async function isSchemaEmpty(client, schema) {
  const { rows } = await client.query(
    `SELECT COUNT(*)::int AS c
       FROM information_schema.tables
      WHERE table_schema = $1`,
    [schema]
  );
  return rows[0].c === 0;
}

/** Ejecuta 01_init.sql para un tenant (solo si el esquema está vacío). */
async function initializeTenantSchemaIfEmpty(schema) {
  const sql = await loadInitSql();
  if (!sql || !sql.trim()) {
    console.warn(`[TENANT INIT] Esquema ${schema}: 01_init.sql vacío/no disponible. Salteando.`);
    return;
  }

  const client = await tenantsPool.connect();
  try {
    // No usamos LOCAL: queremos que el search_path persista en esta conexión mientras dura el script
    await client.query('BEGIN');
    await client.query(`SET search_path TO ${qi(schema)}, public`);

    const empty = await isSchemaEmpty(client, schema);
    if (!empty) {
      await client.query('ROLLBACK');
      console.log(`[TENANT INIT] Esquema ${schema}: ya tiene tablas. No se aplica 01_init.sql.`);
      return;
    }

    await client.query(sql); // acepta múltiples sentencias separadas por ';'
    await client.query('COMMIT');
    console.log(`[TENANT INIT] Esquema ${schema}: 01_init.sql aplicado.`);
  } catch (e) {
    try { await client.query('ROLLBACK'); } catch {}
    console.error(`[TENANT INIT] Error aplicando 01_init.sql sobre ${schema}:`, e.message);
    throw e;
  } finally {
    client.release();
  }
}


// -----------------------------------------------------------------------------
// Utilidades
// -----------------------------------------------------------------------------
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const RedisStore = connectRedis.default || connectRedis.RedisStore;

function requiredEnv(keys) {
  const missing = keys.filter((k) => !process.env[k]);
  if (missing.length) {
    console.warn(chalk.yellow(`⚠ Falta configurar variables de entorno: ${missing.join(', ')}`));
  }
}

function onFatal(err, msg = 'Error fatal') {
  console.error(chalk.red(`\n${msg}:`));
  console.error(err);
  process.exit(1);
}

function genTempPassword(len = 12) {
  const base = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@$%';
  let out = '';
  for (let i = 0; i < len; i++) out += base[Math.floor(Math.random() * base.length)];
  return out;
}

// -----------------------------------------------------------------------------
// Configuración Express
// -----------------------------------------------------------------------------
const app = express();
app.set('trust proxy', 1);
app.use(cors({ origin: true, credentials: true }));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Vistas EJS
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

// Archivos estáticos opcionales (ajusta si tu estructura difiere)
app.use(express.static(path.join(__dirname, 'public')));
app.use('/pages', express.static(path.join(__dirname, 'pages')));

// -----------------------------------------------------------------------------
// Sesión (Redis)
// -----------------------------------------------------------------------------
requiredEnv(['SESSION_SECRET', 'REDIS_URL']);
const redis = createRedisClient({ url: process.env.REDIS_URL || 'redis://sessions-redis:6379' });
await redis.connect().catch((e) => onFatal(e, 'No se pudo conectar a Redis (sesiones)'));

app.use(
  session({
    name: SESSION_COOKIE_NAME,
    store: new RedisStore({ client: redis, prefix: 'sess:' }),
    secret: process.env.SESSION_SECRET || 'change-me',
    resave: false,
    saveUninitialized: false,
    cookie: {
      httpOnly: true,
      sameSite: 'lax',
      secure: process.env.NODE_ENV === 'production',
    },
  })
);

// Exponer usuario a las vistas (no tocar req.session)
app.use((req, res, next) => {
  res.locals.user = req.session?.user || null;
  next();
});

// -----------------------------------------------------------------------------
// PostgreSQL — DB tenants (usuarios de suitecoffee)
// -----------------------------------------------------------------------------
const tenantsPool = new Pool({
  host: process.env.TENANTS_HOST || 'dev-tenants',
  port: Number(process.env.TENANTS_PORT || 5432),
  user: process.env.TENANTS_USER || 'dev-user-postgres',
  password: process.env.TENANTS_PASS || 'dev-pass-postgres',
  database: process.env.TENANTS_DB || 'dev-postgres',
  max: 10,
});

// -----------------------------------------------------------------------------
// PostgreSQL — DB principal (metadatos de negocio)
// -----------------------------------------------------------------------------
requiredEnv(['DB_HOST', 'DB_USER', 'DB_PASS', 'DB_NAME']);
const pool = new Pool({
  host: process.env.DB_HOST || 'dev-db',
  port: Number(process.env.DB_PORT || 5432),
  user: process.env.DB_USER || 'dev-user-suitecoffee',
  password: process.env.DB_PASS || 'dev-pass-suitecoffee',
  database: process.env.DB_NAME || 'dev-suitecoffee',
  max: 10,
  idleTimeoutMillis: 30_000,
});

async function verificarConexion() {
  try {
    const client = await pool.connect();
    const { rows } = await client.query('SELECT NOW() AS ahora');
    console.log(`\nConexión con ${chalk.green(process.env.DB_NAME)} OK. Hora DB:`, rows[0].ahora);
    client.release();
  } catch (error) {
    console.error('Error al conectar con la base de datos al iniciar:', error.message);
    console.error('Revisar DB_HOST/USER/PASS/NAME, accesos de red y firewall.');
  }
}

// -----------------------------------------------------------------------------
// OIDC (Authentik) — discovery + cliente
// -----------------------------------------------------------------------------
requiredEnv(['OIDC_ISSUER', 'OIDC_CLIENT_ID', 'OIDC_CLIENT_SECRET', 'OIDC_REDIRECT_URI']);


async function discoverOIDCWithRetry(issuerUrl, { retries = 30, delayMs = 2000 } = {}) {
  let lastErr;
  for (let i = 1; i <= retries; i++) {
    try {
      const issuer = await Issuer.discover(issuerUrl);
      console.log(`[OIDC] issuer OK en intento ${i}:`, issuer.metadata.issuer);
      return issuer;
    } catch (err) {
      lastErr = err;
      console.warn(`[OIDC] intento ${i}/${retries} falló: ${err.code || err.message}`);
      await sleep(delayMs);
    }
  }
  // No abortamos el proceso; dejamos el servidor vivo y seguimos reintentando en background
  throw lastErr;
}

let oidcClient;
(async () => {
  try {
    const issuer = await discoverOIDCWithRetry(process.env.OIDC_ISSUER, { retries: 60, delayMs: 2000 });
    oidcClient = new issuer.Client({
      client_id: process.env.OIDC_CLIENT_ID,
      client_secret: process.env.OIDC_CLIENT_SECRET,
      redirect_uris: [process.env.OIDC_REDIRECT_URI],
      response_types: ['code'],
    });
  } catch (e) {
    console.error('⚠ No se pudo inicializar OIDC aún. Seguirá reintentando cada 10s en background.');
    // reintento en background cada 10s sin tumbar el proceso
    (async function loop() {
      try {
        const issuer = await Issuer.discover(process.env.OIDC_ISSUER);
        oidcClient = new issuer.Client({
          client_id: process.env.OIDC_CLIENT_ID,
          client_secret: process.env.OIDC_CLIENT_SECRET,
          redirect_uris: [process.env.OIDC_REDIRECT_URI],
          response_types: ['code'],
        });
        console.log('[OIDC] inicializado correctamente en reintento tardío');
      } catch {
        setTimeout(loop, 10000);
      }
    })();
  }
})();

// -----------------------------------------------------------------------------
// Rutas OIDC
// -----------------------------------------------------------------------------
app.get('/auth/login', (req, res, next) => {
  try {

    if (req.session?.oidc) {
      return nukeSession(req, res, '/auth/login', 'stale_oidc');
    }

    const code_verifier  = generators.codeVerifier();
    const code_challenge = generators.codeChallenge(code_verifier);

    // Podés usar generators.state() y generators.nonce(); ambas son válidas
    const state = generators.state();                // crypto.randomBytes(24).toString('base64url') también sirve
    const nonce = generators.nonce();



    // Guardamos TODO dentro de un objeto para evitar claves sueltas
    req.session.oidc = { code_verifier, state, nonce };

    // Guardar sesión ANTES de redirigir
    req.session.save((err) => {
      if (err) return next(err);

      const url = oidcClient.authorizationUrl({
        scope: 'openid profile email offline_access',
        code_challenge,
        code_challenge_method: 'S256',
        state,
        nonce,
      });

      return res.redirect(url); // importantísimo: return
    });
  } catch (e) {
    return next(e);
  }
});

app.get('/auth/callback', async (req, res, next) => {
  try {
    // Log útil para debug
    console.log('[OIDC] cb sid=%s query=%j', req.sessionID, req.query);

    // Recuperar lo que guardamos en /auth/login
    const { oidc } = req.session || {};
    const code_verifier = oidc?.code_verifier;
    const stateStored   = oidc?.state;
    const nonceStored   = oidc?.nonce;

    // Si por algún motivo no está la info (sesión perdida/expirada), reiniciamos el flujo
    if (!code_verifier || !stateStored) {
      console.warn('[OIDC] Falta code_verifier/state en sesión; reiniciando login');
      return res.redirect(303, '/auth/login');
    }

    const params = oidcClient.callbackParams(req);

    // openid-client validará que el "state" recibido coincida con el que pasamos aquí
    let tokenSet;
    try {
      tokenSet = await oidcClient.callback(
        process.env.OIDC_REDIRECT_URI,
        params,
        { code_verifier, state: stateStored, nonce: nonceStored }
      );
    } catch (err) {
      console.warn('[OIDC] callback error, resetting session:', err?.message || err);
      return nukeSession(req, res, '/auth/login', 'callback_error');
    }

    // Limpiar datos OIDC de la sesión
    delete req.session.oidc;

    const claims = tokenSet.claims();
    const email = (claims.email || '').toLowerCase();

    // tenant desde claim, Authentik o fallback a tu DB
    let tenantHex = cleanUuid(claims.tenant_uuid);
    if (!tenantHex) {
      const akUser = await akFindUserByEmail(email).catch(() => null);
      tenantHex = cleanUuid(akUser?.attributes?.tenant_uuid);

      if (!tenantHex) {
        const q = await pool.query(
          'SELECT tenant_uuid FROM app_user WHERE LOWER(email)=LOWER($1) LIMIT 1',
          [email]
        );
        tenantHex = cleanUuid(q.rows?.[0]?.tenant_uuid);
      }
    }

    // Regenerar sesión para evitar fijación y guardar el usuario
    req.session.regenerate((err) => {
      if (err) {
        if (!res.headersSent) res.status(500).send('No se pudo crear la sesión.');
        return;
      }
      req.session.user = {
        sub: claims.sub,
        email,
        tenant_uuid: tenantHex || null,
      };
      req.session.save((e2) => {
        if (e2) {
          if (!res.headersSent) res.status(500).send('No se pudo guardar la sesión.');
          return;
        }
        if (!res.headersSent) return res.redirect('/'); // te llevará a /comandas si ya implementaste ese redirect
      });
    });

    return res.redirect('/');

  } catch (e) {
    console.error('[OIDC] callback error:', e);
    if (!res.headersSent) return next(e);
  }
});


app.post('/auth/logout', (req, res) => {
  req.session.destroy(() => {
    res.clearCookie(SESSION_COOKIE_NAME);
    res.status(204).end();
  });
});

app.get('/auth/me', (req, res) => {
  if (!req.session?.user) return res.status(401).json({ error: 'no-auth' });
  res.json({ user: req.session.user });
});

// -----------------------------------------------------------------------------
// Registro de usuario (DB principal + Authentik)
// -----------------------------------------------------------------------------

// Helpers defensivos (si ya los tenés, podés omitir estas definiciones)
const extractAkUserUuid = (u) =>
  (u && (u.uuid || u?.user?.uuid || (Array.isArray(u.results) && u.results[0]?.uuid))) || null;
const extractAkUserPk = (u) =>
  (u && (u.pk ?? u?.user?.pk ?? null));

async function akDeleteUser(pkOrUuid) {
  try {
    if (!pkOrUuid || !globalThis.fetch) return;
    const base = process.env.AUTHENTIK_BASE_URL?.replace(/\/+$/, '') || '';
    const id   = String(pkOrUuid);
    const url  = `${base}/api/v3/core/users/${encodeURIComponent(id)}/`;
    await fetch(url, {
      method: 'DELETE',
      headers: { 'Authorization': `Bearer ${process.env.AUTHENTIK_TOKEN}` }
    });
  } catch (e) {
    console.warn('[AK] No se pudo borrar usuario (compensación):', e?.message || e);
  }
}

// ==============================
// POST /api/users/register
// ==============================
app.post('/api/users/register', async (req, res, next) => {
  // 0) input
  const {
    email,
    display_name,
    role,
    tenant_uuid: requestedTenantUuid, // opcional
  } = req.body || {};

  const normEmail = String(email || '').trim().toLowerCase();
  if (!normEmail) return res.status(400).json({ error: 'email requerido' });

  // 1) Resolver tenant uuid (existente o nuevo)
  let tenantHex = null;
  try {
    if (typeof resolveExistingTenantUuid === 'function') {
      tenantHex = await resolveExistingTenantUuid({
        email: normEmail,
        requestedTenantUuid,
      });
    } else {
      tenantHex = cleanUuid(requestedTenantUuid);
    }

    // Crear/asegurar tenant en una transacción (ahí adentro corre 01_init.sql)
    if (tenantHex) {
      // si no existe, ensureTenant lo crea
      await ensureTenant({ tenant_uuid: tenantHex });
    } else {
      const created = await ensureTenant({ tenant_uuid: null }); // genera uuid
      tenantHex = cleanUuid(created?.tenant_uuid);
    }
  } catch (e) {
    return next(new Error(`No se pudo preparar el tenant: ${e.message}`));
  }

  // 2) Transacción DB principal + Authentik con compensación
  const client = await pool.connect();
  let createdAkUser = null; // para compensación
  try {
    await client.query('BEGIN');

    // Duplicados (ajusta a tu constraint real: por email o por (email,tenant))
    const dup = await client.query(
      'SELECT id FROM app_user WHERE LOWER(email)=LOWER($1)',
      [normEmail]
    );
    if (dup.rowCount) {
      await client.query('ROLLBACK');
      return res.status(409).json({
        error: 'user-exists',
        message: 'Ya existe un usuario con este email.',
        next: '/auth/login',
      });
    }

    // Authentik: buscar o crear
    let akUser = await akFindUserByEmail(normEmail).catch(() => null);
    if (!akUser) {
      akUser = await akCreateUser({
        email: normEmail,
        displayName: display_name || null,
        tenantUuid: tenantHex,                        // attributes.tenant_uuid
        addToGroupId: DEFAULT_GROUP_ID || null,
        isActive: true,
      });
      createdAkUser = akUser; // marcar que lo creamos nosotros
    }

    // Asegurar uuid/pk
    let akUserUuid = extractAkUserUuid(akUser);
    let akUserPk   = extractAkUserPk(akUser);
    if (!akUserUuid || akUserPk == null) {
      const ref = await akFindUserByEmail(normEmail).catch(() => null);
      akUserUuid = akUserUuid || extractAkUserUuid(ref);
      akUserPk   = akUserPk   ?? extractAkUserPk(ref);
    }
    if (!akUserUuid) throw new Error('No se pudo obtener uuid del usuario en Authentik');

    // Insert en tu DB principal
    const finalRole = role || 'owner';
    await client.query(
      `INSERT INTO app_user (email, display_name, tenant_uuid, ak_user_uuid, role)
       VALUES ($1,$2,$3,$4,$5)`,
      [normEmail, display_name || null, tenantHex, akUserUuid, finalRole]
    );

    await client.query('COMMIT');

    // 3) Marcar sesión para set-password (si usás este flujo)
    req.session.pendingPassword = {
      email: normEmail,
      ak_user_uuid: akUserUuid,
      ak_user_pk: akUserPk,
      exp: Date.now() + 10 * 60 * 1000,
    };

    return req.session.save(() => {
      const accept = String(req.headers['accept'] || '');
      if (accept.includes('text/html')) {
        return res.redirect(303, '/set-password');
      }
      return res.status(201).json({
        message: 'Usuario registrado',
        email: normEmail,
        tenant_uuid: tenantHex,
        role: finalRole,
        authentik_user_uuid: akUserUuid,
        next: '/set-password',
      });
    });
  } catch (err) {
    // Rollbacks + Compensaciones
    try { await client.query('ROLLBACK'); } catch {}
    try {
      // Si creamos el usuario en Authentik y luego falló algo → borrar
      if (createdAkUser) {
        const id = extractAkUserPk(createdAkUser) ?? extractAkUserUuid(createdAkUser);
        if (id) await akDeleteUser(id);
      }
    } catch {}
    return next(err);
  } finally {
    client.release();
  }
});



// Definir contraseña
app.post('/auth/password/set', async (req, res, next) => {
  try {
    const pp = req.session?.pendingPassword;
    if (!pp || (pp.exp && Date.now() > pp.exp)) {
      // token de sesión vencido o ausente
      if (!res.headersSent) return res.redirect(303, '/set-password');
      return;
    }

    const { password, password2 } = req.body || {};
    if (!password || password.length < 8 || password !== password2) {
      return res.status(400).send('Contraseña inválida o no coincide.');
    }

    // Buscar el usuario en Authentik y setear la clave
    const u = await akFindUserByEmail(pp.email);
    if (!u) return res.status(404).send('No se encontró el usuario en Authentik.');

    await akSetPassword(u.pk, password, true); // true = force change handled; ajusta a tu helper

    // Limpiar marcador y continuar al SSO
    delete req.session.pendingPassword;
    return req.session.save(() => res.redirect(303, '/auth/login'));
  } catch (e) {
    next(e);
  }
});


// Espera: { email, display_name?, tenant_uuid }
// app.post('/api/users/register', async (req, res, next) => {

//   const { email, display_name, tenant_uuid: rawTenant, role } = req.body || {};
//   if (!email) return res.status(400).json({ error: 'email es obligatorio' });
//   // Si no vino tenant: lo creamos
//   const { tenant_uuid, schema, role: dbRole } = await ensureTenant({ tenant_uuid: rawTenant });

//   const client = await pool.connect();
//   try {
//     await client.query('BEGIN');

//     // ¿ya existe en tu DB?
//     const { rows: dup } = await client.query(
//       'SELECT id FROM app_user WHERE email=$1 AND tenant_uuid=$2',
//       [email.toLowerCase(), tenant_uuid.replace(/-/g, '')]
//     );
//     if (dup.length) {
//       await client.query('ROLLBACK');
//       return res.status(409).json({ error: 'El usuario ya existe en SuiteCoffee' });
//     }

//     // Authentik: crear si no existe
//     let akUser = await akFindUserByEmail(email);
//     if (!akUser) {
//       akUser = await akCreateUser({
//         email,
//         displayName: display_name,
//         tenantUuid: tenant_uuid,                         // se normaliza dentro de ak.js
//         addToGroupId: DEFAULT_GROUP_ID || null,
//         isActive: true,
//       });
//       // Si querés forzar clave inicial (opcional; depende de tus políticas):
//       // await akSetPassword(akUser.pk, 'ClaveTemporal123!', true);
//     }

//     const _role = role || 'owner';
//     await client.query(
//       `INSERT INTO app_user (email, display_name, tenant_uuid, ak_user_uuid, role)
//        VALUES ($1,$2,$3,$4,$5)`,
//       [email.toLowerCase(), display_name || null, tenant_uuid.replace(/-/g, ''), akUser.uuid, _role]
//     );

//     await client.query('COMMIT');
//     return res.status(201).json({
//       message: 'Usuario registrado',
//       email,
//       tenant_uuid,
//       role: _role,
//       authentik_user_uuid: akUser.uuid,
//       next: '/auth/login'
//     });
//   } catch (err) {
//     try { await client.query('ROLLBACK'); } catch {}
//     next(err);
//   } finally {
//     client.release();
//   }
// });


// -----------------------------------------------------------------------------
// Healthcheck
// -----------------------------------------------------------------------------
app.get('/health', (_req, res) => res.status(200).json({ status: 'ok' }));

// -----------------------------------------------------------------------------
// 404 + Manejo de errores
// -----------------------------------------------------------------------------
app.use((req, res) => res.status(404).json({ error: 'Error 404, No se encontró la página', path: req.originalUrl }));

app.use((err, _req, res, _next) => {
  console.error('❌ Error:', err);
  if (res.headersSent) return;
  res.status(500).json({ error: '¡Oh! A ocurrido un error en el servidor auth.', detail: err.stack || String(err) });
});

// -----------------------------------------------------------------------------
// Arranque
// -----------------------------------------------------------------------------
const PORT = Number(process.env.PORT || 4040);

(async () => {
  const env = (process.env.NODE_ENV || 'development').toUpperCase();
  console.log(`Activando entorno de -> ${env === 'PRODUCTION' ? chalk.red('PRODUCTION') : chalk.green('DEVELOPMENT')}`);
  await verificarConexion();
  app.listen(PORT, () => {
    console.log(`Servidor de autenticación de SuiteCoffee corriendo en ${chalk.yellow(`http://localhost:${PORT}`)}`);
  });
})();
