import { BadRequestException, ConflictException, Injectable, NotFoundException } from "@nestjs/common";
import * as bcrypt from "bcryptjs";
import * as mysql from "mysql2/promise";
import { Usuario, UserRole } from "../auth/entities/usuario.entity";
import { CredentialsCryptoService } from "../control-plane/credentials-crypto.service";
import { DeviationSeveridad, SchemaDeviation } from "../control-plane/entities/schema-deviation.entity";
import { Tenant, TenantEstado } from "../control-plane/entities/tenant.entity";
import { IdentityService } from "../control-plane/identity.service";
import { TenantRecordsService } from "../control-plane/tenant-records.service";
import { TallerParametroClave } from "../taller-parametros/taller-parametros.catalogo";
import { TallerParametrosService } from "../taller-parametros/taller-parametros.service";
import { TenantConnectionRegistry } from "../tenancy/tenant-connection-registry.service";
import { listSourceMigrationClassNames } from "../migrations/verification/migration-source-list";
import { OmitDeviationDto } from "./dto/omit-deviation.dto";
import { ProvisionarAccesoDto } from "./dto/provisionar-acceso.dto";
import { RegisterTenantDto } from "./dto/register-tenant.dto";
import { TestConnectionDto } from "./dto/test-connection.dto";
import { UpdateTogglesOperativosDto } from "./dto/update-toggles-operativos.dto";
import { ReconciliationResult, TenantReconciliationService } from "./tenant-reconciliation.service";
import { ColumnDeviation, TenantSchemaCheckService } from "./tenant-schema-check.service";

export type TenantResumen = {
  id: number;
  codigo: string;
  nombre: string;
  estado: string;
  entorno: string;
  dbHost: string;
  dbPort: number;
  dbName: string;
  migracionesPendientes: string[];
  desviaciones: {
    grave: number;
    advertencia: number;
    informativo: number;
  };
  acceso: { ok: true } | { ok: false; error: string };
};

@Injectable()
export class TenantsAdminService {
  constructor(
    // Nunca se inyecta un repositorio de Tenant/SchemaDeviation directo
    // aca — eso seria la misma ofensa que auth/ inyectando Identidad fuera
    // de contexto. TenantRecordsService vive DENTRO de control-plane/ (ya
    // en la lista blanca del escaner); este servicio, en
    // control-plane-admin/, lo consume en vez de tocar los repositorios el
    // mismo.
    private readonly tenantRecords: TenantRecordsService,
    private readonly crypto: CredentialsCryptoService,
    private readonly schemaCheck: TenantSchemaCheckService,
    private readonly identityService: IdentityService,
    private readonly tenantRegistry: TenantConnectionRegistry,
    private readonly reconciliationService: TenantReconciliationService,
    private readonly tallerParametrosService: TallerParametrosService,
  ) {}

  // Sin persistir nada — a proposito (ver diseño "Preparar"): un typo en el
  // formulario no debe dejar una fila de Tenant rota. Conexion corta,
  // cerrada siempre, nunca pasa por TenantConnectionRegistry (ese registro
  // es solo para tenants YA REGISTRADOS, con un id real).
  async testConnection(dto: TestConnectionDto): Promise<{ ok: true } | { ok: false; error: string }> {
    let conn: mysql.Connection;
    try {
      conn = await mysql.createConnection({
        host: dto.dbHost,
        port: dto.dbPort,
        user: dto.dbUser,
        password: dto.dbPassword,
        database: dto.dbName,
        connectTimeout: 5000,
      });
    } catch (error: any) {
      return { ok: false, error: error?.message || String(error) };
    }
    try {
      await conn.query("SELECT 1");
      return { ok: true };
    } catch (error: any) {
      return { ok: false, error: error?.message || String(error) };
    } finally {
      await conn.end();
    }
  }

