import * as mysql from "mysql2/promise";
import { loadRootEnv } from "../../src/migrations/verification/schema-snapshot";
import { ALLOWED_ISOLATION_TEST_DATABASES } from "../e2e-helpers";

// Carga las filas centinela minimas que tenant-isolation-required.e2e-spec.ts
// necesita para correr: un usuario local en cada base (para tenant_membership
// y para VARIANTE 5), un cliente distinguible por base (VARIANTE 5 compara
// datos de A contra B), un vehiculo y un mecanico. Nunca a mano, nunca
// pegado en el chat — un script versionado que se puede volver a correr
// tantas veces como haga falta (cada limpieza + remigracion de las bases
// sentinela deja estas filas en cero otra vez).
//
// Vive en test/fixtures/, DELIBERADAMENTE fuera de src/migrations/ — el glob
// del CLI (typeorm.config.ts, "migrations/*{.ts,.js}") cargaria esto como si
// fuera una migracion real (ver migrations-shape.spec.ts, la guardia que
// existe justamente para este tipo de error).
//
// IDEMPOTENTE: cada INSERT esta precedido por un SELECT de existencia por la
// clave de negocio (tarjeta/username/placa/el par vehiculo+cliente) — nunca
// un INSERT con manejo de duplicado. Correrlo dos veces seguidas es un no-op
// la segunda vez.
//
// Solo contra la lista blanca fija (ALLOWED_ISOLATION_TEST_DATABASES,
// e2e-helpers.ts) — mismo criterio que el resto del aislamiento e2e: aborta
// si SELECT DATABASE() no devuelve una de las tres bases descartables.
//
// Sin LAST_INSERT_ID() ni el insertId que devuelve el driver: el id del
// vehiculo (que hace falta para el join en taller_clientes_vehiculos) se
// obtiene con un SELECT explicito por placa DESPUES de asegurar que la fila
// existe — funciona igual si la fila se inserto recien o si ya estaba de una
// corrida anterior.
const DEFAULT_SENTINEL_USERNAME = "zzz_centinela_default";

const TENANT_FIXTURES: Array<{ db: string; prefix: string }> = [
  { db: "test_tenant_a_aislamiento", prefix: "ZZZTNA" },
  { db: "test_tenant_b_aislamiento", prefix: "ZZZTNB" },
];

function keysFor(prefix: string) {
  return {
    tarjeta: `${prefix}-0001`,
    placa: `${prefix}-001`,
    username: `zzz_${prefix.toLowerCase()}_usuario`,
    mecanicoNombre: `${prefix} Mecanico Centinela`,
    // Nombres DELIBERADAMENTE distintos del seed real (seedCatalogos() en
    // catalogos.service.ts: Carro/Moto/Camion/Pickup/SUV,
    // Radio/Gata/Herramientas/Llanta de repuesto/Triangulos/Extintor,
    // Golpe/Rayon/Quebrado) — si coincidieran, un dato centinela de A no se
    // podria distinguir de uno sembrado de verdad, ni de uno de B.
    tipoVehiculoNombre: `${prefix} Tipo Vehiculo Centinela`,
    tipoDanoNombre: `${prefix} Tipo Dano Centinela`,
    cosaTraeNombre: `${prefix} Cosa Trae Centinela`,
  };
}

// No es un hash real (nunca se usa para login en estas filas) — solo tiene
// que satisfacer NOT NULL varchar(255) sin quedar vacio.
const PLACEHOLDER_PASSWORD_HASH = "$2b$10$" + "z".repeat(53);

async function connectTo(database: string): Promise<mysql.Connection> {
  const conn = await mysql.createConnection({
    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,
  });
  const [rows] = await conn.query<mysql.RowDataPacket[]>("SELECT DATABASE() as db");
  const actual = (rows[0] as any)?.db;
  if (!ALLOWED_ISOLATION_TEST_DATABASES.includes(actual)) {
    await conn.end();
    throw new Error(
      `[seed-sentinel] La conexion apunta a '${actual}', fuera de la lista blanca fija ` +
        `(${ALLOWED_ISOLATION_TEST_DATABASES.join(", ")}). ABORTA — no se toca nada.`,
    );
  }
  return conn;
}

async function ensureRow(
  conn: mysql.Connection,
  label: string,
  existsSql: string,
  existsParams: unknown[],
  insertSql: string,
  insertParams: unknown[],
): Promise<void> {
  const [rows] = await conn.query<mysql.RowDataPacket[]>(existsSql, existsParams);
  if (rows.length > 0) {
    return; // ya existe — no-op, esto es lo que hace la corrida idempotente
  }
  await conn.query(insertSql, insertParams);
}

