import { ConflictException, Injectable, NotFoundException, UnauthorizedException } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import * as bcrypt from "bcryptjs";
import { Repository } from "typeorm";
import { Auditoria, AuditoriaVia } from "./entities/auditoria.entity";
import { Identidad, IdentidadEstado, RolPlataforma } from "./entities/identidad.entity";
import { MembershipEstado, TenantMembership } from "./entities/tenant-membership.entity";
import { Tenant, TenantEstado } from "./entities/tenant.entity";

// La identidad (persona + contraseña + rol de plataforma) vive SOLO aca —
// auth/ nunca inyecta Identidad/TenantMembership/Auditoria directo (eso
// seria una ofensa igual que inyectar Usuario/Tienda fuera de contexto).
// AuthService orquesta login/refresh llamando a este servicio para todo lo
// que es plano de control, y al bypass de TenantConnectionRegistry solo para
// el usuario local del tenant.
// El literal "controlPlane" (no CONTROL_PLANE_CONNECTION importado de
// control-plane.module.ts) es deliberado: ese modulo importa esta clase para
// registrarla como provider, asi que importar la constante de vuelta desde
// aca cierra un ciclo de modulos ES — confirmado corriendo esto (Nest lo
// reporta como "circular dependency detected inside ControlPlaneModule",
// mensaje que no deja adivinar que el ciclo es de imports de archivo, no de
// modulos de Nest). Mismo patron ya usado en
// tenancy/tenant-connection-registry.service.ts para el mismo motivo.
@Injectable()
export class IdentityService {
  constructor(
    @InjectRepository(Identidad, "controlPlane")
    private readonly identidadRepo: Repository<Identidad>,
    @InjectRepository(TenantMembership, "controlPlane")
    private readonly membershipRepo: Repository<TenantMembership>,
    @InjectRepository(Auditoria, "controlPlane")
    private readonly auditoriaRepo: Repository<Auditoria>,
    @InjectRepository(Tenant, "controlPlane")
    private readonly tenantRepo: Repository<Tenant>,
  ) {}

  // Busqueda pura, sin verificar nada — separada de verifyPassword() para
  // que AuthService pueda intentar esto primero (¿el input ES un email de
  // una identidad?) y, si no encuentra nada, seguir con el fallback de
  // username local SIN haber tirado una excepcion todavia.
  async findByEmail(email: string): Promise<Identidad | null> {
    return this.identidadRepo.findOne({ where: { email } });
  }

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

  // Mensaje generico tanto si la identidad esta suspendida como si el
  // password no coincide — a proposito, para no confirmarle a quien intenta
  // adivinar cual de las dos cosas fallo.
  async verifyPassword(identidad: Identidad, password: string): Promise<void> {
    if (identidad.estado !== IdentidadEstado.ACTIVA) {
      throw new UnauthorizedException("Credenciales inválidas");
    }
    const valido = await bcrypt.compare(password, identidad.passwordHash);
    if (!valido) {
      throw new UnauthorizedException("Credenciales inválidas");
    }
  }

  async getActiveTenants(): Promise<Tenant[]> {
    return this.tenantRepo.find({ where: { estado: TenantEstado.ACTIVO } });
  }

  async findActiveMembershipByUsuarioLocal(
    tenantId: number,
    usuarioLocalId: number,
  ): Promise<TenantMembership | null> {
    return this.membershipRepo.findOne({
      where: { tenantId, usuarioLocalId, estado: MembershipEstado.ACTIVA },
    });
  }

  // Filtra por tenant.estado=ACTIVO ademas de membership.estado=ACTIVA: una
  // membresia hacia un tenant suspendido u onboarding no debe ofrecerse como
  // destino de login — TenantConnectionRegistry rechazaria la conexion de
  // todos modos (mismo chequeo que createDataSource()), pero fallar aca da
  // un mensaje de login claro en vez de un error generico del registro.
  async getActiveMemberships(identidadId: number): Promise<TenantMembership[]> {
    const memberships = await this.membershipRepo.find({
      where: { identidadId, estado: MembershipEstado.ACTIVA },
      relations: ["tenant"],
    });
    return memberships.filter((m) => m.tenant?.estado === TenantEstado.ACTIVO);
  }

  async findActiveMembershipByTenantCodigo(
    identidadId: number,
    tenantCodigo: string,
  ): Promise<TenantMembership | null> {
    const memberships = await this.getActiveMemberships(identidadId);
    return memberships.find((m) => m.tenant?.codigo === tenantCodigo) ?? null;
  }

