import { KIND } from "./constants/KIND.ts";
import { MODULE } from "./constants/MODULE.ts";
import { OPTIONS } from "./constants/OPTIONS.ts";
import { AlephaError } from "./errors/AlephaError.ts";
import { CircularDependencyError } from "./errors/CircularDependencyError.ts";
import { ContainerLockedError } from "./errors/ContainerLockedError.ts";
import { TooLateSubstitutionError } from "./errors/TooLateSubstitutionError.ts";
import { coerceObject } from "./helpers/coerceStrings.ts";
import { Primitive } from "./helpers/primitive.ts";
import { __alephaRef } from "./helpers/ref.ts";
import type { Async } from "./interfaces/Async.ts";
import type { LoggerInterface } from "./interfaces/LoggerInterface.ts";
import {
  type InstantiableClass,
  isClass,
  type RunFunction,
  type Service,
  type ServiceEntry,
} from "./interfaces/Service.ts";
import type { Atom, AtomStatic, TAtomObject } from "./primitives/$atom.ts";
import type { Computed } from "./primitives/$computed.ts";
import type { InjectOptions } from "./primitives/$inject.ts";
import { Module, type WithModule } from "./primitives/$module.ts";
import { AlsProvider, type StateScope } from "./providers/AlsProvider.ts";
import { CodecManager } from "./providers/CodecManager.ts";
import { EventManager } from "./providers/EventManager.ts";
import { StateManager } from "./providers/StateManager.ts";
import {
  type Static,
  type TObject,
  type TSchema,
  z,
} from "./providers/TypeProvider.ts";

/**
 * Core container of the Alepha framework.
 *
 * It is responsible for managing the lifecycle of services,
 * handling dependency injection,
 * and providing a unified interface for the application.
 *
 * @example
 * ```ts
 * import { Alepha, run } from "alepha";
 *
 * class MyService {
 *   // business logic here
 * }
 *
 * const alepha = Alepha.create({
 *   // state, env, and other properties
 * })
 *
 * alepha.with(MyService);
 *
 * run(alepha); // trigger .start (and .stop) automatically
 * ```
 *
 * ### Alepha Factory
 *
 * Alepha.create() is an enhanced version of new Alepha().
 * - It merges `process.env` with the provided state.env when available.
 * - It populates the test hooks for Vitest or Jest environments when available.
 *
 * new Alepha() is fine if you don't need these helpers.
 *
 * ### Platforms & Environments
 *
 * Alepha is designed to work in various environments:
 * - **Browser**: Runs in the browser, using the global `window` object.
 * - **Serverless**: Runs in serverless environments like Vercel or Vite.
 * - **Test**: Runs in test environments like Jest or Vitest.
 * - **Production**: Runs in production environments, typically with NODE_ENV set to "production".
 * * You can check the current environment using the following methods:
 *
 * - `isBrowser()`: Returns true if the App is running in a browser environment.
 * - `isServerless()`: Returns true if the App is running in a serverless environment.
 * - `isTest()`: Returns true if the App is running in a test environment.
 * - `isProduction()`: Returns true if the App is running in a production environment.
 *
 * ### State & Environment
 *
 * The state of the Alepha container is stored in the `store` property.
 * Most important property is `store.env`, which contains the environment variables.
 *
 * ```ts
 * const alepha = Alepha.create({ env: { MY_VAR: "value" } });
 *
 * // You can access the environment variables using alepha.env
 * console.log(alepha.env.MY_VAR); // "value"
 *
 * // But you should use $env() primitive to get typed values from the environment.
 * class App {
 *   env = $env(
 *     z.object({
 *  	   MY_VAR: z.text(),
 *     })
 *   );
 * }
 * ```
 *
 * ### Modules
 *
 * Modules are a way to group services together.
 * You can register a module using the `$module` primitive.
 *
 * ```ts
 * import { $module } from "alepha";
 *
 * class MyLib {}
 *
 * const myModule = $module({
 *   name: "my.project.module",
 *   services: [MyLib],
 * });
 * ```
 *
 * Do not use modules for small applications.
 *
 * ### Hooks
 *
 * Hooks are a way to run async functions from all registered providers/services.
 * You can register a hook using the `$hook` primitive.
 *
 * ```ts
 * import { $hook } from "alepha";
 *
 * class App {
 * 	 log = $logger();
 * 	 onCustomerHook = $hook({
 * 			on: "my:custom:hook",
 * 			handler: () => {
 * 		 	  this.log?.info("App is being configured");
 * 	 		},
 * 	  });
 * 	}
 *
 * Alepha.create()
 * 	 .with(App)
 * 	 .start()
 * 	 .then(alepha => alepha.events.emit("my:custom:hook"));
 * ```
 *
 * 	Hooks are fully typed. You can create your own hooks by using module augmentation:
 *
 * 	```ts
 * 	declare module "alepha" {
 * 		interface Hooks {
 * 		  "my:custom:hook": {
 * 				arg1: string;
 * 		  }
 * 		}
 * 	}
 * 	```
 *
 * 	@module alepha
 */
