// =============================================================================
// api.ts — Shared module for the templating-vault skill
//
// Part of: .opencode/skills/templating-vault/scripts/
//
// Provides: config loading (cascading YAML), store resolution (prefix→directory
// mapping), file I/O with atomic writes, per-key metadata, Nunjucks template
// environment, type validation, and literal/write-once enforcement.
//
// Imported by all other vault scripts. Not a standalone CLI.
//
// DEPENDENCIES: nunjucks, js-yaml, Node.js >= 18
// =============================================================================
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import nunjucks from 'nunjucks';
import yaml from 'js-yaml';

// ─────────────────────────────────────────────────────────────────────────────
// TYPES
// ─────────────────────────────────────────────────────────────────────────────

export type AccessLevel = 'readonly' | 'write' | 'admin';

export interface StoreMapping {
  prefix: string;        // dot-separated key prefix (empty = catch-all)
  path: string;          // absolute or relative directory path
  access: AccessLevel;
  init?: {
    source: string;      // YAML definition file to bootstrap from
    on_exists: 'ignore' | 'validate' | 'fix';
  };
}

export interface VaultConfig {
  stores: StoreMapping[];
}

export interface KeyMeta {
  literal?: boolean;
  readonly?: boolean;
  write_once?: boolean;
  owner?: string;
  type?: string;           // string | number | integer | positive_integer | boolean | url | hex | nonempty
  validate?: {
    regex?: string;
    message?: string;
  };
  template?: boolean;
  on_absent?: string | number;  // default value for missing template deps, or "error"
  description?: string;
}

export interface StoreDefaults {
  literal?: boolean;
  readonly?: boolean;
  write_once?: boolean;
  type?: string;
  template?: boolean;
  on_absent?: string | number;
}

export interface StoreYaml {
  defaults?: StoreDefaults;
}

export interface SchemaVariable {
  literal: boolean;
  type?: string;
  description?: string;
  validate?: { regex?: string; message?: string };
}

export interface TemplateSchema {
  variables: Record<string, SchemaVariable>;
}

export interface GetResult {
  key: string;
  value: string;
  found: boolean;
  metadata?: KeyMeta;
  evaluated?: boolean;  // true if template was evaluated
}

// ─────────────────────────────────────────────────────────────────────────────
// CONSTANTS
// ─────────────────────────────────────────────────────────────────────────────

const SKILL_DIR = path.resolve(
  path.dirname(new URL(import.meta.url).pathname),
  '..',
);

const DEFAULT_STORE_DIR = '/tmp/templating-vault';

const BUILTIN_DEFAULTS: StoreDefaults = {
  literal: false,
  readonly: false,
  write_once: false,
  type: 'string',
  template: false,
  on_absent: 'error',
};

// ─────────────────────────────────────────────────────────────────────────────
// CONFIG LOADING
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Load and merge vault configs from cascading sources.
 * Later sources override earlier ones (by prefix match).
 */
export function loadConfig(cliConfigPath?: string, cliStoreDirs?: string[]): VaultConfig {
  const configs: VaultConfig[] = [];

  // 1. Built-in default
  configs.push({
    stores: [{ prefix: '', path: DEFAULT_STORE_DIR, access: 'write' }],
  });

  // 2. Repo-level
  const repoConfig = path.join(SKILL_DIR, 'config.yaml');
  if (fs.existsSync(repoConfig)) {
    const parsed = loadConfigFile(repoConfig);
    if (parsed) configs.push(parsed);
  }

  // 3. User-level
  const userConfig = path.join(os.homedir(), '.config', 'templating-vault', 'config.yaml');
  if (fs.existsSync(userConfig)) {
    const parsed = loadConfigFile(userConfig);
    if (parsed) configs.push(parsed);
  }

  // 4. CLI config file
  if (cliConfigPath && fs.existsSync(cliConfigPath)) {
    const parsed = loadConfigFile(cliConfigPath);
    if (parsed) configs.push(parsed);
  }

  // 5. CLI --store-dir entries (may contain multiple, with optional prefix:path syntax)
  if (cliStoreDirs && cliStoreDirs.length > 0) {
    const cliStores: StoreMapping[] = [];
    for (const entry of cliStoreDirs) {
      const parsed = parseStoreDirEntry(entry);
      cliStores.push(parsed);
    }
    configs.push({ stores: cliStores });
  }

  const merged = mergeConfigs(configs);

  // Validate: must have a default (empty prefix) store
  const hasDefault = merged.stores.some((s) => s.prefix === '');
  if (!hasDefault) {
    throw new Error(
      'No default store configured. At least one --store-dir without a prefix (or a catch-all in config) is required.',
    );
  }

  return merged;
}