async function seedDefaultSentinel(): Promise<void> {
  const conn = await connectTo("test_default_sentinel");
  try {
    await ensureRow(
      conn,
      "usuario centinela default",
      "SELECT id FROM taller_usuarios WHERE username = ?",
      [DEFAULT_SENTINEL_USERNAME],
      `INSERT INTO taller_usuarios (username, email, password_hash, nombre, rol, activo)
       VALUES (?, ?, ?, ?, 'operativo', 1)`,
      [
        DEFAULT_SENTINEL_USERNAME,
        `${DEFAULT_SENTINEL_USERNAME}@example.invalid`,
        PLACEHOLDER_PASSWORD_HASH,
        "ZZZ Centinela Default",
      ],
    );
  } finally {
    await conn.end();
  }
}

async function seedTenant(db: string, prefix: string): Promise<void> {
  const {
    tarjeta,
    placa,
    username,
    mecanicoNombre,
    tipoVehiculoNombre,
    tipoDanoNombre,
    cosaTraeNombre,
  } = keysFor(prefix);
  const conn = await connectTo(db);
  try {
    // Catalogos (tipos_vehiculo, tipos_dano, cosas_trae): todavia sin
    // aserciones de VARIANTE 5 en el e2e — solo los datos, listos para
    // cuando ots/catalogos se conviertan y haga falta comparar A contra B
    // ahi tambien.
    await ensureRow(
      conn,
      `taller_tipos_vehiculo ${tipoVehiculoNombre}`,
      "SELECT id FROM taller_tipos_vehiculo WHERE nombre = ?",
      [tipoVehiculoNombre],
      "INSERT INTO taller_tipos_vehiculo (nombre, plantilla_danos) VALUES (?, 'carro')",
      [tipoVehiculoNombre],
    );

    await ensureRow(
      conn,
      `taller_tipos_dano ${tipoDanoNombre}`,
      "SELECT id FROM taller_tipos_dano WHERE nombre = ?",
      [tipoDanoNombre],
      "INSERT INTO taller_tipos_dano (nombre, icono, color) VALUES (?, 'centinela', '#000000')",
      [tipoDanoNombre],
    );

    await ensureRow(
      conn,
      `taller_cosas_trae ${cosaTraeNombre}`,
      "SELECT id FROM taller_cosas_trae WHERE nombre = ?",
      [cosaTraeNombre],
      "INSERT INTO taller_cosas_trae (nombre) VALUES (?)",
      [cosaTraeNombre],
    );

    await ensureRow(
      conn,
      `maecli ${tarjeta}`,
      "SELECT id FROM maecli WHERE tarjeta = ?",
      [tarjeta],
      "INSERT INTO maecli (tarjeta, nombre, direccion, nit) VALUES (?, ?, ?, ?)",
      [tarjeta, `${prefix} Cliente Centinela`, `${prefix} Direccion Centinela`, `${prefix}-NIT`],
    );

    await ensureRow(
      conn,
      `taller_clientes ${tarjeta}`,
      "SELECT tarjeta FROM taller_clientes WHERE tarjeta = ?",
      [tarjeta],
      "INSERT INTO taller_clientes (tarjeta, nombre, direccion, nit) VALUES (?, ?, ?, ?)",
      [tarjeta, `${prefix} Cliente Centinela`, `${prefix} Direccion Centinela`, `${prefix}-NIT`],
    );

    await ensureRow(
      conn,
      `taller_usuarios ${username}`,
      "SELECT id FROM taller_usuarios WHERE username = ?",
      [username],
      `INSERT INTO taller_usuarios (username, email, password_hash, nombre, rol, activo)
       VALUES (?, ?, ?, ?, 'operativo', 1)`,
      [username, `${username}@example.invalid`, PLACEHOLDER_PASSWORD_HASH, `${prefix} Usuario Centinela`],
    );

    await ensureRow(
      conn,
      `taller_vehiculos ${placa}`,
      "SELECT id FROM taller_vehiculos WHERE placa = ?",
      [placa],
      // tipo_vehiculo_id se deja NULL a proposito: taller_tipos_vehiculo esta
      // vacio en un tenant nuevo (seedCatalogos todavia no corre en estas
      // bases, ver diagnostico de la ronda anterior) — la columna es
      // nullable, asi que no hace falta un catalogo para este fixture.
      "INSERT INTO taller_vehiculos (placa, marca, modelo, cliente_id) VALUES (?, ?, ?, ?)",
      [placa, `${prefix} Marca`, `${prefix} Modelo`, tarjeta],
    );

    const [vehRows] = await conn.query<mysql.RowDataPacket[]>(
      "SELECT id FROM taller_vehiculos WHERE placa = ?",
      [placa],
    );
    const vehiculoId = (vehRows[0] as any).id;

    await ensureRow(
      conn,
      `taller_clientes_vehiculos ${placa}/${tarjeta}`,
      "SELECT id FROM taller_clientes_vehiculos WHERE vehiculo_id = ? AND cliente_tarjeta = ?",
      [vehiculoId, tarjeta],
      "INSERT INTO taller_clientes_vehiculos (vehiculo_id, cliente_tarjeta) VALUES (?, ?)",
      [vehiculoId, tarjeta],
    );

    await ensureRow(
      conn,
      `taller_mecanicos ${mecanicoNombre}`,
      "SELECT id FROM taller_mecanicos WHERE nombre = ?",
      [mecanicoNombre],
      "INSERT INTO taller_mecanicos (nombre, activo) VALUES (?, 1)",
      [mecanicoNombre],
    );
  } finally {
    await conn.end();
  }
}

