import { EMPTY_MODULE_SECRET_SOURCE, type ModuleSecretSource } from "@ekklesia/contracts";
import type {
  DiscoveredModule,
  ModuleConfigurationLookup,
  ModuleConfigurationRecord,
  ModuleConfigurationSettings,
  ModuleConfigurationStore,
  ReplaceModuleConfigurationInput,
} from "@ekklesia/core/modules";
import {
  ModuleConfigurationStoreError,
  resolveModuleConfigurationFor,
} from "@ekklesia/core/modules";

const compositeKey = (moduleId: string, key: string): string => `${moduleId} ${key}`;

function settingsFrom(records: ReadonlyMap<string, string>): ModuleConfigurationSettings {
  return Object.freeze({
    records: Object.freeze(
      [...records.entries()]
        .map(([composite, value]) => {
          const [moduleId = "", key = ""] = composite.split(" ");
          return Object.freeze({ key, moduleId, value });
        })
        .sort((left, right) =>
          left.moduleId === right.moduleId
            ? left.key < right.key
              ? -1
              : left.key > right.key
                ? 1
                : 0
            : left.moduleId < right.moduleId
              ? -1
              : 1,
        ),
    ),
  });
}

/**
 * An in-memory `ModuleConfigurationStore` for tests that exercise **routing and resolution**, not
 * persistence.
 *
 * It reproduces only what those tests depend on — a replace is whole-document and scoped to one
 * module, records read back in deterministic order, and an invalid module identifier is rejected —
 * and deliberately reproduces nothing else. The real table's constraints, its composite primary key,
 * its value ceiling, and the transactional replace are proven against real PostgreSQL in
 * `core/tests/module-configuration-store.integration.ts`, so this double can never quietly stand in
 * for a persistence guarantee nobody checked.
 */
export function createInMemoryModuleConfigurationStore(
  seed: readonly ModuleConfigurationRecord[] = [],
): ModuleConfigurationStore {
  const records = new Map<string, string>(
    seed.map((record) => [compositeKey(record.moduleId, record.key), record.value] as const),
  );

  return Object.freeze({
    async readSettings(): Promise<ModuleConfigurationSettings> {
      return settingsFrom(records);
    },
    async replace(
      input: ReplaceModuleConfigurationInput,
    ): Promise<readonly ModuleConfigurationRecord[]> {
      if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(input.moduleId)) {
        throw new ModuleConfigurationStoreError("MODULE_CONFIGURATION_MODULE_ID_INVALID");
      }
      const prefix = `${input.moduleId} `;
      for (const composite of [...records.keys()]) {
        if (composite.startsWith(prefix)) {
          records.delete(composite);
        }
      }
      for (const entry of input.values) {
        records.set(compositeKey(input.moduleId, entry.key), entry.value);
      }
      return Object.freeze(
        input.values.map((entry) => Object.freeze({ ...entry, moduleId: input.moduleId })),
      );
    },
  });
}

/**
 * A secret source over a plain `{ "module-id": { key: value } }` map, so a test can supply a
 * deployment's secrets without touching `process.env`.
 *
 * Scoped exactly like the real one: a lookup needs both the module id and the key, so a test cannot
 * accidentally prove something the production source could not do.
 */
export function createFakeModuleSecretSource(
  secrets: Readonly<Record<string, Readonly<Record<string, string>>>>,
): ModuleSecretSource {
  return Object.freeze({
    get: (moduleId: string, key: string): string | undefined => secrets[moduleId]?.[key],
  });
}

export interface ModuleConfigurationOption {
  readonly discoveredModules: readonly DiscoveredModule[];
  readonly bootSettings: ModuleConfigurationSettings;
  readonly secrets: ModuleSecretSource;
  readonly store: ModuleConfigurationStore;
  readonly snapshots: ModuleConfigurationLookup;
}

/**
 * Builds the `createApiApp` configuration option the same way `startApi` does: resolve every
 * discovered module's configuration from one settings document and one secret source, then hand over
 * the resulting snapshots as the boot state.
 *
 * Composed rather than hand-written so a test can never assert against a shape the real composition
 * would not produce — in particular, `bootSettings` and `snapshots` here are always consistent with
 * each other, exactly as they are in production.
 */
export function moduleConfigurationOptionFor(
  discoveredModules: readonly DiscoveredModule[],
  options: {
    readonly records?: readonly ModuleConfigurationRecord[];
    readonly secrets?: ModuleSecretSource;
  } = {},
): ModuleConfigurationOption {
  const seed = options.records ?? [];
  const secrets = options.secrets ?? EMPTY_MODULE_SECRET_SOURCE;
  const store = createInMemoryModuleConfigurationStore(seed);
  const bootSettings = settingsFrom(
    new Map(seed.map((record) => [compositeKey(record.moduleId, record.key), record.value])),
  );
  const snapshots = new Map<
    string,
    NonNullable<ReturnType<typeof resolveModuleConfigurationFor>["configuration"]>
  >();
  for (const module of discoveredModules) {
    const resolved = resolveModuleConfigurationFor(module.manifest, {
      secrets,
      settings: bootSettings,
    });
    if (resolved.configuration) {
      snapshots.set(module.manifest.id, resolved.configuration);
    }
  }
  return { bootSettings, discoveredModules, secrets, snapshots, store };
}

/** The configuration option for a `createApiApp` caller that exercises no modules at all — the
 * explicit "no modules, no configuration" statement the required option asks for, never an implicit
 * default. */
export function emptyModuleConfiguration(): ModuleConfigurationOption {
  return moduleConfigurationOptionFor([]);
}
