"use strict";
// src/orchestrator/scaffold/ScaffoldEngine.ts
// v1.16.0 — Apply a template to a target directory.
//
// Contract:
//   1. Refuses to write into a non-empty target unless `force: true`.
//   2. Copies every file under the template root EXCEPT `manifest.yaml`
//      and anything matched by manifest.exclude[].
//   3. Performs Mustache-lite token substitution in TEXT files only.
//      Binary files (matched by a small extension blocklist) are copied
//      byte-for-byte. Tokens: {{project_name}}, {{description}},
//      {{node_version}}, plus anything in `vars`.
//   4. Renames files containing tokens in their basename
//      (e.g. `__project_name__.config.ts`).
//   5. Returns a structured report with files copied + substitutions done
//      + post-install steps from the manifest.
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;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ScaffoldEngine = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const TemplateRegistry_1 = require("./TemplateRegistry");
// Binary files we copy verbatim without attempting token substitution.
const BINARY_EXTENSIONS = new Set([
    '.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg-binary', '.ico', '.icns',
    '.woff', '.woff2', '.ttf', '.otf', '.eot',
    '.mp4', '.webm', '.mp3', '.wav', '.ogg',
    '.zip', '.tar', '.gz', '.tgz', '.bz2', '.7z', '.rar',
    '.pdf', '.psd', '.sketch', '.fig',
    '.bin', '.dat', '.exe', '.dll', '.so', '.dylib',
]);
function isBinaryFile(filePath) {
    return BINARY_EXTENSIONS.has(path.extname(filePath).toLowerCase());
}
function applySubstitutions(content, vars) {
    let count = 0;
    const out = content.replace(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_.-]*)\s*\}\}/g, (match, key) => {
        if (Object.prototype.hasOwnProperty.call(vars, key)) {
            count++;
            return vars[key];
        }
        return match; // Unknown tokens stay literal — surfaced via warning in caller.
    });
    return { content: out, count };
}
function renameFromTokens(name, vars) {
    return name.replace(/__([a-zA-Z_][a-zA-Z0-9_-]*)__/g, (match, key) => {
        return Object.prototype.hasOwnProperty.call(vars, key) ? vars[key] : match;
    });
}
function isDirEmpty(dir) {
    try {
        const entries = fs.readdirSync(dir);
        return entries.length === 0;
    }
    catch {
        return true; // Doesn't exist yet — treat as empty.
    }
}
function matchesExclude(relPath, exclude) {
    if (!exclude || exclude.length === 0)
        return false;
    // Normalize separators so patterns work uniformly on Windows + POSIX.
    const norm = relPath.replace(/\\/g, '/');
    for (const pattern of exclude) {
        // Exact match (typical file pattern).
        if (pattern === norm)
            return true;
        if (pattern.endsWith('/')) {
            // Directory pattern: matches the dir itself OR anything below it.
            const dirNoSlash = pattern.slice(0, -1);
            if (norm === dirNoSlash)
                return true;
            if (norm.startsWith(pattern))
                return true;
        }
        // Extension glob (e.g. `*.log`).
        if (pattern.startsWith('*.') && norm.endsWith(pattern.slice(1)))
            return true;
    }
    return false;
}
class ScaffoldEngine {
    registry;
    constructor(registry = new TemplateRegistry_1.TemplateRegistry()) {
        this.registry = registry;
    }
    /**
     * Apply `templateId` into `targetDir`. Creates targetDir if absent.
     * Returns a ScaffoldResult with file counts + warnings + errors.
     */
    apply(templateId, targetDir, opts) {
        const manifest = this.registry.get(templateId);
        if (!manifest || !manifest.rootDir) {
            return {
                ok: false,
                templateId,
                targetDir,
                manifest: { id: templateId, name: '', description: '', stack: {} },
                filesCopied: 0,
                filesSubstituted: 0,
                postInstallSteps: [],
                warnings: [],
                errors: [`template_not_found: ${templateId}`],
            };
        }
        const result = {
            ok: true,
            templateId,
            targetDir,
            manifest,
            filesCopied: 0,
            filesSubstituted: 0,
            // Post-install steps get the same substitution treatment as file content
            // so messages like `cd {{project_name}}` render with the real name.
            postInstallSteps: [],
            warnings: [],
            errors: [],
        };
        if (!opts.dryRun) {
            fs.mkdirSync(targetDir, { recursive: true });
            if (!opts.force && !isDirEmpty(targetDir)) {
                result.ok = false;
                result.errors.push(`target_not_empty: ${targetDir} (pass force: true to override)`);
                return result;
            }
        }
        const vars = {
            project_name: opts.project_name,
            description: opts.description || `A project scaffolded from ${manifest.name}`,
            node_version: process.version,
            ...(opts.vars || {}),
        };
        // Apply substitution to post-install steps before they reach the user.
        result.postInstallSteps = (manifest.post_install_steps || []).map((step) => applySubstitutions(step, vars).content);
        this.walkAndCopy(manifest.rootDir, manifest.rootDir, targetDir, vars, manifest, result, opts.dryRun === true);
        // Warn on unresolved tokens (best-effort — scans copied text files post-hoc).
        if (!opts.dryRun) {
            const unresolved = this.findUnresolvedTokens(targetDir);
            for (const u of unresolved.slice(0, 5)) {
                result.warnings.push(`unresolved_token: ${u}`);
            }
            if (unresolved.length > 5) {
                result.warnings.push(`...and ${unresolved.length - 5} more unresolved tokens`);
            }
        }
        return result;
    }
    walkAndCopy(rootDir, currentDir, destDir, vars, manifest, result, dryRun) {
        let entries;
        try {
            entries = fs.readdirSync(currentDir, { withFileTypes: true });
        }
        catch (err) {
            result.errors.push(`readdir_failed: ${currentDir} — ${err instanceof Error ? err.message : String(err)}`);
            return;
        }
        for (const ent of entries) {
            const srcPath = path.join(currentDir, ent.name);
            const relPath = path.relative(rootDir, srcPath);
            // Skip the manifest itself.
            if (relPath === 'manifest.yaml')
                continue;
            if (matchesExclude(relPath, manifest.exclude))
                continue;
            const renamed = renameFromTokens(ent.name, vars);
            const destPath = path.join(destDir, renamed);
            if (ent.isDirectory()) {
                if (!dryRun)
                    fs.mkdirSync(destPath, { recursive: true });
                this.walkAndCopy(rootDir, srcPath, destPath, vars, manifest, result, dryRun);
                continue;
            }
            if (!ent.isFile())
                continue;
            if (isBinaryFile(srcPath)) {
                if (!dryRun)
                    fs.copyFileSync(srcPath, destPath);
                result.filesCopied++;
                continue;
            }
            try {
                const content = fs.readFileSync(srcPath, 'utf-8');
                const { content: substituted, count } = applySubstitutions(content, vars);
                if (!dryRun)
                    fs.writeFileSync(destPath, substituted, 'utf-8');
                result.filesCopied++;
                if (count > 0)
                    result.filesSubstituted++;
            }
            catch (err) {
                result.errors.push(`copy_failed: ${relPath} — ${err instanceof Error ? err.message : String(err)}`);
            }
        }
    }
    /** Scan target for `{{token}}` patterns that survived. Returns ref strings. */
    findUnresolvedTokens(targetDir) {
        const out = [];
        function walk(dir) {
            let entries;
            try {
                entries = fs.readdirSync(dir, { withFileTypes: true });
            }
            catch {
                return;
            }
            for (const ent of entries) {
                const p = path.join(dir, ent.name);
                if (ent.isDirectory()) {
                    walk(p);
                    continue;
                }
                if (!ent.isFile() || isBinaryFile(p))
                    continue;
                try {
                    const content = fs.readFileSync(p, 'utf-8');
                    const m = content.match(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_.-]*)\s*\}\}/g);
                    if (m && m.length > 0) {
                        for (const token of new Set(m))
                            out.push(`${path.relative(targetDir, p)}:${token}`);
                    }
                }
                catch { /* skip unreadable */ }
            }
        }
        walk(targetDir);
        return out;
    }
}
exports.ScaffoldEngine = ScaffoldEngine;