export class Alepha {
  /**
   * Creates a new instance of the Alepha container with some helpers:
   *
   * - merges `process.env` with the provided state.env when available.
   * - populates the test hooks for Vitest or Jest environments when available.
   *
   * If you are not interested about these helpers, you can use the constructor directly.
   */
  public static create(state: Partial<State> = {}): Alepha {
    // merge process.env with the state.env
    if (typeof process === "object" && typeof process.env === "object") {
      state.env = {
        ...process.env,
        ...state.env,
      };
    }

    // force production mode when building with vite
    // vite's define replaces `process.env.NODE_ENV` with `"production"` at build time,
    // but the spread above doesn't carry it (especially in workerd/cloudflare).
    if (typeof process === "object" && process.env?.NODE_ENV === "production") {
      state.env ??= {};
      Object.assign(state.env, {
        NODE_ENV: "production",
      });
    }

    // Browser/Vite builds have no `process`, so both blocks above are
    // stripped as dead code — leaving `NODE_ENV` unset and `isProduction()`
    // always false in the browser bundle. Fall back to Vite's build-time
    // `import.meta.env.PROD` flag, baked at build time. It is `undefined`
    // outside Vite (server bundles, workerd) so this is a no-op there.
    if (state.env?.NODE_ENV == null) {
      const viteEnv = (import.meta as { env?: { PROD?: boolean } }).env;
      if (viteEnv?.PROD) {
        state.env ??= {};
        Object.assign(state.env, {
          NODE_ENV: "production",
        });
      }
    }

    const alepha = new Alepha(state);

    if (alepha.isTest()) {
      // inject global hooks for testing purposes
      // > for vitest, { globals: true } is required in the config
      const g = globalThis as any;
      const beforeAll = state["alepha.test.beforeAll"] ?? g.beforeAll;
      const afterAll = state["alepha.test.afterAll"] ?? g.afterAll;
      const afterEach = state["alepha.test.afterEach"] ?? g.afterEach;
      const onTestFinished =
        state["alepha.test.onTestFinished"] ?? g.onTestFinished;

      beforeAll?.(() => alepha.start());
      afterAll?.(() => alepha.stop());

      try {
        onTestFinished?.(() => alepha.stop());
      } catch (_error) {
        // ignore
      }

      alepha.store
        .set("alepha.test.beforeAll", beforeAll)
        .set("alepha.test.afterAll", afterAll)
        .set("alepha.test.afterEach", afterEach)
        .set("alepha.test.onTestFinished", onTestFinished);
    } else {
      state["alepha.logger"] ??= {
        trace: console.log,
        debug: console.debug,
        info: console.log,
        warn: console.warn,
        error: console.error,
      };
    }

    return alepha;
  }

  /**
   * Flag indicating whether the App won't accept any further changes.
   * Pass to true when #start() is called.
   */
  protected locked = false;

  /**
   * True if the App has been configured.
   */
  protected configured = false;

  /**
   * True if the App has started.
   */
  protected started = false;

  /**
   * True if the App is ready.
   */
  protected ready = false;

  /**
   * In-flight startup promise returned by boot().
   *
   * Concurrent callers of start() share this same promise. Cleared on
   * success, failure, or stale-detection.
   */
  protected startPromise?: Promise<this>;

  /**
   * Timestamp (performance.now) when the current boot() began.
   *
   * In serverless environments (e.g. Cloudflare Workers), the runtime can
   * kill an invocation mid-startup without running cleanup. The global
   * Alepha instance persists, leaving startPromise as a never-settling
   * promise. We detect this by comparing elapsed time against STARTUP_TIMEOUT.
   */
  protected startedAt = 0;

  /**
   * During the instantiation process, we keep a list of pending instantiations.
   * > It allows us to detect circular dependencies.
   */
  protected pendingInstantiations: Service[] = [];

  /**
   * Cache for environment variables.
   * > It allows us to avoid parsing the same schema multiple times.
   */
  protected cacheEnv: Map<TSchema, any> = new Map();

  /**
   * List of modules that are registered in the container.
   *
   * Modules are used to group services and provide a way to register them in the container.
   */
  protected modules: Array<Module> = [];

  /**
   * List of service substitutions.
   *
   * Services registered here will be replaced by the specified service when injected.
   */
  protected substitutions = new Map<Service, { use: Service }>();

  /**
   * Registry of primitives.
   */
  protected primitiveRegistry = new Map<Service<Primitive>, Array<Primitive>>();

  /**
   *  List of all services + how they are provided.
   */
  protected registry: Map<Service, ServiceDefinition> = new Map();

  // -------------------------------------------------------------------------------------------------------------------

  /**
   * Node.js feature that allows to store context across asynchronous calls.
   *
   * This is used for logging, tracing, and other context-related features.
   *
   * Mocked for browser environments.
   */
  public context: AlsProvider;

  /**
   * Event manager to handle lifecycle events and custom events.
   */
  public events: EventManager;

  /**
   * State manager to store arbitrary values.
   */
  public store: StateManager<State>;

  /**
   * Codec manager for encoding and decoding data with different formats.
   *
   * Supports multiple codec formats (JSON, Protobuf, etc.) with a unified interface.
   */
  public codec: CodecManager;

