import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common";
import * as path from "path";
import { DataSource } from "typeorm";
import { CredentialsCryptoService } from "../control-plane/credentials-crypto.service";
import { MigrationJobEstado } from "../control-plane/entities/migration-job.entity";
import { Tenant, TenantEstado } from "../control-plane/entities/tenant.entity";
import { TenantRecordsService } from "../control-plane/tenant-records.service";
import { listSourceMigrationClassNames } from "../migrations/verification/migration-source-list";

const LOCK_TTL_MS = 10 * 60 * 1000; // 10 minutos — una corrida real de 24 migraciones tarda segundos, no minutos; el margen es para no competir con un proceso que se cuelga.

export type ReconciliationResult = {
  jobId: number;
  estado: MigrationJobEstado;
  aplicadas: string[];
  falloEnMigracion: string | null;
  errorMensaje: string | null;
};

// Aplica el esquema pendiente de UN tenant explicito — el "Reconciliar" del
// flujo Preparar -> Verificar compatibilidad -> Reconciliar -> Activar.
// NUNCA toca datos existentes del ERP: corre exactamente las mismas clases
// de migracion de TypeORM que ya corren en cualquier otro despliegue, contra
// la base de ESE tenant, ni una linea de SQL distinta. Vive en
// control-plane-admin/ (no en tenancy/) porque esto es una operacion de
// PLATAFORMA sobre un tenant elegido por id — misma justificacion
// estructural que tenant-schema-check.service.ts.
@Injectable()
export class TenantReconciliationService {
  private readonly logger = new Logger(TenantReconciliationService.name);

  constructor(
    private readonly tenantRecords: TenantRecordsService,
    private readonly crypto: CredentialsCryptoService,
  ) {}

  async reconcile(tenantId: number, identidadId: number): Promise<ReconciliationResult> {
    const tenant = await this.tenantRecords.findById(tenantId);
    if (!tenant) {
      throw new NotFoundException(`Tenant ${tenantId} no encontrado`);
    }
    if (tenant.estado === TenantEstado.SUSPENDIDO) {
      throw new BadRequestException("No se puede reconciliar un tenant suspendido.");
    }

    const job = await this.tenantRecords.createMigrationJob({
      tenantId,
      iniciadoPorIdentidadId: identidadId,
      esSimulacion: false,
    });

    try {
      await this.tenantRecords.acquireMigrationLock(tenantId, job.id, LOCK_TTL_MS);
    } catch (error: any) {
      await this.tenantRecords.updateMigrationJob(job.id, {
        estado: MigrationJobEstado.INCOMPLETO,
        errorMensaje: error?.message,
      });
      throw new BadRequestException(error?.message || "No se pudo tomar el lock de reconciliación.");
    }

    const beforeNames = await this.readAppliedNames(tenant);
    let dataSource: DataSource | undefined;

    try {
      dataSource = await this.buildMigrationsDataSource(tenant);
      await dataSource.initialize();

      // "each": cada migracion en su propia transaccion donde el motor lo
      // permite — MySQL igual hace commit implicito en cada DDL, asi que
      // esto no da atomicidad real dentro de una migracion, pero SI asegura
      // que TypeORM registra en `migrations` cada una apenas termina, no
      // todas juntas al final — exactamente lo que hace falta para poder
      // reintentar desde el paso exacto donde fallo.
      const executed = await dataSource.runMigrations({ transaction: "each" });
      const aplicadas = executed.map((m) => m.name);

      for (const nombre of aplicadas) {
        await this.tenantRecords.addMigrationJobStep(job.id, nombre);
      }

      await this.tenantRecords.updateMigrationJob(job.id, { estado: MigrationJobEstado.COMPLETO });

      this.logger.log(
        `Reconciliacion completa para tenant ${tenantId}: ${aplicadas.length} migracion(es) aplicada(s).`,
      );

      return {
        jobId: job.id,
        estado: MigrationJobEstado.COMPLETO,
        aplicadas,
        falloEnMigracion: null,
        errorMensaje: null,
      };
    } catch (error: any) {
      // El DDL de MySQL no es transaccional entre migraciones: si esto
      // fallo en la migracion N, las 1..N-1 ya quedaron aplicadas de
      // verdad en la base del tenant. En vez de adivinar cual fallo por el
      // mensaje de error, se releen los nombres YA aplicados y se
      // comparan contra el estado ANTES de empezar — la diferencia son las
      // que si se alcanzaron a correr en este intento.
      const afterNames = await this.readAppliedNames(tenant).catch(() => beforeNames);
      const aplicadasEnEsteIntento = [...afterNames].filter((n) => !beforeNames.has(n));
      for (const nombre of aplicadasEnEsteIntento) {
        await this.tenantRecords.addMigrationJobStep(job.id, nombre);
      }

      const sourceNames = listSourceMigrationClassNames();
      const siguientePendiente = sourceNames.find((n) => !afterNames.has(n)) || null;

      await this.tenantRecords.updateMigrationJob(job.id, {
        estado: MigrationJobEstado.INCOMPLETO,
        falloEnMigracion: siguientePendiente,
        errorMensaje: error?.message || String(error),
      });

      this.logger.error(
        `Reconciliacion incompleta para tenant ${tenantId}: fallo en '${siguientePendiente}'. ${error?.message}`,
      );

      return {
        jobId: job.id,
        estado: MigrationJobEstado.INCOMPLETO,
        aplicadas: aplicadasEnEsteIntento,
        falloEnMigracion: siguientePendiente,
        errorMensaje: error?.message || String(error),
      };
    } finally {
      if (dataSource?.isInitialized) {
        await dataSource.destroy().catch(() => {});
      }
      await this.tenantRecords.releaseMigrationLock(tenantId);
    }
  }

