import * as fs from "fs";
import * as mysql from "mysql2/promise";
import * as path from "path";
import { DataSource } from "typeorm";
import { CredentialsCryptoService } from "../src/control-plane/credentials-crypto.service";
import { Auditoria } from "../src/control-plane/entities/auditoria.entity";
import { Identidad, IdentidadEstado, RolPlataforma } from "../src/control-plane/entities/identidad.entity";
import { MigrationJob } from "../src/control-plane/entities/migration-job.entity";
import { MigrationJobStep } from "../src/control-plane/entities/migration-job-step.entity";
import { SchemaDeviation } from "../src/control-plane/entities/schema-deviation.entity";
import { TenantConfig } from "../src/control-plane/entities/tenant-config.entity";
import { MembershipEstado, TenantMembership } from "../src/control-plane/entities/tenant-membership.entity";
import { TenantMigrationLock } from "../src/control-plane/entities/tenant-migration-lock.entity";
import { Tenant, TenantEntorno, TenantEstado } from "../src/control-plane/entities/tenant.entity";

// Migra la identidad de UN tenant (taller_usuarios -> identidades +
// tenant_membership en mbinvtaller) y registra ese tenant en
// mbinvtaller.tenants con su credencial cifrada de verdad. Es la pieza que
// faltaba para poder loguearse con el login nuevo — sin esto,
// IdentityService.verifyCredentials() busca en una tabla vacia y CUALQUIER
// intento de login falla con "Credenciales invalidas", sin importar la
// contraseña.
//
// IDEMPOTENTE: cada INSERT de identidad/membership/tenant esta precedido por
// un SELECT de existencia (por email, por (identidad,tenant), por
// (host,port,db_name) respectivamente) — correrlo dos veces es un no-op la
// segunda vez.
//
// REVERSIBLE: `--revert` borra EXACTAMENTE lo que `--apply` creo (por email
// de las identidades migradas, y la fila de tenant por host+port+db_name),
// y reactiva taller1/taller2 si este script los desactivo. Nunca toca el
// esquema (eso son migraciones de TypeORM, no esto) — solo las filas que el
// creo.
//
// PRESERVA el hash bcrypt existente de taller_usuarios.password_hash — se
// copia tal cual a identidades.password_hash. Nadie tiene que restablecer su
// contraseña: el mismo bcrypt.compare() que ya usaba el login viejo sigue
// funcionando contra el mismo hash.
//
// Decisiones YA TOMADAS (no las vuelve a preguntar este script — estan
// hardcodeadas abajo, en EMAIL_OVERRIDES y USERNAMES_TO_DEACTIVATE):
//   - admin, mb, superadmin: sin correo real -> placeholder reservado
//     "{username}@sin-correo.invalid" (dominio .invalid, RFC 2606: nunca
//     resoluble, nunca se confunde con un correo real entregable). CAMBIO
//     DE DECISION explicito sobre la version anterior de este script (que
//     pedia un correo real para mb, pensando en mb como identidad de
//     plataforma): en ESTE despliegue mb es un usuario mas del tenant, sin
//     panel, sin via de plataforma, sin switch — no hay ninguna identidad
//     de plataforma real todavia. Ponerle un correo personal ahora la
//     ataria a algo que despues hay que separar.
//   - DEUDA anotada, no resuelta aca: cuando exista el panel y la identidad
//     de plataforma sea real, se crea una identidad NUEVA con correo real y
//     rolPlataforma=admin_plataforma, separada de esta — el "mb operativo
//     del tenant" y el "mb administrador de plataforma" son dos identidades
//     distintas, nunca la misma fila.
//   - taller1, taller2: comparten correoprueba@gmail.com (confirmado con un
//     SELECT real) — NO se migran, se desactivan en taller_usuarios
//     (activo=0) para que tampoco puedan usarse por la via vieja.
const EMAIL_OVERRIDES: Record<string, string> = {
  admin: "admin@sin-correo.invalid",
  mb: "mb@sin-correo.invalid",
  superadmin: "superadmin@sin-correo.invalid",
};

const USERNAMES_TO_DEACTIVATE = new Set(["taller1", "taller2"]);

// Codigo propuesto, no impuesto — es un varchar(64) unico en mbinvtaller.tenants,
// facil de cambiar despues con un UPDATE si no gusta. Nombre visible, tambien
// editable.
const TENANT_CODIGO = "autocentro-rodriguez";
const TENANT_NOMBRE = "Autocentro Rodriguez";