  // Envuelve testConnection() para un tenant YA GUARDADO: descifra su
  // credencial primero. El descifrado puede fallar (llave de cifrado
  // distinta a la que se uso para guardar, blob corrupto) — bug real,
  // encontrado corriendo esto contra un tenant real: un solo tenant con la
  // llave equivocada tiraba una excepcion sin capturar que rompia
  // listTenantsResumen() ENTERO, no solo ese tenant. El descifrado tiene que
  // estar DENTRO del mismo try/catch que la conexion, nunca antes.
  private async testStoredConnection(tenant: Tenant): Promise<{ ok: true } | { ok: false; error: string }> {
    let password: string;
    try {
      password = this.crypto.decrypt(tenant.dbPasswordEncrypted);
    } catch (error: any) {
      return {
        ok: false,
        error: `No se pudo descifrar la credencial guardada: ${error?.message || error}`,
      };
    }
    return this.testConnection({
      dbHost: tenant.dbHost,
      dbPort: tenant.dbPort,
      dbName: tenant.dbName,
      dbUser: tenant.dbUser,
      dbPassword: password,
    });
  }

  async registerTenant(dto: RegisterTenantDto): Promise<Tenant> {
    const existingCodigo = await this.tenantRecords.findByCodigo(dto.codigo);
    if (existingCodigo) {
      throw new ConflictException(`Ya existe un tenant con código '${dto.codigo}'`);
    }

    const existingConexion = await this.tenantRecords.findByConnection(dto.dbHost, dto.dbPort, dto.dbName);
    if (existingConexion) {
      throw new ConflictException(
        `Ya existe un tenant registrado con esa misma conexión (${dto.dbHost}:${dto.dbPort}/${dto.dbName}) — ` +
          `es '${existingConexion.codigo}'.`,
      );
    }

    return this.tenantRecords.create({
      codigo: dto.codigo,
      nombre: dto.nombre,
      entorno: dto.entorno,
      dbHost: dto.dbHost,
      dbPort: dto.dbPort,
      dbName: dto.dbName,
      dbUser: dto.dbUser,
      dbPasswordEncrypted: this.crypto.encrypt(dto.dbPassword),
      // estado se deja en su default (ONBOARDING) — activar es un paso
      // explicito y posterior, nunca automatico al registrar.
    });
  }

  private async findTenantOrThrow(tenantId: number): Promise<Tenant> {
    const tenant = await this.tenantRecords.findById(tenantId);
    if (!tenant) {
      throw new NotFoundException(`Tenant ${tenantId} no encontrado`);
    }
    return tenant;
  }

  // Desviaciones YA OMITIDAS (con nota, ver SchemaDeviation) se excluyen de
  // lo que se le vuelve a mostrar al operador — es exactamente el
  // comportamiento que el comentario de esa entidad describe: "evita que el
  // chequeo de compatibilidad vuelva a insistir con lo mismo en cada alta".
  private async filterOmitted(tenantId: number, deviations: ColumnDeviation[]): Promise<ColumnDeviation[]> {
    const omitted = await this.tenantRecords.findDeviationsForTenant(tenantId);
    const omittedKeys = new Set(omitted.map((d) => `${d.tabla}.${d.columna}`));
    return deviations.filter((d) => !omittedKeys.has(`${d.tabla}.${d.columna}`));
  }

  async getEstadoDetallado(tenantId: number): Promise<{
    tenant: Tenant;
    acceso: { ok: true } | { ok: false; error: string };
    migracionesPendientes: string[];
    desviaciones: ColumnDeviation[];
  }> {
    const tenant = await this.findTenantOrThrow(tenantId);

    const acceso = await this.testStoredConnection(tenant);

    if (!acceso.ok) {
      // Sin conexion no hay nada mas que verificar — devolver listas vacias
      // seria mentir "todo bien" por omision.
      return { tenant, acceso, migracionesPendientes: [], desviaciones: [] };
    }

    const [migracionesPendientes, desviacionesCrudas] = await Promise.all([
      this.schemaCheck.getPendingMigrations(tenantId),
      this.schemaCheck.getColumnDeviations(tenantId),
    ]);
    const desviaciones = await this.filterOmitted(tenantId, desviacionesCrudas);

    return { tenant, acceso, migracionesPendientes, desviaciones };
  }

