import {
  ConflictException,
  BadRequestException,
  ForbiddenException,
  Injectable,
  Logger,
  NotFoundException,
  UnauthorizedException,
} from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import * as bcrypt from "bcryptjs";
import * as crypto from "crypto";
import { EntityTarget, Repository } from "typeorm";
import { AuditoriaVia } from "../control-plane/entities/auditoria.entity";
import { Identidad, RolPlataforma } from "../control-plane/entities/identidad.entity";
import { CredentialsCryptoService } from "../control-plane/credentials-crypto.service";
import { IdentityService } from "../control-plane/identity.service";
import { TenantConnectionRegistry } from "../tenancy/tenant-connection-registry.service";
import { TallerParametrosService } from "../taller-parametros/taller-parametros.service";
import { TallerParametroClave } from "../taller-parametros/taller-parametros.catalogo";
import { CreateUserDto } from "./dto/create-user.dto";
import { LoginDto } from "./dto/login.dto";
import { UpdateUserDto } from "./dto/update-user.dto";
import { UpdateTiendaPrintDto } from "./dto/update-tienda-print.dto";
import { CreateTiendaDto } from "./dto/create-tienda.dto";
import { UpdateTiendaFacturacionPosDto } from "./dto/update-tienda-facturacion-pos.dto";
import { Tienda } from "./entities/tienda.entity";
import { UserRole, Usuario } from "./entities/usuario.entity";
import { PLATFORM_SYSTEM_USERNAME } from "./platform-system-user";
import { DEFAULT_PERMISSIONS_BY_ROLE } from "./default-permissions";

type AccesoResuelto = {
  tenantId: number;
  via: AuditoriaVia;
  usuarioLocalId: number | null;
};

@Injectable()
export class AuthService {
  private readonly logger = new Logger(AuthService.name);

  constructor(
    private readonly tenantRegistry: TenantConnectionRegistry,
    private readonly identityService: IdentityService,
    private jwtService: JwtService,
    private readonly credentialsCrypto: CredentialsCryptoService,
    private readonly tallerParametros: TallerParametrosService,
  ) {}

  // Unico punto de entrada textual para el bypass de UN repositorio — todo
  // metodo de este servicio que necesite solo Usuario O solo Tienda pasa por
  // aca. AuthService RESUELVE el tenant (login) o lo recibe ya resuelto por
  // un JWT valido (el resto de los metodos) — nunca puede depender del
  // contexto ambiental de tenant, que en el primer caso todavia no existe y
  // que, mientras no exista el TenantGuard, nadie puebla en el segundo.
  private async withTenantRepo<T extends object, R>(
    tenantId: number,
    entity: EntityTarget<T>,
    fn: (repo: Repository<T>) => Promise<R>,
  ): Promise<R> {
    return this.tenantRegistry.withTenantRepositoryExplicit(tenantId, entity, fn);
  }

  private buildPermissions(
    usuario: Usuario,
  ): Record<string, "none" | "read" | "write"> {
    const base =
      DEFAULT_PERMISSIONS_BY_ROLE[usuario.rol] ||
      DEFAULT_PERMISSIONS_BY_ROLE[UserRole.OPERATIVO];
    return { ...base, ...(usuario.permisos || {}) };
  }

  private serializeUser(usuario: Usuario) {
    return {
      id: usuario.id,
      username: usuario.username,
      nombre: usuario.nombre,
      email: usuario.email,
      rol: usuario.rol,
      tiendaId: usuario.tiendaId,
      tienda: usuario.tienda ? this.serializeTienda(usuario.tienda) : null,
      activo: usuario.activo,
      ultimoLogin: usuario.ultimoLogin,
      createdAt: usuario.createdAt,
      permissions: this.buildPermissions(usuario),
      facturacionPosTiendaIds: usuario.facturacionPosTiendaIds ?? null,
      estadosVisibles: usuario.estadosVisibles ?? null,
    };
  }

  private serializeTienda(tienda: Tienda) {
    const nombreBase =
      tienda.nombre || tienda.descripcion || `Tienda ${tienda.id}`;

    return {
      id: tienda.id,
      codigo: tienda.codigo,
      nombre: nombreBase,
      descripcion: tienda.descripcion,
      direccion: tienda.direccion,
      nombreImpresion: tienda.nombreImpresion || nombreBase,
      direccionImpresion: tienda.direccionImpresion || tienda.direccion || null,
      telefonoImpresion: tienda.telefonoImpresion,
      logoUrl: tienda.logoUrl,
      creadaEnTaller: tienda.creadaEnTaller,
      // Nunca la contraseña (ni cifrada ni en claro) -- solo lo que la UI
      // necesita para mostrar el estado y permitir reconfigurar.
      facturacionPosHabilitada: tienda.facturacionPosHabilitada,
      facturacionPosUrl: tienda.facturacionPosUrl,
      facturacionPosUsername: tienda.facturacionPosUsername,
      facturacionPosComputadora: tienda.facturacionPosComputadora,
      facturacionPosPrecioDecimales: tienda.facturacionPosPrecioDecimales,
    };
  }

