/**
 * Project Scaffolder - New project generation
 * Handles directory structure and boilerplate file creation
 */
import { EnvFileBackfill } from '../env/EnvFileBackfill.js';
import { EnvData } from '../scaffolding/env.js';
import { Logger } from '../../config/logger.js';
import { ErrorFactory } from '../../exceptions/ZintrustError.js';
import { randomBytes } from '../../node-singletons/crypto.js';
import fs from '../../node-singletons/fs.js';
import * as path from '../../node-singletons/path.js';
import { fileURLToPath } from '../../node-singletons/url.js';
const loadCoreVersion = () => {
    try {
        const packageUrl = new URL('../../../package.json', import.meta.url);
        const packageJson = JSON.parse(fs.readFileSync(packageUrl, 'utf-8'));
        return typeof packageJson.version === 'string' ? packageJson.version : '0.0.0';
    }
    catch {
        return '0.0.0';
    }
};
const toCompatibleGovernanceVersion = (version) => {
    const match = /(\d+)\.(\d+)\.(\d+)/.exec(version);
    if (match !== null) {
        const [, major, minor] = match;
        return `^${major}.${minor}.0`;
    }
    return '^0.4.0';
};
const createDirectories = (projectPath, directories) => {
    let count = 0;
    if (!fs.existsSync(projectPath)) {
        fs.mkdirSync(projectPath, { recursive: true });
        count++;
    }
    for (const dir of directories) {
        const fullPath = path.join(projectPath, dir);
        if (!fs.existsSync(fullPath)) {
            fs.mkdirSync(fullPath, { recursive: true });
            count++;
        }
    }
    return count;
};
const createFiles = (projectPath, files, variables) => {
    const renderPathVar = (value) => {
        if (value === undefined || value === null)
            return '';
        if (typeof value === 'string')
            return value;
        if (typeof value === 'number' || typeof value === 'bigint')
            return value.toString();
        if (typeof value === 'boolean')
            return value ? 'true' : 'false';
        return '';
    };
    const renderContentVar = (value) => {
        if (value === undefined || value === null)
            return '';
        if (typeof value === 'string')
            return value;
        if (typeof value === 'number' || typeof value === 'bigint')
            return value.toString();
        if (typeof value === 'boolean')
            return value ? 'true' : 'false';
        if (typeof value === 'symbol')
            return value.toString();
        if (typeof value === 'function')
            return '[Function]';
        try {
            return JSON.stringify(value);
        }
        catch {
            return '';
        }
    };
    let count = 0;
    for (const [file, content] of Object.entries(files)) {
        const renderedPath = file.replaceAll(/\{\{\s*(\w+)\s*\}\}/g, (_m, key) => renderPathVar(variables[key]));
        const fullPath = path.join(projectPath, renderedPath);
        const rendered = content.replaceAll(/\{\{\s*(\w+)\s*\}\}/g, (_m, key) => renderContentVar(variables[key]));
        fs.mkdirSync(path.dirname(fullPath), { recursive: true });
        fs.writeFileSync(fullPath, rendered);
        count++;
    }
    return count;
};
const createProjectConfigFile = (projectPath, variables) => {
    try {
        if (!fs.existsSync(projectPath)) {
            fs.mkdirSync(projectPath, { recursive: true });
        }
        const fullPath = path.join(projectPath, '.zintrust.json');
        const config = {
            name: variables['projectName'] ?? variables['name'],
            database: {
                connection: variables['database'] ?? 'sqlite',
            },
            server: {
                port: variables['port'] ?? 7777,
            },
        };
        fs.writeFileSync(fullPath, JSON.stringify(config, null, 2));
        return true;
    }
    catch {
        return false;
    }
};
const buildDbOverrides = (dbConnection) => {
    const normalized = dbConnection.trim().toLowerCase();
    if (normalized === 'mysql') {
        return {
            dbLines: [
                'DB_HOST=localhost',
                'DB_PORT=3306',
                'DB_DATABASE=zintrust',
                'DB_USERNAME=root',
                'DB_PASSWORD=',
            ],
        };
    }
    if (normalized === 'postgres' || normalized === 'postgresql') {
        return {
            dbLines: [
                'DB_HOST=localhost',
                'DB_PORT=5432',
                'DB_DATABASE=zintrust',
                'DB_USERNAME=postgres',
                'DB_PASSWORD=',
            ],
        };
    }
    if (normalized === 'sqlserver' || normalized === 'mssql') {
        return {
            dbLines: [
                'DB_HOST_MSSQL=localhost',
                'DB_PORT_MSSQL=1433',
                'DB_DATABASE_MSSQL=zintrust',
                'DB_USERNAME_MSSQL=sa',
                'DB_PASSWORD_MSSQL=',
            ],
        };
    }
    // sqlite/d1/d1-remote/etc: no overrides here (DB_PATH is handled via SQLITE_DB_PATH placeholder)
    return { dbLines: [] };
};
const toSafeDbBasename = (raw) => {
    const trimmed = raw.trim();
    if (trimmed === '')
        return 'zintrust';
    const lower = trimmed.toLowerCase();
    let out = '';
    let prevWasDash = false;
    for (const char of lower) {
        const isAlphaNum = (char >= 'a' && char <= 'z') || (char >= '0' && char <= '9');
        const isContent = isAlphaNum || char === '_';
        if (isContent) {
            out += char;
            prevWasDash = false;
            continue;
        }
        if (out.length > 0 && !prevWasDash) {
            out += '-';
            prevWasDash = true;
        }
    }
    if (out.endsWith('-')) {
        out = out.slice(0, -1);
    }
    return out === '' ? 'zintrust' : out;
};
const createEnvFile = (projectPath, variables) => {
    try {
        if (!fs.existsSync(projectPath)) {
            fs.mkdirSync(projectPath, { recursive: true });
        }
        const fullPath = path.join(projectPath, '.env');
        // If an .env already exists (e.g., from a template), do not overwrite user values.
        // But we *do* backfill safe defaults for common bootstrap keys when missing/blank.
        if (fs.existsSync(fullPath)) {
            EnvFileBackfill.backfillEnvDefaults(fullPath, {
                HOST: 'localhost',
                PORT: String(Number(variables['port'] ?? 7777)),
                LOG_LEVEL: 'debug',
                CSRF_SKIP_PATHS: '/api/*,/queue-monitor/*,/api/_sys/queue/*,/api/_sys/schedule/*',
            });
            return true;
        }
        const name = typeof variables['projectName'] === 'string' ? variables['projectName'] : 'zintrust-app';
        const port = Number(variables['port'] ?? 7777);
        const database = typeof variables['database'] === 'string' ? variables['database'] : 'sqlite';
        const baseUrl = `http://localhost:${port}`;
        const dbConnection = database.trim().toLowerCase();
        const sqliteDbPath = `.zintrust/dbs/${toSafeDbBasename(name)}.sqlite`;
        // Generate a secure APP_KEY (32 bytes = 256-bit, base64 encoded)
        const appKeyBytes = randomBytes(32);
        // Keep consistent with `zin key:generate` output.
        const appKey = `base64:${appKeyBytes.toString('base64')}`;
        const { dbLines } = buildDbOverrides(dbConnection);
        const content = EnvData(name, port, baseUrl, appKey, dbConnection, dbLines, sqliteDbPath).join('\n');
        fs.writeFileSync(fullPath, content.endsWith('\n') ? content : content + '\n');
        return true;
    }
    catch {
        Logger.error('Failed to create .env file');
        return false;
    }
};
const getProjectTemplatesRoot = () => {
    // src/cli/scaffolding/ -> src/templates/project/
    const templatesUrl = new URL('../../templates/project', import.meta.url);
    return fileURLToPath(templatesUrl);
};
const listTemplateFilesRecursive = (dirPath) => {
    const entries = fs.readdirSync(dirPath, { withFileTypes: true });
    const files = [];
    for (const entry of entries) {
        const full = path.join(dirPath, entry.name);
        if (entry.isDirectory()) {
            files.push(...listTemplateFilesRecursive(full));
            continue;
        }
        if (entry.isFile()) {
            files.push(full);
        }
    }
    return files;
};
const coerceStringArray = (value) => {
    if (!Array.isArray(value))
        return [];
    const out = [];
    for (const item of value) {
        if (typeof item === 'string')
            out.push(item);
    }
    return out;
};
const readTemplateJson = (templateJsonPath) => {
    try {
        return JSON.parse(fs.readFileSync(templateJsonPath, 'utf8'));
    }
    catch {
        return {};
    }
};
const resolveTemplateMetadata = (templateName, meta, fallback) => {
    const name = typeof meta.name === 'string' ? meta.name : (fallback?.name ?? templateName);
    const description = typeof meta.description === 'string' ? meta.description : (fallback?.description ?? '');
    const directories = Array.isArray(meta.directories)
        ? coerceStringArray(meta.directories)
        : (fallback?.directories ?? []);
    return { name, description, directories };
};
const loadTemplateFiles = (templateDir) => {
    const files = {};
    const allFiles = listTemplateFilesRecursive(templateDir);
    const allowedConfigFiles = new Set([
        'config/broadcast.ts',
        'config/cache.ts',
        'config/database.ts',
        'config/middleware.ts',
        'config/logging/HttpLogger.ts',
        'config/mail.ts',
        'config/notification.ts',
        'config/queue.ts',
        'config/storage.ts',
    ]);
    const isENOENT = (error) => error !== null &&
        typeof error === 'object' &&
        'code' in error &&
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        error.code === 'ENOENT';
    const normalizeRelPath = (relPath) => relPath.replaceAll('\\', '/');
    const getOutputRelPath = (relPath) => relPath.endsWith('.tpl') ? relPath.slice(0, -'.tpl'.length) : relPath;
    const shouldIncludeTemplateFile = (relPath) => {
        if (relPath === 'template.json')
            return false;
        const normalized = normalizeRelPath(relPath);
        // Project `.env` is generated by createEnvFile() so it can set defaults and create a secure APP_KEY.
        // Some templates ship `.env.tpl` (which would become `.env`), but that file is intentionally ignored.
        const outputRel = normalizeRelPath(getOutputRelPath(relPath));
        if (outputRel === '.env')
            return false;
        if (!normalized.startsWith('config/'))
            return true;
        // Starter apps should only ship app-level config modules.
        // Core/framework config internals (e.g. config/logging/*) remain core-owned.
        return allowedConfigFiles.has(outputRel);
    };
    const readUtf8FileOrUndefined = (absPath) => {
        try {
            return fs.readFileSync(absPath, 'utf8');
        }
        catch (error) {
            // Some tests temporarily create/delete template files in parallel; if a file
            // disappears between directory listing and read, skip it.
            if (isENOENT(error))
                return undefined;
            throw error;
        }
    };
    for (const absPath of allFiles) {
        const rel = path.relative(templateDir, absPath);
        if (!shouldIncludeTemplateFile(rel))
            continue;
        const content = readUtf8FileOrUndefined(absPath);
        if (content === undefined)
            continue;
        const outputRel = getOutputRelPath(rel);
        files[outputRel] = content;
    }
    return files;
};
const loadTemplateFromDisk = (templateName, fallback) => {
    const root = getProjectTemplatesRoot();
    const templateDir = path.join(root, templateName);
    const templateJsonPath = path.join(templateDir, 'template.json');
    if (!fs.existsSync(templateDir) || !fs.existsSync(templateJsonPath)) {
        return undefined;
    }
    const meta = readTemplateJson(templateJsonPath);
    const resolved = resolveTemplateMetadata(templateName, meta, fallback);
    const files = loadTemplateFiles(templateDir);
    return { ...resolved, files };
};
/**
 * Project Scaffolder Factory
 */
