'use strict';

/**
 * Contract: schema introspect
 *
 * Reverse-engineer database existing menjadi schema definition (dbschema-kit
 * factory function). Mendukung single-table, single-schema, multi-schema, dan
 * all-schemas modes. --dry-run print ke stdout tanpa menulis file.
 *
 * Inline migration v4.0.0: business logic dari dbschema-introspect.js
 * dipindah ke handler.
 */

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

const { loadConfig } = require('../../lib/dbschema-kit/connection');
const { mapTableMetaToIR } = require('../../lib/dbschema-kit/introspect-mapper');
const { serialize } = require('../../lib/dbschema-kit/schema-printer');
const { resolveConfig, printDefaultConfigWarning } = require('../../lib/utils/config-resolver');

function loadIntrospector() {
    const stubPath = process.env.DBSCHEMA_KIT_TEST_INTROSPECT_STUB;
    if (stubPath) {
        return require(path.resolve(stubPath));
    }
    return defaultIntrospector;
}

const defaultIntrospector = {
    listSchemas: async ({ config }) => {
        const dbType = mapDialectToDbType(config.dialect);
        if (dbType === 'sqlite') {
            throw new Error('dbschema:introspect does not yet support sqlite. Use postgres, mysql, or oracle.');
        }
        const { DatabaseIntrospector } = require('../../lib/utils/database-introspector');
        const introspector = new DatabaseIntrospector({ quiet: true });
        const tmpPath = createTmpEnvFile(buildEphemeralEnvFile(config, dbType));
        try {
            if (!introspector.loadConfig(tmpPath)) throw new Error('Failed to load config into DatabaseIntrospector');
            if (!(await introspector.connect())) throw new Error('Failed to connect to database');
            return await introspector.listUserSchemas();
        } finally {
            try { await introspector.disconnect(); } catch (e) { /* ignore */ }
            try { fs.unlinkSync(tmpPath); } catch (e) { /* ignore */ }
        }
    },
    introspect: async ({ config, table, schemas }) => {
        const dbType = mapDialectToDbType(config.dialect);
        if (dbType === 'sqlite') {
            throw new Error("dbschema:introspect does not yet support sqlite. Use postgres, mysql, or oracle.");
        }

        const { DatabaseIntrospector } = require('../../lib/utils/database-introspector');
        const introspector = new DatabaseIntrospector({ quiet: true });

        const ephemeralEnv = buildEphemeralEnvFile(config, dbType);
        const tmpPath = createTmpEnvFile(ephemeralEnv);

        try {
            const ok = introspector.loadConfig(tmpPath);
            if (!ok) throw new Error('Failed to load config into DatabaseIntrospector');
            const connected = await introspector.connect();
            if (!connected) throw new Error('Failed to connect to database');

            const targets = [];

            if (table) {
                const parsed = parseQualifiedTable(table);
                targets.push({ schemaName: parsed.schemaName, tableName: parsed.tableName });
            } else if (Array.isArray(schemas) && schemas.length > 0) {
                for (const sch of schemas) {
                    const tables = await introspector.listTables({ schemaFilter: sch, includeSystem: false });
                    for (const t of tables) {
                        if (t.type === 'view') continue;
                        targets.push({ schemaName: sch, tableName: t.name });
                    }
                }
            } else {
                const tables = await introspector.listTables({ includeSystem: false });
                for (const t of tables) {
                    if (t.type === 'view') continue;
                    targets.push({ schemaName: null, tableName: t.name });
                }
            }

            const out = [];
            for (const target of targets) {
                out.push(await collectTableMeta(introspector, target));
            }
            return { tables: out };
        } finally {
            try { await introspector.disconnect(); } catch (e) { /* ignore */ }
            try { fs.unlinkSync(tmpPath); } catch (e) { /* ignore */ }
        }
    }
};

function mapDialectToDbType(dialect) {
    switch (dialect) {
        case 'postgres': return 'postgresql';
        case 'mysql': return 'mysql';
        case 'oracle': return 'oracle';
        case 'sqlite': return 'sqlite';
        default: return dialect;
    }
}