  /**
   * Get logger instance.
   */
  public get log(): LoggerInterface | undefined {
    return this.store.get("alepha.logger");
  }

  /**
   * The environment variables for the App.
   */
  public get env(): Readonly<Env> {
    return this.store.get("env") ?? {};
  }

  constructor(state: Partial<State> = {}) {
    this.store = this.inject(StateManager, {
      args: [state],
    });
    this.events = this.inject(EventManager);
    this.events.logFn = () => this.log;
    this.context = this.inject(AlsProvider);
    this.codec = this.inject(CodecManager);
  }

  public fork<R>(callback: () => R, data: Record<string, any> = {}): R {
    return this.context.run(callback, data);
  }

  public get<R>(target: Computed<R>, scope?: StateScope): R;
  public get<T extends TAtomObject>(
    target: Atom<T>,
    scope?: StateScope,
  ): Static<T>;
  public get<Key extends keyof State>(
    target: Key,
    scope?: StateScope,
  ): State[Key] | undefined;
  public get(target: any, scope?: StateScope): any {
    return this.store.get(target, scope);
  }

  public set<T extends TAtomObject>(
    target: Atom<T>,
    value: AtomStatic<T>,
  ): this;
  public set<Key extends keyof State>(
    target: Key,
    value: State[Key] | undefined,
  ): this;
  public set(target: any, value: any): this {
    this.store.set(target, value);
    return this;
  }

  /**
   * Reset an atom back to its declared default value.
   */
  public reset<T extends TAtomObject>(target: Atom<T>): this {
    this.store.reset(target);
    return this;
  }

  /**
   * True when start() is called.
   *
   * -> No more services can be added, it's over, bye!
   */
  public isLocked(): boolean {
    return this.locked;
  }

  /**
   * Returns whether the App is configured.
   *
   * It means that Alepha#configure() has been called.
   *
   * > By default, configure() is called automatically when start() is called, but you can also call it manually.
   */
  public isConfigured(): boolean {
    return this.configured;
  }

  /**
   * Returns whether the App has started.
   *
   * It means that #start() has been called but maybe not all services are ready.
   */
  public isStarted(): boolean {
    return this.started;
  }

  /**
   * True if the App is ready. It means that Alepha is started AND ready() hook has beed called.
   */
  public isReady(): boolean {
    return this.ready;
  }

  /**
   * True if the App is running in a Continuous Integration environment.
   */
  public isCI(): boolean {
    if (this.env.GITHUB_ACTIONS) {
      return true;
    }

    return !!this.env.CI;
  }

  /**
   * True if the App is running in a browser environment.
   */
  public isBrowser(): boolean {
    return typeof window !== "undefined"; // pretty cheap check
  }

  /**
   * Returns whether the App is running in Vite dev mode.
   */
  public isViteDev(): boolean {
    if (this.isBrowser()) {
      return false;
    }

    return !!this.env.VITE_ALEPHA_DEV;
  }

  /**
   * Returns whether the App is running in Bun.js environment.
   */
  public isBun(): boolean {
    return "Bun" in globalThis;
  }

  /**
   * Returns whether the App is running in a serverless environment.
   */
  public isServerless(): boolean {
    if (this.isBrowser()) {
      return false;
    }

    if (this.env.ALEPHA_SERVERLESS) {
      return true;
    }

    // Vercel support
    if (this.env.VERCEL_REGION) {
      return true;
    }

    // Cloudflare Workers support
    if (
      typeof global === "object" &&
      typeof (global as any).Cloudflare === "object"
    ) {
      return true;
    }

    return false;
  }

  /**
   * Returns whether the App is in test mode. (Running in a test environment)
   *
   * > This is automatically set when running tests with Jest or Vitest.
   */
  public isTest(): boolean {
    const env = this.env.NODE_ENV;
    return env === "test";
  }

  /**
   * Returns whether the App is in production mode. (Running in a production environment)
   *
   * > This is automatically set by Vite or Vercel. However, you have to set it manually when running Docker apps.
   */
  public isProduction(): boolean {
    const env = this.env.NODE_ENV;
    return env === "production";
  }

  // -------------------------------------------------------------------------------------------------------------------

  /**
   * Max time (ms) a boot() is allowed to run before being considered stale.
   *
   * In serverless runtimes (Cloudflare Workers, etc.) an invocation can be
   * killed mid-startup. The global Alepha instance survives, but
   * `startPromise` becomes a zombie that never settles.
   * Any new invocation that sees an older-than-STARTUP_TIMEOUT promise
   * discards it and boots fresh.
   */
  protected static readonly STARTUP_TIMEOUT = 30_000;

  /**
   * Starts the App.
   *
   * - Lock any further changes to the container.
   * - Run "configure" hook for all services. Primitives will be processed.
   * - Run "start" hook for all services. Providers will connect/listen/...
   * - Run "ready" hook for all services. This is the point where the App is ready to serve requests.
   *
   * Concurrent callers share the same boot promise. If a previous boot was
   * abandoned (serverless invocation killed), the stale promise is detected
   * and a fresh boot is triggered.
   *
   * @return A promise that resolves when the App has started.
   */
  public async start(): Promise<this> {
    if (this.ready) {
      this.log?.debug("App is already started, skipping...");
      return this;
    }

    if (this.startPromise) {
      const elapsed = performance.now() - this.startedAt;
      if (elapsed > Alepha.STARTUP_TIMEOUT) {
        this.log?.warn(
          `Previous start attempt is stale (${Math.round(elapsed)}ms ago), resetting...`,
        );
        this.resetStartup();
      } else {
        this.log?.warn("App is already starting, waiting for it to finish...");
        return this.startPromise;
      }
    }

    this.startedAt = performance.now();
    this.startPromise = this.boot();
    return this.startPromise;
  }