// Defensivo a proposito: dentro del contenedor (docker compose run
// backend-tools ...) no existe ningun archivo .env — las variables ya
// llegan inyectadas por docker-compose.yml directo al proceso. Sin este
// chequeo, correr esto en el contenedor tiraria ENOENT antes de leer ninguna
// variable real. Corriendo local (con Node, sin Docker) sigue leyendo el
// .env de la raiz del repo como siempre.
function loadRootEnv(): void {
  const envPath = path.join(__dirname, "..", "..", ".env");
  if (!fs.existsSync(envPath)) return;
  for (const line of fs.readFileSync(envPath, "utf8").split(/\r?\n/)) {
    const m = line.match(/^([A-Z0-9_]+)=(.*)$/);
    if (m && !process.env[m[1]]) process.env[m[1]] = m[2];
  }
}

function buildCryptoService(): CredentialsCryptoService {
  const fakeConfigService = {
    get: (key: string) => process.env[key],
  };
  const crypto = new CredentialsCryptoService(fakeConfigService as any);
  crypto.onModuleInit();
  return crypto;
}

function buildControlPlaneDataSource(): DataSource {
  return new DataSource({
    type: "mysql",
    host: process.env.CONTROL_PLANE_DB_HOST,
    port: parseInt(process.env.CONTROL_PLANE_DB_PORT || "3306", 10),
    username: process.env.CONTROL_PLANE_DB_USERNAME,
    password: process.env.CONTROL_PLANE_DB_PASSWORD,
    database: process.env.CONTROL_PLANE_DB_DATABASE,
    entities: [
      Tenant,
      Identidad,
      TenantMembership,
      TenantConfig,
      Auditoria,
      MigrationJob,
      MigrationJobStep,
      SchemaDeviation,
      TenantMigrationLock,
    ],
    synchronize: false,
  });
}

async function connectTenant(): Promise<mysql.Connection> {
  return 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: process.env.TENANT_CLI_DB_DATABASE,
  });
}

type LocalUsuario = {
  id: number;
  username: string;
  email: string | null;
  password_hash: string;
};

async function apply(): Promise<void> {
  loadRootEnv();
  const crypto = buildCryptoService();
  const ds = buildControlPlaneDataSource();
  await ds.initialize();

  const tenantConn = await connectTenant();

  try {
    // 1) Tenant — registrar (o confirmar que ya esta) ANTES de las
    // identidades: tenant_membership.tenant_id necesita esta fila.
    const tenantRepo = ds.getRepository(Tenant);
    let tenant = await tenantRepo.findOne({
      where: {
        dbHost: process.env.TENANT_CLI_DB_HOST,
        dbPort: Number(process.env.TENANT_CLI_DB_PORT),
        dbName: process.env.TENANT_CLI_DB_DATABASE,
      },
    });

    if (!tenant) {
      tenant = await tenantRepo.save(
        tenantRepo.create({
          codigo: TENANT_CODIGO,
          nombre: TENANT_NOMBRE,
          estado: TenantEstado.ACTIVO,
          entorno: TenantEntorno.PRODUCCION,
          dbHost: process.env.TENANT_CLI_DB_HOST,
          dbPort: Number(process.env.TENANT_CLI_DB_PORT),
          dbName: process.env.TENANT_CLI_DB_DATABASE,
          dbUser: process.env.TENANT_CLI_DB_USERNAME,
          dbPasswordEncrypted: crypto.encrypt(process.env.TENANT_CLI_DB_PASSWORD || ""),
        }),
      );
      console.log(`[migrate-identidades] tenant creado: id=${tenant.id} codigo=${tenant.codigo}`);
    } else {
      console.log(`[migrate-identidades] tenant ya existia: id=${tenant.id} codigo=${tenant.codigo}`);
    }

    // 2) Desactivar taller1/taller2 en el tenant — nunca se migran.
    for (const username of USERNAMES_TO_DEACTIVATE) {
      const [result] = await tenantConn.query<mysql.ResultSetHeader>(
        "UPDATE taller_usuarios SET activo = 0 WHERE username = ? AND activo = 1",
        [username],
      );
      console.log(
        `[migrate-identidades] ${username}: activo=0 (${(result as any).affectedRows} fila(s) — 0 si ya estaba desactivado)`,
      );
    }

    // 3) Identidades + membership — todos los usuarios locales ACTIVOS que
    // no esten en la lista de exclusion (ya desactivados arriba, asi que un
    // solo filtro por activo=1 alcanza).
    const [usuarios] = await tenantConn.query<mysql.RowDataPacket[]>(
      "SELECT id, username, email, password_hash FROM taller_usuarios WHERE activo = 1 ORDER BY id",
    );

    const identidadRepo = ds.getRepository(Identidad);
    const membershipRepo = ds.getRepository(TenantMembership);

    for (const row of usuarios as unknown as LocalUsuario[]) {
      const email = EMAIL_OVERRIDES[row.username] || row.email;
      if (!email) {
        throw new Error(
          `Usuario local '${row.username}' (id=${row.id}) no tiene email y no esta en EMAIL_OVERRIDES — ` +
            "decidir que correo le corresponde antes de correr esto (no hay un default seguro).",
        );
      }

      let identidad = await identidadRepo.findOne({ where: { email } });
      if (!identidad) {
        identidad = await identidadRepo.save(
          identidadRepo.create({
            email,
            nombre: row.username,
            passwordHash: row.password_hash,
            estado: IdentidadEstado.ACTIVA,
            rolPlataforma: RolPlataforma.NINGUNO,
          }),
        );
        console.log(
          `[migrate-identidades] identidad creada: ${row.username} -> ${email} (identidad.id=${identidad.id})`,
        );
      } else {
        console.log(
          `[migrate-identidades] identidad ya existia: ${row.username} -> ${email} (identidad.id=${identidad.id})`,
        );
      }

      const existingMembership = await membershipRepo.findOne({
        where: { identidadId: identidad.id, tenantId: tenant.id },
      });
      if (!existingMembership) {
        await membershipRepo.save(
          membershipRepo.create({
            identidadId: identidad.id,
            tenantId: tenant.id,
            usuarioLocalId: row.id,
            estado: MembershipEstado.ACTIVA,
            // Bootstrap de migracion, no una concesion real de un admin —
            // no hay "quien" real todavia, asi que la identidad se otorga
            // la membresia a si misma. Documentado, no un dato inventado:
            // el valor es real (la propia identidad), el SIGNIFICADO de
            // "otorgado por" es el que hay que leer con este contexto.
            otorgadoPorIdentidadId: identidad.id,
          }),
        );
        console.log(
          `[migrate-identidades] membership creada: ${row.username} (usuario_local_id=${row.id}) -> tenant ${tenant.codigo}`,
        );
      } else {
        console.log(`[migrate-identidades] membership ya existia: ${row.username} -> tenant ${tenant.codigo}`);
      }
    }
  } finally {
    await tenantConn.end();
    await ds.destroy();
  }
}