  // Anida un segundo bypass (mismo helper, mismo unico punto de entrada
  // textual — ver withTenantRepo mas arriba) dentro del que ya sostiene
  // Usuario en createUser/updateUser. Seguro por diseño: adquirir de nuevo
  // el mismo tenant ya cacheado solo suma un contador de uso, nunca abre una
  // conexion nueva ni compite por otra.
  private async ensureTiendaActivaPorId(tenantId: number, tiendaId?: number | null) {
    if (!tiendaId) {
      return null;
    }

    return this.withTenantRepo(tenantId, Tienda, async (tiendaRepo) => {
      const tienda = await tiendaRepo.findOne({ where: { id: tiendaId } });

      if (!tienda || tienda.status === 1 || tienda.alta === "N") {
        throw new BadRequestException("La tienda seleccionada no existe o está inactiva en maetie");
      }

      return tienda;
    });
  }

  private ensureNotPlatformSystemUsername(username?: string) {
    if (username === PLATFORM_SYSTEM_USERNAME) {
      throw new BadRequestException(
        `El username '${PLATFORM_SYSTEM_USERNAME}' está reservado para la fila de sistema de acceso ` +
          "de plataforma — no se puede crear ni asignar desde la gestión de usuarios del tenant.",
      );
    }
  }

  // Decide A QUE tenant entra esta identidad y CON QUE via — el corazon de
  // "tenant travels in JWT". Tres casos, en orden:
  //  1) tenantCodigo dado y hay membresia activa para ese tenant -> via
  //     tenant, usuarioLocalId sale de la membresia.
  //  2) tenantCodigo dado, SIN membresia, pero la identidad es
  //     admin_plataforma -> via plataforma. usuarioLocalId se resuelve
  //     despues (fila de sistema, ver loadUsuarioLocalParaAcceso) — aca
  //     solo se confirma que el tenant exista y este activo.
  //  3) sin tenantCodigo: si hay exactamente una membresia activa, se
  //     autoselecciona; con mas de una hay que elegir explicitamente; con
  //     cero y admin_plataforma, hay que indicar a que tenant se quiere
  //     entrar como plataforma (nunca se autoselecciona ese acceso).
  private async resolveTenantAccess(
    identidad: Identidad,
    tenantCodigo?: string,
  ): Promise<AccesoResuelto> {
    const memberships = await this.identityService.getActiveMemberships(identidad.id);

    if (tenantCodigo) {
      const membership = memberships.find((m) => m.tenant?.codigo === tenantCodigo);
      if (membership) {
        return {
          tenantId: membership.tenantId,
          via: AuditoriaVia.TENANT,
          usuarioLocalId: membership.usuarioLocalId,
        };
      }

      if (identidad.rolPlataforma === RolPlataforma.ADMIN_PLATAFORMA) {
        const tenant = await this.identityService.findActiveTenantByCodigo(tenantCodigo);
        if (!tenant) {
          throw new UnauthorizedException(`El tenant '${tenantCodigo}' no existe o no está activo`);
        }
        return { tenantId: tenant.id, via: AuditoriaVia.PLATAFORMA, usuarioLocalId: null };
      }

      throw new UnauthorizedException(`Sin acceso al tenant '${tenantCodigo}'`);
    }

    if (memberships.length === 1) {
      const unica = memberships[0];
      return {
        tenantId: unica.tenantId,
        via: AuditoriaVia.TENANT,
        usuarioLocalId: unica.usuarioLocalId,
      };
    }

    if (memberships.length > 1) {
      const codigos = memberships.map((m) => m.tenant?.codigo).filter(Boolean);
      throw new BadRequestException(
        `La identidad tiene acceso a varios tenants (${codigos.join(", ")}) — indica tenantCodigo.`,
      );
    }

    if (identidad.rolPlataforma === RolPlataforma.ADMIN_PLATAFORMA) {
      throw new BadRequestException("Indica tenantCodigo para acceder como plataforma.");
    }

    throw new UnauthorizedException("La identidad no tiene acceso a ningún tenant.");
  }

