/**
 * Compile Command
 *
 * Loads configuration and features from disk, calls the remote compiler API,
 * writes the generated files to the project root, and runs post-compile commands.
 *
 * Project structure:
 *   my-app/
 *   ├── quickback/
 *   │   ├── quickback.config.ts
 *   │   ├── definitions/features/...
 *   │   └── drizzle/                ← migration SQL + meta (compiler-owned)
 *   ├── src/                        ← compiled code written here
 *   ├── supabase/migrations/        ← for Supabase provider
 *   └── package.json
 */
import { join, dirname, resolve, relative } from 'path';
import fs from 'fs/promises';
import pc from 'picocolors';
import ora from 'ora';
import { callCompiler, getApiUrl } from '../lib/api-client.js';
import { ensureAuthForCompile, getStoredUser } from '../lib/auth.js';
import { loadConfig, loadFeatures, findConfigPath, findFeaturesDir, findServicesDir, loadServices, loadDrizzleMeta, loadProjectLib, loadProjectHooks, loadProjectPlugins, loadProjectEmailTemplates, loadProjectMiddlewares } from '../lib/file-loader.js';
import { writeFilesWithMerge } from '../lib/file-writer.js';
import { prepareGeneratedDir } from '../lib/prepare-output.js';
import { copyPublicAssets, PublicAssetCollisionError } from '../lib/copy-public-assets.js';
import { copySourceApps, SourceAppCollisionError } from '../lib/copy-source-apps.js';
import { isSecurityReportArtifactContent, partitionGeneratedFiles } from '../lib/artifact-routing.js';
import { runCommand } from '../lib/shell.js';
import { syncConfigFile } from '../lib/config-sync.js';
import { login } from './login.js';
function areSamePath(a, b) {
    return resolve(a) === resolve(b);
}
async function resolveQuickbackStateDir(projectRoot, outputDir) {
    const quickbackDir = join(projectRoot, 'quickback');
    if (areSamePath(quickbackDir, outputDir))
        return undefined;
    return await pathExists(quickbackDir) ? quickbackDir : undefined;
}
async function pathExists(path) {
    try {
        await fs.access(path);
        return true;
    }
    catch {
        return false;
    }
}
async function syncSecurityReportsToQuickback(projectRoot, outputDir, generatedFiles) {
    const quickbackDir = join(projectRoot, 'quickback');
    if (areSamePath(quickbackDir, outputDir))
        return 0;
    if (!await pathExists(quickbackDir))
        return 0;
    const reportFiles = generatedFiles.filter((file) => file.path.endsWith('.json') && isSecurityReportArtifactContent(file.content));
    let synced = 0;
    for (const file of reportFiles) {
        const targetPath = join(quickbackDir, file.path);
        await fs.mkdir(dirname(targetPath), { recursive: true });
        await fs.writeFile(targetPath, file.content, 'utf-8');
        synced += 1;
    }
    return synced;
}
export async function compile(options = {}) {
    const syncConfig = options.syncConfig !== false;
    console.log(pc.bold('Quickback Compiler'));
    // Show auth status (and prompt if missing).
    const apiUrl = getApiUrl();
    const authResult = await ensureAuthForCompile({ apiUrl, loginFn: login });
    if (authResult.status === 'cancelled') {
        console.log(pc.gray('\nNo problem — re-run when you\'re ready. You can also `quickback login` directly.'));
        process.exit(0);
    }
    if (authResult.status === 'authenticated') {
        if (authResult.via === 'api-key') {
            console.log(pc.gray('Auth: API key (x-api-key)'));
        }
        else {
            const user = await getStoredUser();
            console.log(pc.gray(`Auth: ${user?.email || 'session'} (Bearer)`));
        }
    }
    console.log(pc.gray(`API:  ${apiUrl}\n`));
    const spinner = ora('Loading configuration...').start();
    try {
        // Find config file
        const configPath = await findConfigPath();
        if (!configPath) {
            spinner.fail('Configuration not found');
            console.error(pc.red('Error: quickback.config.ts not found'));
            console.log("Run 'quickback init' to create a new project structure");
            process.exit(1);
        }
        // Determine project root (parent of quickback/ folder)
        const quickbackDir = dirname(configPath);
        const projectRoot = configPath.includes('/quickback/')
            ? dirname(quickbackDir) // Config in quickback/ folder → parent is project root
            : process.cwd(); // Config at cwd → cwd is project root
        // Load config
        const config = await loadConfig(configPath);
        spinner.succeed(`Configuration loaded: ${pc.cyan(config.name)}`);
        // Load features
        spinner.start('Loading features...');
        const featuresDir = await findFeaturesDir();
        const features = featuresDir ? await loadFeatures(featuresDir) : [];
        spinner.succeed(`Loaded ${features.length} feature(s)`);
        // Load project-level shared lib (quickback/lib/) — cross-feature TypeScript
        // imported via the `~/lib/*` alias by any feature.
        const projectLib = await loadProjectLib(projectRoot);
        if (Object.keys(projectLib).length > 0) {
            console.log(pc.gray(`  Loaded ${Object.keys(projectLib).length} shared lib file(s) from quickback/lib/`));
        }
        // Load project-level auth hooks (quickback/hooks/) — wired into Better Auth's databaseHooks.
        const projectHooks = await loadProjectHooks(projectRoot);
        if (Object.keys(projectHooks).length > 0) {
            console.log(pc.gray(`  Loaded ${Object.keys(projectHooks).length} auth hook(s) from quickback/hooks/`));
        }
        // Load user-authored Better Auth plugin source files (quickback/plugins/) —
        // referenced from defineAuth("better-auth", { customPlugins: [{ from: "./plugins/..." }] }).
        const projectPlugins = await loadProjectPlugins(projectRoot);
        if (Object.keys(projectPlugins).length > 0) {
            console.log(pc.gray(`  Loaded ${Object.keys(projectPlugins).length} plugin file(s) from quickback/plugins/`));
        }
        // Load project-supplied email templates (quickback/email-templates/) —
        // referenced from defineAuth("better-auth", { plugins: { emailOtp: { templates: { '<type>': './email-templates/...' } } } }).
        const projectEmailTemplates = await loadProjectEmailTemplates(projectRoot);
        if (Object.keys(projectEmailTemplates).length > 0) {
            console.log(pc.gray(`  Loaded ${Object.keys(projectEmailTemplates).length} email template(s) from quickback/email-templates/`));
        }
        // Load project-level Hono middlewares (quickback/middlewares/) — declarative
        // cross-cutting slot mounted between platform middleware and route registration.
        const projectMiddlewares = await loadProjectMiddlewares(projectRoot);
        if (Object.keys(projectMiddlewares).length > 0) {
            console.log(pc.gray(`  Loaded ${Object.keys(projectMiddlewares).length} middleware(s) from quickback/middlewares/`));
        }
        // Load services (queues, realtime, embeddings)
        spinner.start('Loading services...');
        const servicesDir = await findServicesDir();
        const services = servicesDir ? await loadServices(servicesDir) : undefined;
        const queueCount = services?.queues?.length || 0;
        const realtimeCount = services?.realtime?.length || 0;
        const embeddingCount = services?.embeddings?.length || 0;
        const totalServices = queueCount + realtimeCount + embeddingCount;
        if (totalServices > 0) {
            const parts = [];
            if (queueCount > 0)
                parts.push(`${queueCount} queue(s)`);
            if (realtimeCount > 0)
                parts.push(`${realtimeCount} realtime namespace(s)`);
            if (embeddingCount > 0)
                parts.push(`${embeddingCount} embedding config(s)`);
            spinner.succeed(`Loaded services: ${parts.join(', ')}`);
        }
        else {
            spinner.info('No services defined');
        }
        // Determine output directory first (needed for loading existing meta)
        const outputDir = config.build?.outputDir
            ? join(quickbackDir, config.build.outputDir)
            : projectRoot;
        // Load existing drizzle meta for incremental migrations
        const drizzleMeta = await loadDrizzleMeta(outputDir);
        if (drizzleMeta) {
            console.log(pc.gray(`  Found existing migration state (${Object.keys(drizzleMeta).length} files)`));
        }
        // Existing wrangler.toml — forwarded so the compiler can preserve the
        // deployed Durable Object [[migrations]] sequence. CF rejects deploys
        // (code 10061) when the migration list isn't append-only, so a new DO
        // inserted into the middle of `bindings.durableObjects` would otherwise
        // bump every later auto-migration into the wrong slot.
        const existingFiles = { ...(drizzleMeta || {}) };
        try {
            const wranglerPath = join(outputDir, 'wrangler.toml');
            const wranglerToml = await fs.readFile(wranglerPath, 'utf-8');
            existingFiles['wrangler.toml'] = wranglerToml;
        }
        catch {
            /* No existing wrangler.toml — first compile or generated tree was wiped. Compiler falls back to config order. */
        }
        // Collect page definitions from all features
        const allPages = features.flatMap((f) => (f.pageFiles || []).map((p) => ({
            fileName: p.fileName,
            slug: p.slug,
            feature: p.feature,
            title: p.title,
            source: p.source,
        })));
        const pageCount = allPages.length;
        if (pageCount > 0) {
            console.log(pc.gray(`  Found ${pageCount} page definition(s)`));
        }
        // Call remote compiler API
        spinner.start('Compiling via Quickback API...');
        const result = await callCompiler({
            config,
            features: features.map((f) => ({
                name: f.name,
                schema: f.schema,
                tables: f.tables,
                resourceSource: f.resourceSource,
                // One-file-per-action shape — see apps/compiler/src/discovery/feature-walker.ts
                actionFiles: f.actionFiles,
                featureLibFiles: f.featureLibFiles,
            })),
            // Project-level shared lib at quickback/lib/, aliased as `~/lib/*`
            projectLib: Object.keys(projectLib).length > 0 ? projectLib : undefined,
            // Project-level auth hooks at quickback/hooks/ — wired into Better Auth's databaseHooks.
            projectHooks: Object.keys(projectHooks).length > 0 ? projectHooks : undefined,
            // User-authored Better Auth plugin source files at quickback/plugins/.
            projectPlugins: Object.keys(projectPlugins).length > 0 ? projectPlugins : undefined,
            // Project-supplied email templates at quickback/email-templates/, referenced by `plugins.emailOtp.templates`.
            projectEmailTemplates: Object.keys(projectEmailTemplates).length > 0 ? projectEmailTemplates : undefined,
            // Project-level Hono middlewares at quickback/middlewares/ — declarative cross-cutting slot.
            projectMiddlewares: Object.keys(projectMiddlewares).length > 0 ? projectMiddlewares : undefined,
            // Include services (queue handlers, realtime, embeddings, pages)
            services: services || allPages.length > 0 ? {
                queues: services?.queues?.map((q) => ({
                    fileName: q.fileName,
                    name: q.name,
                    messageType: q.messageType,
                    source: q.source,
                })),
                realtime: services?.realtime?.map((r) => ({
                    fileName: r.fileName,
                    name: r.name,
                    events: r.events,
                    source: r.source,
                })),
                embeddings: services?.embeddings?.map((e) => ({
                    fileName: e.fileName,
                    name: e.name,
                    sourceTable: e.source,
                    source: e.sourceCode,
                })),
                pages: allPages.length > 0 ? allPages : undefined,
            } : undefined,
            // Include existing migration state for incremental migrations + the
            // existing wrangler.toml so the compiler preserves the deployed
            // [[migrations]] order (Cloudflare rejects out-of-order migrations).
            existingFiles: Object.keys(existingFiles).length > 0 ? existingFiles : undefined,
        }, // Type assertion for existingFiles
        (msg) => spinner.text = msg);
        const totalSecs = result.meta.timings ? (result.meta.timings.totalMs / 1000).toFixed(1) : null;
        spinner.succeed(`Compiled ${result.meta.fileCount} files (v${result.meta.version})${totalSecs ? ` in ${totalSecs}s` : ''}`);
        // Show what was generated — SPA builds, schema registry, OpenAPI
        const spaBuilds = result.meta.timings?.commands?.filter((c) => c.description.includes('SPA build')) || [];
        for (const spa of spaBuilds) {
            const name = spa.description.replace(' SPA build', '');
            const secs = (spa.ms / 1000).toFixed(1);
            if (spa.success) {
                // Count SPA files in the output
                const spaPrefix = `src/apps/${name}/`;
                const spaFiles = result.files.filter((f) => f.path.startsWith(spaPrefix));
                const assetFiles = spaFiles.filter((f) => f.path.includes('/assets/'));
                const info = assetFiles.length > 0
                    ? `${assetFiles.length} assets, ${secs}s`
                    : `${secs}s`;
                console.log(pc.gray(`  ${pc.green('●')} ${name} SPA built (${info})`));
            }
            else {
                console.log(pc.yellow(`  ${pc.red('●')} ${name} SPA build failed`));
            }
        }
        // Show skipped SPAs
        const cmsObj = typeof config.cms === 'object' ? config.cms : null;
        const accountObj = typeof config.account === 'object' ? config.account : null;
        if (config.cms && cmsObj?.build === false) {
            console.log(pc.gray(`  ${pc.dim('○')} cms SPA skipped (build: false)`));
        }
        if (config.account && accountObj?.build === false) {
            console.log(pc.gray(`  ${pc.dim('○')} account SPA skipped (build: false)`));
        }
        const hasSchemaRegistry = result.files.some((f) => f.path === 'schema-registry.json');
        if (hasSchemaRegistry) {
            console.log(pc.gray(`  ${pc.green('●')} CMS schema registry`));
        }
        const hasOpenAPI = result.files.some((f) => f.path === 'openapi.json');
        if (hasOpenAPI) {
            console.log(pc.gray(`  ${pc.green('●')} OpenAPI specification`));
        }
        // Show timing breakdown
        if (result.meta.timings) {
            const t = result.meta.timings;
            const migrationCmd = t.commands.find((c) => c.description.includes('migration'));
            const parts = [];
            parts.push(`parse ${(t.parseMs / 1000).toFixed(1)}s`);
            parts.push(`generate ${(t.generateMs / 1000).toFixed(1)}s`);
            if (migrationCmd)
                parts.push(`migrations ${(migrationCmd.ms / 1000).toFixed(1)}s`);
            if (spaBuilds.length > 0) {
                const spaTotal = spaBuilds.reduce((sum, s) => sum + s.ms, 0);
                parts.push(`SPAs ${(spaTotal / 1000).toFixed(1)}s`);
            }
            console.log(pc.dim(`  ⏱  ${parts.join(' → ')}`));
        }
        // Display warnings if any
        if (result.warnings && result.warnings.length > 0) {
            console.log(pc.yellow('\n⚠ Compilation warnings:'));
            for (const warning of result.warnings) {
                console.log(pc.yellow(`  • ${warning}`));
            }
            console.log();
        }
        const quickbackStateDir = await resolveQuickbackStateDir(projectRoot, outputDir);
        const shouldRouteStateArtifactsToQuickback = Boolean(quickbackStateDir);
        const { runtimeFiles, stateFiles } = partitionGeneratedFiles(result.files);
        spinner.start('Writing output...');
        const filesToWrite = shouldRouteStateArtifactsToQuickback ? runtimeFiles : result.files;
        // src/ is hermetic — every file there is generated. Archive the
        // existing tree, then wipe it before writing the fresh manifest.
        // This is the only way to make removed features, renamed handlers,
        // and obsolete SPA bundles actually disappear from the output;
        // additive merge writes leave them as orphans. The archive at
        // quickback/.archives/src-<ISO>/ is the recovery path if a generator
        // regression silently stops emitting something.
        const archiveDir = quickbackStateDir
            ? join(quickbackStateDir, '.archives')
            : join(outputDir, 'quickback', '.archives');
        const prepareResult = await prepareGeneratedDir(outputDir, 'src', filesToWrite.map((f) => f.path), {
            archiveDir,
            archiveKeep: 3,
        });
        if (prepareResult.wiped && prepareResult.archivePath) {
            console.log(pc.gray(`  Archived previous src/ → ${pc.cyan(relative(projectRoot, prepareResult.archivePath))}`));
        }
        else if (prepareResult.skipped === 'empty-manifest') {
            console.log(pc.yellow(`  ⚠ Skipping src/ wipe — compiler returned no files for src/. Output may contain stale code.`));
        }
        // Write the generated files. src/ is fresh from the wipe above;
        // migration SQL files outside src/ are kept by writeFilesWithMerge
        // itself; package.json is merged.
        await writeFilesWithMerge(outputDir, filesToWrite);
        if (quickbackStateDir && stateFiles.length > 0) {
            await writeFilesWithMerge(quickbackStateDir, stateFiles);
            spinner.succeed(`Output written to ${pc.cyan(outputDir)} (state artifacts synced to ${pc.cyan(quickbackStateDir)})`);
        }
        else {
            spinner.succeed(`Output written to ${pc.cyan(outputDir)}`);
        }
        // User-supplied static assets — anything under quickback/public/ rides
        // past the compiler verbatim into src/apps/, where the wrangler ASSETS
        // binding already serves it. Compiler stays out of the binary path.
        try {
            const publicResult = await copyPublicAssets({
                projectRoot,
                outputDir,
                manifestPaths: filesToWrite.map((f) => f.path),
            });
            if (publicResult.copied > 0) {
                console.log(pc.gray(`  ${pc.green('●')} Copied ${publicResult.copied} static asset(s) from quickback/public/ → src/apps/`));
            }
        }
        catch (error) {
            if (error instanceof PublicAssetCollisionError) {
                spinner.fail('Static asset collision');
                console.error(pc.red(error.message));
                process.exit(1);
            }
            throw error;
        }
        // User-authored source apps — anything under quickback/apps/<name>/
        // rides past the compiler verbatim into src/apps/<name>/, alongside the
        // compiler-emitted SPA shells (cms/, account/). Use this for hand-written
        // companion mini-SPAs (e.g. m/, admin/) that share the worker's ASSETS
        // binding. Static binaries belong in quickback/public/ instead.
        try {
            const appsResult = await copySourceApps({
                projectRoot,
                outputDir,
                manifestPaths: filesToWrite.map((f) => f.path),
            });
            if (appsResult.copied > 0) {
                console.log(pc.gray(`  ${pc.green('●')} Copied ${appsResult.copied} source app file(s) from quickback/apps/ → src/apps/`));
            }
        }
        catch (error) {
            if (error instanceof SourceAppCollisionError) {
                spinner.fail('Source app collision');
                console.error(pc.red(error.message));
                process.exit(1);
            }
            throw error;
        }
        // Run post-compile commands from providers
        if (result.commands && result.commands.length > 0) {
            console.log(pc.gray('\nRunning post-compile commands...\n'));
            for (const cmd of result.commands) {
                // Replace {{OUTPUT_DIR}} placeholder with actual path
                const command = cmd.command.replace(/\{\{OUTPUT_DIR\}\}/g, outputDir);
                // Determine working directory based on command's cwd setting
                // 'output' means project root (where compiled code lives)
                const commandCwd = cmd.cwd === 'output' ? outputDir : projectRoot;
                spinner.start(cmd.description);
                try {
                    await runCommand(command, { cwd: commandCwd });
                    spinner.succeed(cmd.description);
                }
                catch (error) {
                    if (cmd.optional) {
                        spinner.warn(`${cmd.description} (skipped)`);
                    }
                    else {
                        spinner.fail(cmd.description);
                        throw error;
                    }
                }
            }
        }
        const syncedSecurityReportFiles = shouldRouteStateArtifactsToQuickback
            ? 0
            : await syncSecurityReportsToQuickback(projectRoot, outputDir, result.files);
        if (syncedSecurityReportFiles > 0) {
            console.log(pc.gray(`  Synced ${syncedSecurityReportFiles} security report file(s) to quickback/reports`));
        }
        // Sync schema-driven kitchen-sink block into quickback.config.ts so the
        // user can discover every option Quickback supports without having to
        // read source. The block is owned by Quickback and replaced wholesale
        // on every compile; user-set keys are omitted server-side. To opt out:
        // `quickback compile --no-sync-config`.
        if (syncConfig && result.syncedConfig) {
            const syncResult = await syncConfigFile(configPath, result.syncedConfig);
            if (syncResult.changed) {
                const omittedNote = syncResult.omittedKeys.length > 0
                    ? ` (you've already set: ${syncResult.omittedKeys.join(', ')})`
                    : '';
                const action = syncResult.inserted ? 'Inserted' : 'Refreshed';
                console.log(pc.gray(`  ${action} options block in ${pc.cyan(relative(projectRoot, configPath))}${omittedNote}`));
            }
            else if (syncResult.skippedReason) {
                console.log(pc.yellow(`  ⚠ Skipped config sync: ${syncResult.skippedReason}`));
            }
        }
        // Print next steps
        console.log(pc.green('\n✔ Compilation complete!'));
        console.log(`\nGenerated files for: ${pc.cyan(result.meta.features.join(', ') || 'no features')}`);
        console.log('\nNext steps:');
        console.log(pc.gray('  npm run dev'));
    }
    catch (error) {
        spinner.fail('Compilation failed');
        if (error instanceof Error) {
            console.error(pc.red('\nError:'), error.message);
            // Check for specific error types
            if (error.message.includes('requires a Pro account')) {
                console.log(pc.yellow('\nThis template requires a Pro account.'));
                console.log('Run: quickback login');
            }
            else if (error.message.includes('fetch')) {
                console.log(pc.yellow('\nCould not connect to the Quickback API.'));
                console.log('Check your internet connection and try again.');
            }
        }
        else {
            console.error(pc.red('\nError:'), error);
        }
        process.exit(1);
    }
}
//# sourceMappingURL=compile.js.map