function buildEphemeralEnvFile(config, dbType) {
    const lines = [`DB_TYPE=${dbType}`];
    if (config.host !== undefined) lines.push(`DB_HOST=${config.host}`);
    if (config.port !== undefined) lines.push(`DB_PORT=${config.port}`);
    if (dbType === 'oracle') {
        lines.push(`DB_NAME=${config.serviceName || ''}`);
    } else if (config.database !== undefined) {
        lines.push(`DB_NAME=${config.database}`);
    }
    if (config.user !== undefined) lines.push(`DB_USER=${config.user}`);
    if (config.password !== undefined) lines.push(`DB_PASSWORD=${config.password}`);
    return lines.join('\n') + '\n';
}

function createTmpEnvFile(content) {
    const tmpDir = os.tmpdir();
    const tmpName = `restforge-introspect-${process.pid}-${Date.now()}.env`;
    const fullPath = path.join(tmpDir, tmpName);
    fs.writeFileSync(fullPath, content, 'utf8');
    return fullPath;
}

function parseQualifiedTable(value) {
    if (value.includes('.')) {
        const dot = value.indexOf('.');
        return {
            schemaName: value.slice(0, dot),
            tableName: value.slice(dot + 1)
        };
    }
    return { schemaName: null, tableName: value };
}

async function collectTableMeta(introspector, target) {
    const ref = target.schemaName ? `${target.schemaName}.${target.tableName}` : target.tableName;
    const detailedColumns = await introspector.getDetailedColumnInfo(ref);
    const constraints = await introspector.getConstraints(ref);
    const foreignKeys = await introspector.getForeignKeys(ref);
    const indexes = await introspector.getIndexes(ref);

    const pkConstraint = constraints.find((c) => c.type === 'PRIMARY KEY');
    const uniqueConstraints = constraints
        .filter((c) => c.type === 'UNIQUE')
        .map((c) => ({ name: c.name, columns: c.columns }));

    return {
        tableName: target.tableName,
        schemaName: target.schemaName || null,
        columns: detailedColumns,
        primaryKey: pkConstraint ? pkConstraint.columns : [],
        uniques: uniqueConstraints,
        foreignKeys,
        indexes,
        checks: []
    };
}

function isJsFilePath(p) {
    return path.extname(p).toLowerCase() === '.js';
}

function ensureDir(dir) {
    fs.mkdirSync(dir, { recursive: true });
}

function parseSchemaFlag(value) {
    if (typeof value !== 'string') return null;
    const parts = value.split(',').map(s => s.trim()).filter(Boolean);
    if (parts.length === 0) return null;
    return parts;
}

function resolveMode(args, schemaList, tableQualified) {
    const out = args.output;
    const isDry = args['dry-run'];

    if (isDry && !out) {
        return { kind: 'dry-stdout', singleTable: args.table || null };
    }

    if (!out) {
        return { error: 'Output path missing.' };
    }

    const looksLikeJs = isJsFilePath(out);

    if (args.table) {
        const targetTableName = tableQualified ? tableQualified.tableName : args.table;
        if (looksLikeJs) {
            const expectedBase = path.basename(out, '.js');
            if (expectedBase !== targetTableName) {
                return {
                    error: `Single-mode output filename '${path.basename(out)}' does not match --table='${targetTableName}'. ` +
                        `Use --output=<folder> or rename the file to '${targetTableName}.js'.`
                };
            }
            return { kind: 'single-file', file: path.resolve(out), table: targetTableName };
        }
        return { kind: 'single-folder', folder: path.resolve(out), table: targetTableName };
    }

    if (looksLikeJs) {
        return { error: `Bulk mode requires a folder for --output, got file '${out}'. Specify a folder or add --table=<name>.` };
    }

    const isMultiSchema = (args['all-schemas']) || (Array.isArray(schemaList) && schemaList.length > 1);
    if (isMultiSchema) {
        return { kind: 'bulk-folder-subdir', folder: path.resolve(out) };
    }
    return { kind: 'bulk-folder', folder: path.resolve(out) };
}

