/**
 * Provider system for Beignet
 *
 * Providers are modular extensions that can add new ports or replace existing ones
 * during application initialization. They support configuration via Standard Schema
 * and optional lifecycle hooks.
 */

import type { StandardSchemaV1 } from "@standard-schema/spec";

type ProviderPorts = Record<string, unknown>;
declare const noProvidedPorts: unique symbol;
type NoProvidedPorts = { [noProvidedPorts]?: never };

/**
 * Extract the output type from a Standard Schema.
 * This is the validated/parsed type that results from schema validation.
 */
export type InferOutput<T extends StandardSchemaV1> =
  StandardSchemaV1.InferOutput<T>;

/**
 * Configuration definition for a service provider.
 * Specifies the schema for validating config and optional environment variable prefix.
 */
export interface ProviderConfigDef<CfgSchema extends StandardSchemaV1> {
  /**
   * Standard Schema for validating provider configuration.
   * Can be Zod, Valibot, ArkType, or any Standard Schema compatible library.
   */
  schema: CfgSchema;

  /**
   * Optional prefix to read env vars, e.g. "REDIS_".
   * When provided, the implementation will read process.env keys starting with this prefix
   * and pass them to the schema for validation.
   */
  envPrefix?: string;

  /**
   * Field-level config overrides, keyed by schema field name. Defined values
   * are merged over the env-derived (or server-supplied) input before
   * validation, so factory options win over environment variables and still
   * satisfy required fields when the env var is absent. `undefined` values
   * are ignored.
   */
  overrides?: Record<string, unknown>;
}

/**
 * Value or promise of that value.
 */
export type MaybePromise<T> = T | Promise<T>;

/**
 * Late-bound service context factory exposed to providers.
 *
 * Calling it before all providers have started throws, so providers should
 * only invoke it from runtime entrypoints such as job dispatch, listeners, or
 * scheduled work.
 *
 * App-local providers can type the factory by declaring `Context` and
 * `ServiceInput` through the curried `createProvider<Requires, Context,
 * ServiceInput>()` form. Untyped providers see `(input: void) =>
 * Promise<unknown>`.
 */
export type ProviderServiceContextFactory<
  Context = unknown,
  ServiceInput = void,
> = (input: ServiceInput) => Promise<Context>;

/**
 * Context passed to provider lifecycle hooks.
 */
export type ProviderLifecycleContext<
  Ports = ProviderPorts,
  Context = unknown,
  ServiceInput = void,
> = {
  /**
   * Final app ports after provider setup.
   */
  ports: Readonly<Ports>;
  /**
   * Build an app service context through the server context blueprint.
   */
  createServiceContext: ProviderServiceContextFactory<Context, ServiceInput>;
};

/**
 * Result returned from provider setup.
 */
export type ProviderSetupResult<
  ProvidedPorts extends ProviderPorts,
  Ports = ProviderPorts,
  Context = unknown,
  ServiceInput = void,
> = {
  /**
   * Ports contributed by this provider.
   * Keys overwrite earlier ports with the same name at runtime. Prefer unique
   * keys unless the replacement implements the same port contract.
   */
  ports?: ProvidedPorts;

  /**
   * Optional hook called after all providers have contributed their ports.
   *
   * Declared as a method so typed providers stay assignable to loosely typed
   * provider lists. Hooks that take `ctx` with an unannotated parameter keep
   * TypeScript from inferring `ProvidedPorts` from the returned `ports`.
   * Prefer closing over setup locals, or annotate `ctx` with
   * `ProviderLifecycleContext<...>`.
   */
  start?(
    ctx: ProviderLifecycleContext<Ports & ProvidedPorts, Context, ServiceInput>,
  ): MaybePromise<void>;

  /**
   * Optional hook called when the server is stopped.
   */
  stop?(
    ctx: ProviderLifecycleContext<Ports & ProvidedPorts, Context, ServiceInput>,
  ): MaybePromise<void>;
};

/**
 * Static provider metadata used by docs and app-local tooling.
 *
 * Metadata is descriptive. It does not change provider setup, ordering, or
 * runtime port merging behavior.
 *
 * Reusable provider packages should also declare package-owned
 * `beignet.provider` metadata in package.json so external tooling can inspect
 * provider facts without importing runtime code.
 */