  // Para via=plataforma: la fila de sistema se crea SOLA, la primera vez que
  // un admin de plataforma entra de verdad a este tenant — nunca hace falta
  // un paso manual de "aprovisionar" antes. DECISION explicita (cambiada de
  // la version anterior, que exigia aprovisionarla a mano desde el panel):
  // un superadmin tiene que poder entrar a CUALQUIER tenant activo sin
  // friccion; "Aprovisionar acceso" en el panel queda solo para crear el
  // primer usuario REAL (no de plataforma) de un cliente nuevo. La
  // contraseña que se le pone es aleatoria y nunca se usa para nada — este
  // acceso nunca se verifica por password (loadUsuarioLocalParaAcceso lo
  // busca directo por username, ver arriba en login()), la columna solo
  // existe porque el esquema la exige NOT NULL.
  private async loadUsuarioLocalParaAcceso(
    tenantId: number,
    via: AuditoriaVia,
    usuarioLocalId: number | null,
  ): Promise<Usuario> {
    if (via === AuditoriaVia.PLATAFORMA) {
      return this.withTenantRepo(tenantId, Usuario, async (repo) => {
        let usuario = await repo.findOne({
          where: { username: PLATFORM_SYSTEM_USERNAME },
          relations: ["tienda"],
        });

        if (!usuario) {
          const randomPassword = crypto.randomBytes(32).toString("hex");
          const created = repo.create({
            username: PLATFORM_SYSTEM_USERNAME,
            nombre: "Administracion de plataforma",
            passwordHash: await bcrypt.hash(randomPassword, 10),
            rol: UserRole.SUPER_ADMIN,
            activo: true,
          });
          usuario = await repo.save(created);
          this.logger.log(
            `Fila de sistema de plataforma creada automaticamente en tenant ${tenantId} (primer acceso de un admin de plataforma).`,
          );
        }

        if (!usuario.activo) {
          throw new UnauthorizedException(
            "El acceso de plataforma para este tenant esta desactivado.",
          );
        }

        return usuario;
      });
    }

    if (usuarioLocalId === null) {
      throw new UnauthorizedException("No se pudo resolver el usuario local para este acceso.");
    }

    const usuario = await this.withTenantRepo(tenantId, Usuario, (repo) =>
      repo.findOne({ where: { id: usuarioLocalId, activo: true }, relations: ["tienda"] }),
    );
    if (!usuario) {
      throw new UnauthorizedException("El usuario local de esta membresía no existe o está inactivo.");
    }
    return usuario;
  }

  private buildTokenPayload(params: {
    usuario: Usuario;
    tenantId: number;
    identidadId: number;
    rolPlataforma: RolPlataforma;
    via: AuditoriaVia;
  }) {
    const { usuario, tenantId, identidadId, rolPlataforma, via } = params;
    return {
      sub: usuario.id,
      username: usuario.username,
      rol: usuario.rol,
      tiendaId: usuario.tiendaId,
      permissions: this.buildPermissions(usuario),
      // null = sin restriccion en ambos casos -- ver comentario de las
      // columnas en usuario.entity.ts. Van en el token (no se releen de la
      // base por request, mismo trade-off ya documentado arriba para
      // rol/permissions: TTL corto en vez de revalidacion instantanea).
      facturacionPosTiendaIds: usuario.facturacionPosTiendaIds ?? null,
      estadosVisibles: usuario.estadosVisibles ?? null,
      tenantId,
      identidadId,
      usuarioLocalId: usuario.id,
      rolPlataforma,
      via,
    };
  }