  /**
   * Perform the actual startup sequence.
   *
   * Separated from start() so that start() remains a thin state-machine
   * and boot() owns the real work. The promise returned here is stored as
   * `startPromise` and shared with concurrent callers.
   */
  protected async boot(): Promise<this> {
    const now = performance.now();
    this.log?.info("Starting App...");

    try {
      for (const [key] of this.substitutions.entries()) {
        this.inject(key);
      }

      const target = this.store.get("alepha.target");
      if (target) {
        this.store.set("alepha.target", undefined);
        this.modules = [];
        this.registry = new Map();
        this.primitiveRegistry = new Map();
        this.pendingInstantiations = [];
        this.events.clear();
        delete (target as any)[MODULE];
        this.with(target);
        for (const [key] of this.substitutions.entries()) {
          this.inject(key);
        }
      }

      this.locked = true;

      await this.events.emit("configure", this, { log: true });

      this.configured = true;

      await this.events.emit("start", this, { log: true });

      this.started = true;

      await this.events.emit("ready", this, { log: true });

      this.log?.info(
        `App is now ready [${Math.round(performance.now() - now)}ms]`,
      );

      this.ready = true;
      return this;
    } catch (error) {
      this.resetStartup();
      throw error;
    }
  }

  /**
   * Reset startup state so that a fresh boot() can be attempted.
   *
   * Called when:
   * - boot() fails (error during configure/start/ready hooks)
   * - a stale startPromise is detected (serverless invocation was killed)
   */
  protected resetStartup(): void {
    this.startPromise = undefined;
    this.startedAt = 0;
    this.locked = false;
    this.configured = false;
    this.started = false;
    this.ready = false;
  }

  /**
   * Stops the App.
   *
   * - Run "stop" hook for all services.
   *
   * Stop will NOT reset the container.
   * Stop will NOT unlock the container.
   *
   * > Stop is used to gracefully shut down the application, nothing more. There is no "restart".
   *
   * @return A promise that resolves when the App has stopped.
   */
  public async stop(): Promise<void> {
    if (!this.started) {
      return;
    }

    this.log?.info("Stopping App...");
    await this.events.emit("stop", this, { log: true });
    this.log?.info("App is now off");

    this.started = false;
    this.ready = false;
    this.startPromise = undefined;
    this.startedAt = 0;
  }

  /**
   * Destroys the App and clears all internal state.
   *
   * Use this for HMR in development mode to prevent duplicate class registrations.
   * Unlike stop(), this method fully resets the container state.
   *
   * @return A promise that resolves when the App has been destroyed.
   */
  public async destroy(): Promise<void> {
    await this.stop();

    // Clear all internal state to prevent duplicate classes on HMR reload
    this.modules = [];
    this.registry = new Map();
    this.primitiveRegistry = new Map();
    this.pendingInstantiations = [];
    this.substitutions = new Map();
    this.cacheEnv = new Map();
    this.events.clear();

    // Reset flags
    this.locked = false;
    this.configured = false;
  }

  // -------------------------------------------------------------------------------------------------------------------

  /**
   * Check if entry is registered in the container.
   */
  public has(
    entry: ServiceEntry,
    opts: {
      /**
       * Check if the entry is registered in the pending instantiation stack.
       *
       * @default true
       */
      inStack?: boolean;
      /**
       * Check if the entry is registered in the container registry.
       *
       * @default true
       */
      inRegistry?: boolean;
      /**
       * Check if the entry is registered in the substitutions.
       *
       * @default true
       */
      inSubstitutions?: boolean;
      /**
       * Where to look for registered services.
       *
       * @default this.registry
       */
      registry?: Map<Service, ServiceDefinition>;
    } = {},
  ): boolean {
    if (entry === Alepha) {
      return true;
    }

    const {
      inStack = true,
      inRegistry = true,
      inSubstitutions = true,
      registry = this.registry,
    } = opts;

    const { provide } =
      typeof entry === "object" && "provide" in entry
        ? entry
        : { provide: entry };

    if (inSubstitutions) {
      const substitute = this.substitutions.get(provide);
      if (substitute) {
        return true;
      }
    }

    if (inRegistry) {
      const match = registry.get(provide);
      if (match) {
        return true;
      }
    }

    if (inStack) {
      const substitute = this.substitutions.get(provide)?.use;
      if (substitute && this.pendingInstantiations.includes(substitute)) {
        return true;
      }

      return this.pendingInstantiations.includes(provide);
    }

    return false;
  }