const BASIC_TEMPLATE = {
    name: 'basic',
    description: 'Basic ZinTrust project structure',
    directories: [
        'config',
        'app/Controllers',
        'app/Middleware',
        'app/Models',
        'database/migrations',
        'database/seeders',
        'logs',
        'storage',
        'tmp',
        'public',
        'routes',
        'src',
        'tests/unit',
        'tests/integration',
    ],
    files: {},
};
const API_TEMPLATE = {
    name: 'api',
    description: 'API-focused ZinTrust project structure',
    directories: [
        'app/Controllers',
        'app/Middleware',
        'app/Models',
        'logs',
        'storage',
        'tmp',
        'src',
        'routes',
        'tests',
    ],
    files: {},
};
const MICROSERVICE_TEMPLATE = {
    name: 'microservice',
    description: 'Microservice-focused ZinTrust project structure',
    directories: [
        'app/Controllers',
        'app/Middleware',
        'app/Models',
        'logs',
        'storage',
        'tmp',
        'routes',
        'tests',
        'src/services',
        'src/microservices',
    ],
    files: {},
};
const FULLSTACK_TEMPLATE = {
    name: 'fullstack',
    description: 'Fullstack ZinTrust project structure',
    directories: [
        'app/Controllers',
        'app/Middleware',
        'app/Models',
        // Starter projects should not include framework config internals.
        'database/migrations',
        'database/seeders',
        'logs',
        'storage',
        'tmp',
        'public',
        'routes',
        'src',
        'tests/unit',
        'tests/integration',
    ],
    files: {},
};
const TEMPLATE_MAP = new Map([
    ['basic', BASIC_TEMPLATE],
    ['api', API_TEMPLATE],
    ['microservice', MICROSERVICE_TEMPLATE],
    ['fullstack', FULLSTACK_TEMPLATE],
]);
export function getAvailableTemplates() {
    return [...TEMPLATE_MAP.keys()];
}
export function getTemplate(name) {
    const fallback = TEMPLATE_MAP.get(name);
    if (!fallback)
        return undefined;
    const disk = loadTemplateFromDisk(name, fallback);
    if (disk)
        return disk;
    // If we don't have a dedicated disk template yet (e.g. fullstack/api/microservice),
    // fall back to the starter project's file set so generated projects are runnable.
    if (name !== 'basic') {
        const basicFallback = TEMPLATE_MAP.get('basic');
        const basicDisk = basicFallback ? loadTemplateFromDisk('basic', basicFallback) : undefined;
        if (basicDisk) {
            return {
                ...fallback,
                files: basicDisk.files,
            };
        }
    }
    return fallback;
}
export function validateOptions(options) {
    const errors = [];
    if (!options.name) {
        errors.push('Project name is required');
    }
    if (options.name && !/^[a-z0-9_-]+$/.test(options.name)) {
        errors.push('Project name must contain only lowercase letters, numbers, hyphens, and underscores');
    }
    if (options.template !== undefined && !getTemplate(options.template)) {
        errors.push(`Template "${options.template}" not found`);
    }
    if (options.port !== undefined && (options.port < 1 || options.port > 65535)) {
        errors.push('Port must be a number between 1 and 65535');
    }
    return { valid: errors.length === 0, errors };
}
const resolveTemplate = (templateName) => getTemplate(templateName) ?? getTemplate('basic');
const prepareContext = (state, options) => {
    state.templateName = options.template ?? 'basic';
    state.projectPath = path.join(state.basePath, options.name);
    const migrationTimestamp = new Date()
        .toISOString()
        .replaceAll(/[-:T.Z]/g, '')
        .slice(0, 14);
    state.variables = {
        coreVersion: loadCoreVersion(),
        governanceVersion: toCompatibleGovernanceVersion(loadCoreVersion()),
        projectName: options.name,
        projectSlug: options.name,
        author: options.author ?? 'Your Name',
        description: options.description ?? '',
        port: options.port ?? 7777,
        database: options.database ?? 'sqlite',
        template: state.templateName,
        migrationTimestamp,
    };
};
const createDirectoriesForState = (state) => {
    const template = resolveTemplate(state.templateName);
    return createDirectories(state.projectPath, template?.directories ?? []);
};
const createFilesForState = (state) => {
    const template = resolveTemplate(state.templateName);
    const variables = state.variables;
    const templateFiles = template?.files;
    const files = templateFiles !== undefined && Object.keys(templateFiles).length > 0
        ? { ...templateFiles }
        : {};
    // Backward-compatible defaults for templates that don't ship these files yet.
    if (!Object.hasOwn(files, '.gitignore')) {
        files['.gitignore'] = `node_modules/
dist/
.env
.env.*
.env.local
.dev*
.DS_Store
coverage/
logs/
storage/
tmp/
.zintrust/
*.log
`;
    }
    if (!Object.hasOwn(files, 'README.md')) {
        files['README.md'] = `# {{projectName}}

Starter Task API built with ZinTrust.

## Run

\`\`\`bash
npm install
zin s
\`\`\`

- Health: http://localhost:{{port}}/health
- Tasks:  http://localhost:{{port}}/api/tasks
- Doc:  http://localhost:{{port}}/zintrust-doc
`;
    }
    return createFiles(state.projectPath, files, variables);
};
const scaffoldWithState = async (state, options
// eslint-disable-next-line @typescript-eslint/require-await
) => {
    try {
        const validation = validateOptions(options);
        if (!validation.valid) {
            return {
                success: false,
                projectPath: path.join(state.basePath, options.name),
                filesCreated: 0,
                directoriesCreated: 0,
                message: validation.errors.join('\n'),
            };
        }
        prepareContext(state, options);
        if (fs.existsSync(state.projectPath)) {
            if (options.overwrite === true) {
                fs.rmSync(state.projectPath, { recursive: true, force: true });
            }
            else {
                return {
                    success: false,
                    projectPath: state.projectPath,
                    filesCreated: 0,
                    directoriesCreated: 0,
                    message: `Project directory "${state.projectPath}" already exists`,
                };
            }
        }
        const directoriesCreated = createDirectoriesForState(state);
        const filesCreated = createFilesForState(state);
        createProjectConfigFile(state.projectPath, state.variables);
        createEnvFile(state.projectPath, state.variables);
        return {
            success: true,
            projectPath: state.projectPath,
            filesCreated,
            directoriesCreated,
            message: `Project "${options.name}" scaffolded successfully.`,
        };
    }
    catch (error) {
        Logger.error('Project scaffolding failed', error);
        const err = error instanceof Error ? error : ErrorFactory.createGeneralError(String(error));
        return {
            success: false,
            projectPath: state.projectPath,
            filesCreated: 0,
            directoriesCreated: 0,
            message: err.message,
            error: err,
        };
    }
};
/**
 * Plain-function scaffolder creator (replaces the function-object factory).
 */
export function createProjectScaffolder(projectPath = process.cwd()) {
    const state = {
        variables: {},
        basePath: projectPath,
        projectPath,
        templateName: 'basic',
    };
    return {
        prepareContext: (options) => prepareContext(state, options),
        getVariables: () => state.variables,
        getTemplateInfo: (templateName) => getTemplate(templateName ?? state.templateName),
        getProjectPath: () => state.projectPath,
        projectDirectoryExists: () => fs.existsSync(state.projectPath),
        createDirectories: () => createDirectoriesForState(state),
        createFiles: (_options) => createFilesForState(state),
        createConfigFile: () => createProjectConfigFile(state.projectPath, state.variables),
        createEnvFile: () => createEnvFile(state.projectPath, state.variables),
        scaffold: async (options) => scaffoldWithState(state, options),
    };
}
export async function scaffoldProject(projectPath, options) {
    return createProjectScaffolder(projectPath).scaffold(options);
}
/**
 * Sealed namespace for ProjectScaffolder
 */
export const ProjectScaffolder = Object.freeze({
    create: createProjectScaffolder,
    getAvailableTemplates,
    getTemplate,
    validateOptions,
    scaffold: scaffoldProject,
});
