/**
 * First-run importer — Phase 6 of the secrets refactor.
 *
 * Walks the Key Registry and seeds the encrypted store from whatever
 * plaintext legacy files exist on disk:
 *
 *   ~/.studiograph/user.json                — workspace settings & secrets
 *   ~/.studiograph/auth.json                — GitHub PAT (bootstrap class, skipped)
 *   ~/.studiograph/connectors/*.json        — connector secrets
 *   <workspace>/.studiograph/workspace.json — workspace + r2 fields
 *
 * Plus the running process environment for any registry entry whose
 * `envAliases` matches a set `process.env.*`. Env values are mirrored
 * into the store but NEVER overwrite an existing row — they remain the
 * runtime source of truth via the resolver's precedence chain. The
 * import is purely a "warm the store" pass so the read-flip doesn't
 * see empty slots on first boot.
 *
 * Idempotency: a `meta.imported_legacy_at` row gates the whole pass.
 * Once set, the importer is a no-op until the row is cleared
 * (operationally: by deleting it via the SQLite CLI, or by burning the
 * secrets.db file).
 *
 * Conflict rule: never overwrites a key already present in the store.
 * Importer fills empty slots only. This means the store ALWAYS wins
 * over a legacy file — the operator may have already used the API to
 * set a value before the importer ran (race during a redeploy that
 * brought legacy files along), and we must not regress that write.
 *
 * Failure handling: per-source failures (parse errors, missing files)
 * are logged and continued. The whole pass succeeds even if some
 * sources are missing — the goal is to copy what's there, not enforce
 * that everything is there.
 *
 * After the scope-collapse refactor, the store is single-tier — the
 * process IS the workspace. The importer no longer routes writes
 * through a scope dimension.
 */
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { REGISTRY, getKeyDef, resolveConnectorKeyDef } from './registry.js';
import { getSecretStore, hasSecretStore } from './index.js';
const IMPORTER_ACTOR = { type: 'system', systemId: 'importer' };
/**
 * `$ENV_VAR` placeholder values must not be mirrored — see dual-write.ts
 * for the rationale (they're a sanitization marker, not a credential).
 */
function looksLikeEnvPlaceholder(s) {
    return typeof s === 'string' && /^\$[A-Z_][A-Z0-9_]*$/.test(s);
}
function isNonEmptyString(v) {
    return typeof v === 'string' && v.length > 0;
}
function readPath(obj, path) {
    if (!obj || typeof obj !== 'object')
        return undefined;
    const parts = path.split('.');
    let cur = obj;
    for (const p of parts) {
        if (cur == null || typeof cur !== 'object')
            return undefined;
        cur = cur[p];
    }
    return cur;
}
function isStoreManaged(c) {
    return c === 'workspace-secret' || c === 'workspace-setting';
}
/**
 * Whether the importer has already run against this store. Operationally
 * checked via the `meta` table — a single row keyed `imported_legacy_at`.
 */
function hasRunBefore(store) {
    const row = store.db
        .prepare("SELECT value FROM meta WHERE key='imported_legacy_at'")
        .get();
    return !!row?.value;
}
function markImported(store) {
    store.db
        .prepare("INSERT INTO meta (key, value) VALUES ('imported_legacy_at', ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value")
        .run(String(Date.now()));
}
/**
 * Decide whether a slot is already occupied (so we should skip the
 * importer write). For settings we use `get()` returning undefined as
 * "empty"; for secrets we use `describe()` returning undefined.
 *
 * Note: settings can legally store `null` as a JSON value — if a future
 * caller chose to model "explicitly unset" with `null`, our "undefined
 * means empty" rule would incorrectly let the importer overwrite. No
 * registry entry uses null defaults today, so this is fine; flagged as
 * a future hazard.
 */