/**
 * Parse a --store-dir CLI entry.
 *
 * Format:
 *   "/tmp/default"            → default store (empty prefix), path = /tmp/default
 *   ":/tmp/default"           → default store (empty prefix), path = /tmp/default
 *   "foo:/tmp/foo-store"      → prefix = "foo", path = /tmp/foo-store
 *   "foo.bar:/tmp/foobar"     → prefix = "foo.bar", path = /tmp/foobar
 *
 * Key prefix rules: [a-zA-Z0-9_-]+ separated by dots, no leading/trailing/consecutive dots.
 * Colons are not valid in key names or directory paths.
 */
function parseStoreDirEntry(entry: string): StoreMapping {
  const colonIdx = entry.indexOf(':');

  // No colon → entire string is a path → default store
  if (colonIdx === -1) {
    return { prefix: '', path: path.resolve(entry), access: 'write' };
  }

  // Colon at position 0 → ":/path" → default store
  if (colonIdx === 0) {
    const dirPath = entry.slice(1);
    if (!dirPath) {
      throw new Error(`Malformed --store-dir: "${entry}" — nothing after the colon`);
    }
    return { prefix: '', path: path.resolve(dirPath), access: 'write' };
  }

  // Characters before and after colon → prefix:path
  const prefixPart = entry.slice(0, colonIdx);
  const pathPart = entry.slice(colonIdx + 1);

  if (!pathPart) {
    throw new Error(`Malformed --store-dir: "${entry}" — nothing after the colon`);
  }

  // Validate key prefix
  validateKeyPrefix(prefixPart);

  return { prefix: prefixPart, path: path.resolve(pathPart), access: 'write' };
}

/** Key prefix segment: alphanumeric, hyphens, underscores */
const KEY_SEGMENT_RE = /^[a-zA-Z0-9_-]+$/;

/**
 * Validate a key prefix string.
 * Must be one or more segments of [a-zA-Z0-9_-]+ separated by dots.
 * No leading/trailing dots, no consecutive dots.
 */
function validateKeyPrefix(prefix: string): void {
  if (prefix.startsWith('.')) {
    throw new Error(`Invalid key prefix "${prefix}": cannot start with a dot`);
  }
  if (prefix.endsWith('.')) {
    throw new Error(`Invalid key prefix "${prefix}": cannot end with a dot`);
  }
  if (prefix.includes('..')) {
    throw new Error(`Invalid key prefix "${prefix}": cannot contain consecutive dots`);
  }
  const segments = prefix.split('.');
  for (const seg of segments) {
    if (!seg) {
      throw new Error(`Invalid key prefix "${prefix}": empty segment`);
    }
    if (!KEY_SEGMENT_RE.test(seg)) {
      throw new Error(
        `Invalid key prefix "${prefix}": segment "${seg}" contains invalid characters (allowed: letters, digits, hyphens, underscores)`,
      );
    }
  }
}