  /**
   * Registers the specified service in the container.
   *
   * - If the service is ALREADY registered, the method does nothing.
   * - If the service is NOT registered, a new instance is created and registered.
   *
   * Method is chainable, so you can register multiple services in a single call.
   *
   * > ServiceEntry allows to provide a service **substitution** feature.
   *
   * @example
   * ```ts
   * class A { value = "a"; }
   * class B { value = "b"; }
   * class M { a = $inject(A); }
   *
   * Alepha.create().with({ provide: A, use: B }).get(M).a.value; // "b"
   * ```
   *
   * > **Substitution** is an advanced feature that allows you to replace a service with another service.
   * > It's useful for testing or for providing different implementations of a service.
   * > If you are interested in configuring a service, use Alepha#configure() instead.
   *
   * @param serviceEntry - The service to register in the container.
   * @return Current instance of Alepha.
   */
  public with<T extends object>(
    serviceEntry: ServiceEntry<T> | { default: ServiceEntry<T> },
  ): this {
    // Early cutoff: if a $mode already claimed the target, skip further registrations.
    // The target's own dependencies are resolved via $inject during its constructor.
    if (this.store?.get("alepha.target")) {
      return this;
    }

    const entry: ServiceEntry<T> =
      "default" in serviceEntry ? serviceEntry.default : serviceEntry;

    // just check if the entry is not present in the pending instantiation stack
    // Alepha#get will handle the rest
    if (
      this.has(entry, {
        inSubstitutions: false,
        inRegistry: false,
      })
    ) {
      return this;
    }

    const isSubstitution = typeof entry === "object";
    if (isSubstitution) {
      if (entry.provide === entry.use) {
        this.inject(entry.provide);
        return this;
      }

      if (!this.substitutions.has(entry.provide) && !this.has(entry.provide)) {
        if (this.started) {
          throw new ContainerLockedError();
        }

        // inherit of module, if service has no module
        if (
          MODULE in entry.provide &&
          typeof entry.provide[MODULE] === "function"
        ) {
          (entry.use as WithModule)[MODULE] ??= entry.provide[MODULE];
        }

        this.substitutions.set(entry.provide, {
          use: entry.use,
        });
      } else if (!entry.optional) {
        throw new TooLateSubstitutionError(entry.provide.name, entry.use.name);
      }

      return this;
    }

    this.inject(entry);

    return this;
  }

  /**
   * @alias {@link Alepha#with}.
   */
  public register<T extends object>(
    serviceEntry: ServiceEntry<T> | { default: ServiceEntry<T> },
  ): this {
    return this.with(serviceEntry);
  }

  /**
   * Get an instance of the specified service from the container.
   *
   * @see {@link InjectOptions} for the available options.
   */
  public inject<T extends object>(
    service: Service<T> | string,
    opts: InjectOptions<T> = {},
  ): T {
    const lifetime = opts.lifetime ?? "singleton";
    const parent =
      opts.parent !== undefined ? opts.parent : (__alephaRef?.parent ?? Alepha);

    const transient = lifetime === "transient";
    // TODO: warn-once when scoped lifetime silently falls back to the global
    //   singleton registry. This happens when AsyncLocalStorage is not available
    //   (typically in the browser, where AlsProvider is a no-op) — the user
    //   asked for per-request isolation and got a cross-request singleton.
    //   Today this is silent. Plan: detect (this.context.get("registry") === undefined)
    //   on a "scoped" inject, log a one-shot warning via the Logger module
    //   ("[alepha] scoped DI requested for <Service> but no AsyncLocalStorage
    //   context — falling back to singleton. This is expected in browser
    //   builds; in server runtimes ensure the request is wrapped in
    //   alepha.context.run()."). Gate behind a Set<Service> so we don't spam.
    const registry =
      lifetime === "scoped"
        ? (this.context.get<Map<Service, ServiceDefinition>>("registry") ??
          this.registry)
        : this.registry;

    // If the requested type is the container, the current instance is returned.
    if ((service as any) === Alepha) {
      return this as any;
    }

    if (typeof service === "string") {
      for (const [key, value] of registry.entries()) {
        if (key.name === service) {
          return value.instance as T;
        }
      }
      throw new AlephaError(`Service not found: ${service}`);
    }

    const substitute = this.substitutions.get(service);
    if (substitute) {
      return this.inject(substitute.use, {
        parent,
        lifetime,
      });
    }

    const index = this.pendingInstantiations.indexOf(service);
    if (index !== -1 && !transient) {
      throw new CircularDependencyError(
        service.name,
        this.pendingInstantiations.slice(0, index).map((it) => it.name),
      );
    }

    if (!transient) {
      // the requested type is searched in the container
      const match = registry.get(service);
      if (match) {
        if (!match.parents.includes(parent) && parent !== service) {
          match.parents.push(parent);
        }

        return match.instance;
      }

      // The locked-container guard protects the GLOBAL registry from gaining new
      // singletons after start. A `scoped` inject targets a per-request registry
      // (a fresh Map created by `alepha.context.run` per request, resolved at
      // `registry` above) and is stored only there — it never mutates the global
      // container. Refusing it after start makes `lifetime: "scoped"` unusable at
      // the only time it matters (while handling a request), so the guard must
      // apply only when we are instantiating into the global registry itself.
      if (this.started && registry === this.registry) {
        const mod = (service as WithModule)[MODULE]?.name;
        throw new ContainerLockedError(
          `Container is locked. No more services can be added. Attempted to inject '${service.name}' from '${parent?.name}'. ${mod ? `Module '${mod}' is not registered ?` : ""}`,
        );
      }
    }

    const module = (service as WithModule)[MODULE];
    if (module && typeof module === "function") {
      this.with(module);
    }

    // check if service has been registered by a module
    if (this.has(service, { registry }) && !transient) {
      // if the service is already registered, we just return the instance
      return this.inject(service, { parent, lifetime });
    }

    const instance: T = this.new(service, opts.args);

    const definition: ServiceDefinition<T> = {
      parents: [parent],
      instance,
    };

    if (!transient) {
      registry.set(service, definition);
    }

    // [feature]: modules - it's just a way to group services together
    if (instance instanceof Module) {
      this.modules.push(instance);

      const parent = __alephaRef.parent;

      // propagate the current module
      __alephaRef.parent = instance.constructor as Service;

      instance.register(this);

      // restore the previous $get context
      __alephaRef.parent = parent;
    }

    return instance;
  }