  // PUENTE TEMPORAL mientras el frontend siga mandando { username, password }
  // en vez de { email, password } — decision explicita para no tocar el
  // frontend en este despliegue. Intenta el input COMO email primero (cubre
  // a cualquiera que ya tenga uno real, y a quien tipee un placeholder
  // literal). Si no hay ninguna identidad con ese string como email, lo
  // trata como USERNAME LOCAL: recorre los tenants activos (hoy: uno solo)
  // buscando un usuario local activo con ese username, y si aparece en
  // EXACTAMENTE uno, resuelve la identidad via tenant_membership
  // (tenantId + usuarioLocalId) — sin adivinar ningun email.
  //
  // NO ESCALA a muchos tenants: cada intento de login sin coincidencia de
  // email recorre TODOS los tenants activos. Con dos tenants que casualmente
  // tengan el mismo username activo (ej. "admin" en ambos), esto rechaza el
  // login pidiendo email en vez de adivinar cual — correcto, pero significa
  // que este puente deja de ser gratis en cuanto haya unos pocos tenants.
  // Reemplazar por login-por-email en el frontend (o un indice de username
  // global) antes de onboardear el tenant #2 con usernames que puedan
  // repetirse.
  private async resolveIdentidadPorLogin(
    input: string,
    tenantCodigoHint?: string,
  ): Promise<{ identidad: Identidad; tenantCodigoResuelto?: string }> {
    const porEmail = await this.identityService.findByEmail(input);
    if (porEmail) {
      return { identidad: porEmail };
    }

    const tenant = tenantCodigoHint
      ? await this.identityService.findActiveTenantByCodigo(tenantCodigoHint)
      : null;
    const candidatos = tenantCodigoHint
      ? tenant
        ? [tenant]
        : []
      : await this.identityService.getActiveTenants();

    const coincidencias: Array<{ tenantId: number; tenantCodigo: string; usuarioLocalId: number }> = [];
    for (const candidato of candidatos) {
      let usuario: Usuario | null;
      try {
        usuario = await this.withTenantRepo(candidato.id, Usuario, (repo) =>
          repo.findOne({ where: { username: input, activo: true } }),
        );
      } catch (error: any) {
        // Un tenant que no conecta (credencial mal cifrada, red caida,
        // suspendido a mitad de este loop) NO puede tumbar el login de TODOS
        // los demas tenants — mismo bug de clase que ya se encontro y
        // arreglo en TenantsAdminService.listTenantsResumen() (un tenant malo
        // rompia el listado entero). Encontrado corriendo esto de verdad con
        // 2+ tenants activos, no razonando sobre el codigo: hasta este punto
        // de la conversion nunca habia mas de un tenant activo a la vez.
        this.logger.warn(
          `Login por username: el tenant '${candidato.codigo}' (id=${candidato.id}) no respondio al buscar ` +
            `'${input}' — se lo salta y se sigue con los demas candidatos. Causa: ${error?.message || error}`,
        );
        continue;
      }
      if (usuario) {
        coincidencias.push({
          tenantId: candidato.id,
          tenantCodigo: candidato.codigo,
          usuarioLocalId: usuario.id,
        });
      }
    }

    if (coincidencias.length === 0) {
      // Misma respuesta al cliente en TODOS los casos (nunca confirmarle a
      // quien intenta adivinar cual pieza fallo) — pero el log de SERVIDOR
      // distingue "no hay ningun tenant registrado todavia" (el sistema no
      // esta inicializado — correr scripts/migrate-identidades.ts) de
      // "usuario/contraseña realmente incorrectos". Sin esto, el operador
      // ve el mismo "Credenciales invalidas" para las dos causas y no tiene
      // ninguna pista de cual es — exactamente lo que paso en el despliegue
      // real que motiva este log.
      if (!tenantCodigoHint && candidatos.length === 0) {
        this.logger.warn(
          `Login rechazado para '${input}': no hay NINGUN tenant activo registrado en el plano de control ` +
            "(mbinvtaller.tenants esta vacio, o ninguno tiene estado='activo'). El sistema no esta " +
            "inicializado todavia — esto no es una contraseña incorrecta. Correr " +
            "scripts/migrate-identidades.ts --apply.",
        );
      }
      throw new UnauthorizedException("Credenciales inválidas");
    }
    if (coincidencias.length > 1) {
      throw new UnauthorizedException(
        "Este username existe en más de un tenant — inicia sesión con tu email, o indica tenantCodigo.",
      );
    }

    const [unica] = coincidencias;
    const membership = await this.identityService.findActiveMembershipByUsuarioLocal(
      unica.tenantId,
      unica.usuarioLocalId,
    );
    if (!membership) {
      throw new UnauthorizedException("Credenciales inválidas");
    }

    const identidad = await this.identityService.findById(membership.identidadId);
    if (!identidad) {
      throw new UnauthorizedException("Credenciales inválidas");
    }

    return { identidad, tenantCodigoResuelto: unica.tenantCodigo };
  }

  async login(dto: LoginDto) {
    const { identidad, tenantCodigoResuelto } = await this.resolveIdentidadPorLogin(
      dto.username,
      dto.tenantCodigo,
    );
    await this.identityService.verifyPassword(identidad, dto.password);
    const acceso = await this.resolveTenantAccess(identidad, dto.tenantCodigo || tenantCodigoResuelto);

    // Auditoria ANTES de tocar el usuario local — si esto tira, login()
    // propaga la excepcion y no llega a emitir ningun token.
    await this.identityService.registrarAcceso({
      identidadId: identidad.id,
      tenantId: acceso.tenantId,
      via: acceso.via,
      accion: "login",
    });

    const usuario = await this.loadUsuarioLocalParaAcceso(
      acceso.tenantId,
      acceso.via,
      acceso.usuarioLocalId,
    );

    await this.withTenantRepo(acceso.tenantId, Usuario, (repo) =>
      repo.update(usuario.id, { ultimoLogin: new Date() }),
    );

    const payload = this.buildTokenPayload({
      usuario,
      tenantId: acceso.tenantId,
      identidadId: identidad.id,
      rolPlataforma: identidad.rolPlataforma,
      via: acceso.via,
    });

    return {
      accessToken: this.jwtService.sign(payload),
      refreshToken: this.jwtService.sign(payload, { expiresIn: "30d" }),
      user: {
        id: usuario.id,
        username: usuario.username,
        nombre: usuario.nombre,
        rol: usuario.rol,
        tiendaId: usuario.tiendaId,
        permissions: payload.permissions,
        facturacionPosTiendaIds: payload.facturacionPosTiendaIds,
        estadosVisibles: payload.estadosVisibles,
        tenantId: acceso.tenantId,
        via: acceso.via,
        rolPlataforma: identidad.rolPlataforma,
      },
    };
  }

