// auth/src/index.js
import chalk from 'chalk';
import express from 'express';
import expressLayouts from 'express-ejs-layouts';
import cors from 'cors';
import { Pool } from 'pg';
import bcrypt from'bcrypt';
import crypto from 'node:crypto';

import session from 'express-session';
import { createClient } from 'redis';
import * as connectRedis from 'connect-redis';
const RedisStore = connectRedis.default || connectRedis.RedisStore;

const redis = createClient({ url: process.env.REDIS_URL || 'redis://authentik-redis:6379' });
await redis.connect();

import { Issuer, generators } from 'openid-client';
import cookieSession from 'cookie-session';

import { akFindUserByEmail, akCreateUser, akSetPassword } from './ak.js';


// Rutas
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Variables de Entorno
import dotenv, { config } from 'dotenv';

// Obtención de la ruta de la variable de entorno correspondiente a NODE_ENV
try {
  if (process.env.NODE_ENV === 'development') {
    dotenv.config({ path: path.resolve(__dirname, '../.env.development' )});
    console.log(`Activando entorno de ->${chalk.green(` DEVELOPMENT `)}`);
  } else if (process.env.NODE_ENV === 'stage') {
    dotenv.config({ path: path.resolve(__dirname, '../.env.test' )});
    console.log(`Activando entorno de ->${chalk.yellow(` TESTING `)}`);
  } else if (process.env.NODE_ENV === 'production') {
    dotenv.config({ path: path.resolve(__dirname, '../.env.production' )});
    console.log(`Activando entorno de ->${chalk.red(` PRODUCTION `)}`);
  }
} catch (error) {
  console.log("A ocurrido un error al seleccionar el entorno. \nError: " + error);
}

// Configuración de renderizado
const app = express();
app.use(cors());
app.use(express.json());
app.set('trust proxy', true);
app.use(express.static(path.join(__dirname, 'pages')));

/* 1) Motor de vistas apuntando a /auth/src/views */
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

/* 2) Estáticos si usás /css/main.css dentro de /auth/src/public */
app.use(express.static(path.join(__dirname, 'public')));

/* 3) Exponer user a las vistas (opcional, cómodo) */
app.use((req, res, next) => {
  res.locals.user = req.session?.user || null;
  next();
});


/* 4) Página de login (renderiza el EJS de arriba)
      - Mantén /auth/login para iniciar OIDC (redirección a Authentik)
      - Usa /login para mostrar la página con el botón */
app.get('/login', (req, res) => {
  res.render('login'); // -> /auth/src/views/login.ejs
});

app.use(session({
  name: 'sc.sid',
  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',
  },
}));

app.use(cookieSession({
  name: 'sid',
  secret: process.env.SESSION_SECRET,
  httpOnly: true,
  sameSite: 'lax',
  secure: false // en prod detrás de https: true
}));

// Configuración de conexión PostgreSQL

const poolMeta = {
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASS,
  database: process.env.DB_NAME,
  port: process.env.DB_LOCAL_PORT
};

const pool = new Pool(poolMeta);

const poolTenants = new Pool({ // apunta al servidor/base multi-tenant
  host: process.env.TENANTS_HOST,      // dev-tenants
  user: process.env.TENANTS_USER,
  password: process.env.TENANTS_PASS,
  database: process.env.TENANTS_DB,    // dev-postgres
  port: process.env.TENANTS_PORT,
});

const tenantsPool = new Pool({
  host: process.env.TENANTS_HOST,  // dev-tenants
  user: process.env.TENANTS_USER,
  password: process.env.TENANTS_PASS,
  database: process.env.TENANTS_DB, // dev-postgres
  port: process.env.TENANTS_PORT
});