  // -------------------------------------------------------------------------------------------------------------------

  /**
   * Merge additional environment variables into the env store at runtime.
   *
   * Serverless entrypoints (Cloudflare Workers) receive their secrets and
   * vars on the runtime `env` binding rather than `process.env`, so they are
   * absent from `alepha.env` — which is frozen at `create()` from
   * `process.env`. Call this from the entrypoint to lift the binding's string
   * values into `alepha.env`, so `$env` and `alepha.env.*` resolve them (e.g.
   * `PUBLIC_URL`).
   *
   * Only string values are lifted; non-string bindings (D1, R2, KV, queues,
   * …) are skipped. Existing env values win, so explicit configuration is
   * never clobbered. The env cache is cleared so subsequent `$env` reads see
   * the merged values.
   */
  public loadEnv(env: Record<string, unknown>): this {
    const incoming: Record<string, string> = {};
    for (const [key, value] of Object.entries(env)) {
      if (typeof value === "string") {
        incoming[key] = value;
      }
    }
    this.store.set("env", { ...incoming, ...this.env });
    this.cacheEnv.clear();
    return this;
  }

  /**
   * Applies environment variables to the provided schema and state object.
   *
   * It replaces also all templated $ENV inside string values.
   *
   * @param schema - The schema object to apply environment variables to.
   * @return The schema object with environment variables applied.
   */
  public parseEnv<T extends TObject>(schema: T): Static<T> {
    if (this.cacheEnv.has(schema)) {
      return this.cacheEnv.get(schema) as Static<T>;
    }

    // Env vars are strings on the wire — coerce declared fields to their
    // schema types (boolean/number) before strict validation.
    const config = this.codec.validate(
      schema,
      coerceObject(schema, this.env),
    ) as Record<string, any>;

    // Sort keys longest-first to prevent substring collisions
    // (e.g. $PORT must not match inside $PORT_NAME).
    const sortedKeys = Object.keys(config).sort((a, b) => b.length - a.length);

    let changed = true;

    // Resolve $KEY references. Multiple passes handle transitive references
    // where a replacement introduces a new $KEY that was already checked
    // (e.g. C=$B, B=$A, A=value — single pass leaves C as "$A").

    for (let pass = 0; changed && pass < 10; pass++) {
      changed = false;
      for (const key in config) {
        if (typeof config[key] !== "string") continue;
        for (const env of sortedKeys) {
          const before = config[key] as string;
          config[key] = before.replaceAll(`$${env}`, String(config[env] ?? ""));
          if (config[key] !== before) {
            changed = true;
          }
        }
      }
    }

    this.cacheEnv.set(schema, config);

    return config as Static<T>;
  }

  /**
   * Get all environment variable schemas and their parsed values.
   *
   * This is useful for DevTools to display all expected environment variables.
   */
  public getEnvSchemas(): Array<{
    schema: TSchema;
    values: Record<string, any>;
  }> {
    const result: Array<{ schema: TSchema; values: Record<string, any> }> = [];
    for (const [schema, values] of this.cacheEnv.entries()) {
      result.push({ schema, values });
    }
    return result;
  }

  // -------------------------------------------------------------------------------------------------------------------

  /**
   * Dump the current dependency graph of the App.
   *
   * This method returns a record where the keys are the names of the services.
   */
  public graph(): Record<
    string,
    { from: string[]; as?: string[]; module?: string }
  > {
    for (const [key] of this.substitutions.entries()) {
      if (!this.has(key)) {
        this.inject(key);
      }
    }

    const graph: Record<
      string,
      { from: string[]; as?: string[]; module?: string }
    > = {};

    for (const [provide, { parents }] of this.registry.entries()) {
      if (provide.name === "") {
        // ignore anonymous classes
        continue;
      }

      if (Module.is(provide)) {
        continue;
      }

      graph[provide.name] = {
        from: parents.filter((it) => !!it).map((it) => it.name),
      };

      const aliases = this.substitutions
        .entries()
        .filter((it) => it[1].use === provide)
        .map((it) => it[0].name)
        .toArray();

      if (aliases.length) {
        graph[provide.name].as = aliases;
      }

      const module = Module.of(provide);
      if (module) {
        graph[provide.name].module = module.name;
      }
    }

    return graph;
  }