function loadConfigFile(filePath: string): VaultConfig | null {
  try {
    const raw = fs.readFileSync(filePath, 'utf-8');
    const doc = yaml.load(raw) as Record<string, unknown>;
    if (!doc || !Array.isArray(doc.stores)) return null;
    return {
      stores: (doc.stores as unknown[]).map((s: unknown) => {
        const store = s as Record<string, unknown>;
        // Normalize prefix: strip trailing dots for consistency
        const rawPrefix = String(store.prefix ?? '');
        const normalizedPrefix = rawPrefix.replace(/\.+$/, '');
        return {
          prefix: normalizedPrefix,
          path: resolvePath(String(store.path ?? DEFAULT_STORE_DIR), path.dirname(filePath)),
          access: (store.access as AccessLevel) ?? 'write',
          init: store.init
            ? {
                source: resolvePath(
                  String((store.init as Record<string, unknown>).source ?? ''),
                  path.dirname(filePath),
                ),
                on_exists: String(
                  (store.init as Record<string, unknown>).on_exists ?? 'ignore',
                ) as 'ignore' | 'validate' | 'fix',
              }
            : undefined,
        };
      }),
    };
  } catch {
    return null;
  }
}

function resolvePath(p: string, base: string): string {
  if (path.isAbsolute(p)) return p;
  return path.resolve(base, p);
}

function mergeConfigs(configs: VaultConfig[]): VaultConfig {
  // Later configs override earlier ones by prefix.
  const byPrefix = new Map<string, StoreMapping>();
  for (const config of configs) {
    for (const store of config.stores) {
      byPrefix.set(store.prefix, store);
    }
  }
  // Sort: longest prefix first, empty string last
  const stores = Array.from(byPrefix.values()).sort((a, b) => {
    if (a.prefix === '') return 1;
    if (b.prefix === '') return -1;
    return b.prefix.length - a.prefix.length;
  });
  return { stores };
}

// ─────────────────────────────────────────────────────────────────────────────
// STORE RESOLUTION
// ─────────────────────────────────────────────────────────────────────────────

export interface ResolvedStore {
  mapping: StoreMapping;
  dir: string;           // absolute path to the data directory
  relativeKey: string;   // key with the prefix stripped
}

/**
 * Resolve a dot-separated key to its data directory and relative path.
 * Matches the longest prefix first. Prefixes are stored WITHOUT trailing dots.
 *
 * A prefix "foo.bar" matches:
 *   - key "foo.bar" exactly
 *   - key "foo.bar.baz" (starts with "foo.bar.")
 *
 * A prefix "" (empty) is the catch-all default that matches everything.
 */
export function resolveStore(config: VaultConfig, key: string): ResolvedStore {
  for (const mapping of config.stores) {
    if (mapping.prefix === '') {
      // Catch-all default — always matches
      return { mapping, dir: mapping.path, relativeKey: key };
    }
    if (key === mapping.prefix || key.startsWith(mapping.prefix + '.')) {
      // Strip the prefix and the dot separator to get the relative key
      const relativeKey = key === mapping.prefix ? '' : key.slice(mapping.prefix.length + 1);
      return { mapping, dir: mapping.path, relativeKey };
    }
  }
  // Should never happen if there's a catch-all, but fallback
  const fallback = config.stores[config.stores.length - 1] ?? {
    prefix: '',
    path: DEFAULT_STORE_DIR,
    access: 'write' as AccessLevel,
  };
  return { mapping: fallback, dir: fallback.path, relativeKey: key };
}

/**
 * Convert a dot-separated key to a filesystem path within a store directory.
 */
export function keyToPath(storeDir: string, relativeKey: string): string {
  if (!relativeKey) return storeDir;
  const segments = relativeKey.split('.');
  return path.join(storeDir, ...segments);
}

/**
 * Get the full filesystem path for a key's value file.
 */
export function keyValuePath(config: VaultConfig, key: string): string {
  const resolved = resolveStore(config, key);
  return path.join(keyToPath(resolved.dir, resolved.relativeKey), 'value');
}

/**
 * Get the full filesystem path for a key's meta.yaml file.
 */
export function keyMetaPath(config: VaultConfig, key: string): string {
  const resolved = resolveStore(config, key);
  return path.join(keyToPath(resolved.dir, resolved.relativeKey), 'meta.yaml');
}

// ─────────────────────────────────────────────────────────────────────────────
// METADATA
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Load store-level defaults from .store.yaml in the data directory.
 */