export interface ServiceProviderMetadata {
  /**
   * Package that exports this provider, when it comes from a reusable package.
   */
  packageName?: string;
  /**
   * App port keys this provider contributes or replaces.
   */
  ports?: readonly string[];
  /**
   * App port keys this provider expects previous providers or base app ports to
   * have installed before setup runs.
   */
  requires?: readonly string[];
  /**
   * Environment variables this provider reads directly or via config loading.
   */
  env?: readonly string[];
  /**
   * Devtools watcher names this provider can emit through provider
   * instrumentation.
   */
  watchers?: readonly string[];
}

/**
 * A service provider that can extend or replace ports during app initialization.
 *
 * Providers support:
 * - Configuration via Standard Schema (any compatible library: Zod, Valibot, etc.)
 * - Returning ports with new capabilities (e.g., cache, mailer)
 * - Replacing existing ports by returning the same key
 * - Optional start/stop hooks
 *
 * @example
 * ```ts
 * const cacheProvider = createProvider({
 *   name: "cache-redis",
 *   config: {
 *     schema: z.object({ URL: z.string().url() }),
 *     envPrefix: "REDIS_",
 *   },
 *   async setup({ config }) {
 *     const client = new Redis(config.URL);
 *     return {
 *       ports: {
 *         cache: {
 *           get: (key) => client.get(key),
 *           set: (key, value) => client.set(key, value),
 *         },
 *       },
 *       stop: () => client.quit(),
 *     };
 *   },
 * });
 * ```
 */
export interface ServiceProvider<
  Ports,
  CfgSchema extends StandardSchemaV1 = StandardSchemaV1<void, void>,
  ProvidedPorts extends ProviderPorts = NoProvidedPorts,
  Context = unknown,
  ServiceInput = void,
> {
  /**
   * Unique name for this provider (used for logging/debugging)
   */
  name: string;

  /**
   * Optional static metadata for docs and diagnostics.
   */
  metadata?: ServiceProviderMetadata;

  /**
   * Optional configuration definition.
   * If provided, the config will be loaded and validated before calling setup.
   */
  config?: ProviderConfigDef<CfgSchema>;

  /**
   * Setup phase: create the ports this provider contributes.
   * Called during server initialization before provider `start` hooks and
   * before the server handles requests.
   *
   * @param ctx.ports - Ports contributed by previous providers
   * @param ctx.config - Validated config (if config was defined), or undefined
   * @param ctx.createServiceContext - Late-bound service context factory.
   * Throws until all providers have started, so call it lazily from runtime
   * entrypoints such as job dispatchers and event listeners.
   */
  setup(ctx: {
    ports: Readonly<Ports>;
    config: InferOutput<CfgSchema> | undefined;
    createServiceContext: ProviderServiceContextFactory<Context, ServiceInput>;
  }): MaybePromise<
    ProviderSetupResult<ProvidedPorts, Ports, Context, ServiceInput>
  >;

  /**
   * Type-only marker for ports this provider contributes.
   * Runtime provider objects do not need to set this property.
   */
  readonly __providedPorts?: ProvidedPorts;
}

/**
 * A provider configuration schema whose concrete validation library and input
 * shape are intentionally erased while its validated output may stay typed.
 *
 * Reusable provider packages use this in their named provider return types so
 * internal Zod schemas do not become part of the package's public API.
 */
export type AnyProviderConfigSchema<
  // biome-ignore lint/suspicious/noExplicitAny: arbitrary provider consumers may erase config output
  Output = any,
> = StandardSchemaV1<
  // biome-ignore lint/suspicious/noExplicitAny: reusable provider return types intentionally erase config input
  any,
  Output
>;

/**
 * Loosely typed service provider.
 *
 * Required-port, config, app-context, and service-input generics are erased
 * here so any provider created with `createProvider(...)` — including the
 * typed curried form — stays assignable. Use this for code that works across
 * arbitrary providers, such as provider lists and test helpers.
 */