  // Lista los tenants a los que esta identidad puede CAMBIARSE sin volver a
  // loguearse: sus membresias activas (via='tenant'), mas — solo si es
  // admin de plataforma — cualquier otro tenant activo (via='plataforma'),
  // igual que resolveTenantAccess() ya distingue esos dos casos en login().
  // Nunca devuelve tenants en onboarding/suspendido: no son destinos validos
  // de acceso (mismo criterio que TenantConnectionRegistry.createDataSource()).
  async listAvailableTenants(
    identidadId: number,
    rolPlataforma: RolPlataforma,
    currentTenantId: number,
  ) {
    const memberships = await this.identityService.getActiveMemberships(identidadId);
    const porTenantId = new Map<
      number,
      { tenantId: number; codigo: string; nombre: string; entorno: string; via: AuditoriaVia; current: boolean }
    >();

    for (const m of memberships) {
      if (!m.tenant) continue;
      porTenantId.set(m.tenantId, {
        tenantId: m.tenantId,
        codigo: m.tenant.codigo,
        nombre: m.tenant.nombre,
        entorno: m.tenant.entorno,
        via: AuditoriaVia.TENANT,
        current: m.tenantId === currentTenantId,
      });
    }

    if (rolPlataforma === RolPlataforma.ADMIN_PLATAFORMA) {
      const activos = await this.identityService.getActiveTenants();
      for (const tenant of activos) {
        if (porTenantId.has(tenant.id)) continue;
        porTenantId.set(tenant.id, {
          tenantId: tenant.id,
          codigo: tenant.codigo,
          nombre: tenant.nombre,
          entorno: tenant.entorno,
          via: AuditoriaVia.PLATAFORMA,
          current: tenant.id === currentTenantId,
        });
      }
    }

    return [...porTenantId.values()].sort((a, b) => a.nombre.localeCompare(b.nombre));
  }

  // El endpoint real de "switch tenant": reemite un token NUEVO para otro
  // tenant, nunca cambia el tenant "en el cliente" sin pasar por aca. Mismo
  // flujo que login() a partir de resolveTenantAccess() — nunca vuelve a
  // pedir contraseña porque quien llama ya probo su identidad con un JWT
  // valido (JwtAuthGuard), y rol_plataforma se relee fresco de identidades
  // (nunca del token entrante) por el mismo motivo que refreshToken().
  async switchTenant(identidadId: number, tenantCodigo: string) {
    const identidad = await this.identityService.findById(identidadId);
    if (!identidad) {
      throw new UnauthorizedException("Identidad no encontrada");
    }

    const acceso = await this.resolveTenantAccess(identidad, tenantCodigo);

    await this.identityService.registrarAcceso({
      identidadId: identidad.id,
      tenantId: acceso.tenantId,
      via: acceso.via,
      accion: "switch",
    });

    const usuario = await this.loadUsuarioLocalParaAcceso(
      acceso.tenantId,
      acceso.via,
      acceso.usuarioLocalId,
    );

    const rolPlataforma = await this.identityService.getFreshRolPlataforma(identidad.id);

    const payload = this.buildTokenPayload({
      usuario,
      tenantId: acceso.tenantId,
      identidadId: identidad.id,
      rolPlataforma,
      via: acceso.via,
    });

    return {
      accessToken: this.jwtService.sign(payload),
      refreshToken: this.jwtService.sign(payload, { expiresIn: "30d" }),
      user: {
        id: usuario.id,
        username: usuario.username,
        nombre: usuario.nombre,
        rol: usuario.rol,
        tiendaId: usuario.tiendaId,
        permissions: payload.permissions,
        facturacionPosTiendaIds: payload.facturacionPosTiendaIds,
        estadosVisibles: payload.estadosVisibles,
        tenantId: acceso.tenantId,
        via: acceso.via,
        // rolPlataforma LOCAL (releida fresca arriba), nunca identidad.rolPlataforma
        // — mismo motivo que el payload del token: no arrastrar un rol de
        // plataforma revocado desde el momento en que se cargo `identidad`.
        rolPlataforma,
      },
    };
  }

  async refreshToken(refreshToken: string) {
    let payload: any;
    try {
      payload = this.jwtService.verify(refreshToken);
    } catch {
      throw new UnauthorizedException("Token inválido o expirado");
    }

    const { tenantId, identidadId, usuarioLocalId, via } = payload;

    // Mismo criterio que login(): auditar antes de re-verificar el usuario
    // local. Un refresh tambien es acceso a datos del tenant, no solo una
    // formalidad de firma.
    await this.identityService.registrarAcceso({
      identidadId,
      tenantId,
      via,
      accion: "refresh",
    });

    const usuario = await this.loadUsuarioLocalParaAcceso(tenantId, via, usuarioLocalId);

    // rol_plataforma SIEMPRE releido de identidades aca — nunca
    // payload.rolPlataforma. Es exactamente lo que evita que un cambio de
    // rol_plataforma (ej. revocar admin_plataforma) quede sin efecto
    // mientras el refresh token viejo siga vivo.
    const rolPlataforma = await this.identityService.getFreshRolPlataforma(identidadId);

    const newPayload = this.buildTokenPayload({
      usuario,
      tenantId,
      identidadId,
      rolPlataforma,
      via,
    });

    return {
      accessToken: this.jwtService.sign(newPayload),
      refreshToken: this.jwtService.sign(newPayload, { expiresIn: "30d" }),
    };
  }

