'use strict';

/**
 * Contract: schema diff
 *
 * Drift detection antara Schema Definition File (SDF) dan struktur tabel
 * actual di database. Read-only. Tidak melakukan modifikasi pada filesystem
 * maupun database.
 *
 * Algoritma bidirectional: tampilkan delta dari kedua sisi (only-in-SDF,
 * only-in-DB, mismatched) di level IR (Intermediate Representation),
 * bypass formatting noise.
 */

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

const { loadSchemaPath } = require('../../lib/dbschema-kit/loader');
const { mapTableMetaToIR } = require('../../lib/dbschema-kit/introspect-mapper');
const { loadConfig } = require('../../lib/dbschema-kit/connection');
const { diffModels } = require('../../lib/dbschema-kit/diff-engine');
const { reportHuman, reportJson, getExitCode } = require('../../lib/dbschema-kit/diff-reporter');
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;
}

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-diff-${process.pid}-${Date.now()}.env`;
    const fullPath = path.join(tmpDir, tmpName);
    fs.writeFileSync(fullPath, content, 'utf8');
    return fullPath;
}

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: []
    };
}

const defaultIntrospector = {
    introspect: async ({ config, tables }) => {
        const dbType = mapDialectToDbType(config.dialect);
        if (dbType === 'sqlite') {
            throw new Error('schema:diff 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 out = [];
            for (const target of tables) {
                try {
                    out.push(await collectTableMeta(introspector, target));
                } catch (err) {
                    // Tabel tidak ada di DB: catat dengan struktur kosong agar
                    // diff engine memperlakukannya sebagai "only-in-SDF" di sisi DB.
                    out.push({
                        tableName: target.tableName,
                        schemaName: target.schemaName || null,
                        columns: [],
                        primaryKey: [],
                        uniques: [],
                        foreignKeys: [],
                        indexes: [],
                        checks: [],
                        _missing: true,
                        _error: err && err.message ? err.message : String(err)
                    });
                }
            }
            return { tables: out };
        } finally {
            try { await introspector.disconnect(); } catch (e) { /* ignore */ }
            try { fs.unlinkSync(tmpPath); } catch (e) { /* ignore */ }
        }
    }
};

module.exports = {
    resource: 'schema',
    verb: 'diff',
    description: 'Drift detection antara SDF dan struktur tabel database (read-only, bidirectional)',
    category: 'introspection',
    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`)'
        },
        table: {
            type: 'string',
            required: false,
            default: null,
            description: 'Diff hanya satu tabel spesifik (default: semua model di SDF)'
        },
        json: {
            type: 'boolean',
            required: false,
            default: false,
            description: 'Output format JSON (default: human-readable plain)'
        }
    },
    examples: [
        'npx restforge schema diff --path=./schema --config=db.env',
        'npx restforge schema diff --path=./schema/visitors.js --config=db.env --table=visitors',
        'npx restforge schema diff --path=./schema --config=db.env --json'
    ],
    async handler(args) {
        // 1. Resolve config
        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");
            const err = new Error('--config=<file> is required');
            err.exitCode = 2;
            throw err;
        }

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

        let config;
        try {
            config = loadConfig(resolvedConfigResult.path);
        } catch (err) {
            console.error(`Error: ${err.message}`);
            const e = new Error(err.message);
            e.exitCode = 2;
            throw e;
        }

        // 2. Load SDF
        const schemaPath = args.path;
        const absPath = path.resolve(process.cwd(), schemaPath);

        let sdfModels;
        try {
            sdfModels = loadSchemaPath(absPath);
        } catch (err) {
            console.error(`Error: ${err.message}`);
            const e = new Error(err.message);
            e.exitCode = 2;
            throw e;
        }

        if (sdfModels.size === 0) {
            console.error(`Error: No schema models found at '${schemaPath}'.`);
            const e = new Error(`No schema models found at '${schemaPath}'`);
            e.exitCode = 2;
            throw e;
        }

        // 3. Resolve target tables
        let targetEntries = Array.from(sdfModels.entries());
        if (args.table) {
            targetEntries = targetEntries.filter(([qualified, ir]) => {
                return ir.tableName === args.table || qualified === args.table;
            });
            if (targetEntries.length === 0) {
                console.error(`Error: Table '${args.table}' not found in SDF.`);
                const e = new Error(`Table '${args.table}' not found in SDF`);
                e.exitCode = 2;
                throw e;
            }
        }

        const targetsForDb = targetEntries.map(([_, ir]) => ({
            tableName: ir.tableName,
            schemaName: ir.schemaName || null
        }));

        // 4. Connect database, introspect setiap tabel
        let introspector;
        try {
            introspector = loadIntrospector();
        } catch (err) {
            console.error(`Error: Failed to load introspector: ${err.message}`);
            const e = new Error(err.message);
            e.exitCode = 2;
            throw e;
        }

        let result;
        try {
            result = await introspector.introspect({ config, tables: targetsForDb });
        } catch (err) {
            const message = err && err.message ? err.message : String(err);
            console.error(`Error: ${message}`);
            const e = new Error(message);
            e.exitCode = 2;
            throw e;
        }

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

        // 5. Map DB results to IR (keyed by tableName supaya match dengan SDF)
        const dialect = config.dialect;
        const dbModels = new Map();
        for (const tableMeta of result.tables) {
            if (tableMeta._missing) {
                // Tabel hilang di DB: register empty IR agar diff engine
                // memperlakukan sebagai onlyInSdf untuk seluruh content.
                const qualified = tableMeta.schemaName
                    ? `${tableMeta.schemaName}.${tableMeta.tableName}`
                    : tableMeta.tableName;
                dbModels.set(qualified, {
                    tableName: tableMeta.tableName,
                    schemaName: tableMeta.schemaName || null,
                    qualifiedName: qualified,
                    fields: {},
                    primaryKey: [],
                    indexes: [],
                    uniques: [],
                    relations: {},
                    checks: []
                });
                continue;
            }
            const ir = mapTableMetaToIR(tableMeta, dialect);
            dbModels.set(ir.qualifiedName || ir.tableName, ir);
        }

        // 6. Build SDF map matching same key convention as dbModels
        const sdfMap = new Map();
        for (const [_, ir] of targetEntries) {
            sdfMap.set(ir.qualifiedName || ir.tableName, ir);
        }

        // 7. Compute diff
        const deltas = diffModels(sdfMap, dbModels);

        // 8. Report
        if (args.json) {
            const obj = reportJson(deltas);
            process.stdout.write(JSON.stringify(obj, null, 2));
            process.stdout.write('\n');
        } else {
            const text = reportHuman(deltas);
            process.stdout.write(text);
            process.stdout.write('\n');
        }

        // 9. Exit code
        const exitCode = getExitCode(deltas);
        if (exitCode !== 0) {
            const err = new Error(`schema diff detected drift in ${deltas.filter((d) => d.hasDrift).length} table(s)`);
            err.exitCode = exitCode;
            throw err;
        }
    }
};