  // Solo para la rama de acceso via='plataforma' (admin_plataforma sin
  // membresia): busca el tenant DESTINO por codigo, sin pasar por
  // tenant_membership. No alcanza con que exista — un tenant en onboarding o
  // suspendido nunca es un destino valido de acceso (mismo criterio que
  // TenantConnectionRegistry.createDataSource()).
  async findActiveTenantByCodigo(tenantCodigo: string): Promise<Tenant | null> {
    const tenant = await this.tenantRepo.findOne({ where: { codigo: tenantCodigo } });
    if (!tenant || tenant.estado !== TenantEstado.ACTIVO) {
      return null;
    }
    return tenant;
  }

  // Releida SIEMPRE desde identidades, nunca desde el payload de un token
  // entrante — es exactamente el punto: rol_plataforma no puede arrastrarse
  // de una emision de token vieja (login, refresh futuro switch).
  async getFreshRolPlataforma(identidadId: number): Promise<RolPlataforma> {
    const identidad = await this.identidadRepo.findOne({ where: { id: identidadId } });
    if (!identidad) {
      throw new UnauthorizedException("Identidad no encontrada");
    }
    return identidad.rolPlataforma;
  }

  // Solo para el flujo de aprovisionamiento (control-plane-admin/): busca
  // por email y, si no existe, crea una identidad NUEVA con el hash ya
  // calculado por quien llama. Si YA existe una identidad con ese email, se
  // devuelve TAL CUAL — nunca se le pisa la contraseña. Aprovisionar acceso
  // para un tenant nuevo con un email que ya es identidad de otro tenant (o
  // de plataforma) debe sumar una membresia mas a esa misma persona, no
  // crear una segunda cuenta ni resetearle la clave sin que lo pida.
  async findOrCreateIdentidadParaProvisioning(
    email: string,
    nombre: string,
    passwordHash: string,
  ): Promise<Identidad> {
    const existing = await this.identidadRepo.findOne({ where: { email } });
    if (existing) {
      return existing;
    }
    return this.identidadRepo.save(
      this.identidadRepo.create({
        email,
        nombre,
        passwordHash,
        estado: IdentidadEstado.ACTIVA,
        rolPlataforma: RolPlataforma.NINGUNO,
      }),
    );
  }

  // El login (AuthService.login -> verifyPassword) valida SIEMPRE contra
  // esta fila, nunca contra taller_usuarios.email/passwordHash -- por eso
  // AuthService.updateUser() tiene que llamar esto cuando el admin del
  // tenant cambia el correo o la contraseña de alguien. Sin este metodo (y
  // sin que updateUser lo llame) el cambio se guardaba solo en la fila local
  // del tenant, invisible para el login: el usuario seguia entrando con su
  // email/password viejo pese al toast de "guardado correctamente".
  async updateCredenciales(
    identidadId: number,
    cambios: { email?: string; passwordHash?: string },
  ): Promise<Identidad> {
    const identidad = await this.identidadRepo.findOne({ where: { id: identidadId } });
    if (!identidad) {
      throw new NotFoundException(`Identidad ${identidadId} no encontrada`);
    }

    if (cambios.email !== undefined && cambios.email !== identidad.email) {
      const otra = await this.identidadRepo.findOne({ where: { email: cambios.email } });
      if (otra && otra.id !== identidadId) {
        throw new ConflictException(
          `El correo ${cambios.email} ya pertenece a otra identidad`,
        );
      }
      identidad.email = cambios.email;
    }

    if (cambios.passwordHash !== undefined) {
      identidad.passwordHash = cambios.passwordHash;
    }

    return this.identidadRepo.save(identidad);
  }

  // Idempotente por (identidadId, tenantId) — el @Unique de la entidad ya lo
  // exige, pero un SELECT antes evita depender de capturar la excepcion del
  // driver para el caso "aprovisionar dos veces por error".
  async createMembership(params: {
    identidadId: number;
    tenantId: number;
    usuarioLocalId: number;
    otorgadoPorIdentidadId: number;
  }): Promise<TenantMembership> {
    const existing = await this.membershipRepo.findOne({
      where: { identidadId: params.identidadId, tenantId: params.tenantId },
    });
    if (existing) {
      return existing;
    }
    return this.membershipRepo.save(
      this.membershipRepo.create({
        ...params,
        estado: MembershipEstado.ACTIVA,
      }),
    );
  }

  // Auditoria ANTES del acceso: quien llama debe invocar esto y esperar que
  // resuelva ANTES de tocar el usuario local del tenant. Si esto tira (falla
  // el INSERT), el llamador tiene que dejar que la excepcion se propague —
  // nunca capturarla para "seguir igual". No hay try/catch aca a proposito.
  async registrarAcceso(params: {
    identidadId: number;
    tenantId: number | null;
    via: AuditoriaVia;
    accion: string;
    detalle?: Record<string, unknown>;
  }): Promise<void> {
    await this.auditoriaRepo.save({
      identidadId: params.identidadId,
      tenantId: params.tenantId,
      via: params.via,
      accion: params.accion,
      detalle: params.detalle ?? null,
    });
  }
}