  async getProfile(userId: number, tenantId: number) {
    const usuario = await this.withTenantRepo(tenantId, Usuario, (repo) =>
      repo.findOne({
        where: { id: userId },
        select: [
          "id",
          "username",
          "nombre",
          "email",
          "rol",
          "tiendaId",
          "createdAt",
          "permisos",
        ],
        relations: ["tienda"],
      }),
    );

    if (!usuario) {
      throw new UnauthorizedException("Usuario no encontrado");
    }

    return this.serializeUser(usuario);
  }

  async listUsers(tenantId: number) {
    const rows = await this.withTenantRepo(tenantId, Usuario, (repo) =>
      repo.find({ relations: ["tienda"], order: { id: "ASC" } }),
    );
    return rows.map((u) => this.serializeUser(u));
  }

  async listTiendas(tenantId: number) {
    const rows = await this.withTenantRepo(tenantId, Tienda, (repo) =>
      repo
        .createQueryBuilder("tienda")
        .where("(tienda.status IS NULL OR tienda.status <> :inactive)", {
          inactive: 1,
        })
        .andWhere("(tienda.alta IS NULL OR tienda.alta <> :altaInactiva)", {
          altaInactiva: "N",
        })
        .andWhere("(tienda.esTransito IS NULL OR tienda.esTransito <> :transito)", {
          transito: "S",
        })
        .orderBy("tienda.id", "ASC")
        .getMany(),
    );

    return rows.map((tienda) => this.serializeTienda(tienda));
  }

  // Solo para tenants sin ERP/POSTouch real detras (ej. un demo de ventas):
  // maetie ahi existe (CreateErpTablesIfMissing) pero nadie la puebla nunca,
  // asi que "Tiendas activas" queda en 0 para siempre sin esto. El id es un
  // correlativo simple (MAX+1) -- decision explicita del usuario, aceptando
  // que si el dia de mañana ese tenant conecta un ERP real, ese ERP podria
  // ya usar ese mismo numero de tienda; en ese caso hay que reconciliar a
  // mano, no es un caso que este metodo intente prevenir.
  async createTienda(tenantId: number, dto: CreateTiendaDto) {
    const permiteTiendas = await this.tallerParametros.getBooleanConFallbackExplicit(
      tenantId,
      "tiendas.permite_crear",
    );
    if (!permiteTiendas) {
      throw new ForbiddenException(
        "Este tenant no permite crear tiendas -- activá 'Permitir crear tiendas' en Configuración → Tiendas.",
      );
    }

    return this.withTenantRepo(tenantId, Tienda, async (tiendaRepo) => {
      const raw = await tiendaRepo
        .createQueryBuilder("tienda")
        .select("MAX(tienda.id)", "max")
        .getRawOne<{ max: number | null }>();
      const nextId = (raw?.max || 0) + 1;

      const tienda = tiendaRepo.create({
        id: nextId,
        nombre: dto.nombre,
        codigo: dto.codigo ?? null,
        direccion: dto.direccion ?? null,
        telefonoImpresion: dto.telefonoImpresion ?? null,
        status: 0,
        alta: "S",
        esTransito: "N",
        creadaEnTaller: true,
      });

      const saved = await tiendaRepo.save(tienda);
      return this.serializeTienda(saved);
    });
  }

  async updateTiendaPrintConfig(tenantId: number, id: number, dto: UpdateTiendaPrintDto) {
    return this.withTenantRepo(tenantId, Tienda, async (tiendaRepo) => {
      const tienda = await tiendaRepo.findOne({ where: { id } });

      if (!tienda) {
        throw new NotFoundException(`Tienda ${id} no encontrada`);
      }

      // maetie es una tabla del ERP con datos reales de otros sistemas. Un
      // save() de la entidad completa reescribiria TODAS sus columnas
      // (status, descripcion, etc.) con lo que se leyo al principio de este
      // metodo — si otro sistema (POS, etc.) cambio algo entre el findOne y
      // este punto, ese cambio se perderia. update() solo toca las 4 columnas
      // taller_* que mbtaller agrego por migracion, nunca las demas.
      const partial: Partial<Tienda> = {};

      if (dto.nombreImpresion !== undefined) {
        partial.nombreImpresion = dto.nombreImpresion?.trim() || null;
      }

      if (dto.direccionImpresion !== undefined) {
        partial.direccionImpresion = dto.direccionImpresion?.trim() || null;
      }

      if (dto.telefonoImpresion !== undefined) {
        partial.telefonoImpresion = dto.telefonoImpresion?.trim() || null;
      }

      if (dto.logoUrl !== undefined) {
        partial.logoUrl = dto.logoUrl?.trim() || null;
      }

      if (Object.keys(partial).length > 0) {
        await tiendaRepo.update(id, partial);
      }

      const updated = await tiendaRepo.findOne({ where: { id } });
      return this.serializeTienda(updated || tienda);
    });
  }

