import { Injectable } from "@nestjs/common";
import { ColumnMetadata } from "typeorm/metadata/ColumnMetadata";
import { listSourceMigrationClassNames } from "../migrations/verification/migration-source-list";
import { TenantConnectionRegistry } from "../tenancy/tenant-connection-registry.service";
import { DeviationSeveridad } from "../control-plane/entities/schema-deviation.entity";

export type ColumnDeviation = {
  tabla: string;
  columna: string;
  severidad: DeviationSeveridad;
  tipoEsperado: string;
  tipoReal: string;
  alterSugerido: string;
};

type ColumnFamily = "string" | "numeric" | "date" | "json" | "other";

// Familias, no nombres exactos — a proposito. Confirmado con datos reales
// (test_default_sentinel, taller_cotizacion_lineas.created_at): TypeORM
// reporta su propio tipo abstracto ("datetime") mientras que MySQL
// materializa @CreateDateColumn() como "timestamp" — son el MISMO campo,
// nunca una desviacion real. Comparar por nombre exacto marcaria como
// "desviacion" absolutamente todas las tablas de este proyecto.
function familyOfMysqlDataType(dataType: string): ColumnFamily {
  const t = dataType.toLowerCase();
  if (["varchar", "char", "text", "tinytext", "mediumtext", "longtext", "enum", "set"].includes(t)) {
    return "string";
  }
  if (["int", "bigint", "smallint", "mediumint", "tinyint", "decimal", "float", "double"].includes(t)) {
    return "numeric";
  }
  if (["date", "datetime", "timestamp", "time", "year"].includes(t)) {
    return "date";
  }
  if (t === "json") return "json";
  return "other";
}

// column.type es a veces un STRING ("decimal", "datetime" — cuando la
// entidad lo declara explicito) y a veces una FUNCION (String, Number,
// Boolean, Date — cuando TypeORM lo infiere del tipo de TypeScript).
// Confirmado leyendo entityMetadatas real, no adivinado: la mayoria de las
// columnas varchar/int/boolean de este proyecto llegan como funcion, no
// como string.
function familyOfEntityColumn(column: ColumnMetadata): ColumnFamily {
  if (column.type === String) return "string";
  // Number Y Boolean caen en la MISMA familia "numeric" a proposito: MySQL
  // no tiene un tipo BOOLEAN real, @Column() boolean se materializa como
  // tinyint(1) — indistinguible de un tinyint entero cualquiera mirando
  // solo information_schema.DATA_TYPE. Tratarlos como familias distintas
  // marcaria a TODAS las columnas boolean de este proyecto (es_servicio,
  // activo, etc. — decenas de ellas) como una desviacion GRAVE contra
  // cualquier tenant real. Bug real, encontrado corriendo esto contra
  // datos de verdad, no razonando sobre el codigo.
  if (column.type === Number || column.type === Boolean) return "numeric";
  if (column.type === Date) return "date";
  if (typeof column.type === "string") {
    return familyOfMysqlDataType(column.type);
  }
  return "other";
}

function expectedLabel(column: ColumnMetadata): string {
  const base =
    typeof column.type === "string"
      ? column.type
      : column.type === String
        ? "varchar"
        : column.type === Number
          ? "int"
          : column.type === Boolean
            ? "tinyint"
            : column.type === Date
              ? "datetime"
              : "desconocido";
  if (column.length) return `${base}(${column.length})`;
  if (column.precision != null) {
    return column.scale != null ? `${base}(${column.precision},${column.scale})` : `${base}(${column.precision})`;
  }
  return base;
}

function realLabel(row: {
  DATA_TYPE: string;
  CHARACTER_MAXIMUM_LENGTH: number | null;
  NUMERIC_PRECISION: number | null;
  NUMERIC_SCALE: number | null;
}): string {
  if (row.CHARACTER_MAXIMUM_LENGTH != null) return `${row.DATA_TYPE}(${row.CHARACTER_MAXIMUM_LENGTH})`;
  if (row.NUMERIC_PRECISION != null) {
    return row.NUMERIC_SCALE != null && row.NUMERIC_SCALE > 0
      ? `${row.DATA_TYPE}(${row.NUMERIC_PRECISION},${row.NUMERIC_SCALE})`
      : `${row.DATA_TYPE}(${row.NUMERIC_PRECISION})`;
  }
  return row.DATA_TYPE;
}

@Injectable()
export class TenantSchemaCheckService {
  constructor(private readonly tenantRegistry: TenantConnectionRegistry) {}