async function verificarConexion() {
  try {
    const client = await pool.connect();
    const res = await client.query('SELECT NOW() AS hora');
    console.log(`\nConexión con la base de datos ${chalk.green(process.env.DB_NAME)} fue exitosa.`);
    console.log('Fecha y hora actual de la base de datos:', res.rows[0].hora);
    client.release(); // liberar el cliente de nuevo al pool
  } catch (error) {
    console.error('Error al conectar con la base de datos al iniciar:', error.message);
    console.error(`Troubleshooting:\n1. Compruebe que las bases de datos se iniciaron correctamente.\n2. Verifique las credenciales y puertos de acceso a la base de datos.\n3. Si está conectandose a una base de datos externa a localhost, verifique las reglas del firewal de entrada y salida de ambos dispositivos.`);
  }
}

// Descubrimiento OIDC (una sola vez)
let oidcClient;
(async () => {
  const issuer = await Issuer.discover(process.env.OIDC_ISSUER); // debe coincidir EXACTO
  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(err => {
  console.error('Error inicializando OIDC:', err);
  process.exit(1);
});


// util para resolver tenant si aún no usás claim tenant_uuid
async function lookupTenantByEmail(email) {
  const { rows } = await poolMeta.query(
    `SELECT tenant_uuid FROM app_user WHERE email = $1 ORDER BY id LIMIT 1`,
    [email.toLowerCase()]
  );
  return rows[0]?.tenant_uuid || null;
}

// === Servir páginas estáticas ===

app.get('/',(req, res) => res.sendFile(path.join(__dirname, 'pages', 'index.html')));

app.get('/planes', async (req, res) => {
  try {
    const { rows: [row] } = await pool.query(
      'SELECT api.get_planes_json($1) AS data;',
      [true]
    );
    res.type('application/json').send(row.data);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Error al cargar planes' });
  }
});

app.post('/api/users/register', async (req, res, next) => {
  const { email, display_name, tenant_uuid, role, password } = req.body;

  if (!email || !tenant_uuid) {
    return res.status(400).json({ error: 'email y tenant_uuid son obligatorios' });
  }

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

    // 0) idempotencia: si ya existe en tu DB, devolvés 409 o retornás el existente
    const { rows: existing } = await client.query(
      `SELECT id, email, ak_sub FROM app_user WHERE email = $1`, [email]
    );
    if (existing.length) {
      await client.query('ROLLBACK');
      return res.status(409).json({ error: 'El usuario ya existe en SuiteCoffee' });
    }

    // 1) crear/obtener usuario en Authentik
    let akUser = await akFindUserByEmail(email);
    if (!akUser) {
      akUser = await akCreateUser({
        email,
        displayName: display_name,
        tenantUuid: tenant_uuid.replace(/-/g, ''),
        addToGroupId: process.env.AUTHENTIK_DEFAULT_GROUP_ID || null,
      });
      // Si querés asignar una clave inicial (no recomendado en prod), descomentá:
      // if (password) await akSetPassword(akUser.pk, password, true);
    }

    // el 'sub' lo tendrás recién tras login OIDC; guardamos el uuid interno si te sirve
    const _role = role || 'owner';

    // 2) crear usuario local (sin password, dependemos del SSO)
    await client.query(
      `INSERT INTO app_user (email, display_name, tenant_uuid, ak_user_uuid, role)
       VALUES ($1, $2, $3, $4, $5)`,
      [email, 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' // redirigí a OIDC
    });

  } catch (err) {
    await client.query('ROLLBACK');
    next(err);
  } finally {
    client.release();
  }
});


// app.post('/api/registro', async (req, res) => {
//   const {
//     nombre_empresa, rut, correo, telefono, direccion, logo,
//     clave_acceso, plan_id
//   } = req.body;

//   const clientMeta = await poolMeta.connect();
//   const clientTen = await poolTenants.connect();

//   try {
//     await clientMeta.query('BEGIN');

//     // 1) Generar UUID sin guiones
//     const uuid = crypto.randomUUID().replace(/-/g, '');
//     const hash = await bcrypt.hash(clave_acceso, 10);

//     // 2) Provisionar en PG (dev-tenants/dev-postgres)
//     const { rows: [prov] } = await clientTen.query(
//       `SELECT public.f_tenant_provision($1::text, $2::text) AS data`,
//       [uuid, `tenant_${uuid}`]
//     );

//     const info = prov.data; // { tenant_uuid, schema, role, user, password, ... }

//     // 3) Guardar metadatos en suitecoffee_db (tu tabla 'tenant')
//     await clientMeta.query(`
//       INSERT INTO tenant (
//         uuid, nombre_empresa, rut, correo, telefono, direccion, logo,
//         clave_acceso, plan_id,
//         schema_name, role_name, user_name
//       ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)
//     `, [
//       uuid, nombre_empresa, rut, correo, telefono, direccion, logo,
//       hash, plan_id,
//       info.schema, info.role, info.user
//     ]);

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

//     // 4) Devolver credenciales del usuario de DB si las necesitás (mejor *no* persistir la password)
//     return res.status(201).json({
//       message: 'Tenant registrado correctamente',
//       uuid,
//       schema: info.schema,
//       db_user: info.user,
//       db_password: info.password  // muéstrala *una vez* y recomendación: NO guardarla
//     });

//   } catch (err) {
//     await clientMeta.query('ROLLBACK');
//     console.error(err);
//     return res.status(500).json({ error: 'Error al registrar tenant' });
//   } finally {
//     clientMeta.release();
//     clientTen.release();
//   }
// });

app.post('/api/login', async (req, res) => {
  const { correo, clave_acceso } = req.body;

  try {
    const client = await pool.connect();

    const result = await client.query(`
      SELECT uuid, clave_acceso, nombre_empresa, nombre_base_datos
      FROM tenant
      WHERE correo = $1 AND estado = true
    `, [correo]);

    client.release();

    if (result.rows.length === 0) {
      return res.status(401).json({ error: 'Correo no registrado o inactivo' });
    }

    const tenant = result.rows[0];
    const coincide = await bcrypt.compare(clave_acceso, tenant.clave_acceso);

    if (!coincide) {
      return res.status(401).json({ error: 'Clave incorrecta' });
    }

    return res.status(200).json({
      message: 'Login correcto',
      uuid: tenant.uuid,
      nombre_empresa: tenant.nombre_empresa,
      base_datos: tenant.nombre_base_datos
    });

  } catch (err) {
    console.error(err);
    return res.status(500).json({ error: 'Error al validar login' });
  }
});

app.get('/auth/login', (req, res) => {
  const code_verifier = generators.codeVerifier();
  const code_challenge = generators.codeChallenge(code_verifier);
  req.session.code_verifier = code_verifier;

  const url = oidcClient.authorizationUrl({
    scope: 'openid profile email offline_access tenant', // incluye tu scope custom “tenant”
    code_challenge: code_challenge,
    code_challenge_method: 'S256',
  });
  res.redirect(url);
});


// -------------  Middleware  ----------------
function getTenantUuid(req) {
  // Ejemplo 1: header
  if (req.headers['x-tenant-uuid']) return String(req.headers['x-tenant-uuid']);
  // Ejemplo 2: si más adelante usás JWT:
  // return req.user?.tenantUuid;
  throw new Error('Tenant no especificado');
}

async function withTenant(req, res, next) {
  const client = await tenantsPool.connect();
  try {
    await client.query('BEGIN');
    const uuid = getTenantUuid(req).replace(/-/g, '');
    const schema = `schema_tenant_${uuid}`;
    await client.query(`SELECT public.f_set_search_path($1)`, [schema]);

    // guardamos el cliente en req para reutilizar en los handlers
    req.pg = client;
    req.pgSchema = schema;
    next();
  } catch (e) {
    if (req.pg) await req.pg.query('ROLLBACK');
    if (req.pg) req.pg.release();
    return res.status(400).json({ error: e.message });
  }
}

// Al final de cada handler, hacé COMMIT y release
async function done(req, res, next) {
  try { if (req.pg) await req.pg.query('COMMIT'); }
  finally { if (req.pg) req.pg.release(); }
}




// --- login: redirige a Authentik con PKCE
app.get('/auth/login', async (req, res) => {
  const client = await getClient();
  const state = generators.state();
  const code_verifier = generators.codeVerifier();
  const code_challenge = generators.codeChallenge(code_verifier);

  req.session.state = state;
  req.session.code_verifier = code_verifier;

  const authUrl = client.authorizationUrl({
    scope: 'openid profile email',
    state,
    code_challenge,
    code_challenge_method: 'S256'
  });

  res.redirect(authUrl);
});

app.use((req,res,next)=>{ res.locals.user = req.session?.user || null; next(); });


// --- callback: intercambia code por tokens y guarda sesión mínima
// --- RUTA: callback (enlaza la sesión con tu usuario local)
app.get('/auth/callback', async (req, res, next) => {
  try {
    const params = oidcClient.callbackParams(req);
    const tokenSet = await oidcClient.callback(
      process.env.OIDC_REDIRECT_URI,
      params,
      { code_verifier: req.session.code_verifier }
    );
    const claims = tokenSet.claims(); // { sub, email, tenant_uuid?, ... }

    const email = (claims.email || '').toLowerCase();
    const sub   = claims.sub;
    const tenantUuid = (claims.tenant_uuid || await lookupTenantByEmail(email))?.replace(/-/g, '');

    if (!tenantUuid) {
      return res.status(403).send('No se pudo determinar el tenant del usuario.');
    }

    // Asegurar presencia del usuario en tu DB y enlazar el sub de OIDC
    const { rows } = await poolMeta.query(
      `SELECT id, ak_sub FROM app_user WHERE email=$1 AND tenant_uuid=$2`,
      [email, tenantUuid]
    );
    let userId;
    if (rows.length) {
      userId = rows[0].id;
      if (!rows[0].ak_sub) {
        await poolMeta.query(`UPDATE app_user SET ak_sub=$1 WHERE id=$2`, [sub, userId]);
      }
    } else {
      // “just in time” create (opcional): lo das de alta si no existe aún en tu app
      const ins = await poolMeta.query(
        `INSERT INTO app_user (email, tenant_uuid, ak_sub, role)
         VALUES ($1,$2,$3,'staff') RETURNING id`,
        [email, tenantUuid, sub]
      );
      userId = ins.rows[0].id;
    }

    // Sesión de aplicación (lo que el resto del backend necesita)
    req.session.user = { id: userId, email, tenant_uuid: tenantUuid, sub };

    req.session.regenerate(err => {
      if (err) return next(err);
      req.session.user = { id: userId, email, tenant_uuid: tenantUuid, sub };
      req.session.save(err2 => {
        if (err2) return next(err2);
        return res.redirect('/');
      });
    });

    // redirige a la app (home o dashboard)
    res.redirect('/');
  } catch (e) { next(e); }
});

// (Opcional) logout “local”
app.post('/auth/logout', (req, res) => {
  req.session.destroy(() => res.clearCookie('sc.sid').status(204).end());
});

// --- ver quién soy (para probar)
app.get('/auth/me', (req, res) => {
  if (!req.session?.user) return res.status(401).json({ error: 'no autenticado' });
  res.json({ user: req.session.user });
});

// --- logout simple (borra cookie)
app.post('/auth/logout', (req, res) => {
  req.session = null;
  res.status(204).end();
});

// Colores personalizados
let primaryColor = chalk.hex('#'+`${process.env.COL_PRI}`);
let secondaryColor = chalk.hex('#'+`${process.env.COL_SEC}`);
// let backgroundColor = chalk.hex('#'+`${process.env.COL_BG}`);


app.use(expressLayouts);
// Iniciar servidor
app.listen( process.env.PORT, () => {
  console.log(`Servidor de ${chalk.yellow('autenticación')} de ${secondaryColor('SuiteCoffee')} corriendo en ${chalk.yellow(`http://localhost:${process.env.PORT}\n`)}` );
  console.log(chalk.grey(`Comprobando accesibilidad a la db ${chalk.green(process.env.DB_NAME)} del host ${chalk.white(`${process.env.DB_HOST}`)} ...`));
  verificarConexion();
});

app.get("/health", async (req, res) => {
  // Podés chequear DB aquí. 200 = healthy; 503 = not ready.
  res.status(200).json({ status: "ok" });
});