  // Config por TIENDA, no por tenant: "computadora" en POSTouch resuelve a
  // una tienda/caja puntual (Domicilio::createOrdenesEdngt -> login($parG)
  // fija $par segun el computadora recibido) -- un tenant con varias tiendas
  // necesita una fila de config por cada una, nunca una sola para todas.
  // Nunca devuelve la contraseña (ni cifrada ni en claro) al cliente.
  async updateTiendaFacturacionPos(
    tenantId: number,
    id: number,
    dto: UpdateTiendaFacturacionPosDto,
    usuarioId: number,
  ): Promise<{ tiendaId: number; habilitada: boolean }> {
    // maetie ya no se toca para esta config: todo vive en taller_parametros
    // (spec §9.1). Solo usamos withTenantRepo para confirmar que la tienda
    // existe antes de escribir el grupo.
    await this.withTenantRepo(tenantId, Tienda, async (tiendaRepo) => {
      const tienda = await tiendaRepo.findOne({ where: { id } });
      if (!tienda) {
        throw new NotFoundException(`Tienda ${id} no encontrada`);
      }
    });

    const valores: Partial<Record<TallerParametroClave, unknown>> = {
      "facturacion_pos.habilitada": dto.habilitada,
    };
    if (dto.url !== undefined) valores["facturacion_pos.url"] = dto.url;
    if (dto.username !== undefined) valores["facturacion_pos.username"] = dto.username;
    if (dto.password) valores["facturacion_pos.password"] = dto.password;
    if (dto.computadora !== undefined) valores["facturacion_pos.computadora"] = dto.computadora;
    if (dto.precioDecimales !== undefined) valores["facturacion_pos.precio_decimales"] = dto.precioDecimales;
    if (dto.precioIncluyeIva !== undefined) valores["facturacion_pos.precio_incluye_iva"] = dto.precioIncluyeIva;
    if (dto.pluProductosTaller !== undefined) {
      valores["facturacion_pos.plu_productos_taller"] = dto.pluProductosTaller.trim();
    }
    if (dto.pluServiciosTaller !== undefined) {
      valores["facturacion_pos.plu_servicios_taller"] = dto.pluServiciosTaller.trim();
    }

    await this.tallerParametros.setGrupoParaTiendaExplicit(tenantId, id, valores, usuarioId);

    return { tiendaId: id, habilitada: dto.habilitada };
  }

  async createUser(tenantId: number, dto: CreateUserDto, actorIdentidadId: number) {
    this.ensureNotPlatformSystemUsername(dto.username);

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

    const saved = await this.withTenantRepo(tenantId, Usuario, async (usuarioRepo) => {
      const existing = await usuarioRepo.findOne({ where: { username: dto.username } });
      if (existing) {
        throw new ConflictException(`El usuario ${dto.username} ya existe`);
      }

      await this.ensureTiendaActivaPorId(tenantId, dto.tiendaId);

      const user = usuarioRepo.create({
        username: dto.username,
        nombre: dto.nombre,
        email: dto.email,
        rol: dto.rol,
        tiendaId: dto.tiendaId ?? null,
        passwordHash: hashedPassword,
        permisos: dto.permisos,
        facturacionPosTiendaIds: dto.facturacionPosTiendaIds ?? null,
        estadosVisibles: dto.estadosVisibles ?? null,
        activo: true,
      });

      return usuarioRepo.save(user);
    });

    // Sin esto, el usuario queda en taller_usuarios pero jamas puede
    // loguearse: el login multi-tenant resuelve el acceso via
    // TenantMembership, no solo por encontrar la fila local por username —
    // encontrado en produccion creando usuarios reales desde el panel que
    // despues no podian entrar. findOrCreateIdentidadParaProvisioning reusa
    // la identidad si el email ya existe en OTRO tenant (misma persona,
    // varios accesos); createMembership es idempotente por (identidad,
    // tenant).
    const identidad = await this.identityService.findOrCreateIdentidadParaProvisioning(
      dto.email,
      dto.nombre,
      hashedPassword,
    );
    await this.identityService.createMembership({
      identidadId: identidad.id,
      tenantId,
      usuarioLocalId: saved.id,
      otorgadoPorIdentidadId: actorIdentidadId,
    });

    const fullSaved = await this.withTenantRepo(tenantId, Usuario, (usuarioRepo) =>
      usuarioRepo.findOne({ where: { id: saved.id }, relations: ["tienda"] }),
    );
    return this.serializeUser(fullSaved || saved);
  }