  // "Migraciones pendientes" para un tenant YA REGISTRADO — mismo criterio
  // que checkBaselineSync() en schema-divergence.spec.ts, ahora contra un
  // tenant cualquiera en vez de la base fija de pruebas. NUNCA compara
  // contra una lista escrita a mano: lee los archivos fuente reales.
  async getPendingMigrations(tenantId: number): Promise<string[]> {
    const sourceNames = new Set(listSourceMigrationClassNames());
    // allowOnboarding: este chequeo es EXACTAMENTE lo que corre antes de
    // activar un tenant — sin esto, TenantConnectionRegistry rechazaria la
    // conexion por estado != 'activo' y "Verificar compatibilidad" nunca
    // podria correr antes de "Activar" (bug real, encontrado corriendo esto
    // de verdad contra un tenant en onboarding, no razonando sobre el
    // codigo). Nunca habilita 'suspendido' — ver el comentario de
    // AcquireOptions en tenant-connection-registry.service.ts.
    // Una base RECIEN registrada (sin ninguna migracion corrida todavia, ni
    // siquiera la tabla `migrations` de TypeORM) no es un caso de error — es
    // el estado de partida normal de "Preparar", y significa "faltan TODAS".
    // Bug real, encontrado corriendo esto contra un tenant asi: el SELECT de
    // abajo tira ER_NO_SUCH_TABLE sin capturar, y como listTenantsResumen()
    // hace Promise.all sobre todos los tenants, UN tenant recien registrado
    // tumbaba el listado ENTERO con 500, no solo su propia fila — mismo
    // patron que ya se arreglo para fallos de conexion (testStoredConnection),
    // esta vez del lado del chequeo de esquema.
    let appliedNames: Array<{ name: string }>;
    try {
      appliedNames = await this.tenantRegistry.withTenantDataSourceExplicit(
        tenantId,
        (ds) => ds.query("SELECT name FROM migrations"),
        { allowOnboarding: true },
      );
    } catch (error: any) {
      if (error?.code === "ER_NO_SUCH_TABLE") {
        return [...sourceNames].sort();
      }
      throw error;
    }
    const applied = new Set(appliedNames.map((r) => r.name));
    return [...sourceNames].filter((n) => !applied.has(n)).sort();
  }

  // Desviaciones de COLUMNAS QUE YA EXISTEN — a proposito, no de tablas o
  // columnas faltantes: eso ya lo cubre getPendingMigrations(). Una columna
  // que no existe todavia no es una "desviacion", es una migracion sin
  // aplicar; mezclar los dos conceptos duplicaria el mismo hallazgo dos
  // veces con vocabulario distinto.
  async getColumnDeviations(tenantId: number): Promise<ColumnDeviation[]> {
    // allowOnboarding: mismo motivo que getPendingMigrations() arriba.
    return this.tenantRegistry.withTenantDataSourceExplicit(
      tenantId,
      async (ds) => {
        const realColumns = await ds.query(
          `SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE
           FROM information_schema.columns WHERE TABLE_SCHEMA = DATABASE()`,
        );
        const realByKey = new Map<string, any>();
        for (const row of realColumns) {
          realByKey.set(`${row.TABLE_NAME}.${row.COLUMN_NAME}`, row);
        }

        const deviations: ColumnDeviation[] = [];
        for (const meta of ds.entityMetadatas) {
          for (const column of meta.columns) {
            const real = realByKey.get(`${meta.tableName}.${column.databaseName}`);
            if (!real) continue; // falta del todo -> migracion pendiente, no desviacion

            const deviation = this.compareColumn(meta.tableName, column, real);
            if (deviation) deviations.push(deviation);
          }
        }
        return deviations;
      },
      { allowOnboarding: true },
    );
  }

  private compareColumn(
    tabla: string,
    column: ColumnMetadata,
    real: {
      COLUMN_NAME: string;
      DATA_TYPE: string;
      CHARACTER_MAXIMUM_LENGTH: number | null;
      NUMERIC_PRECISION: number | null;
      NUMERIC_SCALE: number | null;
    },
  ): ColumnDeviation | null {
    const familiaEsperada = familyOfEntityColumn(column);
    const familiaReal = familyOfMysqlDataType(real.DATA_TYPE);
    const tipoEsperado = expectedLabel(column);
    const tipoReal = realLabel(real);

    if (familiaEsperada !== familiaReal && familiaEsperada !== "other" && familiaReal !== "other") {
      return {
        tabla,
        columna: column.databaseName,
        severidad: DeviationSeveridad.GRAVE,
        tipoEsperado,
        tipoReal,
        alterSugerido: `ALTER TABLE \`${tabla}\` MODIFY COLUMN \`${column.databaseName}\` ${tipoEsperado};`,
      };
    }

    const longitudEsperada = column.length ? Number(column.length) : column.precision ?? null;
    const longitudReal = real.CHARACTER_MAXIMUM_LENGTH ?? real.NUMERIC_PRECISION ?? null;

    if (longitudEsperada != null && longitudReal != null && longitudReal !== longitudEsperada) {
      const severidad =
        longitudReal < longitudEsperada ? DeviationSeveridad.ADVERTENCIA : DeviationSeveridad.INFORMATIVO;
      return {
        tabla,
        columna: column.databaseName,
        severidad,
        tipoEsperado,
        tipoReal,
        alterSugerido: `ALTER TABLE \`${tabla}\` MODIFY COLUMN \`${column.databaseName}\` ${tipoEsperado};`,
      };
    }

    return null;
  }
}
