import { IModuleServer, ModuleParams } from "@erp/core/api";
import { logger } from "../logger";
import { initModels, registerModels } from "./model-loader";
import { getService, listServices, registerService } from "./service-registry";

const registeredModules: Map<string, IModuleServer> = new Map();
const initializedModules = new Set<string>();
const visiting = new Set<string>(); // para detección de ciclos
const initializationOrder: string[] = []; // orden de init → para warmup

// Config opcional para warmup (valores por defecto seguros)
const WARMUP_TIMEOUT_MS = Number(process.env.WARMUP_TIMEOUT_MS) || 10_000;
const WARMUP_STRICT =
  String(process.env.WARMUP_STRICT ?? "false")
    .toLowerCase()
    .trim() === "true";

/**
Registra un módulo del servidor en el registry.
Lanza error si el nombre ya existe.
*/
export function registerModule(pkg: IModuleServer) {
  if (!pkg?.name) {
    throw new Error('❌ Invalid module: missing "name" property');
  }
  if (registeredModules.has(pkg.name)) {
    throw new Error(`❌ Module "${pkg.name}" already registered`);
  }
  registeredModules.set(pkg.name, pkg);
  logger.info(`📦 Module enqueued: "${pkg.name}"`, { label: "moduleRegistry" });
}

/**
Inicializa todos los módulos registrados (resolviendo dependencias),
luego inicializa los modelos (Sequelize) en bloque y, por último, ejecuta warmups opcionales.
*/
export async function initModules(params: ModuleParams) {
  for (const name of registeredModules.keys()) {
    await loadModule(name, params, []); // secuencial para logs deterministas
  }

  await withPhase("global", "initModels", async () => {
    await initModels(params);
  });

  await warmupModules(params);
}

/**
Carga recursivamente un módulo y sus dependencias con detección de ciclos.
*/
async function loadModule(name: string, params: ModuleParams, stack: string[]) {
  if (initializedModules.has(name)) return;

  if (visiting.has(name)) {
    // Ciclo detectado: construir traza legible
    const cyclePath = [...stack, name].join(" -> ");
    throw new Error(`❌ Cyclic dependency detected: ${cyclePath}`);
  }

  const pkg = registeredModules.get(name);
  if (!pkg) {
    throw new Error(
      `❌ Module "${name}" not found (required by: ${stack[stack.length - 1] ?? "root"})`
    );
  }

  visiting.add(name);
  stack.push(name);

  // 1) Resolver dependencias primero (en orden)
  const deps = pkg.dependencies || [];
  for (const dep of deps) {
    await loadModule(dep, params, stack.slice());
  }

  // 2) Inicializar el módulo (permite async)
  await withPhase(name, "init", async () => {
    await Promise.resolve(pkg.init(params));
  });

  // 3) Registrar dependencias que expone (permite async)
  const pkgApi = await withPhase(name, "registerDependencies", async () => {
    return await Promise.resolve(
      pkg.registerDependencies?.({
        ...params,
        listServices,
        getService,
      })
    );
  });

  // 4) Registrar modelos de Sequelize, si existen
  if (pkgApi?.models) {
    await withPhase(name, "registerModels", async () => {
      await Promise.resolve(registerModels(pkgApi.models, params, { moduleName: pkg.name }));
    });
  }

  // 5) Registrar servicios, si existen
  if (pkgApi?.services && typeof pkgApi.services === "object") {
    await withPhase(name, "registerServices", async () => {
      await Promise.resolve(registerService(pkg.name, pkgApi.services));
    });
  }

  initializedModules.add(name);
  initializationOrder.push(name); // recordamos el orden para warmup

  visiting.delete(name);
  stack.pop();

  logger.info(`✅ Module "${name}" registered`, { label: "moduleRegistry" });
}

/**
Ejecuta warmup() opcional de cada módulo en orden de inicialización.
Si WARMUP_STRICT=true, un fallo aborta el arranque; si no, se avisa y continúa.
*/
async function warmupModules(params: ModuleParams) {
  logger.info("🌡️ Warmup: starting...", { label: "moduleRegistry" });

  for (const name of initializationOrder) {
    const pkg = registeredModules.get(name);
    if (!pkg) continue;

    const maybeWarmup = (pkg as unknown as { warmup?: (p: ModuleParams) => Promise<void> | void })
      .warmup;

    if (typeof maybeWarmup === "function") {
      try {
        await withPhase(name, "warmup", () =>
          withTimeout(Promise.resolve(maybeWarmup(params)), WARMUP_TIMEOUT_MS, `${name}.warmup`)
        );
      } catch (error) {
        if (WARMUP_STRICT) {
          logger.error("⛔️ Warmup failed (strict=true). Aborting.", {
            label: "moduleRegistry",
            module: name,
            error,
          });
          throw error;
        }

        logger.warn("⚠️ Warmup failed but continuing (strict=false)", {
          label: "moduleRegistry",
          module: name,
          error,
        });
      }
    }
  }

  logger.info("🌡️ Warmup: done.", { label: "moduleRegistry" });
}

/**
Helper para anotar fase y módulo en logs (inicio/fin/duración) y errores.
*/
async function withPhase<T>(
  moduleName: string,
  phase:
    | "init"
    | "registerDependencies"
    | "registerModels"
    | "registerServices"
    | "initModels"
    | "warmup",
  fn: () => Promise<T> | T
): Promise<T> {
  const startedAt = Date.now();
  logger.info(`▶️ phase=start module=${moduleName} ${phase}`, {
    label: "moduleRegistry",
    module: moduleName,
    phase,
  });

  try {
    const out = await fn();
    const duration = Date.now() - startedAt;
    logger.info(`✅ phase=done module=${moduleName} ${phase} durationMs=${duration}`, {
      label: "moduleRegistry",
      module: moduleName,
      phase,
      durationMs: duration,
    });
    return out;
  } catch (error: any) {
    const duration = Date.now() - startedAt;
    logger.error(
      `⛔️ Module "${moduleName}" failed at phase="${phase}" after ${duration}ms: ${error?.message ?? error}`,
      {
        label: "moduleRegistry",
        module: moduleName,
        phase,
        durationMs: duration,
        stack: error?.stack,
      }
    );
    // Re-lanzamos con contexto preservado
    const err = new Error(
      `[module=${moduleName}] phase=${phase} failed: ${error?.message ?? "Unknown error"}`
    );
    (err as any).cause = error;
    throw err;
  }
}

/**
Promesa con timeout; rechaza si excede el tiempo dado.
*/
function withTimeout<T>(p: Promise<T>, ms: number, label: string): Promise<T> {
  let timer: NodeJS.Timeout | null = null;
  const timeout = new Promise<T>((_, reject) => {
    timer = setTimeout(() => {
      reject(new Error(`Timeout after ${ms}ms (${label})`));
    }, ms).unref();
  });
  return Promise.race([p, timeout]).finally(() => {
    if (timer) clearTimeout(timer);
  }) as Promise<T>;
}