  async listTenantsResumen(): Promise<TenantResumen[]> {
    const tenants = await this.tenantRecords.findAll();

    return Promise.all(
      tenants.map(async (tenant): Promise<TenantResumen> => {
        const acceso = await this.testStoredConnection(tenant);

        let migracionesPendientes: string[] = [];
        let desviaciones: ColumnDeviation[] = [];
        if (acceso.ok) {
          [migracionesPendientes, desviaciones] = await Promise.all([
            this.schemaCheck.getPendingMigrations(tenant.id),
            this.schemaCheck.getColumnDeviations(tenant.id).then((d) => this.filterOmitted(tenant.id, d)),
          ]);
        }

        return {
          id: tenant.id,
          codigo: tenant.codigo,
          nombre: tenant.nombre,
          estado: tenant.estado,
          entorno: tenant.entorno,
          dbHost: tenant.dbHost,
          dbPort: tenant.dbPort,
          dbName: tenant.dbName,
          migracionesPendientes,
          desviaciones: {
            grave: desviaciones.filter((d) => d.severidad === DeviationSeveridad.GRAVE).length,
            advertencia: desviaciones.filter((d) => d.severidad === DeviationSeveridad.ADVERTENCIA).length,
            informativo: desviaciones.filter((d) => d.severidad === DeviationSeveridad.INFORMATIVO).length,
          },
          acceso,
        };
      }),
    );
  }

  async omitDeviation(tenantId: number, dto: OmitDeviationDto, identidadId: number): Promise<SchemaDeviation> {
    await this.findTenantOrThrow(tenantId);

    if (dto.severidad !== DeviationSeveridad.GRAVE) {
      throw new BadRequestException(
        "Solo las desviaciones de severidad 'grave' necesitan omitirse explícitamente — " +
          "'advertencia'/'informativo' no bloquean nada.",
      );
    }

    const existing = await this.tenantRecords.findDeviation(tenantId, dto.tabla, dto.columna);
    if (existing) {
      return existing;
    }

    return this.tenantRecords.saveDeviation({
      tenantId,
      tabla: dto.tabla,
      columna: dto.columna,
      severidad: dto.severidad,
      tipoEsperado: dto.tipoEsperado,
      tipoReal: dto.tipoReal,
      alterSugerido: dto.alterSugerido,
      omitidoPorIdentidadId: identidadId,
    });
  }

  // El paso "Reconciliar" del flujo Preparar -> Verificar compatibilidad ->
  // Reconciliar -> Activar. Delega TODO el trabajo real a
  // TenantReconciliationService (que ademas es quien mide que aplico contra
  // la base real, en vez de confiar en lo que devuelva runMigrations() si
  // algo se corta a mitad de camino).
  //
  // Encadena "Activar" automaticamente si la reconciliacion termino
  // completa: DECISION explicita (pedida despues de ver el flujo en uso
  // real) — si ya no queda nada pendiente, obligar un segundo clic aparte
  // es friccion sin proposito. activarTenant() sigue siendo la UNICA fuente
  // del criterio de "esta listo" (conexion ok, 0 graves sin omitir) — si
  // todavia falta algo, tira y se atrapa aca: la reconciliacion en si fue
  // exitosa igual, el tenant sencillamente sigue en preparacion hasta que
  // alguien resuelva lo que falta desde la seccion "Activar".
  async reconciliar(tenantId: number, identidadId: number): Promise<ReconciliationResult> {
    const resultado = await this.reconciliationService.reconcile(tenantId, identidadId);
    if (resultado.estado === "completo") {
      await this.activarTenant(tenantId).catch(() => undefined);
    }
    return resultado;
  }

