import * as mysql from "mysql2/promise";

// Corre ANTES de NestFactory.create() — a proposito, sin tocar Nest ni
// TypeORM. Si una variable obligatoria falta, ControlPlaneModule.forRootAsync
// (getOrThrow) o CredentialsCryptoService.onModuleInit (throw propio) recien
// fallarian DENTRO del bootstrap de Nest, con un stack trace que no dice que
// variable falta ni que valor poner. Este archivo existe para que el
// contenedor, si va a morir, muera ANTES de eso, con un mensaje que un
// operador pueda actuar sin leer el codigo.
type RequiredVar = { name: string; hint: string };

const REQUIRED_VARS: RequiredVar[] = [
  { name: "TENANT_CLI_DB_HOST", hint: "host de MySQL del tenant actual (ej. 132.226.40.48)" },
  { name: "TENANT_CLI_DB_PORT", hint: "puerto de MySQL del tenant actual (ej. 3310)" },
  { name: "TENANT_CLI_DB_DATABASE", hint: "nombre de la base del tenant actual (ej. mbinvautocentrorodriguez)" },
  { name: "TENANT_CLI_DB_USERNAME", hint: "usuario de MySQL con acceso a esa base" },
  { name: "TENANT_CLI_DB_PASSWORD", hint: "password de ese usuario" },
  { name: "CONTROL_PLANE_DB_HOST", hint: "host de MySQL del plano de control (mbinvtaller) — puede ser el mismo servidor y puerto que el tenant" },
  { name: "CONTROL_PLANE_DB_DATABASE", hint: "nombre de la base del plano de control (mbinvtaller)" },
  { name: "CONTROL_PLANE_DB_USERNAME", hint: "usuario de MySQL con acceso a mbinvtaller" },
  { name: "CONTROL_PLANE_DB_PASSWORD", hint: "password de ese usuario" },
  { name: "JWT_SECRET", hint: "secreto para firmar tokens — un string largo y aleatorio propio, NUNCA el valor de ejemplo del repo" },
];

function fail(lines: string[]): never {
  // eslint-disable-next-line no-console
  console.error(
    "\n================================================================\n" +
      "  ARRANQUE ABORTADO — falta configuracion, no es un bug de codigo.\n" +
      "================================================================\n" +
      lines.join("\n") +
      "\n================================================================\n",
  );
  process.exit(1);
}

// Mismo formato visual que fail() (imposible de confundir con un log
// normal de Nest) pero SIN process.exit — ver assertControlPlanePoblado()
// para la justificacion completa de por que esto avisa fuerte en vez de
// abortar.
function warnLoud(lines: string[]): void {
  // eslint-disable-next-line no-console
  console.warn(
    "\n================================================================\n" +
      "  AVISO DE ARRANQUE — el contenedor va a levantar IGUAL, pero leé esto.\n" +
      "================================================================\n" +
      lines.join("\n") +
      "\n================================================================\n",
  );
}

function assertRequiredEnvVars(): void {
  const missing = REQUIRED_VARS.filter((v) => !process.env[v.name]?.trim());

  const hasEncryptionKey =
    !!process.env.CONTROL_PLANE_CREDENTIALS_ENCRYPTION_KEY?.trim() ||
    !!process.env.CONTROL_PLANE_CREDENTIALS_ENCRYPTION_KEYS?.trim();

  if (missing.length === 0 && hasEncryptionKey) return;

  const lines: string[] = [];
  for (const v of missing) {
    lines.push(`  - ${v.name} — falta. ${v.hint}`);
  }
  if (!hasEncryptionKey) {
    lines.push(
      "  - CONTROL_PLANE_CREDENTIALS_ENCRYPTION_KEY — falta (o CONTROL_PLANE_CREDENTIALS_ENCRYPTION_KEYS, plural, " +
        "para rotacion). Un string propio de al menos 16 caracteres, generado una sola vez y guardado — sin ella, " +
        "las credenciales de tenant ya cifradas en mbinvtaller son irrecuperables para siempre.",
    );
  }
  fail(lines);
}