function resolveTargetPath(mode, item) {
    if (mode.kind === 'single-file') return mode.file;
    if (mode.kind === 'single-folder') return path.join(mode.folder, `${item.tableName}.js`);
    if (mode.kind === 'bulk-folder') return path.join(mode.folder, `${item.tableName}.js`);
    if (mode.kind === 'bulk-folder-subdir') {
        const sub = item.schemaName || '_default';
        return path.join(mode.folder, sub, `${item.tableName}.js`);
    }
    throw new Error(`Cannot resolve target path for mode '${mode.kind}'`);
}

module.exports = {
    resource: 'schema',
    verb: 'introspect',
    description: 'Reverse-engineer database existing menjadi schema definition (dbschema-kit factory function)',
    category: 'introspection',
    flags: {
        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`)'
        },
        output: {
            type: 'string',
            required: true,
            description: 'Folder atau file output schema'
        },
        table: {
            type: 'string',
            required: false,
            default: null,
            description: 'Hanya satu tabel (unqualified atau qualified schema.table)'
        },
        schema: {
            type: 'string',
            required: false,
            default: null,
            description: 'Filter schema (single atau comma-separated)'
        },
        'all-schemas': {
            type: 'boolean',
            required: false,
            default: false,
            description: 'Auto-detect seluruh user schemas (skip system schemas)'
        },
        force: {
            type: 'boolean',
            required: false,
            default: false,
            description: 'Timpa file existing'
        },
        'dry-run': {
            type: 'boolean',
            required: false,
            default: false,
            description: 'Print factory function ke stdout tanpa menulis file'
        }
    },
    examples: [
        'npx restforge schema introspect --config=db.env --output=./schema',
        'npx restforge schema introspect --config=db.env --output=./schema --table=users --dry-run',
        'npx restforge schema introspect --config=db.env --output=./schema --all-schemas --force'
    ],
    async handler(args) {
        const resolvedConfigResult = resolveConfig(args.config, process.cwd());
        if (!resolvedConfigResult) {
            console.error('Error: --config=<file> is required.');
            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 (!args['dry-run'] && !args.output) {
            console.error('Error: --output=<path> is required (or use --dry-run to preview to stdout).');
            throw new Error('--output=<path> is required');
        }

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

        const schemaList = parseSchemaFlag(args.schema);
        const tableQualified = args.table ? parseQualifiedTable(args.table) : null;

        if (args['all-schemas'] && schemaList) {
            console.error('Error: --all-schemas conflicts with --schema. Use one or the other.');
            throw new Error('--all-schemas conflicts with --schema');
        }
        if (args['all-schemas'] && args.table) {
            console.error('Error: --all-schemas conflicts with --table. Use one or the other.');
            throw new Error('--all-schemas conflicts with --table');
        }
        if (schemaList && tableQualified && tableQualified.schemaName) {
            console.error("Error: --schema conflicts with a qualified --table (schema is implied by 'schema.table').");
            throw new Error('--schema conflicts with a qualified --table');
        }
        if (schemaList && tableQualified && schemaList.length > 1) {
            console.error('Error: --schema with multiple values cannot be combined with --table. Pick a single schema.');
            throw new Error('--schema with multiple values cannot be combined with --table');
        }

        const resolvedConfigPath = resolvedConfigResult.path;

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

        const mode = resolveMode(args, schemaList, tableQualified);
        if (mode.error) {
            console.error(`Error: ${mode.error}`);
            throw new Error(mode.error);
        }

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

        const startTime = Date.now();

        let schemasToFetch = null;
        if (args['all-schemas']) {
            if (typeof introspector.listSchemas !== 'function') {
                console.error('Error: --all-schemas requires the introspector to implement listSchemas().');
                throw new Error('--all-schemas requires listSchemas()');
            }
            try {
                schemasToFetch = await introspector.listSchemas({ config });
            } catch (err) {
                const message = err && err.message ? err.message : String(err);
                console.error(`Error: ${message}`);
                throw err;
            }
            if (!Array.isArray(schemasToFetch) || schemasToFetch.length === 0) {
                console.error('Error: No user schemas detected.');
                throw new Error('No user schemas detected');
            }
        } else if (schemaList) {
            schemasToFetch = schemaList;
        } else if (tableQualified && tableQualified.schemaName) {
            schemasToFetch = [tableQualified.schemaName];
        }

        let tableForIntrospect = args.table;
        if (schemaList && schemaList.length === 1 && tableQualified && !tableQualified.schemaName) {
            tableForIntrospect = `${schemaList[0]}.${tableQualified.tableName}`;
        }

        let result;
        try {
            result = await introspector.introspect({
                config,
                table: tableForIntrospect || null,
                schemas: schemasToFetch
            });
        } catch (err) {
            const message = err && err.message ? err.message : String(err);
            if (/ECONNREFUSED|ETIMEDOUT|ENOTFOUND|connection refused|cannot connect/i.test(message)) {
                console.error(`Error: Cannot connect to ${config.dialect}. ${message}`);
                throw new Error(`Cannot connect to ${config.dialect}. ${message}`);
            }
            console.error(`Error: ${message}`);
            throw err;
        }

        if (!result || !Array.isArray(result.tables)) {
            console.error('Error: Introspector did not return a tables array.');
            throw new Error('Introspector did not return a tables array');
        }

        if (args.table && result.tables.length === 0) {
            console.error(`Error: Table '${args.table}' not found in database.`);
            throw new Error(`Table '${args.table}' not found in database`);
        }
        if (!args.table && result.tables.length === 0) {
            console.error('Error: No user tables found in the database.');
            throw new Error('No user tables found in the database');
        }

        const dialect = config.dialect;
        const emitted = result.tables.map((tableMeta) => {
            const ir = mapTableMetaToIR(tableMeta, dialect);
            const source = serialize(ir);
            return {
                tableName: ir.tableName,
                schemaName: ir.schemaName,
                qualifiedName: ir.qualifiedName,
                source
            };
        });

        const schemaSet = new Set();
        for (const item of emitted) {
            if (item.schemaName) schemaSet.add(item.schemaName);
        }

        if (args['dry-run']) {
            for (const item of emitted) {
                const label = item.qualifiedName || item.tableName;
                const banner = `// ── ${label} `;
                console.log(banner.padEnd(70, '─'));
                process.stdout.write(item.source);
                console.log('');
            }
            const durationMs = Date.now() - startTime;
            const schemaCount = schemaSet.size;
            if (schemaCount > 0) {
                console.error(`Dry-run complete: ${emitted.length} table(s) previewed across ${schemaCount} schema(s) in ${durationMs}ms.`);
            } else {
                console.error(`Dry-run complete: ${emitted.length} table(s) previewed in ${durationMs}ms.`);
            }
            const dryErr = new Error('schema introspect dry-run complete');
            dryErr.exitCode = 2;
            throw dryErr;
        }

        let writtenCount = 0;
        for (const item of emitted) {
            const target = resolveTargetPath(mode, item);
            const parent = path.dirname(target);
            ensureDir(parent);

            if (fs.existsSync(target) && !args.force) {
                console.error(`Error: File already exists: ${target}. Use --force to overwrite.`);
                throw new Error(`File already exists: ${target}. Use --force to overwrite.`);
            }

            fs.writeFileSync(target, item.source, 'utf8');
            writtenCount++;
        }

        const durationMs = Date.now() - startTime;
        const schemaCount = schemaSet.size;
        if (schemaCount > 0) {
            console.error(`✓ Wrote ${writtenCount} file(s) across ${schemaCount} schema(s) in ${durationMs}ms.`);
        } else {
            console.error(`✓ Wrote ${writtenCount} file(s) in ${durationMs}ms.`);
        }
    }
};