  // "Forzar" (ver comentario en TenantReconciliationService.reconcileForzado):
  // para cuando la tabla `migrations` del tenant ya dice que todo esta
  // aplicado pero el esquema real no lo refleja -- reconciliar() normal no
  // puede detectar ni arreglar ese caso porque confia en esa tabla.
  async reconciliarForzado(tenantId: number, identidadId: number): Promise<ReconciliationResult> {
    return this.reconciliationService.reconcileForzado(tenantId, identidadId);
  }

  // Progreso de UNA reconciliacion en curso: nunca inventa un numero, cuenta
  // lo que YA quedo confirmado en la tabla `migrations` de la base real del
  // tenant en este instante — cada migracion hace su propio commit apenas
  // termina (MySQL no tiene DDL transaccional), asi que este conteo crece de
  // verdad mientras la reconciliacion sigue corriendo en otra request. Pensado
  // para que el frontend lo consulte con un intervalo corto MIENTRAS espera
  // la respuesta del POST /reconciliar (que no vuelve hasta terminar).
  async getProgresoReconciliacion(tenantId: number): Promise<{ aplicadas: number; total: number }> {
    await this.findTenantOrThrow(tenantId);
    const pendientes = await this.schemaCheck.getPendingMigrations(tenantId);
    const total = listSourceMigrationClassNames().length;
    return { aplicadas: total - pendientes.length, total };
  }

  // El paso "Activar" del flujo Preparar -> Verificar compatibilidad ->
  // Reconciliar -> Activar. Reusa getEstadoDetallado() para las
  // precondiciones — NUNCA vuelve a implementar el criterio de "esta listo"
  // en un segundo lugar: acceso ok, cero migraciones pendientes, cero
  // desviaciones graves sin omitir. Un tenant suspendido no se reactiva
  // desde aca a proposito: pausar es una decision explicita de otro flujo,
  // reactivar deberia serlo tambien, no un efecto lateral de este boton.
  async activarTenant(tenantId: number): Promise<Tenant> {
    const tenant = await this.findTenantOrThrow(tenantId);

    if (tenant.estado === TenantEstado.ACTIVO) {
      return tenant;
    }
    if (tenant.estado === TenantEstado.SUSPENDIDO) {
      throw new BadRequestException(
        "Este tenant está suspendido — reactivarlo es una decisión explícita aparte, no 'activar'.",
      );
    }

    const estado = await this.getEstadoDetallado(tenantId);

    if (!estado.acceso.ok) {
      throw new BadRequestException(
        `No se puede activar: la conexión a la base del tenant falla (${estado.acceso.error}).`,
      );
    }
    if (estado.migracionesPendientes.length > 0) {
      throw new BadRequestException(
        `No se puede activar: hay ${estado.migracionesPendientes.length} migración(es) pendiente(s) ` +
          "— completar 'Reconciliar' primero.",
      );
    }
    const graves = estado.desviaciones.filter((d) => d.severidad === DeviationSeveridad.GRAVE);
    if (graves.length > 0) {
      throw new BadRequestException(
        `No se puede activar: hay ${graves.length} desviación(es) de columna 'grave' sin omitir.`,
      );
    }

    // Checklist operativo (tiendas/productos/facturacion) como precondicion
    // de "Activar", no solo informativa -- pedido explicito: un tenant puede
    // pasar los chequeos de esquema y aun asi no poder facturar ni una OT.
    // Cada pieza solo se exige si su toggle esta prendido (default true, ver
    // AddOperationalTogglesToTenantConfig) -- un tenant que de verdad no usa
    // tiendas/productos/facturacion propia puede apagar esa pieza puntual y
    // activar igual. `null` (no se pudo calcular, ej. columna/tabla
    // inesperadamente ausente) se trata como bloqueante -- a esta altura ya
    // se confirmo 0 migraciones pendientes, asi que un null aca es una señal
    // real de que algo mas no calzo, no un falso positivo a ignorar.
    const resumen = await this.getResumenOperativo(tenantId);
    const faltantes: string[] = [];
    // !== false (true O null) trata "no se pudo calcular" igual que "esta
    // prendido": el chequeo se aplica por las dudas, en vez de saltarselo en
    // silencio -- mismo criterio de "null es bloqueante" que ya usan
    // tiendas/productos/tiendasConFacturacionPos mas abajo.
    if (
      resumen.toggles.permiteTiendas !== false &&
      (resumen.tiendas ?? 0) === 0
    ) {
      faltantes.push("no hay ninguna tienda creada");
    }
    if (
      resumen.toggles.facturaHabilitada !== false &&
      (resumen.tiendasConFacturacionPos ?? 0) === 0
    ) {
      faltantes.push("ninguna tienda tiene facturacion POS habilitada");
    }
    if (
      resumen.toggles.permiteProductosPropios !== false &&
      (resumen.productos ?? 0) === 0
    ) {
      faltantes.push("no hay ningun producto propio creado");
    }
    if (faltantes.length > 0) {
      throw new BadRequestException(
        `No se puede activar: ${faltantes.join("; ")}. Si este tenant no necesita alguna de estas piezas, ` +
          "apagá su toggle correspondiente en 'Checklist operativo' antes de activar.",
      );
    }

    return this.tenantRecords.updateEstado(tenantId, TenantEstado.ACTIVO);
  }