export function loadStoreDefaults(storeDir: string): StoreDefaults {
  const storeYamlPath = path.join(storeDir, '.store.yaml');
  if (!fs.existsSync(storeYamlPath)) return { ...BUILTIN_DEFAULTS };
  try {
    const doc = yaml.load(fs.readFileSync(storeYamlPath, 'utf-8')) as StoreYaml | null;
    return { ...BUILTIN_DEFAULTS, ...(doc?.defaults ?? {}) };
  } catch {
    return { ...BUILTIN_DEFAULTS };
  }
}

/**
 * Load per-key metadata, merged with store defaults.
 */
export function loadKeyMeta(config: VaultConfig, key: string): KeyMeta {
  const resolved = resolveStore(config, key);
  const defaults = loadStoreDefaults(resolved.dir);
  const metaPath = path.join(keyToPath(resolved.dir, resolved.relativeKey), 'meta.yaml');

  let keyMeta: KeyMeta = {};
  if (fs.existsSync(metaPath)) {
    try {
      keyMeta = (yaml.load(fs.readFileSync(metaPath, 'utf-8')) as KeyMeta) ?? {};
    } catch {
      // ignore parse errors, use defaults
    }
  }

  return {
    literal: keyMeta.literal ?? defaults.literal ?? BUILTIN_DEFAULTS.literal,
    readonly: keyMeta.readonly ?? defaults.readonly ?? BUILTIN_DEFAULTS.readonly,
    write_once: keyMeta.write_once ?? defaults.write_once ?? BUILTIN_DEFAULTS.write_once,
    type: keyMeta.type ?? defaults.type ?? BUILTIN_DEFAULTS.type,
    template: keyMeta.template ?? defaults.template ?? BUILTIN_DEFAULTS.template,
    on_absent: keyMeta.on_absent ?? defaults.on_absent ?? BUILTIN_DEFAULTS.on_absent,
    owner: keyMeta.owner,
    validate: keyMeta.validate,
    description: keyMeta.description,
  };
}

/**
 * Save per-key metadata.
 */
export function saveKeyMeta(config: VaultConfig, key: string, meta: KeyMeta): void {
  const resolved = resolveStore(config, key);
  const keyDir = keyToPath(resolved.dir, resolved.relativeKey);
  fs.mkdirSync(keyDir, { recursive: true });
  const metaPath = path.join(keyDir, 'meta.yaml');
  atomicWrite(metaPath, yaml.dump(meta, { lineWidth: -1 }));
}

/**
 * Load template schema (schema.yaml) for a template key.
 */