  // "Forzar": re-corre el up() de CADA migracion, sin mirar la tabla
  // `migrations` del tenant para nada -- ni para decidir que saltar, ni antes
  // de empezar. Existe para el caso que reconcile() no puede resolver:
  // encontrado en produccion (Demo Ventas, incidente del 2026-09-09) un
  // tenant cuya tabla `migrations` ya tenia las 27 filas registradas pero
  // cuyo esquema real nunca recibio el DDL de una de ellas (la base se armo
  // copiando esa tabla de otro tenant) -- reconcile() decia "0 pendientes" y
  // no habia forma de arreglarlo desde el sistema sin esto.
  //
  // Seguro porque TODA migracion de este proyecto sigue el mismo patron:
  // verifica el estado real (columna/tabla/fila) antes de tocar nada, nunca
  // asume por la tabla `migrations` -- confirmado migracion por migracion
  // (columnas: `if (!table.findColumnByName(...))`; tablas: `if (!table)`;
  // hasta el unico DROP del proyecto, DropUnusedSyncEvents, verifica 0 filas
  // antes de borrar y ABORTA si no). Por eso re-ejecutar TODAS es un no-op
  // seguro para las que de verdad ya estan aplicadas -- nunca revierte ni
  // duplica nada.
  //
  // Un error puntual en una migracion (ej. una vieja que no siguiera el
  // patron) se atrapa y se reporta, pero NO aborta el resto del lote -- cada
  // migracion es independiente, el fallo de una no debe ocultar que las
  // demas si se repararon.
  async reconcileForzado(tenantId: number, identidadId: number): Promise<ReconciliationResult> {
    const tenant = await this.tenantRecords.findById(tenantId);
    if (!tenant) {
      throw new NotFoundException(`Tenant ${tenantId} no encontrado`);
    }
    if (tenant.estado === TenantEstado.SUSPENDIDO) {
      throw new BadRequestException("No se puede reconciliar un tenant suspendido.");
    }

    const job = await this.tenantRecords.createMigrationJob({
      tenantId,
      iniciadoPorIdentidadId: identidadId,
      esSimulacion: false,
    });

    try {
      await this.tenantRecords.acquireMigrationLock(tenantId, job.id, LOCK_TTL_MS);
    } catch (error: any) {
      await this.tenantRecords.updateMigrationJob(job.id, {
        estado: MigrationJobEstado.INCOMPLETO,
        errorMensaje: error?.message,
      });
      throw new BadRequestException(error?.message || "No se pudo tomar el lock de reconciliación.");
    }

    let dataSource: DataSource | undefined;
    const aplicadas: string[] = [];
    const errores: Array<{ nombre: string; error: string }> = [];

    try {
      dataSource = await this.buildMigrationsDataSource(tenant);
      await dataSource.initialize();
      const queryRunner = dataSource.createQueryRunner();
      await queryRunner.connect();

      // Orden por timestamp (sufijo numerico del nombre de clase), igual que
      // TypeORM ordena las pendientes en runMigrations() normal -- las que
      // crean tablas tienen que correr antes que las que les agregan
      // columnas.
      const instancias = [...dataSource.migrations].sort((a, b) => {
        const ta = Number(a.name?.match(/(\d+)$/)?.[1] ?? 0);
        const tb = Number(b.name?.match(/(\d+)$/)?.[1] ?? 0);
        return ta - tb;
      });

      for (const migracion of instancias) {
        const nombre = migracion.name || migracion.constructor.name;
        try {
          await migracion.up(queryRunner);
          aplicadas.push(nombre);
          await this.tenantRecords.addMigrationJobStep(job.id, nombre);
        } catch (error: any) {
          if (this.esErrorYaAplicado(error)) {
            // No todas las migraciones de este proyecto siguen el patron
            // "verificar antes de actuar" (encontrado en produccion,
            // Demo Ventas 2026-09-10: 6 migraciones viejas hacen ADD
            // INDEX/COLUMN a ciegas). Contra un tenant que ya las tiene, eso
            // tira "Duplicate key/column name" -- que es la confirmacion de
            // que la pieza SI esta aplicada, no un fallo real. Contarla como
            // error aca contradice literalmente el titulo de la seccion
            // ("verificada(s) sin error") y asusta al operador con un
            // mensaje rojo por algo que ya esta bien.
            aplicadas.push(nombre);
            await this.tenantRecords.addMigrationJobStep(job.id, nombre);
            continue;
          }
          errores.push({ nombre, error: error?.message || String(error) });
          this.logger.warn(
            `Reconciliacion forzada: '${nombre}' fallo para tenant ${tenantId} (puede ser real, o una migracion sin el patron de verificacion previa): ${error?.message}`,
          );
        }
      }

      await queryRunner.release();

      const estadoFinal = errores.length === 0 ? MigrationJobEstado.COMPLETO : MigrationJobEstado.INCOMPLETO;
      const errorMensaje =
        errores.length > 0 ? errores.map((e) => `${e.nombre}: ${e.error}`).join(" | ") : null;

      await this.tenantRecords.updateMigrationJob(job.id, { estado: estadoFinal, errorMensaje });

      this.logger.log(
        `Reconciliacion forzada completa para tenant ${tenantId}: ${aplicadas.length} migracion(es) re-ejecutada(s) sin error, ${errores.length} con error.`,
      );

      return {
        jobId: job.id,
        estado: estadoFinal,
        aplicadas,
        falloEnMigracion: errores[0]?.nombre ?? null,
        errorMensaje,
      };
    } finally {
      if (dataSource?.isInitialized) {
        await dataSource.destroy().catch(() => {});
      }
      await this.tenantRecords.releaseMigrationLock(tenantId);
    }
  }