  public dump(): AlephaDump {
    // Force-instantiate the graph FIRST. `$env` is lazy — a key only lands in
    // `cacheEnv` once its owning service is constructed. `graph()` injects
    // every registered substitution (without emitting start/ready hooks), so
    // calling it before reading `cacheEnv` ensures `env` reflects the whole
    // app, not just whatever happened to be instantiated already. Reading
    // `cacheEnv` first (the old order) under-reported env on a not-yet-started
    // app and was internally inconsistent: dump() instantiated providers for
    // `providers` but never saw their env.
    const providers = this.graph();

    const env: Record<string, AlephaDumpEnvVariable> = {};
    for (const [schema] of this.cacheEnv.entries()) {
      const ref = schema as any;
      // zod object: `.properties` aliases `.shape`; `required` field names come
      // from `z.schema.requiredKeys` (zod has no `.required` array — that name
      // is the `.required()` method). Metadata (description/enum/default) lives
      // on the unwrapped inner schema, under `.meta()`.
      const shape = (ref.properties ?? {}) as Record<string, TSchema>;
      const required = new Set(z.schema.requiredKeys(ref));
      for (const [key, value] of Object.entries(shape)) {
        const prop = value as any;
        const inner = z.schema.unwrap(prop) as any;
        const enumValues = z.schema.isEnum(inner)
          ? z.schema.enumValues(inner)
          : undefined;
        env[key] = {
          description: inner?.description ?? prop?.description,
          default: z.schema.getDefault(prop) as string | undefined,
          required: required.has(key) ? true : undefined,
          enum: enumValues?.length
            ? ([...enumValues] as Array<string>)
            : undefined,
        };
      }
    }

    return {
      env,
      providers,
    };
  }

  public services<T extends object>(base: Service<T>): Array<T> {
    const list: Array<T> = [];
    for (const [key, value] of this.registry.entries()) {
      if (value.instance instanceof base) {
        list.push(value.instance as T);
      }
    }
    return list;
  }

  /**
   * Get all primitives of the specified type.
   */
  public primitives<TPrimitive extends Primitive>(
    factory:
      | {
          [KIND]: InstantiableClass<TPrimitive>;
        }
      | string,
  ): Array<TPrimitive> {
    if (typeof factory === "string") {
      const key1 = factory.toLowerCase().replace("$", "");
      const key2 = `${key1}primitive`;
      for (const [key, value] of this.primitiveRegistry.entries()) {
        const name = key.name.toLowerCase();
        if (name === key1 || name === key2) {
          return value as Array<TPrimitive>;
        }
      }
      return [];
    }
    return (this.primitiveRegistry.get(factory[KIND]) ??
      []) as Array<TPrimitive>;
  }

  // -------------------------------------------------------------------------------------------------------------------

  protected new<T extends object>(service: Service<T>, args: any[] = []): T {
    // we keep a tree of dependencies to detect circular dependencies
    // it's also useful for cleaning are global cursor
    this.pendingInstantiations.push(service);

    //
    // we use a global cursor to store the current context and definition
    // as new() is synchronous, there is no worry to do that
    //
    __alephaRef.alepha = this;
    __alephaRef.service = service;

    const instance: T = isClass(service)
      ? new service(...args)
      : (((service as RunFunction)(...args) ?? {}) as T);

    const obj = instance as unknown as Record<string, any>;
    for (const [key, value] of Object.entries(obj)) {
      if (value instanceof Primitive) {
        this.processPrimitive(value, key);
      }
      if (
        typeof value === "object" &&
        value !== null &&
        typeof value[OPTIONS] === "object" &&
        "getter" in value[OPTIONS]
      ) {
        const getter = value[OPTIONS].getter as keyof State;
        Object.defineProperty(obj, key, {
          get: () => this.store.get(getter),
        });
      }
    }

    this.pendingInstantiations.pop();

    // tree is empty, now we can clean the global cursor
    if (this.pendingInstantiations.length === 0) {
      __alephaRef.alepha = undefined;
    }

    __alephaRef.service =
      this.pendingInstantiations[this.pendingInstantiations.length - 1];

    return instance;
  }

  protected processPrimitive(value: Primitive, propertyKey = "") {
    value.config.propertyKey = propertyKey;
    (value as any).onInit();

    const kind = value.constructor as Service;
    const existing = this.primitiveRegistry.get(kind);
    if (existing) {
      existing.push(value);
    } else {
      this.primitiveRegistry.set(kind, [value]);
    }
  }
}

// ---------------------------------------------------------------------------------------------------------------------

export interface Hook<T extends keyof Hooks = any> {
  caller?: Service;
  priority?: "first" | "last";
  before?: Service[];
  after?: Service[];
  callback: (payload: Hooks[T]) => Async<void>;
}

// ---------------------------------------------------------------------------------------------------------------------

