"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 __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
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 __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var InstallService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.InstallService = void 0;
const api_prisma_1 = require("@hed-hog/api-prisma");
const common_1 = require("@nestjs/common");
const config_1 = require("@nestjs/config");
const fs_1 = require("fs");
const promises_1 = require("fs/promises");
const path_1 = require("path");
const security_service_1 = require("../security/security.service");
const setting_service_1 = require("../setting/setting.service");
let InstallService = InstallService_1 = class InstallService {
    constructor(security, prisma, configService, settingService) {
        this.security = security;
        this.prisma = prisma;
        this.configService = configService;
        this.settingService = settingService;
        this.logger = new common_1.Logger(InstallService_1.name);
    }
    normalizeUrl(value) {
        return value.trim().replace(/\/$/, '');
    }
    async forceReset() {
        await new Promise((resolve) => setTimeout(resolve, 1000));
        // In development, touching main.ts triggers restart; in production only dist exists.
        const candidatePaths = [
            (0, path_1.resolve)(process.cwd(), 'src', 'main.ts'),
            (0, path_1.resolve)(process.cwd(), 'dist', 'apps', 'api', 'src', 'main.js'),
            (0, path_1.resolve)(process.cwd(), 'dist', 'src', 'main.js'),
        ];
        const mainFilePath = candidatePaths.find((filePath) => (0, fs_1.existsSync)(filePath));
        if (!mainFilePath) {
            this.logger.warn('Skip force reset: no main entry file found to touch.');
            return;
        }
        let mainContent;
        try {
            mainContent = (0, fs_1.readFileSync)(mainFilePath, 'utf-8');
        }
        catch (err) {
            throw new common_1.BadRequestException(`Failed to read main entry file: ${err.message}`);
        }
        try {
            this.logger.verbose(`Forcing application restart by touching ${mainFilePath}...`);
            await (0, promises_1.writeFile)(mainFilePath, mainContent, 'utf-8');
        }
        catch (err) {
            throw new common_1.BadRequestException(`Failed to write main entry file: ${err.message}`);
        }
    }
    async checkInstallation() {
        const installedSetting = await this.prisma.setting.findFirst({
            where: {
                slug: 'installed',
            },
            select: {
                value: true,
            },
        });
        if ((installedSetting === null || installedSetting === void 0 ? void 0 : installedSetting.value) === 'true') {
            return true;
        }
        const usersCount = await this.prisma.user.count();
        return usersCount > 0;
    }
    async updateEnvSecrets(pepper) {
        const envFilePath = (0, path_1.resolve)(process.cwd(), '..', '..', 'apps', 'api', '.env');
        if (!(0, fs_1.existsSync)(envFilePath)) {
            throw new common_1.BadRequestException('.env file not found.');
        }
        let envContent;
        try {
            envContent = await (0, fs_1.readFileSync)(envFilePath, 'utf-8');
        }
        catch (err) {
            throw new common_1.BadRequestException(`Failed to read .env file: ${err.message}`);
        }
        const envConfig = this.parseEnv(envContent);
        if (!('DATABASE_URL' in envConfig)) {
            const envExamplePath = (0, path_1.resolve)(process.cwd(), '..', '..', 'apps', 'api', '.env.example');
            if (!(0, fs_1.existsSync)(envExamplePath)) {
                throw new common_1.BadRequestException('.env.example file not found.');
            }
            try {
                const exampleContent = await (0, fs_1.readFileSync)(envExamplePath, 'utf-8');
                await (0, promises_1.writeFile)(envFilePath, exampleContent, 'utf-8');
                envContent = exampleContent;
                Object.assign(envConfig, this.parseEnv(envContent));
            }
            catch (err) {
                throw new common_1.BadRequestException(`Failed to copy .env.example: ${err.message}`);
            }
        }
        envConfig['JWT_SECRET'] = this.base64Encode(this.security.randomOpaque(32));
        envConfig['PEPPER'] = pepper;
        // Only wrap in double quotes if the value contains spaces, #, or special characters
        const newEnvContent = Object.entries(envConfig)
            .map(([key, value]) => {
            // If value contains only safe characters, no quotes needed
            if (/^[A-Za-z0-9._-]+$/.test(value)) {
                return `${key}=${value}`;
            }
            // Otherwise, escape quotes and wrap in double quotes
            return `${key}="${value.replace(/"/g, '\\"')}"`;
        })
            .join('\n');
        try {
            this.logger.verbose('Updating .env file with new secrets...');
            await (0, promises_1.writeFile)(envFilePath, newEnvContent, 'utf-8');
        }
        catch (err) {
            throw new common_1.BadRequestException(`Failed to write .env file: ${err.message}`);
        }
    }
    parseEnv(content) {
        const lines = content.split('\n').filter(line => line.trim() !== '');
        const envConfig = {};
        for (const line of lines) {
            if (line.startsWith('#') || !line.includes('='))
                continue;
            const [key, ...rest] = line.split('=');
            if (!key || rest.length === 0)
                continue;
            let value = rest.join('=').trim();
            // Remove surrounding double quotes if present
            if (value.startsWith('"') && value.endsWith('"')) {
                value = value.slice(1, -1).replace(/\\"/g, '"');
            }
            envConfig[key.trim()] = value;
        }
        return envConfig;
    }
    base64Encode(str) {
        return Buffer.from(str).toString('base64');
    }
    async install({ adminUrl, apiUrl, appName, email, password, slogan, userName, }) {
        if (await this.checkInstallation()) {
            throw new common_1.BadRequestException('Application is already installed.');
        }
        const normalizedAdminUrl = this.normalizeUrl(adminUrl !== null && adminUrl !== void 0 ? adminUrl : 'http://localhost:3200');
        const normalizedApiUrl = this.normalizeUrl(apiUrl !== null && apiUrl !== void 0 ? apiUrl : 'http://localhost:3100');
        await this.prisma.$transaction(async (prisma) => {
            this.logger.log('Starting installation process...');
            await prisma.setting.updateMany({
                where: {
                    slug: { in: ['system-name', 'mfa-issuer'] }
                },
                data: {
                    value: appName
                }
            });
            this.logger.log(`Set system name to: ${appName}`);
            this.logger.log(`Setting system slogan to: ${slogan}`);
            await prisma.setting.update({
                where: {
                    slug: 'system-slogan'
                },
                data: {
                    value: slogan
                }
            });
            await prisma.setting.update({
                where: {
                    slug: 'url',
                },
                data: {
                    value: normalizedAdminUrl,
                },
            });
            await prisma.setting.update({
                where: {
                    slug: 'api-url',
                },
                data: {
                    value: normalizedApiUrl,
                },
            });
            this.logger.log('System slogan set.');
            this.logger.log(`Setting system email to: ${email}`);
            const check = await prisma.user.findFirst({
                where: {
                    user_identifier: {
                        some: {
                            type: 'email',
                            value: email,
                            enabled: true
                        }
                    }
                }
            });
            if (check) {
                throw new common_1.BadRequestException('A user with this email already exists.');
            }
            this.logger.log(`Creating admin user: ${userName} <${email}>`);
            const pepper = this.configService.get('PEPPER');
            if (!pepper) {
                throw new common_1.BadRequestException('PEPPER is not configured. Set PEPPER before installation.');
            }
            const passwordHash = await this.security.hashArgon2(password, pepper);
            const user = await prisma.user.create({
                data: {
                    name: userName,
                    user_identifier: {
                        create: {
                            type: 'email',
                            value: email,
                            enabled: true,
                        }
                    },
                    user_credential: {
                        create: {
                            type: 'password',
                            hash: passwordHash,
                        }
                    }
                }
            });
            this.logger.log(`Admin user created with ID: ${user.id}`);
            const roles = await prisma.role.findMany();
            if (roles.length === 0) {
                throw new common_1.BadRequestException('No roles found. Please seed the roles before installation.');
            }
            this.logger.log(`Assigning roles to user ID: ${user.id}`);
            await prisma.role_user.createMany({
                data: roles.map((role) => ({
                    role_id: role.id,
                    user_id: user.id,
                })),
            });
            this.logger.log(`Roles assigned to user ID: ${user.id}`);
            await prisma.setting.update({
                where: {
                    slug: 'installed',
                },
                data: {
                    value: 'true',
                },
            });
            this.logger.log('Installation flag set to true.');
            const isDevelopment = process.env.NODE_ENV !== 'production';
            if (isDevelopment) {
                await this.updateEnvSecrets(pepper);
                await this.forceReset();
            }
            else {
                this.logger.log('Skipping env secret update and force reset outside development.');
            }
            this.logger.log('Installation process completed successfully.');
        });
        this.settingService.clearCache();
        return { success: true };
    }
    async check() {
        return {
            success: true,
            installed: await this.checkInstallation(),
        };
    }
    async generateMailMigration({ slug, translations, variables, }) {
        try {
            const timestamp = new Date().toISOString().replace(/[-:T]/g, '').split('.')[0];
            const migrationName = `${timestamp}_mail_${slug.replace(/[^a-z0-9]/gi, '_')}`;
            const migrationsPath = (0, path_1.resolve)(process.cwd(), 'prisma', 'migrations', migrationName);
            const fs = await Promise.resolve().then(() => __importStar(require('fs/promises')));
            await fs.mkdir(migrationsPath, { recursive: true });
            const escapeSQL = (str) => str.replace(/'/g, "''");
            let sql = `-- CreateMailTemplate: ${slug}\n\n`;
            sql += `-- Insert mail template\n`;
            sql += `INSERT INTO "mail" ("slug", "created_at", "updated_at")\n`;
            sql += `VALUES ('${escapeSQL(slug)}', NOW(), NOW())\n`;
            sql += `ON CONFLICT ("slug") DO NOTHING;\n\n`;
            if (translations && translations.length > 0) {
                sql += `-- Insert mail locales\n`;
                sql += `INSERT INTO "mail_locale" ("mail_id", "locale_id", "subject", "body")\n`;
                sql += `VALUES \n`;
                const localeValues = translations.map((translation, index) => {
                    const comma = index < translations.length - 1 ? ',' : '';
                    return `  ((SELECT "id" FROM "mail" WHERE "slug" = '${escapeSQL(slug)}'), (SELECT "id" FROM "locale" WHERE "code" = '${escapeSQL(translation.locale_code)}'), '${escapeSQL(translation.subject)}', '${escapeSQL(translation.body)}')${comma}`;
                }).join('\n');
                sql += localeValues;
                sql += `\nON CONFLICT ("mail_id", "locale_id") DO UPDATE\n`;
                sql += `  SET "subject" = EXCLUDED."subject",\n`;
                sql += `      "body" = EXCLUDED."body";\n\n`;
            }
            if (variables && variables.length > 0) {
                sql += `-- Insert mail variables\n`;
                sql += `INSERT INTO "mail_var" ("mail_id", "name")\n`;
                sql += `VALUES \n`;
                const varValues = variables.map(v => `  ((SELECT "id" FROM "mail" WHERE "slug" = '${escapeSQL(slug)}'), '${escapeSQL(v)}')`).join(',\n');
                sql += varValues;
                sql += `\nON CONFLICT ("mail_id", "name") DO NOTHING;\n`;
            }
            const migrationFilePath = (0, path_1.resolve)(migrationsPath, 'migration.sql');
            await fs.writeFile(migrationFilePath, sql, 'utf-8');
            this.logger.log(`Migration created: ${migrationName}`);
            return {
                success: true,
                migrationName,
                path: migrationsPath,
            };
        }
        catch (error) {
            this.logger.error('Error generating mail migration:', error);
            throw new common_1.BadRequestException(`Failed to generate migration: ${error.message}`);
        }
    }
};
exports.InstallService = InstallService;
exports.InstallService = InstallService = InstallService_1 = __decorate([
    (0, common_1.Injectable)(),
    __metadata("design:paramtypes", [security_service_1.SecurityService,
        api_prisma_1.PrismaService,
        config_1.ConfigService,
        setting_service_1.SettingService])
], InstallService);
//# sourceMappingURL=install.service.js.map