// Verifica lo que de verdad quedo en la base, nunca asume que el INSERT (o
// el no-op de ensureRow) salio bien.
async function reportCounts(): Promise<void> {
  const targets: Array<{ db: string; tables: string[] }> = [
    { db: "test_default_sentinel", tables: ["taller_usuarios"] },
    ...TENANT_FIXTURES.map(({ db }) => ({
      db,
      tables: [
        "maecli",
        "taller_clientes",
        "taller_usuarios",
        "taller_vehiculos",
        "taller_clientes_vehiculos",
        "taller_mecanicos",
        "taller_tipos_vehiculo",
        "taller_tipos_dano",
        "taller_cosas_trae",
      ],
    })),
  ];

  for (const { db, tables } of targets) {
    const conn = await connectTo(db);
    try {
      for (const table of tables) {
        const [rows] = await conn.query<mysql.RowDataPacket[]>(
          `SELECT COUNT(*) as c FROM ${table}`,
        );
        // eslint-disable-next-line no-console
        console.log(`[seed-sentinel] ${db}.${table} = ${(rows[0] as any).c}`);
      }
    } finally {
      await conn.end();
    }
  }
}

// Precondicion para el e2e de aislamiento: en vez de dejar que falle mas
// adelante con un error generico ("no tiene ningun usuario en
// taller_usuarios"), chequea de entrada las filas centinela exactas que este
// archivo crea y, si falta alguna, aborta con un mensaje que dice
// exactamente que correr.
export async function assertSentinelFixturesSeeded(): Promise<void> {
  loadRootEnv();
  const checks: Array<{ db: string; label: string; sql: string; params: unknown[] }> = [
    {
      db: "test_default_sentinel",
      label: "usuario centinela default",
      sql: "SELECT id FROM taller_usuarios WHERE username = ?",
      params: [DEFAULT_SENTINEL_USERNAME],
    },
  ];

  for (const { db, prefix } of TENANT_FIXTURES) {
    const { tarjeta, placa, username } = keysFor(prefix);
    checks.push(
      { db, label: `maecli ${tarjeta}`, sql: "SELECT id FROM maecli WHERE tarjeta = ?", params: [tarjeta] },
      {
        db,
        label: `taller_clientes ${tarjeta}`,
        sql: "SELECT tarjeta FROM taller_clientes WHERE tarjeta = ?",
        params: [tarjeta],
      },
      {
        db,
        label: `taller_usuarios ${username}`,
        sql: "SELECT id FROM taller_usuarios WHERE username = ?",
        params: [username],
      },
      {
        db,
        label: `taller_vehiculos ${placa}`,
        sql: "SELECT id FROM taller_vehiculos WHERE placa = ?",
        params: [placa],
      },
    );
  }

  const missing: string[] = [];
  for (const check of checks) {
    const conn = await connectTo(check.db);
    try {
      const [rows] = await conn.query<mysql.RowDataPacket[]>(check.sql, check.params);
      if (rows.length === 0) missing.push(`${check.db}: ${check.label}`);
    } finally {
      await conn.end();
    }
  }

  if (missing.length > 0) {
    throw new Error(
      `Faltan fixtures centinela — ${missing.length} fila(s) ausente(s):\n` +
        missing.map((m) => `  - ${m}`).join("\n") +
        "\n\nCorré 'npx ts-node -r tsconfig-paths/register test/fixtures/seed-sentinel-databases.ts' " +
        "antes de este e2e.",
    );
  }
}

async function main(): Promise<void> {
  loadRootEnv();
  await seedDefaultSentinel();
  for (const { db, prefix } of TENANT_FIXTURES) {
    await seedTenant(db, prefix);
  }
  await reportCounts();
}

if (require.main === module) {
  main().catch((e) => {
    // eslint-disable-next-line no-console
    console.error(e);
    process.exit(1);
  });
}