export interface AlephaDump {
  env: Record<string, AlephaDumpEnvVariable>;
  providers: Record<string, { from: string[]; as?: string[]; module?: string }>;
}

export interface AlephaDumpEnvVariable {
  description: string;
  default?: string;
  required?: boolean;
  enum?: Array<string>;
}

// ---------------------------------------------------------------------------------------------------------------------

/**
 * This is how we store services in the Alepha container.
 */
interface ServiceDefinition<T extends object = any> {
  /**
   * The instance of the class or type definition.
   * Mostly used for caching / singleton but can be used for other purposes like forcing the instance.
   */
  instance: T;

  /**
   * List of classes which use this class.
   */
  parents: Array<Service | null>;
}

// ---------------------------------------------------------------------------------------------------------------------

export interface Env {
  [key: string]: string | boolean | number | undefined;

  /**
   * Optional environment variable that indicates the current environment.
   */
  NODE_ENV?: string;

  /**
   * Optional name of the application.
   */
  APP_NAME?: string;

  /**
   * Optional root module name.
   */
  MODULE_NAME?: string;

  /**
   * The secret key used for signing JWTs, encrypting cookies, and other security features.
   */
  APP_SECRET?: string;

  /**
   * Public-facing base URL of the deployed app (e.g. "https://lore.alepha.dev").
   *
   * Used to render absolute links — emails, OAuth callbacks, sitemap. On the
   * Cloudflare platform it is auto-derived from the configured production
   * domain and pushed as a Worker secret by `alepha platform up`; otherwise
   * set it explicitly in `.env.<env>`. Unset → empty, and absolute-link
   * builders fall back to relative URLs.
   */
  PUBLIC_URL?: string;
}

// ---------------------------------------------------------------------------------------------------------------------

export interface State {
  [key: string]: unknown;

  /**
   * Environment variables for the application.
   */
  env?: Readonly<Env>;

  /**
   * Logger instance to be used by the Alepha container.
   *
   * @internal
   */
  "alepha.logger"?: LoggerInterface;

  /**
   * If defined, the Alepha container will only register this service and its dependencies.
   *
   * @example
   * ```ts
   * class MigrateCmd {
   *   db = $inject(DatabaseProvider);
   *   alepha = $inject(Alepha);
   *   env = $env(
   *     z.object({
   *       MIGRATE: z.boolean().optional(),
   *     }),
   *   );
   *
   *   constructor() {
   *     if (this.env.MIGRATE) {
   *       this.alepha.set("alepha.target", MigrateCmd);
   *     }
   *   }
   *
   *   ready = $hook({
   *     on: "ready",
   *     handler: async () => {
   *       if (this.env.MIGRATE) {
   *         await this.db.migrate();
   *       }
   *     },
   *   });
   * }
   * ```
   */
  "alepha.target"?: Service;

  // test hooks

  /**
   * Bind to Vitest 'beforeAll' hook.
   * Used for testing purposes.
   * This is automatically attached if Alepha#create() detects a test environment and global 'beforeAll' is available.
   */
  "alepha.test.beforeAll"?: (run: any) => any;

  /**
   * Bind to Vitest 'afterAll' hook.
   * Used for testing purposes.
   * This is automatically attached if Alepha#create() detects a test environment and global 'afterAll' is available.
   */
  "alepha.test.afterAll"?: (run: any) => any;

  /**
   * Bind to Vitest 'afterEach' hook.
   * Used for testing purposes.
   * This is automatically attached if Alepha#create() detects a test environment and global 'afterEach' is available.
   */
  "alepha.test.afterEach"?: (run: any) => any;

  /**
   * Bind to Vitest 'onTestFinished' hook.
   * Used for testing purposes.
   * This is automatically attached if Alepha#create() detects a test environment and global 'onTestFinished' is available.
   */
  "alepha.test.onTestFinished"?: (run: any) => any;

  /**
   * List of static assets to be copied to the output directory during the build process.
   *
   * Used for Alepha-based applications that require static assets.
   *
   * See cli/services/ViteUtils for more details.
   */
  "alepha.build.assets"?: Array<string>;
}

// ---------------------------------------------------------------------------------------------------------------------

export interface Hooks {
  /**
   * Used for testing purposes.
   */
  echo: unknown;

  /**
   * Triggered during the configuration phase. Before the start phase.
   */
  configure: Alepha;

  /**
   * Triggered during the start phase. When `Alepha#start()` is called.
   */
  start: Alepha;

  /**
   * Triggered during the ready phase. After the start phase.
   */
  ready: Alepha;

  /**
   * Triggered during the stop phase.
   *
   * - Stop should be called after a SIGINT or SIGTERM signal in order to gracefully shutdown the application. (@see `run()` method)
   *
   */
  stop: Alepha;

  /**
   * Triggered when a state value is mutated.
   */
  "state:mutate": {
    /**
     * The key of the state that was mutated.
     */
    key: keyof State;

    /**
     * The new value of the state.
     */
    value: any;

    /**
     * The previous value of the state.
     */
    prevValue: any;
  };

  /**
   * Triggered the first time an atom is registered in the state manager.
   */
  "state:register": {
    /**
     * The atom that was registered.
     */
    atom: Atom<any, any>;
  };
}
