import * as fs from "fs";
import * as path from "path";

// Detecta accesos a datos que NO pasan por el contexto de tenant. Es
// detección de texto, no de grafo de llamadas — no ve que un servicio ya
// convertido llame a otro que no lo está. Por eso se complementa con las
// pruebas e2e (auth-guard-coverage / tenant-isolation-required), que sí
// atraviesan el grafo real.
//
// LIMITACION CONOCIDA, dejada asi a proposito: no ignora comentarios antes
// de buscar los patrones (un comentario que mencione, por ejemplo,
// "forFeature([...])" en prosa cuenta como ofensa — paso de verdad en esta
// misma ronda de trabajo). Evaluado y descartado un stripper de
// comentarios: hacerlo BIEN significa no comerse un "//" que este dentro de
// un string literal (ej. una URL como "http://..." en la config de
// WhatsApp, que existe en este mismo repo) — un stripper ingenuo podria
// CORTAR contenido real de una linea y ESCONDER una ofensa de verdad, que es
// peor que el falso positivo que resuelve. Mientras tanto: si un comentario
// dispara un falso positivo, reformular el comentario (evitar escribir los
// patrones de arriba de forma literal en prosa) es mas seguro que tocar el
// escaner.
//
// Dos categorías, deliberadamente separadas (no una sola lista):
//   OFENSAS       — deben llegar a cero. Es "LISTO".
//   BYPASSES      — withTenantRepositoryExplicit() / withTenantDataSourceExplicit(),
//                   el escape explícito del
//                   contexto ambiental. auth lo necesita (resuelve el tenant,
//                   no puede depender de un contexto que él mismo produce) y
//                   va a seguir necesitándolo incluso terminada la
//                   conversión — por diseño, no por deuda. Mezclarlo con las
//                   ofensas hacía que la baseline nunca pudiera llegar a
//                   cero, lo cual bloqueaba para siempre la regla 7(a)
//                   (nunca se podría registrar el switch de tenant). Un
//                   bypass aprobado no es una ofensa pendiente; es una
//                   excepción de diseño, contada y con lista blanca propia.
const OFFENSE_PATTERNS = [
  /@InjectRepository\s*\(/g,
  /@InjectEntityManager\s*\(/g,
  /InjectDataSource\s*\(/g,
  /@InjectConnection\s*\(/g,
  /getRepositoryToken\s*\(/g,
  /\bgetRepository\s*\(/g,
  /\bgetManager\s*\(/g,
  /\bgetConnection\s*\(/g,
  /\.manager\.transaction\s*\(/g,
  /\bdataSource\.transaction\s*\(/g,
  // DataSource/EntityManager inyectados por tipo en un constructor.
  /constructor\s*\([^)]*:\s*DataSource\b/g,
  /constructor\s*\([^)]*:\s*EntityManager\b/g,
];

const BYPASS_PATTERN = /withTenant(?:Repository|DataSource)Explicit\s*\(/g;

function countMatches(content: string, patterns: RegExp[]): number {
  let count = 0;
  for (const pattern of patterns) {
    const matches = content.match(pattern);
    count += matches ? matches.length : 0;
  }
  return count;
}

export function countOffenses(content: string): number {
  return countMatches(content, OFFENSE_PATTERNS) + countForFeatureWithoutConnectionName(content);
}

export function countBypasses(content: string): number {
  return countMatches(content, [BYPASS_PATTERN]);
}

// TypeOrmModule.forFeature([Entity1, Entity2]) sin segundo argumento ata los
// repositorios a la conexion POR DEFECTO — exactamente el mismo problema que
// @InjectRepository. Hace falta parsing con conteo de parentesis porque el
// primer argumento es un array que puede tener comas propias.
function countForFeatureWithoutConnectionName(content: string): number {
  const marker = "forFeature(";
  let searchFrom = 0;
  let count = 0;

  while (true) {
    const idx = content.indexOf(marker, searchFrom);
    if (idx === -1) return count;

    const argsStart = idx + marker.length;
    let depth = 1;
    let i = argsStart;
    let topLevelCommaFound = false;

    for (; i < content.length && depth > 0; i++) {
      const ch = content[i];
      if (ch === "(" || ch === "[" || ch === "{") depth++;
      else if (ch === ")" || ch === "]" || ch === "}") {
        depth--;
      } else if (ch === "," && depth === 1) {
        topLevelCommaFound = true;
      }
    }

    if (!topLevelCommaFound) {
      count++;
    }

    searchFrom = i;
  }
}

export function listTsFiles(dir: string, base: string): string[] {
  const entries = fs.readdirSync(dir, { withFileTypes: true });
  let files: string[] = [];
  for (const entry of entries) {
    const full = path.join(dir, entry.name);
    const rel = path.relative(base, full);
    if (entry.isDirectory()) {
      if (entry.name === "node_modules" || entry.name === "dist") continue;
      files = files.concat(listTsFiles(full, base));
    } else if (entry.name.endsWith(".ts") && !entry.name.endsWith(".spec.ts")) {
      files.push(rel.replace(/\\/g, "/"));
    }
  }
  return files;
}

// tenancy/* y control-plane/* son la implementacion del registro y el plano
// de control mismo — por definicion acceden a DataSources directamente.
// Cualquier excepcion nueva fuera de estas dos rutas requiere aprobacion
// explicita (mostrar el diff antes de agregarla).
const ALLOWLIST = [/^control-plane\//, /^tenancy\//];

// Mapa archivo -> cantidad. Cada mapa solo incluye archivos con count > 0.
export function scanCounts(srcDir: string): {
  offenses: Record<string, number>;
  bypasses: Record<string, number>;
} {
  const offenses: Record<string, number> = {};
  const bypasses: Record<string, number> = {};

  for (const relPath of listTsFiles(srcDir, srcDir)) {
    if (ALLOWLIST.some((pattern) => pattern.test(relPath))) continue;

    const content = fs.readFileSync(path.join(srcDir, relPath), "utf8");

    const offenseCount = countOffenses(content);
    if (offenseCount > 0) offenses[relPath] = offenseCount;

    const bypassCount = countBypasses(content);
    if (bypassCount > 0) bypasses[relPath] = bypassCount;
  }

  return { offenses, bypasses };
}