async function assertDatabaseReachableAndMigrated(params: {
  label: string;
  host: string;
  port: number;
  user: string;
  password: string;
  database: string;
  expectedMigrations: string[];
  howToMigrate: string;
}): Promise<void> {
  let conn: mysql.Connection;
  try {
    conn = await mysql.createConnection({
      host: params.host,
      port: params.port,
      user: params.user,
      password: params.password,
      database: params.database,
      connectTimeout: 5000,
    });
  } catch (error: any) {
    fail([
      `  - No se pudo conectar a ${params.label} (${params.host}:${params.port}/${params.database}).`,
      `    Error real: ${error?.message || error}`,
      "    Revisar host/puerto/usuario/password, y que el servidor MySQL acepte conexiones desde este contenedor.",
    ]);
    return;
  }

  try {
    const [rows] = await conn.query<mysql.RowDataPacket[]>(
      "SELECT name FROM migrations",
    );
    const applied = new Set(rows.map((r: any) => r.name));
    const missing = params.expectedMigrations.filter((m) => !applied.has(m));

    if (missing.length > 0) {
      fail([
        `  - ${params.label} (${params.database}) esta alcanzable pero NO tiene sus migraciones aplicadas.`,
        `    Faltan: ${missing.join(", ")}`,
        `    Correr: ${params.howToMigrate}`,
      ]);
    }
  } catch (error: any) {
    fail([
      `  - ${params.label} (${params.database}) esta alcanzable pero no tiene tabla 'migrations' (o algo` +
        ` mas fallo leyendola).`,
      `    Error real: ${error?.message || error}`,
      `    Correr: ${params.howToMigrate}`,
    ]);
  } finally {
    await conn.end();
  }
}

// El bug real que motiva esto: el 12/08 el contenedor arranco "healthy" con
// mbinvtaller alcanzable y migrado (esquema correcto, tablas vacias) — y
// nadie pudo loguearse, sin ninguna pista en los logs de POR QUE. El
// preflight de arriba (assertDatabaseReachableAndMigrated) prueba que el
// ESQUEMA esta bien, nunca que haya DATOS — un plano de control vacio pasa
// ese chequeo sin problema. Esto chequea lo que faltaba: que haya al menos
// un tenant y al menos una identidad, es decir, que alguien ya corrio
// scripts/migrate-identidades.ts --apply.
//
// ABORTA o AVISA — decision explicita, no el default de copiar el patron de
// arriba: un plano de control vacio rompe SOLO login/gestion de usuarios,
// nunca el resto de la app (health check, rutas publicas, lo que sea que no
// dependa de resolver un tenant) — a diferencia de una base inalcanzable o
// sin migrar, que rompe TODO. Y se arregla con un solo comando sin
// redesplegar nada. Abortar el arranque por esto cambiaria "una feature
// rota, corregible en un minuto" por "el sitio entero caido hasta que
// alguien redeploye" -- peor, no mejor. Por eso avisa fuerte (mismo formato
// visual que un fail(), imposible de confundir con un log normal) pero deja
// arrancar. Si mas adelante ALGUNA otra parte de la app tambien necesitara
// el plano de control poblado para funcionar, esa parte es la que debe
// fallar (fail-closed, como ya hace TenantConnectionRegistry), no este
// preflight generico.
async function warnIfControlPlaneVacio(): Promise<void> {
  const conn = await mysql.createConnection({
    host: process.env.CONTROL_PLANE_DB_HOST,
    port: Number(process.env.CONTROL_PLANE_DB_PORT || "3306"),
    user: process.env.CONTROL_PLANE_DB_USERNAME,
    password: process.env.CONTROL_PLANE_DB_PASSWORD,
    database: process.env.CONTROL_PLANE_DB_DATABASE,
    connectTimeout: 5000,
  });

  try {
    const [tenantRows] = await conn.query<mysql.RowDataPacket[]>(
      "SELECT COUNT(*) as c FROM tenants",
    );
    const [identidadRows] = await conn.query<mysql.RowDataPacket[]>(
      "SELECT COUNT(*) as c FROM identidades",
    );
    const tenantCount = Number((tenantRows[0] as any).c);
    const identidadCount = Number((identidadRows[0] as any).c);

    if (tenantCount === 0 || identidadCount === 0) {
      warnLoud([
        `  - El plano de control (${process.env.CONTROL_PLANE_DB_DATABASE}) esta migrado pero VACIO:` +
          ` ${tenantCount} tenant(s), ${identidadCount} identidad(es).`,
        "    NADIE va a poder iniciar sesion — el login busca la identidad ahi y no va a encontrar nada.",
        "    Esto NO aborta el arranque (a proposito — ver el comentario de warnIfControlPlaneVacio en preflight.ts).",
        "    Corregir con:",
        "      docker compose --profile tools run --rm backend-tools scripts/migrate-identidades.ts --apply",
        "    (o, sin Docker, 'cd backend && npx ts-node scripts/migrate-identidades.ts --apply')",
      ]);
      return;
    }

    // eslint-disable-next-line no-console
    console.log(
      `[preflight] Plano de control OK: ${tenantCount} tenant(s), ${identidadCount} identidad(es). El login deberia funcionar.`,
    );
  } finally {
    await conn.end();
  }
}