function slotEmpty(store, key, isSecret) {
    if (isSecret) {
        return store.describe(key) === undefined;
    }
    return store.get(key) === undefined;
}
async function writeOne(store, def, value, result) {
    if (value === undefined || value === null || value === '')
        return;
    if (looksLikeEnvPlaceholder(value))
        return;
    const isSecret = def.classification.endsWith('-secret');
    if (!slotEmpty(store, def.name, isSecret))
        return;
    if (isSecret) {
        if (!isNonEmptyString(value))
            return;
        await store.set({ key: def.name, value }, IMPORTER_ACTOR);
        result.secrets++;
        return;
    }
    await store.set({ key: def.name, value }, IMPORTER_ACTOR);
    result.settings++;
}
async function importFromUserJson(store, homePath, result) {
    const path = join(homePath, '.studiograph', 'user.json');
    if (!existsSync(path))
        return;
    result.sources.push(path);
    let parsed;
    try {
        parsed = JSON.parse(readFileSync(path, 'utf-8'));
    }
    catch (err) {
        // eslint-disable-next-line no-console
        console.warn(`[secrets] importer: failed to parse ${path}: ${err instanceof Error ? err.message : err}`);
        return;
    }
    for (const def of REGISTRY) {
        if (def.migrationSource?.file !== 'user.json')
            continue;
        if (!isStoreManaged(def.classification))
            continue; // bootstrap / user-* — not the store's job.
        const value = readPath(parsed, def.migrationSource.path);
        await writeOne(store, def, value, result);
    }
}
async function importFromConnectors(store, homePath, result) {
    // Connector configs live in TWO locations:
    //   ~/.studiograph/connectors/*.json    — full configs with literal secrets
    //   <workspace>/.studiograph/connectors/*.json — sanitized configs ($ENV_VAR)
    // We only mine the homedir layer — workspace files carry placeholders, not
    // values, and the dual-write layer (Phase 5) already kept them empty.
    const dir = join(homePath, '.studiograph', 'connectors');
    if (!existsSync(dir))
        return;
    let entries;
    try {
        entries = readdirSync(dir);
    }
    catch {
        return;
    }
    for (const file of entries) {
        if (!file.endsWith('.json'))
            continue;
        const filePath = join(dir, file);
        result.sources.push(filePath);
        let parsed;
        try {
            parsed = JSON.parse(readFileSync(filePath, 'utf-8'));
        }
        catch (err) {
            // eslint-disable-next-line no-console
            console.warn(`[secrets] importer: failed to parse ${filePath}: ${err instanceof Error ? err.message : err}`);
            continue;
        }
        if (!parsed || typeof parsed.name !== 'string')
            continue;
        const name = String(parsed.name).toLowerCase();
        const auth = parsed.headers?.['Authorization'] ?? parsed.headers?.['authorization'];
        if (isNonEmptyString(auth) && !looksLikeEnvPlaceholder(auth)) {
            const key = `connector.${name}.auth`;
            const def = resolveConnectorKeyDef(key);
            if (def)
                await writeOne(store, def, auth, result);
        }
        const oauthSecret = parsed.oauth?.client_secret;
        if (isNonEmptyString(oauthSecret) && !looksLikeEnvPlaceholder(oauthSecret)) {
            const key = `connector.${name}.oauth_secret`;
            const def = resolveConnectorKeyDef(key);
            if (def)
                await writeOne(store, def, oauthSecret, result);
        }
    }
}
async function importFromWorkspaceJson(store, workspacePath, result) {
    if (!workspacePath)
        return;
    const path = join(workspacePath, '.studiograph', 'workspace.json');
    if (!existsSync(path))
        return;
    result.sources.push(path);
    let parsed;
    try {
        parsed = JSON.parse(readFileSync(path, 'utf-8'));
    }
    catch (err) {
        // eslint-disable-next-line no-console
        console.warn(`[secrets] importer: failed to parse ${path}: ${err instanceof Error ? err.message : err}`);
        return;
    }
    // R2 credentials (workspace.json has its own nested shape that doesn't
    // match the registry's flat paths — translate explicitly).
    const accessKey = readPath(parsed, 'r2_config.access_key_id');
    const secretKey = readPath(parsed, 'r2_config.secret_access_key');
    const accessDef = getKeyDef('r2.access_key_id');
    const secretDef = getKeyDef('r2.secret_access_key');
    if (accessDef)
        await writeOne(store, accessDef, accessKey, result);
    if (secretDef)
        await writeOne(store, secretDef, secretKey, result);
    // Search api_key — provider-keyed.
    const searchApiKey = readPath(parsed, 'search.api_key');
    const searchProvider = readPath(parsed, 'search.provider');
    if (isNonEmptyString(searchApiKey) && !looksLikeEnvPlaceholder(searchApiKey)) {
        const providerKey = searchProviderKey(typeof searchProvider === 'string' ? searchProvider : undefined);
        if (providerKey) {
            const def = getKeyDef(providerKey);
            if (def)
                await writeOne(store, def, searchApiKey, result);
        }
    }
    const googleCx = readPath(parsed, 'search.google_cx');
    const cxDef = getKeyDef('search.google.cx');
    if (cxDef)
        await writeOne(store, cxDef, googleCx, result);
}
async function importFromEnv(store, result) {
    result.sources.push('env');
    for (const def of REGISTRY) {
        // Bootstrap, user-secret, user-setting all skip the store entirely.
        if (!isStoreManaged(def.classification))
            continue;
        for (const alias of def.envAliases) {
            const raw = process.env[alias];
            if (!isNonEmptyString(raw))
                continue;
            if (looksLikeEnvPlaceholder(raw))
                continue;
            await writeOne(store, def, raw, result);
            break; // first non-empty env alias wins, matching resolver order
        }
    }
}
function searchProviderKey(provider) {
    switch (provider) {
        case 'brave': return 'search.brave.api_key';
        case 'google': return 'search.google.api_key';
        case 'tavily': return 'search.tavily.api_key';
        default: return undefined;
    }
}
/**
 * Touch every legacy source once and copy registry-mapped values into
 * the encrypted store. Safe to call repeatedly — guarded by
 * `meta.imported_legacy_at`. Caller (serve.ts) decides whether to log
 * the result; the importer itself is silent on success and
 * `console.warn`s on per-source parse failures.
 *
 * Returns counts so the caller can produce the boot one-liner the plan
 * specifies (`[secrets] Imported N settings, M secrets from legacy files`).
 */