export function loadSchema(config: VaultConfig, key: string): TemplateSchema | null {
  const resolved = resolveStore(config, key);
  const schemaPath = path.join(keyToPath(resolved.dir, resolved.relativeKey), 'schema.yaml');
  if (!fs.existsSync(schemaPath)) return null;
  try {
    const doc = yaml.load(fs.readFileSync(schemaPath, 'utf-8')) as TemplateSchema | null;
    return doc ?? null;
  } catch {
    return null;
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// VALUE I/O
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Read a raw value from the store. Returns null if not found.
 */
export function readValue(config: VaultConfig, key: string): string | null {
  const valuePath = keyValuePath(config, key);
  if (!fs.existsSync(valuePath)) return null;
  return fs.readFileSync(valuePath, 'utf-8');
}

/**
 * Write a raw value to the store with atomic write.
 */
export function writeValue(config: VaultConfig, key: string, value: string): void {
  const resolved = resolveStore(config, key);
  const keyDir = keyToPath(resolved.dir, resolved.relativeKey);
  fs.mkdirSync(keyDir, { recursive: true });
  const valuePath = path.join(keyDir, 'value');
  atomicWrite(valuePath, value);
}

/**
 * Check if a key has a value file.
 */
export function hasValue(config: VaultConfig, key: string): boolean {
  return fs.existsSync(keyValuePath(config, key));
}

/**
 * Delete a key's value and meta files. Returns true if deleted.
 */
export function deleteValue(config: VaultConfig, key: string): boolean {
  const resolved = resolveStore(config, key);
  const keyDir = keyToPath(resolved.dir, resolved.relativeKey);
  let deleted = false;

  const valuePath = path.join(keyDir, 'value');
  if (fs.existsSync(valuePath)) {
    fs.unlinkSync(valuePath);
    deleted = true;
  }

  const metaPath = path.join(keyDir, 'meta.yaml');
  if (fs.existsSync(metaPath)) {
    fs.unlinkSync(metaPath);
  }

  // Clean up empty parent directories
  cleanEmptyDirs(keyDir, resolved.dir);
  return deleted;
}

function cleanEmptyDirs(dir: string, stopAt: string): void {
  const resolved = path.resolve(dir);
  const stop = path.resolve(stopAt);
  if (resolved === stop || !resolved.startsWith(stop)) return;
  try {
    const entries = fs.readdirSync(resolved);
    if (entries.length === 0) {
      fs.rmdirSync(resolved);
      cleanEmptyDirs(path.dirname(resolved), stopAt);
    }
  } catch {
    // ignore
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// ATOMIC WRITES
// ─────────────────────────────────────────────────────────────────────────────

export function atomicWrite(filePath: string, content: string): void {
  const dir = path.dirname(filePath);
  fs.mkdirSync(dir, { recursive: true });
  const tmpPath = filePath + '.tmp.' + process.pid;
  fs.writeFileSync(tmpPath, content, 'utf-8');
  fs.renameSync(tmpPath, filePath);
}

// ─────────────────────────────────────────────────────────────────────────────
// VALIDATION
// ─────────────────────────────────────────────────────────────────────────────

const TYPE_VALIDATORS: Record<string, (val: string) => string | null> = {
  string: () => null,
  nonempty: (v) => (v.length === 0 ? 'Value must not be empty' : null),
  number: (v) => (isNaN(Number(v)) ? `"${v}" is not a valid number` : null),
  integer: (v) => (!Number.isInteger(Number(v)) || isNaN(Number(v)) ? `"${v}" is not a valid integer` : null),
  positive_integer: (v) => {
    const n = Number(v);
    if (isNaN(n) || !Number.isInteger(n)) return `"${v}" is not a valid integer`;
    if (n <= 0) return `"${v}" is not a positive integer`;
    return null;
  },
  boolean: (v) => (v !== 'true' && v !== 'false' ? `"${v}" must be "true" or "false"` : null),
  url: (v) => (v.startsWith('http://') || v.startsWith('https://') ? null : `"${v}" is not a valid URL`),
  hex: (v) => (/^[0-9a-fA-F]+$/.test(v) ? null : `"${v}" is not a valid hex string`),
};

/**
 * Validate a value against a key's metadata (type + custom regex).
 * Returns null on success, error message on failure.
 */
export function validateValue(value: string, meta: KeyMeta): string | null {
  // Type validation
  const typeCheck = meta.type ? TYPE_VALIDATORS[meta.type] : undefined;
  if (typeCheck) {
    const err = typeCheck(value);
    if (err) return err;
  }

  // Custom regex validation
  if (meta.validate?.regex) {
    const re = new RegExp(meta.validate.regex);
    if (!re.test(value)) {
      return meta.validate.message ?? `Value does not match pattern: ${meta.validate.regex}`;
    }
  }

  return null;
}

// ─────────────────────────────────────────────────────────────────────────────
// NUNJUCKS ENVIRONMENT
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Create a Nunjucks environment configured for vault template evaluation.
 * Variables are passed as a flat context object.
 */
export function createNunjucksEnv(): nunjucks.Environment {
  const env = new nunjucks.Environment(null, {
    autoescape: false,   // We're not generating HTML
    throwOnUndefined: false, // We handle missing vars via | default()
  });

  // Add custom filters
  env.addFilter('max', function (...args: unknown[]) {
    // Support both {{ [a, b] | max }} and {{ a | max(b) }}
    const nums = args.flat().map(Number).filter((n) => !isNaN(n));
    return Math.max(...nums);
  });

  env.addFilter('min', function (...args: unknown[]) {
    const nums = args.flat().map(Number).filter((n) => !isNaN(n));
    return Math.min(...nums);
  });

  env.addFilter('ceil', function (val: unknown) {
    return Math.ceil(Number(val));
  });

  env.addFilter('floor', function (val: unknown) {
    return Math.floor(Number(val));
  });

  env.addFilter('abs', function (val: unknown) {
    return Math.abs(Number(val));
  });

  return env;
}

/**
 * Evaluate a Nunjucks template string with the given variables.
 */
export function evaluateTemplate(
  template: string,
  variables: Record<string, unknown>,
): string {
  const env = createNunjucksEnv();
  return env.renderString(template, variables);
}

// ─────────────────────────────────────────────────────────────────────────────
// TEMPLATE VALUE RESOLUTION (lazy compute-on-read)
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Get a value, evaluating Nunjucks templates lazily if the key is marked
 * as template: true. Recursively resolves dependencies.
 */
export function getValue(
  config: VaultConfig,
  key: string,
  _visited?: Set<string>,
): GetResult {
  const visited = _visited ?? new Set<string>();
  if (visited.has(key)) {
    return { key, value: '', found: false, metadata: undefined, evaluated: false };
  }
  visited.add(key);

  const rawValue = readValue(config, key);
  if (rawValue === null) {
    return { key, value: '', found: false };
  }

  const meta = loadKeyMeta(config, key);

  if (!meta.template) {
    return { key, value: rawValue, found: true, metadata: meta, evaluated: false };
  }

  // Collect all vault variables accessible from this key's store context
  const variables = collectVariables(config, key, visited);

  try {
    const evaluated = evaluateTemplate(rawValue, variables).trim();
    return { key, value: evaluated, found: true, metadata: meta, evaluated: true };
  } catch (err) {
    return {
      key,
      value: `TEMPLATE_ERROR: ${err instanceof Error ? err.message : String(err)}`,
      found: true,
      metadata: meta,
      evaluated: true,
    };
  }
}

/**
 * Collect all resolvable variables for template evaluation.
 * Walks all stores and builds a flat variable map.
 *
 * IMPORTANT: Only collects RAW values (non-template keys). Template keys
 * are NOT recursively evaluated here to avoid infinite recursion. If a
 * template depends on another template's output, the dependency must be
 * a non-template key.
 */
function collectVariables(
  config: VaultConfig,
  _contextKey: string,
  _visited: Set<string>,
): Record<string, unknown> {
  const vars: Record<string, unknown> = {};

  // Walk all stores and collect non-template keys with raw values
  for (const mapping of config.stores) {
    if (!fs.existsSync(mapping.path)) continue;
    const keys = listKeysRecursive(mapping.path, mapping.prefix);
    for (const k of keys) {
      const meta = loadKeyMeta(config, k);
      // Skip template keys to avoid recursive evaluation
      if (meta.template) continue;
      const rawValue = readValue(config, k);
      if (rawValue !== null) {
        // Store by full key (dots replaced with underscores for Nunjucks compatibility)
        const nunjucksKey = k.replace(/\./g, '_');
        vars[nunjucksKey] = rawValue;
        // Also store by last segment (short name) if not already set
        const shortName = k.split('.').pop()!;
        if (!(shortName in vars)) {
          vars[shortName] = rawValue;
        }
      }
    }
  }

  return vars;
}

// ─────────────────────────────────────────────────────────────────────────────
// KEY LISTING
// ─────────────────────────────────────────────────────────────────────────────

/**
 * List all keys with values under a prefix, within a store directory.
 * Returns fully-qualified dot-separated keys.
 */
export function listKeysRecursive(storeDir: string, storePrefix: string): string[] {
  const keys: string[] = [];

  function walk(dir: string, keyParts: string[]): void {
    if (!fs.existsSync(dir)) return;
    const entries = fs.readdirSync(dir, { withFileTypes: true });

    // Check if this directory has a value file
    if (entries.some((e) => e.isFile() && e.name === 'value') && keyParts.length > 0) {
      const relativePart = keyParts.join('.');
      const fullKey = storePrefix ? storePrefix + '.' + relativePart : relativePart;
      keys.push(fullKey);
    }

    // Recurse into subdirectories (skip dot-prefixed like .store.yaml)
    for (const entry of entries) {
      if (entry.isDirectory() && !entry.name.startsWith('.')) {
        walk(path.join(dir, entry.name), [...keyParts, entry.name]);
      }
    }
  }

  walk(storeDir, []);
  return keys;
}

/**
 * List immediate child keys under a prefix (one level deep).
 */
export function listKeysImmediate(
  config: VaultConfig,
  prefix: string,
): string[] {
  const resolved = resolveStore(config, prefix || '_root_');
  // For empty prefix targeting catch-all, we need to handle differently
  const relKey = prefix ? resolved.relativeKey : '';
  const baseDir = keyToPath(resolved.dir, relKey);

  if (!fs.existsSync(baseDir)) return [];

  const keys: string[] = [];
  const entries = fs.readdirSync(baseDir, { withFileTypes: true });

  for (const entry of entries) {
    if (entry.isDirectory() && !entry.name.startsWith('.')) {
      const childKey = prefix ? `${prefix}.${entry.name}` : entry.name;
      const childValuePath = path.join(baseDir, entry.name, 'value');
      if (fs.existsSync(childValuePath)) {
        keys.push(childKey);
      }
    }
  }

  return keys;
}

/**
 * List all keys with values under a prefix across all matching stores.
 */
export function listAllKeys(config: VaultConfig, prefix: string, recursive: boolean): string[] {
  const allKeys = new Set<string>();

  for (const mapping of config.stores) {
    // Check if this store could contain keys under the prefix
    if (prefix && mapping.prefix && !prefix.startsWith(mapping.prefix) && !mapping.prefix.startsWith(prefix)) {
      continue;
    }

    if (!fs.existsSync(mapping.path)) continue;

    const storeKeys = listKeysRecursive(mapping.path, mapping.prefix);
    for (const k of storeKeys) {
      if (!prefix || k.startsWith(prefix)) {
        if (recursive || k.split('.').length <= (prefix ? prefix.split('.').length + 1 : 1)) {
          allKeys.add(k);
        }
      }
    }
  }

  return Array.from(allKeys).sort();
}

// ─────────────────────────────────────────────────────────────────────────────
// CLI HELPERS
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Parse common CLI flags from process.argv.
 *
 * --store-dir can be specified multiple times:
 *   --store-dir /tmp/default              (default store, no prefix)
 *   --store-dir foo.bar:/tmp/foobar       (prefix "foo.bar" → /tmp/foobar)
 *   --store-dir :/tmp/also-default        (explicit default)
 */
export function parseCommonFlags(argv: string[]): {
  config?: string;
  storeDirs: string[];
  rest: string[];
} {
  const rest: string[] = [];
  let config: string | undefined;
  const storeDirs: string[] = [];

  const args = argv.slice(2);
  for (let i = 0; i < args.length; i++) {
    switch (args[i]) {
      case '--config':
        config = args[++i];
        break;
      case '--store-dir':
        storeDirs.push(args[++i]);
        break;
      default:
        rest.push(args[i]);
        break;
    }
  }

  return { config, storeDirs, rest };
}

/**
 * Extract a named flag value from an args array.
 */
export function getFlag(args: string[], flag: string): string | undefined {
  const idx = args.indexOf(flag);
  if (idx === -1 || idx >= args.length - 1) return undefined;
  return args[idx + 1];
}

/**
 * Check if a boolean flag is present.
 */
export function hasFlag(args: string[], flag: string): boolean {
  return args.includes(flag);
}

/**
 * Write JSON to stdout.
 */
export function output(data: unknown): void {
  process.stdout.write(JSON.stringify(data, null, 2) + '\n');
}

/**
 * Write a log message to stderr.
 */
export function log(msg: string): void {
  process.stderr.write(msg + '\n');
}