async function revert(): Promise<void> {
  loadRootEnv();
  const ds = buildControlPlaneDataSource();
  await ds.initialize();
  const tenantConn = await connectTenant();

  try {
    const tenantRepo = ds.getRepository(Tenant);
    const tenant = await tenantRepo.findOne({
      where: {
        dbHost: process.env.TENANT_CLI_DB_HOST,
        dbPort: Number(process.env.TENANT_CLI_DB_PORT),
        dbName: process.env.TENANT_CLI_DB_DATABASE,
      },
    });

    if (tenant) {
      await ds.getRepository(TenantMembership).delete({ tenantId: tenant.id });
      console.log(`[migrate-identidades] membership del tenant ${tenant.codigo} borrada`);

      const emails = Object.values(EMAIL_OVERRIDES).filter(Boolean);
      const [usuarios] = await tenantConn.query<mysql.RowDataPacket[]>(
        "SELECT email FROM taller_usuarios WHERE email IS NOT NULL",
      );
      const allEmails = [...emails, ...usuarios.map((u: any) => u.email)];

      for (const email of allEmails) {
        const identidad = await ds.getRepository(Identidad).findOne({ where: { email } });
        if (identidad) {
          await ds.getRepository(Identidad).delete({ id: identidad.id });
          console.log(`[migrate-identidades] identidad borrada: ${email}`);
        }
      }

      await tenantRepo.delete({ id: tenant.id });
      console.log(`[migrate-identidades] tenant ${tenant.codigo} borrado de mbinvtaller.tenants`);
    } else {
      console.log("[migrate-identidades] no habia tenant que borrar — nada que revertir de ese lado.");
    }

    for (const username of USERNAMES_TO_DEACTIVATE) {
      const [result] = await tenantConn.query<mysql.ResultSetHeader>(
        "UPDATE taller_usuarios SET activo = 1 WHERE username = ? AND activo = 0",
        [username],
      );
      console.log(
        `[migrate-identidades] ${username}: activo=1 restaurado (${(result as any).affectedRows} fila(s))`,
      );
    }
  } finally {
    await tenantConn.end();
    await ds.destroy();
  }
}

async function main(): Promise<void> {
  const mode = process.argv[2];
  if (mode === "--apply") {
    await apply();
  } else if (mode === "--revert") {
    await revert();
  } else {
    console.error("Uso: npx ts-node scripts/migrate-identidades.ts --apply|--revert");
    process.exit(1);
  }
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});