export type AnyServiceProvider = ServiceProvider<
  unknown,
  AnyProviderConfigSchema,
  ProviderPorts,
  // biome-ignore lint/suspicious/noExplicitAny: provider context types are erased at this level
  any,
  // biome-ignore lint/suspicious/noExplicitAny: provider service-input types are erased at this level
  any
>;

/**
 * Extract the ports a provider contributes.
 */
export type ProvidedPortsOf<TProvider> =
  TProvider extends ServiceProvider<
    infer _Ports,
    infer _CfgSchema,
    infer ProvidedPorts,
    infer _Context,
    infer _ServiceInput
  >
    ? ProvidedPorts
    : NoProvidedPorts;

type UnionToIntersection<T> = (
  T extends unknown
    ? (value: T) => void
    : never
) extends (value: infer I) => void
  ? I
  : never;

/**
 * Extract and merge the ports contributed by a provider list.
 *
 * Use this with `typeof providers` to type provider-contributed ports in app
 * code without hand-written casts:
 *
 * @example
 * ```ts
 * import type { InferProviderPorts } from "@beignet/core/providers";
 * import type { providers } from "@/server/providers";
 * import type { AppPorts } from "@/ports";
 *
 * export type AppRuntimePorts = AppPorts & InferProviderPorts<typeof providers>;
 * ```
 */
export type InferProviderPorts<TProviders> =
  TProviders extends readonly unknown[]
    ? [TProviders[number]] extends [never]
      ? NoProvidedPorts
      : UnionToIntersection<ProvidedPortsOf<TProviders[number]>>
    : NoProvidedPorts;

/**
 * Helper function to create a provider with proper type inference.
 *
 * This is a simple identity function that helps TypeScript infer the correct types
 * for the provider definition.
 *
 * App-local providers can use the curried zero-argument form to declare the
 * ports they require from earlier providers plus their app context and
 * service-context input. The required ports, `ctx.ports`, and
 * `ctx.createServiceContext` are then fully typed with no casts.
 *
 * @example
 * ```ts
 * export const myProvider = createProvider({
 *   name: "my-provider",
 *   config: {
 *     schema: z.object({ apiKey: z.string() }),
 *     envPrefix: "MY_SERVICE_",
 *   },
 *   async setup({ config }) {
 *     return { ports: { myService: createMyService(config) } };
 *   },
 * });
 *
 * // Typed app-local provider:
 * export const appDatabaseProvider = createProvider<
 *   { db: DbPort<typeof schema>; devtools?: DevtoolsPort },
 *   AppContext,
 *   AppServiceContextInput
 * >()({
 *   name: "app-database",
 *   async setup({ ports, createServiceContext }) {
 *     const repositories = createRepositories(ports.db.drizzle);
 *     return { ports: repositories };
 *   },
 * });
 * ```
 */
export function createProvider<
  Requires = unknown,
  Context = unknown,
  ServiceInput = void,
>(): <
  CfgSchema extends StandardSchemaV1 = StandardSchemaV1<void, void>,
  Provided extends ProviderPorts = NoProvidedPorts,
>(
  def: ServiceProvider<Requires, CfgSchema, Provided, Context, ServiceInput>,
) => ServiceProvider<Requires, CfgSchema, Provided, Context, ServiceInput>;
export function createProvider<
  Ports = unknown,
  CfgSchema extends StandardSchemaV1 = StandardSchemaV1<void, void>,
  ProvidedPorts extends ProviderPorts = NoProvidedPorts,
>(
  def: ServiceProvider<Ports, CfgSchema, ProvidedPorts>,
): ServiceProvider<Ports, CfgSchema, ProvidedPorts>;
export function createProvider(
  def?: ServiceProvider<unknown, StandardSchemaV1, ProviderPorts>,
):
  | ServiceProvider<unknown, StandardSchemaV1, ProviderPorts>
  | ((
      definition: ServiceProvider<unknown, StandardSchemaV1, ProviderPorts>,
    ) => ServiceProvider<unknown, StandardSchemaV1, ProviderPorts>) {
  if (def === undefined) {
    return (definition) => definition;
  }
  return def;
}
