import { Injectable } from "@nestjs/common";
import { AsyncLocalStorage } from "node:async_hooks";
import { DataSource } from "typeorm";

export type TenantContext = {
  tenantId: number;
  identidadId: number;
  usuarioLocalId: number | null;
  rolTenant: string | null;
  tiendaId: number | null;
  rolPlataforma: "ninguno" | "admin_plataforma";
  via: "tenant" | "plataforma" | "publico";
  dataSource: DataSource;
};

// Nunca una variable de modulo, nunca un singleton mutable — ese es
// exactamente el bug de concurrencia que este diseño existe para evitar
// (TenantAdminJobProcessor.php de cxp: flags de proceso que se filtran entre
// jobs de tenants distintos). AsyncLocalStorage propaga el contexto por la
// cadena de promesas de UN request especifico, sin que dos requests
// concurrentes de tenants distintos puedan pisarse.
//
// DISEÑO IMPORTANTE, corregido antes de conectar esto a un Guard: un Guard
// de Nest NO puede envolver el resto del pipeline (interceptors, handler,
// respuesta) en su propio AsyncLocalStorage.run() — canActivate() devuelve
// antes de que el handler corra; ese run() se cerraria sin nada adentro. El
// contexto tiene que abrirse en un MIDDLEWARE de Express (que si envuelve
// todo el resto vía next()), como un objeto MUTABLE vacio, y TenantGuard lo
// COMPLETA despues (mismo objeto, misma referencia, seguis dentro del mismo
// run() abierto por el middleware). Por eso el store es Partial<TenantContext>
// y "completo" se define por tenantId estar presente, no por la forma del
// tipo.
@Injectable()
export class TenantContextStorage {
  private readonly storage = new AsyncLocalStorage<Partial<TenantContext>>();

  // Lo llama SOLO el middleware, una vez por request, antes de que corra
  // cualquier guard. El objeto arranca vacio a proposito.
  runEmpty<T>(fn: () => T): T {
    return this.storage.run({}, fn);
  }

  // Lo llama SOLO TenantGuard, una vez que resolvio el tenant — muta el
  // MISMO objeto que ya esta circulando en el AsyncLocalStorage del
  // middleware, no crea un contexto nuevo.
  //
  // DE UNA SOLA ESCRITURA: una vez que tenantId queda poblado, cualquier
  // segundo intento de poblar (un guard mal escrito, un interceptor que
  // "corrige" el tenant a mitad de request) LANZA en vez de pisar el
  // valor — el tenant de un request no puede cambiar despues de resuelto.
  populate(fields: TenantContext): void {
    const store = this.storage.getStore();
    if (!store) {
      throw new Error(
        "TenantContextStorage.populate() llamado sin un contexto abierto por " +
          "el middleware. Revisa que TenancyMiddleware este registrado antes " +
          "de TenantGuard en el pipeline.",
      );
    }
    if (store.tenantId !== undefined) {
      throw new Error(
        `TenantContextStorage.populate() ya fue llamado en este request (tenantId=${store.tenantId}). ` +
          "El tenant de un request se resuelve una sola vez — un segundo intento " +
          "de poblar el contexto es un bug de quien llama, nunca un cambio legitimo.",
      );
    }
    Object.assign(store, fields);
  }

  get(): Partial<TenantContext> | undefined {
    return this.storage.getStore();
  }

  // Fail-closed: quien llama esto DEBE tener un contexto CON tenantId
  // poblado (paso por TenantGuard), o el bug es de quien llama. Nunca hay
  // un tenant "por defecto".
  getOrThrow(): TenantContext {
    const store = this.storage.getStore();
    if (!store || store.tenantId === undefined) {
      throw new Error(
        "No hay contexto de tenant resuelto. Esto significa que se intento " +
          "acceder a datos de tenant fuera de una request que paso por " +
          "TenantGuard, o dentro de un callback que perdio la cadena de " +
          "promesas de AsyncLocalStorage (ej. un callback de libreria que no " +
          "preserva el contexto). Nunca hay un tenant 'por defecto'.",
      );
    }
    return store as TenantContext;
  }
}