  // Crea el PRIMER acceso operable de un tenant: usuario local en SU base +
  // identidad de plataforma (nueva o reusada por email) + la membresia que
  // las une. allowOnboarding:true a proposito — este paso tipicamente corre
  // ANTES de "Activar" (alguien tiene que poder loguearse para terminar de
  // verificar el tenant antes de darlo por activo), igual que
  // TenantSchemaCheckService. Nunca se permite si el tenant esta suspendido.
  async provisionarPrimerAcceso(
    tenantId: number,
    dto: ProvisionarAccesoDto,
    actorIdentidadId: number,
  ): Promise<{ identidadId: number; usuarioLocalId: number; membershipId: number; email: string; username: string }> {
    const tenant = await this.findTenantOrThrow(tenantId);
    if (tenant.estado === TenantEstado.SUSPENDIDO) {
      throw new BadRequestException("No se puede aprovisionar acceso en un tenant suspendido.");
    }

    const passwordHash = await bcrypt.hash(dto.password, 10);

    const usuario = await this.tenantRegistry.withTenantRepositoryExplicit(
      tenantId,
      Usuario,
      async (usuarioRepo) => {
        const existing = await usuarioRepo.findOne({ where: { username: dto.username } });
        if (existing) {
          throw new ConflictException(`Ya existe un usuario local '${dto.username}' en este tenant.`);
        }
        const created = usuarioRepo.create({
          username: dto.username,
          nombre: dto.nombre,
          email: dto.email,
          passwordHash,
          rol: UserRole.SUPER_ADMIN,
          activo: true,
        });
        return usuarioRepo.save(created);
      },
      { allowOnboarding: true },
    );

    // Si la identidad YA existia (mismo email en otro tenant, o de
    // plataforma), se reusa tal cual — su contraseña real es la que ya
    // tenia, nunca la que se acaba de tipear aca.
    const identidad = await this.identityService.findOrCreateIdentidadParaProvisioning(
      dto.email,
      dto.nombre,
      passwordHash,
    );

    const membership = await this.identityService.createMembership({
      identidadId: identidad.id,
      tenantId,
      usuarioLocalId: usuario.id,
      otorgadoPorIdentidadId: actorIdentidadId,
    });

    return {
      identidadId: identidad.id,
      usuarioLocalId: usuario.id,
      membershipId: membership.id,
      email: dto.email,
      username: dto.username,
    };
  }

