import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { MigrationJob, MigrationJobEstado } from "./entities/migration-job.entity";
import { MigrationJobStep } from "./entities/migration-job-step.entity";
import { DeviationSeveridad, SchemaDeviation } from "./entities/schema-deviation.entity";
import { TenantMigrationLock } from "./entities/tenant-migration-lock.entity";
import { Tenant, TenantEstado } from "./entities/tenant.entity";

// Acceso a las tablas PROPIAS del plano de control (tenants,
// schema_deviations) para quien necesite administrar tenants —
// control-plane-admin/ nunca inyecta estos repositorios directo (eso seria
// una ofensa igual que auth/ inyectando Identidad/TenantMembership fuera de
// contexto). Mismo patron que IdentityService: el repositorio vive DENTRO
// de control-plane/ (ya en la lista blanca del escaner), quien lo consume
// esta afuera.
@Injectable()
export class TenantRecordsService {
  constructor(
    @InjectRepository(Tenant, "controlPlane")
    private readonly tenantRepo: Repository<Tenant>,
    @InjectRepository(SchemaDeviation, "controlPlane")
    private readonly deviationRepo: Repository<SchemaDeviation>,
    @InjectRepository(MigrationJob, "controlPlane")
    private readonly migrationJobRepo: Repository<MigrationJob>,
    @InjectRepository(MigrationJobStep, "controlPlane")
    private readonly migrationJobStepRepo: Repository<MigrationJobStep>,
    @InjectRepository(TenantMigrationLock, "controlPlane")
    private readonly migrationLockRepo: Repository<TenantMigrationLock>,
  ) {}

  async findAll(): Promise<Tenant[]> {
    return this.tenantRepo.find({ order: { id: "ASC" } });
  }

  async findById(id: number): Promise<Tenant | null> {
    return this.tenantRepo.findOne({ where: { id } });
  }

  async findByCodigo(codigo: string): Promise<Tenant | null> {
    return this.tenantRepo.findOne({ where: { codigo } });
  }

  async findByConnection(dbHost: string, dbPort: number, dbName: string): Promise<Tenant | null> {
    return this.tenantRepo.findOne({ where: { dbHost, dbPort, dbName } });
  }

  async create(data: Partial<Tenant>): Promise<Tenant> {
    return this.tenantRepo.save(this.tenantRepo.create(data));
  }

  // Unico punto de escritura de Tenant.estado fuera del alta — activar/
  // suspender siempre pasa por aca, nunca por un UPDATE armado a mano en
  // otro servicio.
  async updateEstado(id: number, estado: TenantEstado): Promise<Tenant> {
    await this.tenantRepo.update(id, { estado });
    const updated = await this.tenantRepo.findOne({ where: { id } });
    if (!updated) {
      throw new Error(`Tenant ${id} desaparecio entre el UPDATE y el re-lectura de su estado.`);
    }
    return updated;
  }

  async findDeviationsForTenant(tenantId: number): Promise<SchemaDeviation[]> {
    return this.deviationRepo.find({ where: { tenantId } });
  }

  async findDeviation(tenantId: number, tabla: string, columna: string): Promise<SchemaDeviation | null> {
    return this.deviationRepo.findOne({ where: { tenantId, tabla, columna } });
  }

  async saveDeviation(data: {
    tenantId: number;
    tabla: string;
    columna: string;
    severidad: DeviationSeveridad;
    tipoEsperado: string;
    tipoReal: string;
    alterSugerido: string;
    omitidoPorIdentidadId: number;
  }): Promise<SchemaDeviation> {
    return this.deviationRepo.save(this.deviationRepo.create(data));
  }

  // Toma el lock de reconciliacion de un tenant, o lo rechaza si otro
  // trabajo lo tiene tomado y todavia no expiro. expiraEn evita que un
  // proceso muerto (contenedor reiniciado a mitad de una corrida) deje el
  // lock trabado para siempre — un lock vencido se puede tomar de nuevo.
  async acquireMigrationLock(tenantId: number, migrationJobId: number, ttlMs: number): Promise<void> {
    const existing = await this.migrationLockRepo.findOne({ where: { tenantId } });
    const now = new Date();
    if (existing && existing.expiraEn > now) {
      throw new Error(
        `Ya hay una reconciliacion en curso para este tenant (job ${existing.migrationJobId}, expira ${existing.expiraEn.toISOString()}).`,
      );
    }
    await this.migrationLockRepo.save(
      this.migrationLockRepo.create({
        tenantId,
        migrationJobId,
        expiraEn: new Date(now.getTime() + ttlMs),
      }),
    );
  }

  async releaseMigrationLock(tenantId: number): Promise<void> {
    await this.migrationLockRepo.delete({ tenantId });
  }

  async createMigrationJob(data: {
    tenantId: number;
    iniciadoPorIdentidadId: number;
    esSimulacion: boolean;
  }): Promise<MigrationJob> {
    return this.migrationJobRepo.save(
      this.migrationJobRepo.create({ ...data, estado: MigrationJobEstado.CORRIENDO }),
    );
  }

  async updateMigrationJob(
    id: number,
    data: Partial<Pick<MigrationJob, "estado" | "falloEnMigracion" | "errorMensaje">>,
  ): Promise<void> {
    await this.migrationJobRepo.update(id, data);
  }

  async addMigrationJobStep(migrationJobId: number, migracionNombre: string): Promise<void> {
    await this.migrationJobStepRepo.save(
      this.migrationJobStepRepo.create({ migrationJobId, migracionNombre }),
    );
  }

  async findMigrationJob(id: number): Promise<MigrationJob | null> {
    return this.migrationJobRepo.findOne({ where: { id } });
  }

  async findMigrationJobSteps(migrationJobId: number): Promise<MigrationJobStep[]> {
    return this.migrationJobStepRepo.find({ where: { migrationJobId }, order: { id: "ASC" } });
  }
}