  async updateUser(
    tenantId: number,
    id: number,
    dto: UpdateUserDto,
    actorIdentidadId: number,
  ) {
    if (dto.username !== undefined) {
      this.ensureNotPlatformSystemUsername(dto.username);
    }

    return this.withTenantRepo(tenantId, Usuario, async (usuarioRepo) => {
      const user = await usuarioRepo.findOne({ where: { id } });
      if (!user) {
        throw new NotFoundException(`Usuario ${id} no encontrado`);
      }

      // La fila de sistema no se crea desde aca (bloqueado arriba por
      // username), pero si ya existe (aprovisionada por el panel) tampoco se
      // edita desde la gestion de usuarios del tenant — mismo motivo: nadie
      // deberia poder renombrarla, cambiarle el rol o desactivarla y romper
      // en silencio el acceso de plataforma.
      if (user.username === PLATFORM_SYSTEM_USERNAME) {
        throw new BadRequestException(
          "La fila de sistema de acceso de plataforma no se puede editar desde la gestion de usuarios.",
        );
      }

      if (dto.username && dto.username !== user.username) {
        const exists = await usuarioRepo.findOne({ where: { username: dto.username } });
        if (exists) {
          throw new ConflictException(`El usuario ${dto.username} ya existe`);
        }
        user.username = dto.username;
      }

      let nuevoPasswordHash: string | undefined;
      if (dto.password) {
        nuevoPasswordHash = await bcrypt.hash(dto.password, 10);
        user.passwordHash = nuevoPasswordHash;
      }

      if (dto.nombre !== undefined) user.nombre = dto.nombre;
      if (dto.email !== undefined) user.email = dto.email;
      if (dto.rol !== undefined) user.rol = dto.rol;
      if (dto.permisos !== undefined) user.permisos = dto.permisos;
      if (dto.facturacionPosTiendaIds !== undefined) user.facturacionPosTiendaIds = dto.facturacionPosTiendaIds;
      if (dto.estadosVisibles !== undefined) user.estadosVisibles = dto.estadosVisibles;
      if (dto.tiendaId !== undefined) {
        await this.ensureTiendaActivaPorId(tenantId, dto.tiendaId);
        user.tiendaId = dto.tiendaId || null;
      }

      // El login valida contra identidades.email/passwordHash, no contra
      // esta fila (ver comentario en IdentityService.updateCredenciales) --
      // sin esto, cambiar el correo o la contraseña aca quedaba invisible
      // para el login pese a guardarse "bien" en taller_usuarios.
      if (dto.email !== undefined || nuevoPasswordHash !== undefined) {
        let membership = await this.identityService.findActiveMembershipByUsuarioLocal(
          tenantId,
          id,
        );

        // Usuarios de antes de que existiera este sistema de identidades
        // (ej. migrados a mano, o de un tenant que nunca corrio el script de
        // backfill) no tienen membership -- reparar aca, en vez de fallar en
        // silencio para siempre, es lo unico que evita que este mismo
        // problema reaparezca cada vez que alguien intente editar a una de
        // esas personas. Mismo flujo que createUser(): reusa la identidad si
        // ya existe por email (misma persona, otro tenant), la crea si no.
        if (!membership) {
          const emailParaIdentidad = dto.email !== undefined ? dto.email : user.email;
          if (!emailParaIdentidad) {
            this.logger.warn(
              `Usuario ${id} del tenant ${tenantId} no tiene membership activa ni correo -- no se pudo reparar el acceso.`,
            );
          } else {
            const identidad = await this.identityService.findOrCreateIdentidadParaProvisioning(
              emailParaIdentidad,
              user.nombre,
              nuevoPasswordHash ?? user.passwordHash,
            );
            membership = await this.identityService.createMembership({
              identidadId: identidad.id,
              tenantId,
              usuarioLocalId: id,
              otorgadoPorIdentidadId: actorIdentidadId,
            });
            this.logger.log(
              `Membership reparada para usuario ${id} del tenant ${tenantId} (identidad ${identidad.id}).`,
            );
          }
        }

        if (membership) {
          await this.identityService.updateCredenciales(membership.identidadId, {
            email: dto.email,
            passwordHash: nuevoPasswordHash,
          });
        }
      }

      const saved = await usuarioRepo.save(user);
      const fullSaved = await usuarioRepo.findOne({
        where: { id: saved.id },
        relations: ["tienda"],
      });
      return this.serializeUser(fullSaved || saved);
    });
  }

  async setUserActive(tenantId: number, id: number, activo: boolean) {
    return this.withTenantRepo(tenantId, Usuario, async (repo) => {
      const user = await repo.findOne({ where: { id } });
      if (!user) {
        throw new NotFoundException(`Usuario ${id} no encontrado`);
      }
      if (user.username === PLATFORM_SYSTEM_USERNAME) {
        throw new BadRequestException(
          "La fila de sistema de acceso de plataforma no se puede desactivar desde la gestion de usuarios.",
        );
      }
      user.activo = activo;
      await repo.save(user);
      return { ok: true };
    });
  }
}