export async function runPreflightChecks(): Promise<void> {
  assertRequiredEnvVars();

  await assertDatabaseReachableAndMigrated({
    label: "el tenant actual (TENANT_CLI_DB_*)",
    host: process.env.TENANT_CLI_DB_HOST!,
    port: Number(process.env.TENANT_CLI_DB_PORT),
    user: process.env.TENANT_CLI_DB_USERNAME!,
    password: process.env.TENANT_CLI_DB_PASSWORD!,
    database: process.env.TENANT_CLI_DB_DATABASE!,
    // Las 23 migraciones REALES, leidas de la tabla `migrations` de la demo
    // (mbinvautocentrorodriguez) el dia de este preflight — no una lista de
    // memoria. Si esta lista queda desactualizada (una migracion nueva de
    // tenant), el preflight fallaria pidiendo correr migration:run —
    // correcto, no un falso positivo: significa que de verdad falta
    // aplicarla ahi.
    expectedMigrations: [
      "CreateTallerTables1738852000000",
      "CreateTallerClientes1739952000000",
      "CreateTallerProductos1740052000000",
      "AddUsersPermissionsVehicularCatalogs1740152000000",
      "AddTipoVehiculoToCatalogoVehicular1741252000000",
      "AddEstallerToTallerProductos1761000000000",
      "CreateOtEventos1761100000000",
      "CreateCotizacionLocks1761200000000",
      "AddPublicCotizacionFlow1761300000000",
      "AddTiendasScope1761400000000",
      "UseMaetieForTiendas1761500000000",
      "AddMechanicsCostAdvisor1761600000000",
      "AddTiendaPrintConfig1761700000000",
      "AddEsTallerEsProductoToMaeplu1761800000000",
      "CreateTallerUsuarios1740102000000",
      "ReconcileOtCosasTrae1770000000000",
      "ReconcileOtFotos1770100000000",
      "ReconcileTiposDanoColorDefault1770200000000",
      "ReconcileOtFirmasNombreLength1770300000000",
      "ReconcileOtDanosZona1770400000000",
      "ReconcileVehiculosTipoVehiculoFk1770500000000",
      "ReconcileCotizacionLineasPluLength1770600000000",
      "DropUnusedSyncEvents1770700000000",
    ],
    howToMigrate: "cd backend && npm run migration:run",
  });

  await assertDatabaseReachableAndMigrated({
    label: "el plano de control (CONTROL_PLANE_DB_*, mbinvtaller)",
    host: process.env.CONTROL_PLANE_DB_HOST!,
    port: Number(process.env.CONTROL_PLANE_DB_PORT || "3306"),
    user: process.env.CONTROL_PLANE_DB_USERNAME!,
    password: process.env.CONTROL_PLANE_DB_PASSWORD!,
    database: process.env.CONTROL_PLANE_DB_DATABASE!,
    expectedMigrations: [
      "CreateControlPlaneTables1761900000000",
      "AddTenantConnectionUniqueness1762000000000",
    ],
    howToMigrate: "cd backend && npm run migration:cp:run",
  });

  await warnIfControlPlaneVacio();
}
