"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const core_1 = require("@oclif/core");
const chalk_1 = __importDefault(require("chalk"));
const ora_1 = __importDefault(require("ora"));
const fs = __importStar(require("node:fs"));
const path = __importStar(require("node:path"));
const api_client_1 = require("../../lib/api-client");
const config_storage_1 = require("../../lib/config-storage");
const prompts_1 = require("../../lib/prompts");
const template_fetcher_1 = require("../../lib/template-fetcher");
const framework_detector_1 = require("../../lib/framework-detector");
const framework_postchecks_1 = require("../../lib/framework-postchecks");
const DEPLOY_FILES_COMMON = ['flui.yaml', 'Dockerfile', '.dockerignore'];
const DEPLOY_FILES_BY_FRAMEWORK = {
    astro: ['nginx/default.conf'],
    angular: ['nginx/default.conf'],
};
function filesForFramework(framework) {
    return [
        ...DEPLOY_FILES_COMMON,
        ...(DEPLOY_FILES_BY_FRAMEWORK[framework] ?? []),
    ];
}
class AppInit extends core_1.Command {
    async run() {
        const { args, flags } = await this.parse(AppInit);
        const configStorage = new config_storage_1.ConfigStorage();
        const apiUrl = configStorage.getApiUrlOrThrow();
        const apiKey = configStorage.getApiKey();
        if (!apiKey) {
            this.error('Not logged in. Run `flui auth login` first.', { exit: 1 });
        }
        const api = new api_client_1.ApiClient({ baseUrl: apiUrl, apiKey });
        if (flags.list) {
            await this.printList(api);
            return;
        }
        if (!args.framework) {
            this.error('Missing framework argument. Run `flui app init --list` to see the supported frameworks.', { exit: 1 });
        }
        const { framework, version } = (0, template_fetcher_1.parseFrameworkArg)(args.framework);
        const targetDir = path.resolve(flags.target ?? process.cwd());
        if (!fs.existsSync(targetDir)) {
            this.error(`Target directory does not exist: ${targetDir}`, { exit: 1 });
        }
        const template = await this.resolveTemplate(api, framework, version, flags.force);
        if (!template) {
            this.exit(1);
        }
        if (template.isDeprecated) {
            console.log(chalk_1.default.yellow(`\n⚠  ${template.displayName} v${template.version} is marked as deprecated. ` +
                `Consider pinning a supported major version.\n`));
        }
        const detected = (0, framework_detector_1.detectFrameworkVersion)(framework, targetDir);
        if (detected && detected.major !== template.version) {
            const stableOutput = ['astro', 'angular', 'sveltekit'].includes(framework);
            if (stableOutput) {
                console.log(chalk_1.default.cyan(`\nℹ  Template targets ${framework}@${template.version}; your project is on ${framework}@${detected.major} (${detected.source}).`));
                console.log(chalk_1.default.dim(`   The Dockerfile is a static-output build (nginx-served) and works the same across recent majors of ${framework}. No action required.\n`));
            }
            else {
                console.log(chalk_1.default.yellow(`\n⚠  Version mismatch: your project pins ${chalk_1.default.bold(framework + '@' + detected.major)} ` +
                    `(from ${detected.source}), but the template targets v${template.version}.`));
                console.log(chalk_1.default.dim(`   The Dockerfile assumes the v${template.version} build artifacts (entrypoint, output paths). Either align the framework version, ` +
                    `pin a matching template (\`flui app init --list\`), or review the Dockerfile before deploying.\n`));
            }
        }
        else if (detected) {
            console.log(chalk_1.default.dim(`  Detected ${framework}@${detected.major} in this project — matches template.\n`));
        }
        console.log('');
        for (const file of filesForFramework(framework)) {
            await this.processFile(template, file, targetDir, flags);
        }
        const renamed = flags['dry-run']
            ? null
            : this.autoFillFluiYamlName(targetDir);
        if (renamed) {
            console.log(chalk_1.default.dim(`  · flui.yaml metadata.name set to "${renamed.name}" (from ${renamed.source}).`));
        }
        this.printPostCheckResults(framework, targetDir);
        this.printNextSteps(targetDir, template, flags['dry-run']);
    }
    /**
     * Replace the template placeholder `metadata.name` (e.g. `my-astro-app`,
     * `my-nextjs-app`, `my-app`) with a value derived from the user's project:
     *  1. `name` from package.json (if present)
     *  2. `name` from pyproject.toml `[project]`
     *  3. basename of the target directory
     * The substitution is conservative: it only triggers when the current value
     * starts with `my-` (template marker) so it never clobbers a name the user
     * has already personalised on a re-run.
     */
    autoFillFluiYamlName(targetDir) {
        const fluiYamlPath = path.join(targetDir, 'flui.yaml');
        if (!fs.existsSync(fluiYamlPath))
            return null;
        const raw = fs.readFileSync(fluiYamlPath, 'utf8');
        const match = /^(\s*)name:\s*([^\s#]+)/m.exec(raw);
        if (!match)
            return null;
        const currentName = match[2];
        if (!currentName.startsWith('my-'))
            return null;
        const derived = this.deriveProjectName(targetDir);
        if (!derived || derived.name === currentName)
            return null;
        const sanitized = this.sanitizeAppName(derived.name);
        if (!sanitized)
            return null;
        const patched = raw.replace(/^(\s*)name:\s*([^\s#]+)/m, `$1name: ${sanitized}`);
        fs.writeFileSync(fluiYamlPath, patched, 'utf8');
        return { name: sanitized, source: derived.source };
    }
    deriveProjectName(targetDir) {
        try {
            const pkgPath = path.join(targetDir, 'package.json');
            if (fs.existsSync(pkgPath)) {
                const parsed = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
                if (parsed.name)
                    return { name: parsed.name, source: 'package.json' };
            }
        }
        catch {
            /* ignore */
        }
        try {
            const pyPath = path.join(targetDir, 'pyproject.toml');
            if (fs.existsSync(pyPath)) {
                const raw = fs.readFileSync(pyPath, 'utf8');
                const m = /^\s*name\s*=\s*['"]([^'"]+)['"]/m.exec(raw);
                if (m)
                    return { name: m[1], source: 'pyproject.toml' };
            }
        }
        catch {
            /* ignore */
        }
        const base = path.basename(path.resolve(targetDir));
        if (base && base !== '.' && base !== '/') {
            return { name: base, source: 'directory name' };
        }
        return null;
    }
    /**
     * Coerce an arbitrary project name into the slug shape expected by the
     * flui/v1 schema (lowercase, alphanumerics + dashes, no leading/trailing
     * dash). Falls back to null if the result is empty.
     */
    sanitizeAppName(raw) {
        const slug = raw
            .toLowerCase()
            .replace(/^@[^/]+\//, '')
            .replaceAll(/[^a-z0-9-]+/g, '-')
            .replaceAll(/^-+|-+$/g, '');
        return slug || null;
    }
    printPostCheckResults(framework, targetDir) {
        const issues = (0, framework_postchecks_1.runFrameworkPostChecks)(framework, targetDir);
        if (issues.length === 0)
            return;
        for (const issue of issues) {
            const icon = issue.level === 'warn' ? chalk_1.default.yellow('⚠') : chalk_1.default.cyan('ℹ');
            console.log('');
            console.log(`  ${icon} ${chalk_1.default.bold(issue.title)}`);
            console.log(chalk_1.default.dim(`     ${issue.detail}`));
            for (const line of issue.hint.split('\n')) {
                console.log(chalk_1.default.dim(`     ${line}`));
            }
        }
    }
    async resolveTemplate(api, framework, version, force) {
        const refLabel = framework + (version ? `@${version}` : '');
        const spinner = (0, ora_1.default)(`Resolving template ${chalk_1.default.bold(refLabel)}…`).start();
        try {
            const template = await (0, template_fetcher_1.getTemplate)(api, framework, version);
            spinner.succeed(`Template: ${chalk_1.default.bold(template.displayName)} v${template.version} (${template.repo})`);
            return template;
        }
        catch (error) {
            spinner.stop();
            const available = await (0, template_fetcher_1.listVersionsFor)(api, framework).catch(() => []);
            if (available.length === 0) {
                console.log(chalk_1.default.red(`\n  Template "${framework}" not found.\n`));
                return undefined;
            }
            if (!version) {
                console.log(chalk_1.default.red(`\n  ${error.message}\n`));
                return undefined;
            }
            const fallback = (0, template_fetcher_1.pickDefault)(available) ?? available[0];
            const availStr = available.map((t) => `v${t.version}`).join(', ');
            console.log(chalk_1.default.yellow(`\n⚠  ${framework}@${version} is not in the registry. Available: ${availStr}.`));
            if (force || !process.stdin.isTTY) {
                console.log(chalk_1.default.yellow(`   Falling back to ${chalk_1.default.bold(fallback.framework + '@' + fallback.version)} ` +
                    `(${force ? '--force' : 'non-TTY'}).\n`));
                return fallback;
            }
            const ok = await (0, prompts_1.confirmPrompt)(`Use ${fallback.framework}@${fallback.version} instead?`, true);
            if (!ok) {
                console.log(chalk_1.default.dim('\n  Cancelled.\n'));
                return undefined;
            }
            console.log('');
            return fallback;
        }
    }
    async printList(api) {
        const spinner = (0, ora_1.default)('Loading templates…').start();
        let templates;
        try {
            templates = await (0, template_fetcher_1.listTemplates)(api);
            spinner.stop();
        }
        catch (error) {
            spinner.fail('Failed to load templates');
            console.log(chalk_1.default.red(`\n  ${error.message}\n`));
            this.exit(1);
        }
        const sorted = [...templates].sort((a, b) => a.framework.localeCompare(b.framework) ||
            a.version.localeCompare(b.version));
        const rows = sorted.map((t) => ({
            ref: `${t.framework}@${t.version}`,
            name: t.displayName,
            lang: t.language,
            cat: t.category,
            flags: [
                t.isDefault ? chalk_1.default.green('default') : '',
                t.isDeprecated ? chalk_1.default.yellow('deprecated') : '',
            ]
                .filter(Boolean)
                .join(' '),
        }));
        const refW = Math.max(...rows.map((r) => r.ref.length), 3);
        const nameW = Math.max(...rows.map((r) => r.name.length), 4);
        const langW = Math.max(...rows.map((r) => r.lang.length), 4);
        console.log('');
        console.log(`  ${chalk_1.default.bold('REF'.padEnd(refW))}  ${chalk_1.default.bold('NAME'.padEnd(nameW))}  ${chalk_1.default.bold('LANG'.padEnd(langW))}  ${chalk_1.default.bold('CATEGORY')}`);
        console.log(`  ${'─'.repeat(refW)}  ${'─'.repeat(nameW)}  ${'─'.repeat(langW)}  ${'─'.repeat(10)}`);
        for (const r of rows) {
            const flagsSuffix = r.flags ? `  ${r.flags}` : '';
            console.log(`  ${r.ref.padEnd(refW)}  ${r.name.padEnd(nameW)}  ${r.lang.padEnd(langW)}  ${r.cat}${flagsSuffix}`);
        }
        console.log('');
        console.log(chalk_1.default.dim('  Use `flui app init <framework>` or `flui app init <framework>@<version>` to copy deploy files.\n'));
    }
    async processFile(template, filename, targetDir, flags) {
        const destPath = path.join(targetDir, filename);
        const relPath = path.relative(process.cwd(), destPath) || filename;
        const spinner = (0, ora_1.default)(`Fetching ${filename}…`).start();
        let content;
        try {
            content = await (0, template_fetcher_1.fetchRawFile)(template.repo, filename);
            spinner.stop();
        }
        catch (error) {
            spinner.stop();
            console.log(chalk_1.default.dim(`  · skipped ${filename} (not in template ${template.repo}): ${error.message}`));
            return;
        }
        const destDir = path.dirname(destPath);
        if (!fs.existsSync(destDir)) {
            fs.mkdirSync(destDir, { recursive: true });
        }
        const exists = fs.existsSync(destPath);
        if (!exists) {
            if (flags['dry-run']) {
                console.log(`  ${chalk_1.default.green('+')} would write ${relPath}`);
                return;
            }
            fs.writeFileSync(destPath, content, 'utf8');
            console.log(`  ${chalk_1.default.green('✔')} wrote ${relPath}`);
            return;
        }
        if (flags.force) {
            if (flags['dry-run']) {
                console.log(`  ${chalk_1.default.yellow('~')} would overwrite ${relPath}`);
                return;
            }
            fs.writeFileSync(destPath, content, 'utf8');
            console.log(`  ${chalk_1.default.yellow('✔')} overwrote ${relPath}`);
            return;
        }
        if (flags['dry-run']) {
            console.log(`  ${chalk_1.default.yellow('?')} ${relPath} exists — would prompt for skip/overwrite/backup`);
            return;
        }
        const action = await this.promptConflict(relPath, destPath, content);
        if (action === 'skip') {
            console.log(`  ${chalk_1.default.dim('·')} skipped ${relPath}`);
            return;
        }
        if (action === 'backup') {
            const backupPath = `${destPath}.bak`;
            fs.copyFileSync(destPath, backupPath);
            console.log(`  ${chalk_1.default.dim('·')} backed up existing → ${path.relative(process.cwd(), backupPath)}`);
        }
        fs.writeFileSync(destPath, content, 'utf8');
        console.log(`  ${chalk_1.default.yellow('✔')} overwrote ${relPath}`);
    }
    async promptConflict(relPath, destPath, newContent) {
        while (true) {
            const choice = await (0, prompts_1.selectWithArrows)(`${relPath} already exists.`, [
                { label: 'Skip (keep existing)' },
                { label: 'Overwrite' },
                { label: 'Backup existing as .bak, then overwrite' },
                { label: 'Show diff against existing' },
            ]);
            if (choice === -1 || choice === 0)
                return 'skip';
            if (choice === 1) {
                const ok = await (0, prompts_1.confirmPrompt)(`Overwrite ${relPath}?`, false);
                if (ok)
                    return 'overwrite';
                continue;
            }
            if (choice === 2)
                return 'backup';
            if (choice === 3) {
                this.printDiff(destPath, newContent);
            }
        }
    }
    printDiff(existingPath, newContent) {
        const existing = fs.readFileSync(existingPath, 'utf8');
        const a = existing.split('\n');
        const b = newContent.split('\n');
        const max = Math.max(a.length, b.length);
        console.log('');
        console.log(chalk_1.default.dim(`  --- existing  +++ template`));
        for (let i = 0; i < max; i++) {
            const left = a[i];
            const right = b[i];
            if (left === right)
                continue;
            if (left !== undefined)
                console.log(chalk_1.default.red(`  - ${left.slice(0, 200)}`));
            if (right !== undefined)
                console.log(chalk_1.default.green(`  + ${right.slice(0, 200)}`));
        }
        console.log('');
    }
    printNextSteps(targetDir, template, dryRun) {
        console.log('');
        if (dryRun) {
            console.log(chalk_1.default.dim('  (dry-run) Re-run without --dry-run to apply.\n'));
            return;
        }
        console.log(chalk_1.default.dim('  Next steps:'));
        if (targetDir !== process.cwd()) {
            console.log(chalk_1.default.dim(`    cd ${path.relative(process.cwd(), targetDir)}`));
        }
        console.log(chalk_1.default.dim(`    # 1. Review the changes`));
        console.log(chalk_1.default.dim(`    git diff`));
        console.log(chalk_1.default.dim(`    # 2. Verify flui.yaml — metadata.name is auto-filled when possible,`));
        console.log(chalk_1.default.dim(`    #    confirm port=${template.port}, healthcheck=${template.healthcheckPath}, and any env vars your app needs`));
        console.log(chalk_1.default.dim(`    # 3. Validate the manifest`));
        console.log(chalk_1.default.dim(`    flui catalog validate ./flui.yaml`));
        console.log(chalk_1.default.dim(`    # 4. Commit and push, then deploy (build runs on GitHub Actions)`));
        console.log(chalk_1.default.dim(`    git add . && git commit -m "chore: add Flui deploy files" && git push`));
        console.log(chalk_1.default.dim(`    flui deploy`));
        console.log('');
    }
}
AppInit.description = 'Copy Flui deploy files (Dockerfile + flui.yaml) from an official framework template into the current directory. ' +
    'Use this on an existing Git repository to make it deployable with `flui deploy`. ' +
    'Run `flui app init --list` to see the supported frameworks and versions.';
AppInit.examples = [
    '<%= config.bin %> <%= command.id %> nextjs',
    '<%= config.bin %> <%= command.id %> nextjs@16',
    '<%= config.bin %> <%= command.id %> fastapi --target ./api',
    '<%= config.bin %> <%= command.id %> --list',
    '<%= config.bin %> <%= command.id %> nestjs --force',
    '<%= config.bin %> <%= command.id %> nuxt --dry-run',
];
AppInit.args = {
    framework: core_1.Args.string({
        description: 'Framework template, optionally with a major version pin (e.g. `nextjs` or `nextjs@16`). Omit when using --list.',
        required: false,
    }),
};
AppInit.flags = {
    list: core_1.Flags.boolean({
        description: 'Print supported frameworks and versions, then exit',
        default: false,
    }),
    force: core_1.Flags.boolean({
        description: 'Overwrite existing files without prompting',
        default: false,
    }),
    'dry-run': core_1.Flags.boolean({
        description: 'Print the actions that would be taken, no file writes',
        default: false,
    }),
    target: core_1.Flags.string({
        description: 'Target directory (default: current working directory)',
    }),
};
exports.default = AppInit;
