/**
 * File Loader
 *
 * Loads configuration and feature definitions from disk.
 * This is CLI-side only - reads files and prepares them for the API.
 *
 * Loads defineTable() definitions from *.ts feature files.
 */
import { promises as fs } from 'fs';
import { join, basename, dirname } from 'path';
import { createHash } from 'crypto';
import { build } from 'esbuild';
import { fileURLToPath } from 'url';
import { pathExists, runWithLimit } from './async-utils.js';
// Get the directory of this file for resolving stubs
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const stubsPath = join(__dirname, 'compiler-stubs.js');
const READ_CONCURRENCY = 8;
let tsModulePromise = null;
async function getTypescript() {
    if (!tsModulePromise) {
        tsModulePromise = import('typescript').catch(() => null);
    }
    return tsModulePromise;
}
async function parseTableDefinitions(source) {
    try {
        const ts = await getTypescript();
        if (!ts)
            throw new Error('typescript not available');
        const sourceFile = ts.createSourceFile('feature.ts', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
        const tableNames = [];
        let hasResourceConfig = false;
        const isTableCallee = (expr) => {
            if (ts.isIdentifier(expr)) {
                return expr.text === 'pgTable' || expr.text === 'sqliteTable';
            }
            if (ts.isPropertyAccessExpression(expr)) {
                // Drizzle: `drizzle.pgTable(...)`, or the Quickback q DSL: `q.table(...)`
                if (expr.name.text === 'pgTable' || expr.name.text === 'sqliteTable')
                    return true;
                if (expr.name.text === 'table' &&
                    ts.isIdentifier(expr.expression) &&
                    expr.expression.text === 'q') {
                    return true;
                }
            }
            return false;
        };
        // True when the callee is `feature`, `q.feature`, or `qb.feature`.
        const isFeatureCallee = (expr) => {
            if (ts.isIdentifier(expr))
                return expr.text === 'feature';
            if (ts.isPropertyAccessExpression(expr)) {
                if (!ts.isIdentifier(expr.expression))
                    return false;
                const ns = expr.expression.text;
                return (ns === 'q' || ns === 'qb') && expr.name.text === 'feature';
            }
            return false;
        };
        const visit = (node) => {
            if (ts.isExportAssignment(node)) {
                const expr = node.expression;
                if (ts.isCallExpression(expr)) {
                    // `export default defineTable(...)` — classic two-export pattern
                    if (ts.isIdentifier(expr.expression) &&
                        expr.expression.text === 'defineTable') {
                        hasResourceConfig = true;
                    }
                    // `export default feature("name", { columns, ... })` — single-export
                    // sugar. Also matches `q.feature(...)` / `qb.feature(...)`.
                    if (isFeatureCallee(expr.expression) && expr.arguments.length >= 2) {
                        const firstArg = expr.arguments[0];
                        if (ts.isStringLiteral(firstArg)) {
                            tableNames.push(firstArg.text);
                            hasResourceConfig = true;
                        }
                    }
                }
            }
            if (ts.isVariableStatement(node) && node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)) {
                for (const decl of node.declarationList.declarations) {
                    if (ts.isIdentifier(decl.name) && decl.initializer && ts.isCallExpression(decl.initializer)) {
                        if (isTableCallee(decl.initializer.expression)) {
                            tableNames.push(decl.name.text);
                        }
                    }
                }
            }
            // Colocated pattern: `const x = feature("name", …)` followed by `export
            // default x` and a sibling `defineActions(x, …)`. The const is not
            // exported directly, so the export-default-feature path above misses it.
            // Recognize the bare const-feature() form so colocated files where the
            // table needs to be named (so it can be referenced by defineActions in
            // the same file) are still picked up as tables.
            if (ts.isVariableStatement(node)) {
                for (const decl of node.declarationList.declarations) {
                    if (ts.isIdentifier(decl.name) &&
                        decl.initializer &&
                        ts.isCallExpression(decl.initializer) &&
                        isFeatureCallee(decl.initializer.expression) &&
                        decl.initializer.arguments.length >= 2) {
                        const firstArg = decl.initializer.arguments[0];
                        if (ts.isStringLiteral(firstArg)) {
                            tableNames.push(firstArg.text);
                            hasResourceConfig = true;
                        }
                    }
                }
            }
            ts.forEachChild(node, visit);
        };
        visit(sourceFile);
        return { tableNames, hasResourceConfig };
    }
    catch {
        // Fallback: no TypeScript available. Match pgTable / sqliteTable / q.table export shapes.
        const tableMatch = source.match(/export const (\w+)\s*=\s*(?:(?:pg|sqlite)Table|q\s*\.\s*table)/);
        const tableNames = tableMatch ? [tableMatch[1]] : [];
        let hasResourceConfig = /export default defineTable/.test(source);
        // Also recognize single-export sugar: `export default feature("name", …)`,
        // or the `q.feature(…)` / `qb.feature(…)` aliases.
        const featureMatch = source.match(/export\s+default\s+(?:(?:q|qb)\s*\.\s*)?feature\s*\(\s*["']([^"']+)["']/);
        if (featureMatch) {
            tableNames.push(featureMatch[1]);
            hasResourceConfig = true;
        }
        // Colocated pattern: `const x = feature("name", …)` (no `export default`
        // wrapper because the table is bound to a name so a sibling
        // `defineActions(x, …)` in the same file can reference it).
        const constFeatureMatch = source.match(/(?:const|let)\s+\w+\s*=\s*(?:(?:q|qb)\s*\.\s*)?feature\s*\(\s*["']([^"']+)["']/);
        if (constFeatureMatch && !tableNames.includes(constFeatureMatch[1])) {
            tableNames.push(constFeatureMatch[1]);
            hasResourceConfig = true;
        }
        return { tableNames, hasResourceConfig };
    }
}
/**
 * Load quickback.config.ts from disk
 */
export async function loadConfig(configPath) {
    if (!(await pathExists(configPath))) {
        throw new Error(`Config file not found: ${configPath}`);
    }
    const config = await loadTypeScriptFile(configPath);
    // Handle default export
    const configData = config.default || config;
    if (!configData.name) {
        throw new Error('Config must have a name');
    }
    // Determine preset from providers or explicit preset
    let preset = configData.preset;
    if (!preset && configData.providers) {
        // Infer preset from runtime provider
        const runtimeName = configData.providers.runtime?.name;
        if (runtimeName === 'cloudflare')
            preset = 'cloudflare';
        else if (runtimeName === 'bun')
            preset = 'bun';
    }
    return {
        ...configData,
        preset: preset || 'cloudflare',
    };
}
/**
 * Load all features from definitions/features/.
 *
 * After each feature loads, conflicts are batched and thrown as one error
 * block — keeps migration of an existing project to a single round-trip
 * instead of N. The loader emits no warnings; everything that needs the
 * user's attention is an actionable conflict pointing at the migration.
 */
export async function loadFeatures(featuresDir) {
    if (!(await pathExists(featuresDir))) {
        return [];
    }
    const entries = await fs.readdir(featuresDir, { withFileTypes: true });
    const featureDirs = entries
        .filter((d) => d.isDirectory())
        .map((d) => d.name);
    const results = new Array(featureDirs.length);
    await runWithLimit(featureDirs.map((name, index) => ({ name, index })), READ_CONCURRENCY, async ({ name, index }) => {
        const featureDir = join(featuresDir, name);
        results[index] = await loadFeature(name, featureDir);
    });
    const loaded = results.filter((f) => Boolean(f));
    // Surface every retired-shape conflict in one batched error block — saves
    // users a "fix one, recompile, fix the next" papercut loop.
    const allConflicts = loaded.flatMap((f) => f.conflicts);
    if (allConflicts.length > 0) {
        const lines = allConflicts.map((c) => `  • [${c.feature}] ${c.message}\n    ${c.paths.map((p) => `- ${p}`).join('\n    ')}`);
        throw new Error(`Action discovery found ${allConflicts.length} conflict(s):\n${lines.join('\n')}\n\n` +
            `Resolve all of the above and recompile.`);
    }
    return loaded.map((f) => f.feature);
}
/**
 * Recursively walk a directory and collect every `.ts` file as
 * `{ relativePath: source }`. The relative path includes the prefix dir name
 * (e.g. `handlers/profiles/publish.ts`), preserved so the compiler can mirror
 * the directory layout in the generated output.
 */
async function walkRecursiveTs(rootDir, prefix) {
    if (!(await pathExists(rootDir)))
        return {};
    const collected = {};
    async function walk(currentDir, relPrefix) {
        const entries = await fs.readdir(currentDir, { withFileTypes: true });
        const tsFiles = entries.filter((e) => e.isFile() && e.name.endsWith('.ts'));
        const subdirs = entries.filter((e) => e.isDirectory());
        if (tsFiles.length > 0) {
            await runWithLimit(tsFiles, READ_CONCURRENCY, async (entry) => {
                const key = `${relPrefix}/${entry.name}`;
                collected[key] = await fs.readFile(join(currentDir, entry.name), 'utf-8');
            });
        }
        for (const sub of subdirs) {
            await walk(join(currentDir, sub.name), `${relPrefix}/${sub.name}`);
        }
    }
    await walk(rootDir, prefix);
    return collected;
}
/**
 * Filename patterns that belonged to the retired `defineActions(...)` shape.
 * Each match is rejected at load time with a migration error pointing the
 * user at the new per-file `actions/<name>.ts` layout.
 */
const RETIRED_FILENAME_PATTERNS = [
    /-actions\.ts$/, //  e.g. `profiles-actions.ts`
    /\.actions\.ts$/, //  e.g. `episodes.actions.ts`
];
/**
 * Load a single feature from its directory.
 *
 * Action authoring is one file per action under `<feature>/actions/`. The
 * loader rejects every retired primitive at discovery time so the user
 * gets a single batched migration error instead of a downstream compiler
 * surprise:
 *
 *  - `actions.ts`         — bundled actions, retired
 *  - `_feature.ts`        — preamble + tableless actions, retired
 *  - `handlers/`          — string-ref handler files, retired
 *  - colocated `defineActions(` inside any feature-root `*.ts`, retired
 *  - `actions/<flat>.ts` containing `defineActions(`, retired
 *  - `*-actions.ts` / `*.actions.ts` filename patterns, retired
 *
 * Returns the loaded feature alongside any discovered conflicts; the
 * caller (`loadFeatures`) batches these across all features before
 * failing.
 */
async function loadFeature(name, dir) {
    const conflicts = [];
    const migrate = (msg) => `${msg} See https://docs.quickback.dev/compiler/definitions/actions for the new shape.`;
    // ── Reject retired root-level files / colocated `defineActions(` ────────────
    const rootEntries = await fs.readdir(dir, { withFileTypes: true });
    const rootTsFiles = rootEntries.filter((e) => e.isFile() && e.name.endsWith('.ts'));
    for (const entry of rootTsFiles) {
        if (entry.name === 'actions.ts') {
            conflicts.push({
                feature: name,
                message: migrate('Bundled `actions.ts` is retired. Move each entry to its own file under `actions/<name>.ts` ' +
                    'using `export default defineAction({...})`.'),
                paths: [join(dir, 'actions.ts')],
            });
            continue;
        }
        if (entry.name === '_feature.ts') {
            conflicts.push({
                feature: name,
                message: migrate('`_feature.ts` is retired. Move shared schemas/helpers to `<feature>/lib/` and tableless ' +
                    'actions to `actions/<name>.ts`.'),
                paths: [join(dir, '_feature.ts')],
            });
            continue;
        }
        if (RETIRED_FILENAME_PATTERNS.some((re) => re.test(entry.name))) {
            conflicts.push({
                feature: name,
                message: migrate(`Filename pattern \`${entry.name}\` is retired. Move action definitions to per-file form ` +
                    `under \`actions/<name>.ts\`.`),
                paths: [join(dir, entry.name)],
            });
            continue;
        }
        const filePath = join(dir, entry.name);
        const src = await fs.readFile(filePath, 'utf-8');
        if (src.includes('defineActions(')) {
            conflicts.push({
                feature: name,
                message: migrate(`Colocated \`defineActions(...)\` inside \`${entry.name}\` is retired. Move each action ` +
                    `to its own \`actions/<name>.ts\` file with \`export default defineAction({...})\`. The ` +
                    `table file should hold only \`defineTable(...)\`.`),
                paths: [filePath],
            });
        }
    }
    // ── Reject retired handlers/ directory ──────────────────────────────────────
    if (await pathExists(join(dir, 'handlers'))) {
        conflicts.push({
            feature: name,
            message: migrate('`handlers/` directory is retired. Inline the `execute` body in each `actions/<name>.ts` ' +
                'file or import shared helpers from `<feature>/lib/`.'),
            paths: [join(dir, 'handlers')],
        });
    }
    // ── Walk actions/ — every `.ts` file is a per-action source ─────────────────
    const actionFiles = {};
    const actionsDir = join(dir, 'actions');
    if (await pathExists(actionsDir)) {
        const allActionFiles = await walkRecursiveTs(actionsDir, 'actions');
        for (const [relativePath, src] of Object.entries(allActionFiles)) {
            if (src.includes('defineActions(')) {
                conflicts.push({
                    feature: name,
                    message: migrate(`\`${relativePath}\` uses the retired \`defineActions(...)\` (plural). Convert each entry ` +
                        `to \`actions/<name>.ts\` with \`export default defineAction({...})\` (singular).`),
                    paths: [join(dir, relativePath)],
                });
                continue;
            }
            actionFiles[relativePath] = src;
        }
    }
    // ── Walk lib/ — feature-local helpers ───────────────────────────────────────
    const libFiles = await walkRecursiveTs(join(dir, 'lib'), 'lib');
    // ── Pages discovery ─────────────────────────────────────────────────────────
    let pageFiles;
    const pagesDir = join(dir, 'pages');
    if (await pathExists(pagesDir)) {
        const pageEntries = await fs.readdir(pagesDir, { withFileTypes: true });
        const tsPages = pageEntries.filter((e) => e.isFile() && e.name.endsWith('.ts'));
        if (tsPages.length > 0) {
            pageFiles = [];
            await runWithLimit(tsPages, READ_CONCURRENCY, async (entry) => {
                const source = await fs.readFile(join(pagesDir, entry.name), 'utf-8');
                if (!source.includes('definePage'))
                    return;
                const slugMatch = source.match(/slug\s*:\s*['"]([^'"]+)['"]/);
                const titleMatch = source.match(/title\s*:\s*['"]([^'"]+)['"]/);
                const featureMatch = source.match(/feature\s*:\s*['"]([^'"]+)['"]/);
                if (slugMatch && titleMatch) {
                    pageFiles.push({
                        fileName: entry.name,
                        slug: slugMatch[1],
                        feature: featureMatch?.[1] || name,
                        title: titleMatch[1],
                        source,
                    });
                }
            });
            if (pageFiles.length === 0)
                pageFiles = undefined;
        }
    }
    // ── Bail early if any retired primitive was hit ─────────────────────────────
    // The conflicts get batched into one error block by `loadFeatures`, so don't
    // try to also discover tables (a legacy table file with colocated
    // `defineActions` still has a real `defineTable`; surfacing it as "loaded"
    // alongside the retired-shape conflict would mislead).
    if (conflicts.length > 0) {
        return {
            feature: { name, schema: { tableName: name, columns: {}, source: '' } },
            conflicts,
        };
    }
    // ── Tables discovery ────────────────────────────────────────────────────────
    const feature = await loadFeatureTables(name, dir);
    if (!feature)
        return null;
    feature.pageFiles = pageFiles;
    if (Object.keys(actionFiles).length > 0)
        feature.actionFiles = actionFiles;
    if (Object.keys(libFiles).length > 0)
        feature.featureLibFiles = libFiles;
    return { feature, conflicts };
}
/**
 * Load feature tables from defineTable() definitions.
 *
 * Tableless features (feature dir with only `actions/` and `lib/`) are
 * still valid — the caller wires their `actionFiles` / `featureLibFiles`
 * onto the returned shell. Calling this with a feature that has neither
 * tables nor any actions returns `null`, which the upstream walker
 * surfaces as a warning.
 */
async function loadFeatureTables(name, dir) {
    const entries = await fs.readdir(dir, { withFileTypes: true });
    const files = entries.filter((e) => e.isFile() &&
        e.name.endsWith('.ts') &&
        !RETIRED_FILENAME_PATTERNS.some((re) => re.test(e.name)));
    // Use index-based results to preserve deterministic file ordering
    const results = new Array(files.length);
    await runWithLimit(files.map((entry, index) => ({ entry, index })), READ_CONCURRENCY, async ({ entry, index }) => {
        const file = entry.name;
        const filePath = join(dir, file);
        const source = await fs.readFile(filePath, 'utf-8');
        const { tableNames, hasResourceConfig } = await parseTableDefinitions(source);
        results[index] = tableNames.map((tableName) => ({
            fileName: file,
            tableName,
            source,
            hasResourceConfig,
        }));
    });
    // Validate: no file should have multiple table exports WITH defineTable()
    for (const fileResults of results) {
        if (!fileResults || fileResults.length <= 1)
            continue;
        if (fileResults.some(t => t.hasResourceConfig)) {
            const tableNames = fileResults.map(t => t.tableName).join(', ');
            throw new Error(`Feature "${name}": File "${fileResults[0].fileName}" exports multiple tables (${tableNames}) ` +
                `alongside a defineTable() resource config. ` +
                `Split each table into its own file so the resource config is unambiguous.`);
        }
    }
    const tables = results.filter(Boolean).flat();
    if (tables.length === 0) {
        // Tableless feature — caller decides whether `actionFiles` will be
        // populated. Returning a shell preserves the actions-only path.
        return {
            name,
            schema: undefined,
            tables: [],
            resourceSource: undefined,
        };
    }
    // Use first table as "primary" for backward compatibility
    const primaryTable = tables[0];
    return {
        name,
        schema: {
            tableName: primaryTable.tableName,
            columns: {}, // Column parsing happens on server
            source: primaryTable.source,
        },
        tables,
        // Resource config comes from defineTable()
        resourceSource: primaryTable.hasResourceConfig ? primaryTable.source : undefined,
    };
}
/**
 * Get or create the cache directory for compiled config files.
 */
async function getCacheDir() {
    const cacheDir = join(process.cwd(), 'node_modules', '.cache', 'quickback');
    if (!(await pathExists(cacheDir))) {
        await fs.mkdir(cacheDir, { recursive: true });
    }
    return cacheDir;
}
/**
 * Load and evaluate a TypeScript file.
 * Caches the esbuild output keyed by source content hash to skip
 * recompilation when the config hasn't changed (~500-800ms savings).
 */
async function loadTypeScriptFile(filePath) {
    const source = await fs.readFile(filePath, 'utf-8');
    const hash = createHash('sha256').update(source).digest('hex').slice(0, 16);
    const cacheDir = await getCacheDir();
    const cachedFile = join(cacheDir, `${basename(filePath, '.ts')}-${hash}.mjs`);
    if (!(await pathExists(cachedFile))) {
        await build({
            entryPoints: [filePath],
            outfile: cachedFile,
            bundle: true,
            format: 'esm',
            platform: 'node',
            target: 'node18',
            external: [
                'drizzle-orm',
                'drizzle-orm/*',
                'zod',
            ],
            alias: {
                '@quickback/compiler': stubsPath,
            },
            logLevel: 'silent',
        });
    }
    const module = await import(cachedFile);
    return module;
}
/**
 * Find the config file path
 */
export async function findConfigPath(startDir = process.cwd()) {
    const candidates = [
        join(startDir, 'quickback.config.ts'),
        join(startDir, 'quickback', 'quickback.config.ts'),
    ];
    for (const candidate of candidates) {
        if (await pathExists(candidate)) {
            return candidate;
        }
    }
    return null;
}
/**
 * Find the features directory
 */
export async function findFeaturesDir(startDir = process.cwd()) {
    const candidates = [
        join(startDir, 'quickback', 'features'), // From project root: quickback/features/
        join(startDir, 'features'), // From quickback dir: features/
        join(startDir, 'definitions', 'features'), // Legacy: definitions/features/
        join(startDir, 'quickback', 'definitions', 'features'), // Legacy nested
    ];
    for (const candidate of candidates) {
        if (await pathExists(candidate)) {
            return candidate;
        }
    }
    return null;
}
/**
 * Load project-level shared lib at `quickback/lib/`.
 *
 * Cross-feature TypeScript that any feature can import via the `~/lib/*`
 * alias. Returns relative paths keyed from `lib/` (e.g. `lib/inputs.ts`)
 * with their source contents. Empty object when the directory is missing.
 *
 * Discipline (enforced compiler-side, not here): files in `quickback/lib/`
 * must not import from `quickback/features/`. Keeps lib genuinely shared
 * and cycle-free.
 */
export async function loadProjectLib(projectRoot) {
    const candidates = [
        join(projectRoot, 'quickback', 'lib'),
        join(projectRoot, 'lib'), // when invoked from inside the quickback/ dir
    ];
    for (const candidate of candidates) {
        if (await pathExists(candidate)) {
            return walkRecursiveTs(candidate, 'lib');
        }
    }
    return {};
}
/**
 * Recognized hook filenames. Mirrors the canonical set in
 * `apps/compiler/src/parser/hooks.ts` (RECOGNIZED_HOOK_EVENTS) — duplicated
 * here so we can fail fast in the CLI before uploading. The compiler
 * re-validates server-side, so this is a UX optimization, not a security
 * boundary. When adding a new event, update both lists.
 */
const RECOGNIZED_HOOK_FILES = new Set([
    'before-user-create.ts',
    'after-user-create.ts',
    'after-session-create.ts',
    'after-admin-user-create.ts',
]);
/**
 * Load project-level auth hooks from `quickback/hooks/`.
 *
 * Flat one-file-per-event convention matching `services/queues/`,
 * `services/realtime/`, `services/embeddings/`. Filename grammar:
 * `(before|after)-<subject>-<verb>.ts` where (subject, verb) is one of
 * the recognized Better Auth `databaseHooks` paths. Returns relative
 * paths keyed from `hooks/` (e.g. `hooks/after-user-create.ts`) with
 * their source contents. Empty object when the directory is missing.
 *
 * Throws on unknown filenames so projects fail compile-fast on typos.
 */
export async function loadProjectHooks(projectRoot) {
    const candidates = [
        join(projectRoot, 'quickback', 'hooks'),
        join(projectRoot, 'hooks'), // when invoked from inside the quickback/ dir
    ];
    let hooksDir = null;
    for (const candidate of candidates) {
        if (await pathExists(candidate)) {
            hooksDir = candidate;
            break;
        }
    }
    if (!hooksDir)
        return {};
    const entries = await fs.readdir(hooksDir, { withFileTypes: true });
    const tsFiles = entries
        .filter((e) => e.isFile() && e.name.endsWith('.ts') && !e.name.startsWith('_'))
        .map((e) => e.name);
    const unknown = tsFiles.filter((name) => !RECOGNIZED_HOOK_FILES.has(name));
    if (unknown.length > 0) {
        const allowed = Array.from(RECOGNIZED_HOOK_FILES).sort().join(', ');
        throw new Error(`quickback/hooks/: unrecognized hook file(s): ${unknown.join(', ')}. ` +
            `Allowed filenames: ${allowed}.`);
    }
    const collected = {};
    await runWithLimit(tsFiles, READ_CONCURRENCY, async (name) => {
        collected[`hooks/${name}`] = await fs.readFile(join(hooksDir, name), 'utf-8');
    });
    return collected;
}
/**
 * Load user-authored Better Auth plugin source files from `quickback/plugins/`.
 *
 * Each `.ts` file is uploaded to the compiler verbatim. Files only flow
 * through the build when referenced from a `customPlugins[].from` entry on
 * the auth config — unreferenced files are quietly ignored server-side.
 *
 * Returns relative paths keyed from `plugins/` (e.g. `plugins/long-session.ts`),
 * matching the path the BA provider rewrites imports against.
 */
export async function loadProjectPlugins(projectRoot) {
    const candidates = [
        join(projectRoot, 'quickback', 'plugins'),
        join(projectRoot, 'plugins'), // when invoked from inside the quickback/ dir
    ];
    for (const candidate of candidates) {
        if (await pathExists(candidate)) {
            return walkRecursiveTs(candidate, 'plugins');
        }
    }
    return {};
}
/**
 * Load project-supplied email templates from `quickback/email-templates/`.
 *
 * Referenced from `defineAuth("better-auth", { plugins: { emailOtp: { templates: { 'forget-password': './email-templates/...' } } } })`.
 * Each `.ts` file is uploaded verbatim; the better-auth provider stages
 * referenced files into `src/email-templates/<name>.ts` and emits the
 * default import in `src/lib/auth.ts`. Files not referenced by any
 * template entry are ignored — the CLI uploads the directory, the
 * compiler picks only what's needed.
 *
 * Returns relative paths keyed from `email-templates/` (e.g.
 * `email-templates/welcome.ts`), matching the path the BA provider
 * rewrites imports against.
 */
export async function loadProjectEmailTemplates(projectRoot) {
    const candidates = [
        join(projectRoot, 'quickback', 'email-templates'),
        join(projectRoot, 'email-templates'), // when invoked from inside the quickback/ dir
    ];
    for (const candidate of candidates) {
        if (await pathExists(candidate)) {
            return walkRecursiveTs(candidate, 'email-templates');
        }
    }
    return {};
}
/**
 * Load project-level Hono middlewares from `quickback/middlewares/`.
 *
 * v0.19+: declarative cross-cutting slot for non-authz middleware (request
 * logging, debug headers, response transforms). Each `.ts` file is uploaded
 * verbatim; the compiler mounts them via `app.use('*', mw)` between
 * platform middleware (auth/db/services) and route registration.
 *
 * Returns relative paths keyed from `middlewares/` (e.g.
 * `middlewares/request-logger.ts`), matching the path the compiler emits
 * into the generated `src/middlewares/` directory.
 */
export async function loadProjectMiddlewares(projectRoot) {
    const candidates = [
        join(projectRoot, 'quickback', 'middlewares'),
        join(projectRoot, 'middlewares'), // when invoked from inside the quickback/ dir
    ];
    for (const candidate of candidates) {
        if (await pathExists(candidate)) {
            return walkRecursiveTs(candidate, 'middlewares');
        }
    }
    return {};
}
/**
 * Find the services directory
 */
export async function findServicesDir(startDir = process.cwd()) {
    const candidates = [
        join(startDir, 'quickback', 'services'), // New: quickback/services/
        join(startDir, 'definitions', 'services'), // Legacy: definitions/services/
        join(startDir, 'quickback', 'definitions', 'services'), // Legacy nested
    ];
    for (const candidate of candidates) {
        if (await pathExists(candidate)) {
            return candidate;
        }
    }
    return null;
}
/**
 * Load all services from definitions/services/
 */
export async function loadServices(servicesDir) {
    const services = {};
    // Load queue handlers from services/queues/
    const queuesDir = join(servicesDir, 'queues');
    if (await pathExists(queuesDir)) {
        services.queues = await loadQueueHandlers(queuesDir);
    }
    // Load realtime definitions from services/realtime/
    const realtimeDir = join(servicesDir, 'realtime');
    if (await pathExists(realtimeDir)) {
        services.realtime = await loadRealtimeDefinitions(realtimeDir);
    }
    // Load embedding definitions from services/embeddings/
    const embeddingsDir = join(servicesDir, 'embeddings');
    if (await pathExists(embeddingsDir)) {
        services.embeddings = await loadEmbeddingDefinitions(embeddingsDir);
    }
    return services;
}
/**
 * Load queue handlers from services/queues/
 */
async function loadQueueHandlers(queuesDir) {
    const handlers = [];
    const entries = await fs.readdir(queuesDir, { withFileTypes: true });
    const files = entries
        .filter((e) => e.isFile() && e.name.endsWith('.ts') && !e.name.startsWith('_'))
        .map((e) => e.name);
    await runWithLimit(files, READ_CONCURRENCY, async (file) => {
        const filePath = join(queuesDir, file);
        const source = await fs.readFile(filePath, 'utf-8');
        // Check for defineQueue
        if (!source.includes('defineQueue'))
            return;
        // Extract name and messageType from source
        const nameMatch = source.match(/name\s*:\s*['"]([^'"]+)['"]/);
        const messageTypeMatch = source.match(/messageType\s*:\s*['"]([^'"]+)['"]/);
        if (!nameMatch || !messageTypeMatch) {
            console.warn(`Queue handler ${file} missing required name or messageType, skipping`);
            return;
        }
        handlers.push({
            fileName: file,
            name: nameMatch[1],
            messageType: messageTypeMatch[1],
            source,
        });
    });
    return handlers;
}
/**
 * Load realtime definitions from services/realtime/
 */
async function loadRealtimeDefinitions(realtimeDir) {
    const definitions = [];
    const entries = await fs.readdir(realtimeDir, { withFileTypes: true });
    const files = entries
        .filter((e) => e.isFile() && e.name.endsWith('.ts') && !e.name.startsWith('_'))
        .map((e) => e.name);
    await runWithLimit(files, READ_CONCURRENCY, async (file) => {
        const filePath = join(realtimeDir, file);
        const source = await fs.readFile(filePath, 'utf-8');
        // Check for defineRealtime
        if (!source.includes('defineRealtime'))
            return;
        // Extract name from source
        const nameMatch = source.match(/name\s*:\s*['"]([^'"]+)['"]/);
        // Extract events array
        const eventsMatch = source.match(/events\s*:\s*\[([^\]]+)\]/);
        let events = [];
        if (eventsMatch) {
            const eventsContent = eventsMatch[1];
            const eventMatches = eventsContent.matchAll(/['"]([^'"]+)['"]/g);
            events = Array.from(eventMatches).map(m => m[1]);
        }
        if (!nameMatch || events.length === 0) {
            console.warn(`Realtime definition ${file} missing required name or events, skipping`);
            return;
        }
        definitions.push({
            fileName: file,
            name: nameMatch[1],
            events,
            source,
        });
    });
    return definitions;
}
/**
 * Load embedding definitions from services/embeddings/
 */
async function loadEmbeddingDefinitions(embeddingsDir) {
    const definitions = [];
    const entries = await fs.readdir(embeddingsDir, { withFileTypes: true });
    const files = entries
        .filter((e) => e.isFile() && e.name.endsWith('.ts') && !e.name.startsWith('_'))
        .map((e) => e.name);
    await runWithLimit(files, READ_CONCURRENCY, async (file) => {
        const filePath = join(embeddingsDir, file);
        const sourceCode = await fs.readFile(filePath, 'utf-8');
        // Check for defineEmbedding
        if (!sourceCode.includes('defineEmbedding'))
            return;
        // Extract name from source
        const nameMatch = sourceCode.match(/name\s*:\s*['"]([^'"]+)['"]/);
        // Extract source table from source
        const sourceMatch = sourceCode.match(/source\s*:\s*['"]([^'"]+)['"]/);
        if (!nameMatch || !sourceMatch) {
            console.warn(`Embedding definition ${file} missing required name or source, skipping`);
            return;
        }
        definitions.push({
            fileName: file,
            name: nameMatch[1],
            source: sourceMatch[1],
            sourceCode,
        });
    });
    return definitions;
}
/**
 * Load existing drizzle migration state (historical SQL + meta JSON) so the
 * compiler can generate incremental migrations (ALTER vs CREATE) and run
 * provider-specific safety passes against the *full* existing chain instead of
 * only whatever drizzle-kit happens to emit on this compile.
 *
 * Migration state lives in ONE place: `<project>/quickback/drizzle/...`.
 * Quickback owns this state — it's never mirrored to or read from a
 * project-root `drizzle/` folder.
 *
 * The keys returned include the `quickback/` prefix so the server stages
 * the files at the same on-disk path that drizzle.config.ts's `out:`
 * resolves to (`<temp>/output/quickback/drizzle/<db>/...`). Without that
 * alignment, drizzle-kit can't find existing journals on recompile and
 * provider hooks cannot sanitize historical migrations already checked into
 * the project tree.
 */
export async function loadDrizzleMeta(projectRoot) {
    const stateFiles = {};
    const quickbackRoot = join(projectRoot, 'quickback');
    if (!await pathExists(quickbackRoot)) {
        return undefined;
    }
    const scopedStateDirs = [
        { absolutePath: join(quickbackRoot, 'drizzle', 'auth'), relativePath: 'quickback/drizzle/auth' },
        { absolutePath: join(quickbackRoot, 'drizzle', 'features'), relativePath: 'quickback/drizzle/features' },
        { absolutePath: join(quickbackRoot, 'drizzle', 'files'), relativePath: 'quickback/drizzle/files' },
        { absolutePath: join(quickbackRoot, 'drizzle', 'webhooks'), relativePath: 'quickback/drizzle/webhooks' },
        { absolutePath: join(quickbackRoot, 'drizzle', 'audit'), relativePath: 'quickback/drizzle/audit' },
    ];
    const existingStateDirs = [];
    for (const dir of scopedStateDirs) {
        if (await pathExists(dir.absolutePath))
            existingStateDirs.push(dir);
    }
    if (existingStateDirs.length === 0) {
        return undefined;
    }
    try {
        await runWithLimit(existingStateDirs, READ_CONCURRENCY, async ({ absolutePath, relativePath }) => {
            const files = await fs.readdir(absolutePath);
            await runWithLimit(files.filter((file) => file.endsWith('.sql')), READ_CONCURRENCY, async (file) => {
                const filePath = join(absolutePath, file);
                stateFiles[`${relativePath}/${file}`] = await fs.readFile(filePath, 'utf-8');
            });
            const metaAbsolutePath = join(absolutePath, 'meta');
            const metaRelativePath = `${relativePath}/meta`;
            if (!await pathExists(metaAbsolutePath))
                return;
            const metaFilesInDir = await fs.readdir(metaAbsolutePath);
            await runWithLimit(metaFilesInDir.filter((file) => file.endsWith('.json')), READ_CONCURRENCY, async (file) => {
                const filePath = join(metaAbsolutePath, file);
                stateFiles[`${metaRelativePath}/${file}`] = await fs.readFile(filePath, 'utf-8');
            });
        });
        return Object.keys(stateFiles).length > 0 ? stateFiles : undefined;
    }
    catch {
        return undefined;
    }
}
//# sourceMappingURL=file-loader.js.map