'use strict';

/**
 * Contract: schema migrate
 *
 * End-to-end migrate workflow: load schema -> validate -> generate DDL ->
 * split + modify -> apply to database via dialect driver. DESTRUCTIVE bila
 * --drop=true dan apply (bukan dry-run).
 *
 * Inline migration v4.0.0: business logic dari dbschema-migrate.js
 * dipindah ke handler. Test seam DBSCHEMA_KIT_TEST_APPLY_STUB preserved.
 */

const fs = require('fs');
const path = require('path');

const { loadSchemaPath } = require('../../lib/dbschema-kit/loader');
const { validateCrossModel } = require('../../lib/dbschema-kit/validator/cross-model-validator');
const { generateDDL } = require('../../lib/dbschema-kit/ddl-generator');
const { loadConfig } = require('../../lib/dbschema-kit/connection');
const { splitStatements } = require('../../lib/dbschema-kit/statement-splitter');
const { applyIfNotExistsModifier } = require('../../lib/dbschema-kit/statement-modifier');
const { resolveConfig, printDefaultConfigWarning } = require('../../lib/utils/config-resolver');

// Mapping dialect (internal naming di dbschema-kit) ke dbType (naming yang
// dipakai db-bootstrap helper). Hanya postgres dan mysql yang relevan untuk
// flow "create database if missing".
const DIALECT_TO_DB_TYPE = {
    postgres: 'postgresql',
    mysql: 'mysql'
};

function loadApplyExecutor() {
    const stubPath = process.env.DBSCHEMA_KIT_TEST_APPLY_STUB;
    if (stubPath) {
        return require(path.resolve(stubPath));
    }
    return require('../../lib/dbschema-kit/apply-executor');
}

function buildConnectionSummary(config) {
    if (config.dialect === 'sqlite') {
        return [
            `  Dialect:  sqlite`,
            `  File:     ${config.file}`
        ].join('\n');
    }
    if (config.dialect === 'oracle') {
        return [
            `  Dialect:  oracle`,
            `  Host:     ${config.host}:${config.port}`,
            `  Service:  ${config.serviceName}`,
            `  User:     ${config.user}`
        ].join('\n');
    }
    return [
        `  Dialect:  ${config.dialect}`,
        `  Host:     ${config.host}:${config.port}`,
        `  Database: ${config.database}`,
        `  User:     ${config.user}`
    ].join('\n');
}

function statementPreview(statement) {
    const firstLine = statement.split('\n')[0];
    if (firstLine.length <= 80) return firstLine;
    return firstLine.slice(0, 77) + '...';
}

function formatStatementForDisplay(stmt, dialect) {
    if (dialect === 'oracle' && /^\s*BEGIN\b/i.test(stmt)) {
        return stmt + '\n/';
    }
    return stmt + ';';
}