export async function runFirstRunImporter(ctx = {}) {
    if (!hasSecretStore()) {
        return { skipped: true, settings: 0, secrets: 0, sources: [] };
    }
    const store = getSecretStore();
    const result = {
        skipped: false,
        settings: 0,
        secrets: 0,
        sources: [],
    };
    if (hasRunBefore(store)) {
        result.skipped = true;
        return result;
    }
    const homePath = ctx.homePath ?? homedir();
    await importFromUserJson(store, homePath, result);
    await importFromConnectors(store, homePath, result);
    await importFromWorkspaceJson(store, ctx.workspacePath, result);
    if (ctx.importFromEnv) {
        await importFromEnv(store, result);
    }
    markImported(store);
    return result;
}
/**
 * Test seam: clear the `imported_legacy_at` marker so the next call
 * re-runs the importer. Tests only — production callers should never
 * unmark a successful import.
 */
export function _resetImportedMarker() {
    if (!hasSecretStore())
        return;
    const store = getSecretStore();
    store.db.prepare("DELETE FROM meta WHERE key='imported_legacy_at'").run();
}
/**
 * Utility for tests + diagnostics — does the store look "fresh"? Used
 * by the boot path to decide whether to log the importer summary line
 * vs. silently confirm "already imported".
 */
export function importerHasRun() {
    if (!hasSecretStore())
        return false;
    return hasRunBefore(getSecretStore());
}
// statSync is imported for parity with other readers that may surface
// file-mode warnings; keeping the import so future hardening can wire
// in `seedWorldReadable`-style checks without a separate change.
void statSync;
//# sourceMappingURL=importer.js.map