  // Checklist de "que le falta a este tenant para operar de verdad" --
  // encontrado en produccion (Demo Ventas): un tenant puede quedar
  // "activo" (conexion ok, 0 migraciones pendientes) sin tener ninguna
  // tienda, sin facturacion configurada en ninguna, y sin productos
  // propios -- nada de eso es una migracion pendiente ni una desviacion de
  // columna, asi que el resto del panel nunca lo mostraba. Cada conteo se
  // atrapa por separado: si CUALQUIERA falla (ej. la migracion de una
  // columna puntual todavia no corrio ahi), ese conteo queda en null en
  // vez de tirar abajo los otros dos.
  async getResumenOperativo(tenantId: number): Promise<{
    tiendas: number | null;
    tiendasConFacturacionPos: number | null;
    productos: number | null;
    toggles: {
      permiteTiendas: boolean | null;
      permiteProductosPropios: boolean | null;
      facturaHabilitada: boolean | null;
    };
  }> {
    const tenant = await this.findTenantOrThrow(tenantId);
    const toggles =
      await this.tallerParametrosService.getTogglesOperativosAdmin(tenantId);
    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,
    });

    const contarSeguro = async (sql: string): Promise<number | null> => {
      try {
        const [rows] = await conn.query<mysql.RowDataPacket[]>(sql);
        return Number((rows as any)[0]?.c ?? 0);
      } catch {
        return null;
      }
    };

    try {
      const [tiendas, tiendasConFacturacionPos, productos] = await Promise.all([
        contarSeguro(
          "SELECT COUNT(*) AS c FROM maetie WHERE (status IS NULL OR status <> 1) " +
            "AND (alta IS NULL OR alta <> 'N') AND (es_transito IS NULL OR es_transito <> 'S')",
        ),
        contarSeguro(
          "SELECT COUNT(DISTINCT tienda_id) AS c FROM taller_parametros " +
            "WHERE clave = 'facturacion_pos.habilitada' AND valor = 'true' AND tienda_id <> 0",
        ),
        contarSeguro("SELECT COUNT(*) AS c FROM taller_productos"),
      ]);

      return { tiendas, tiendasConFacturacionPos, productos, toggles };
    } finally {
      await conn.end();
    }
  }

  // Actor "0" a propósito: esta escritura viene del panel de PLATAFORMA
  // (operador de MacroBase), no de un Usuario local del tenant -- 0 nunca es
  // un Usuario.id real (autoincrement empieza en 1), así que queda como
  // centinela distinguible en taller_parametros_historial, igual que
  // tienda_id=0 ya significa "no aplica" en esa misma tabla.
  private static readonly ACTOR_PLATAFORMA = 0;

  async updateTogglesOperativos(
    tenantId: number,
    dto: UpdateTogglesOperativosDto,
  ): Promise<{ permiteTiendas: boolean; permiteProductosPropios: boolean; facturaHabilitada: boolean }> {
    await this.findTenantOrThrow(tenantId);

    const mapa: Array<{
      campo: keyof UpdateTogglesOperativosDto;
      clave: TallerParametroClave;
    }> = [
      { campo: "permiteTiendas", clave: "tiendas.permite_crear" },
      { campo: "permiteProductosPropios", clave: "productos.permite_crear" },
      { campo: "facturaHabilitada", clave: "facturacion.usa_facturacion" },
    ];

    for (const { campo, clave } of mapa) {
      const valor = dto[campo];
      if (valor !== undefined) {
        await this.tallerParametrosService.setExplicit(
          tenantId,
          clave,
          valor,
          TenantsAdminService.ACTOR_PLATAFORMA,
          { allowOnboarding: true },
        );
      }
    }

    const actualizado =
      await this.tallerParametrosService.getTogglesOperativosAdmin(tenantId);
    return {
      permiteTiendas: actualizado.permiteTiendas ?? true,
      permiteProductosPropios: actualizado.permiteProductosPropios ?? true,
      facturaHabilitada: actualizado.facturaHabilitada ?? true,
    };
  }
}