module.exports = {
    resource: 'schema',
    verb: 'migrate',
    description: 'Apply schema definition ke database (load → validate → generate DDL → execute). Destruktif.',
    category: 'management',
    flags: {
        path: {
            type: 'string',
            required: true,
            description: 'Path file atau folder schema (mis. ./schema atau ./schema/users.js)'
        },
        config: {
            type: 'string',
            required: false,
            default: null,
            description: 'File config database (.env). Fallback ke `.restforge/defaults.json` bila tidak disediakan eksplisit (set via `config set-default`)'
        },
        drop: {
            type: 'boolean',
            required: false,
            default: false,
            description: 'Drop tabel sebelum re-create (destruktif, data hilang)'
        },
        'dry-run': {
            type: 'boolean',
            required: false,
            default: false,
            description: 'Preview DDL tanpa apply ke database'
        },
        'max-name-length': {
            type: 'number',
            required: false,
            default: null,
            description: 'Batas panjang identifier (override default per dialect: postgres=63, mysql=64, oracle=128, sqlite=63)'
        },
        'auto-create-db': {
            type: 'boolean',
            required: false,
            default: false,
            description: 'Khusus postgres/mysql. Otomatis buat database jika belum ada (skip konfirmasi interaktif). Berguna untuk mode non-interaktif (CI/CD).'
        }
    },
    examples: [
        'npx restforge schema migrate --path=./schema --config=db.env --dry-run',
        'npx restforge schema migrate --path=./schema --config=db.env',
        'npx restforge schema migrate --path=./schema --config=db.env --auto-create-db'
    ],
    async handler(args) {
        const resolved = resolveConfig(args.config, process.cwd());
        if (!resolved) {
            console.error('Error: --config=<file> is required. Specify the database config file.');
            console.error("Tip: set a default with 'npx restforge config set-default --config=<file>' to omit --config in future runs");
            throw new Error('--config=<file> is required');
        }

        if (resolved.source === 'default') {
            printDefaultConfigWarning(resolved.defaultName);
        }

        const resolvedConfigPath = resolved.path;

        let config;
        try {
            config = loadConfig(resolvedConfigPath);
        } catch (err) {
            console.error(`Error: ${err.message}`);
            throw err;
        }

        const schemaPathArg = args.path;
        const absPath = path.resolve(process.cwd(), schemaPathArg);

        if (!fs.existsSync(absPath)) {
            console.error(`Error: Schema path not found: ${absPath}`);
            throw new Error(`Schema path not found: ${absPath}`);
        }

        const isFolder = fs.statSync(absPath).isDirectory();

        let models;
        try {
            models = loadSchemaPath(absPath);
        } catch (err) {
            console.error(`Error: ${err.message}`);
            throw err;
        }

        if (isFolder) {
            const crossModelIssues = validateCrossModel(models);
            const crossModelErrors = crossModelIssues.filter((i) => i.severity === 'error');
            if (crossModelErrors.length > 0) {
                for (const issue of crossModelErrors) {
                    console.error(`Error: Cross-model validation failed: ${issue.message}`);
                }
                throw new Error(`Cross-model validation failed with ${crossModelErrors.length} error(s)`);
            }
        }

        let ddl;
        try {
            const ddlOptions = { dialect: config.dialect, drop: args.drop };
            if (args['max-name-length'] !== null && args['max-name-length'] !== undefined) {
                ddlOptions.maxNameLength = args['max-name-length'];
            }
            ddl = generateDDL(models, ddlOptions);
        } catch (err) {
            console.error(`Error: DDL generation failed: ${err.message}`);
            throw err;
        }

        if (args.drop === true) {
            console.error(`⚠ --drop=true will DROP all tables defined in ${schemaPathArg}. Existing data will be lost.`);
            console.error('');
        }

        let statements;
        try {
            statements = splitStatements(ddl, config.dialect);
        } catch (err) {
            console.error(`Error: ${err.message}`);
            throw err;
        }

        if (args.drop === false) {
            statements = statements.map((s) => applyIfNotExistsModifier(s, config.dialect));
        }

        if (args['dry-run']) {
            console.log('-- DDL Preview --');
            for (const s of statements) {
                console.log(formatStatementForDisplay(s, config.dialect));
                console.log('');
            }
            console.log('Dry-run complete. No changes applied.');
            // Dry-run sukses: selesai bersih (exit 0) tanpa melempar error.
            // Sebelumnya throw exitCode=2 dipakai sebagai sinyal "no changes",
            // tetapi menampilkan baris "Error: ..." menyesatkan user padahal
            // tidak ada kegagalan.
            return;
        }

        if (statements.length === 0) {
            console.log('No statements to apply.');
            return;
        }

        // ------------------------------------------------------------------
        // Konfirmasi destruktif untuk mode --drop (apply nyata saja; dry-run
        // sudah return di atas). Dialog y/N hanya ditampilkan di terminal
        // interaktif (TTY) dengan default No. Pada mode non-interaktif (CI/pipe)
        // prompt dilewati agar proses tidak hang, perilaku apply tetap seperti
        // sebelumnya.
        // ------------------------------------------------------------------
        if (args.drop === true) {
            const { isInteractive, promptYesNo } = require('../../../src/utils/db-bootstrap');
            if (isInteractive()) {
                const confirmed = await promptYesNo('Drop and re-create all tables now? Existing data will be lost. (y/N): ');
                if (!confirmed) {
                    console.log('Aborted by user. No changes applied.');
                    return;
                }
            }
        }

        // ------------------------------------------------------------------
        // Pre-check database existence (khusus postgres/mysql).
        // Bila database tujuan tidak ada, tawarkan pembuatan otomatis via
        // dialog konfirmasi (TTY) atau flag --auto-create-db (non-TTY).
        //
        // Setelah database berhasil dibuat, command exit dengan instruksi
        // re-run agar user dapat eksekusi migrate ulang dengan state yang
        // bersih (sesuai keputusan: create lalu exit, bukan auto-continue).
        //
        // Pre-check di-skip bila test stub aktif (DBSCHEMA_KIT_TEST_APPLY_STUB)
        // agar unit test tidak melakukan real network call ke database.
        // ------------------------------------------------------------------
        const targetDbType = DIALECT_TO_DB_TYPE[config.dialect];
        if (targetDbType && !process.env.DBSCHEMA_KIT_TEST_APPLY_STUB) {
            const {
                handleMissingDatabase,
                isDbNotExistsError,
                testConnection
            } = require('../../../src/utils/db-bootstrap');

            const connectionConfig = {
                host: config.host,
                port: config.port,
                user: config.user,
                password: config.password,
                database: config.database
            };

            let testErr = null;
            try {
                await testConnection(targetDbType, connectionConfig);
            } catch (err) {
                testErr = err;
            }

            if (testErr && isDbNotExistsError(targetDbType, testErr)) {
                const hintParts = ['npx restforge schema migrate', `--path=${args.path}`, `--config=${args.config}`];
                if (args.drop === true) hintParts.push('--drop=true');
                if (args['auto-create-db']) hintParts.push('--auto-create-db');
                const commandHint = hintParts.join(' ');

                const bootstrapResult = await handleMissingDatabase({
                    dbType: targetDbType,
                    error: testErr,
                    connectionConfig,
                    autoCreate: args['auto-create-db'],
                    commandHint
                });

                if (bootstrapResult.handled && bootstrapResult.created) {
                    // Stop here. User akan re-run command setelah db dibuat.
                    return;
                }

                if (bootstrapResult.handled && bootstrapResult.hint) {
                    console.log('');
                    console.log(`Tip: ${bootstrapResult.hint}`);
                }

                console.error(`Error: Cannot connect to ${config.dialect}. ${testErr.message}`);
                throw testErr;
            }
            // Selain "database tidak ada" (misal credentials salah, host unreachable),
            // biarkan executor yang menangani error agar pesan tetap konsisten
            // dengan behavior sebelumnya.
        }

        console.log('Connection target:');
        console.log(buildConnectionSummary(config));
        console.log('');
        console.log(`Applying ${statements.length} statement(s) to ${config.dialect}...`);

        function progressHandler({ index, total, statement, status, error }) {
            if (status === 'running') return;
            const preview = statementPreview(statement);
            if (status === 'success') {
                process.stdout.write(`  ✓ [${index}/${total}] ${preview}\n`);
                return;
            }
            if (status === 'error') {
                console.error(`  ✗ [${index}/${total}] ${preview}`);
                const message = error && error.message ? error.message : String(error);
                console.error(`    Error: ${message}`);
            }
        }

        let executor;
        try {
            executor = loadApplyExecutor();
        } catch (err) {
            console.error(`Error: Failed to load apply executor: ${err.message}`);
            throw err;
        }

        let result;
        try {
            result = await executor.applyStatements({
                statements,
                dialect: config.dialect,
                config,
                onProgress: progressHandler
            });
        } catch (err) {
            if (err && err.status === 'ROLLBACK') {
                console.error('');
                console.error('ROLLBACK applied. Database state unchanged.');
                console.error(`  Statements applied before rollback: ${err.applied || 0}`);
                console.error(`  Failed statement index: ${err.failedIndex}`);
                throw err;
            }
            if (err && err.code === 'DRIVER_NOT_FOUND') {
                console.error(`Error: ${err.message}`);
                throw err;
            }
            const driverMessage = err && err.message ? err.message : String(err);
            if (/ECONNREFUSED|ETIMEDOUT|ENOTFOUND/i.test(driverMessage)) {
                console.error(`Error: Cannot connect to ${config.dialect}. ${driverMessage}`);
                throw err;
            }
            console.error(`Error: ${driverMessage}`);
            throw err;
        }

        if (result && result.status === 'SUCCESS') {
            console.log('');
            console.log('Status: SUCCESS');
            console.log(`  Statements applied: ${result.applied}`);
            console.log(`  Duration: ${result.durationMs}ms`);
            return;
        }

        if (result && result.status === 'PARTIAL') {
            const skipped = Math.max(0, statements.length - (result.applied || 0) - (result.failed || 0));
            console.error('');
            console.error('Partial apply detected:');
            console.error(`  ${result.applied || 0} applied`);
            console.error(`  ${result.failed || 1} failed`);
            console.error(`  ${skipped} skipped`);
            console.error('');
            console.error('Manual cleanup may be required.');
            throw new Error('Partial apply detected. Manual cleanup may be required.');
        }

        if (result && result.status === 'ROLLBACK') {
            console.error('');
            console.error('ROLLBACK applied. Database state unchanged.');
            throw new Error('ROLLBACK applied. Database state unchanged.');
        }

        console.log('');
        console.log(`Status: ${result ? result.status : 'UNKNOWN'}`);
    }
};