  // Codigos reales de MySQL para "esto ya existe" -- ER_DUP_KEYNAME (indice
  // duplicado), ER_DUP_FIELDNAME (columna duplicada) y ER_TABLE_EXISTS_ERROR
  // (tabla duplicada). Confirmado contra el caso real de produccion: son
  // exactamente los que tiran las migraciones viejas sin el patron
  // "verificar antes de actuar" cuando se re-corren contra un tenant que ya
  // las tiene. Cualquier otro codigo (columna no encontrada, sintaxis,
  // conexion perdida, etc.) sigue contando como fallo real.
  private esErrorYaAplicado(error: any): boolean {
    return ["ER_DUP_KEYNAME", "ER_DUP_FIELDNAME", "ER_TABLE_EXISTS_ERROR"].includes(error?.code);
  }

  private async readAppliedNames(tenant: Tenant): Promise<Set<string>> {
    const mysql = await import("mysql2/promise");
    const password = this.crypto.decrypt(tenant.dbPasswordEncrypted);
    const conn = await mysql.createConnection({
      host: tenant.dbHost,
      port: tenant.dbPort,
      user: tenant.dbUser,
      password,
      database: tenant.dbName,
    });
    try {
      const [rows] = await conn.query("SELECT name FROM migrations");
      return new Set((rows as Array<{ name: string }>).map((r) => r.name));
    } catch (error: any) {
      if (error?.code === "ER_NO_SUCH_TABLE") {
        return new Set();
      }
      throw error;
    } finally {
      await conn.end();
    }
  }

  // DataSource DEDICADA, nunca la del pool de TenantConnectionRegistry: esa
  // esta ajustada para queries cortas de negocio (connectionLimit chico,
  // sin `migrations` configurado) — correr runMigrations() ahi mezclaria
  // una operacion de minutos con el pool que el resto de la app usa para
  // requests normales. Se crea, se usa, se destruye — nunca se cachea.
  private async buildMigrationsDataSource(tenant: Tenant): Promise<DataSource> {
    const password = this.crypto.decrypt(tenant.dbPasswordEncrypted);
    return new DataSource({
      type: "mysql",
      host: tenant.dbHost,
      port: tenant.dbPort,
      username: tenant.dbUser,
      password,
      database: tenant.dbName,
      migrations: [path.join(__dirname, "..", "migrations", "*{.ts,.js}")],
      synchronize: false,
    });
  }
}
