'use strict';

/**
 * ALTER TABLE Emitters — generate dialect-aware DDL fragments untuk apply-engine.
 *
 * Lingkup:
 *   - ADD COLUMN <name> <type> [NULL|NOT NULL] [DEFAULT ...]
 *   - DROP COLUMN <name>
 *   - ALTER/MODIFY COLUMN <name> ... (length / nullable; type change defer)
 *   - CREATE INDEX
 *   - DROP INDEX (dialect-aware)
 *   - ADD CONSTRAINT UNIQUE
 *   - DROP CONSTRAINT (unique)
 *   - ADD CONSTRAINT FOREIGN KEY
 *   - DROP CONSTRAINT FOREIGN KEY (MySQL: DROP FOREIGN KEY)
 *
 * Konvensi penamaan constraint/index reuse `naming.js` (generateConstraintName,
 * deriveCompositeShortName) agar nama yang di-emit identik dengan create-table
 * dan create-index. Output adalah string single-statement tanpa trailing
 * semicolon (apply-executor / statement-splitter yang menangani separator).
 *
 * SQLite tidak mendukung ALTER COLUMN / DROP COLUMN (legacy mode). Modul ini
 * tetap mendukung ADD COLUMN dan CREATE INDEX/DROP INDEX untuk sqlite; semua
 * operasi MODIFY column dan DROP COLUMN harus dicegah di apply-engine sebelum
 * sampai ke emitter.
 *
 * @module lib/dbschema-kit/emitters/alter-table
 */

const { generateConstraintName, deriveCompositeShortName } = require('../naming');

// ─────────────────────────────────────────────────────────────
// ADD COLUMN
// ─────────────────────────────────────────────────────────────

function emitAddColumn(tableIR, fieldName, field, dialect) {
    if (!tableIR || typeof tableIR.tableName !== 'string') {
        throw new Error('emitAddColumn: tableIR with tableName is required');
    }
    if (typeof fieldName !== 'string' || fieldName === '') {
        throw new Error('emitAddColumn: fieldName must be a non-empty string');
    }
    if (!field || typeof field !== 'object') {
        throw new Error(`emitAddColumn: field definition required for '${fieldName}'`);
    }
    const tableIdentifier = dialect.formatTableIdentifier(tableIR);
    const column = dialect.quoteIdentifier(fieldName);
    const typeFrag = dialect.mapType(field);
    const parts = [column, typeFrag];
    if (field.notnull === true) {
        parts.push('NOT NULL');
    }
    if (field.default !== undefined && field.autoUpdate !== true) {
        parts.push(`DEFAULT ${dialect.formatDefault(field.default)}`);
    }

    // Penanda boolean untuk dialect varchar (selaras dengan create-table emitter):
    // CHECK inline pada kolom agar introspeksi dapat mengenalinya kembali sebagai
    // boolean. PostgreSQL (nativeBoolean) memakai tipe BOOLEAN native, dikecualikan.
    if (field.type === 'boolean' && dialect.nativeBoolean !== true) {
        parts.push(`CHECK (${column} IN ('true', 'false'))`);
    }

    if (dialect.name === 'oracle') {
        // Oracle membungkus column definition di dalam parentheses untuk ADD,
        // konvensi yang juga mendukung multi-column add bila diperlukan nanti.
        return `ALTER TABLE ${tableIdentifier} ADD (${parts.join(' ')})`;
    }
    return `ALTER TABLE ${tableIdentifier} ADD COLUMN ${parts.join(' ')}`;
}

// ─────────────────────────────────────────────────────────────
// DROP COLUMN
// ─────────────────────────────────────────────────────────────

function emitDropColumn(tableIR, fieldName, dialect) {
    if (!tableIR || typeof tableIR.tableName !== 'string') {
        throw new Error('emitDropColumn: tableIR with tableName is required');
    }
    if (typeof fieldName !== 'string' || fieldName === '') {
        throw new Error('emitDropColumn: fieldName must be a non-empty string');
    }
    if (dialect.name === 'sqlite') {
        throw new Error(
            `emitDropColumn: sqlite does not support DROP COLUMN reliably (requires table rebuild). ` +
            `Filter DROP COLUMN operations in apply-engine before calling emitter.`
        );
    }
    const tableIdentifier = dialect.formatTableIdentifier(tableIR);
    const column = dialect.quoteIdentifier(fieldName);
    if (dialect.name === 'oracle') {
        return `ALTER TABLE ${tableIdentifier} DROP (${column})`;
    }
    return `ALTER TABLE ${tableIdentifier} DROP COLUMN ${column}`;
}

// ─────────────────────────────────────────────────────────────
// MODIFY COLUMN (length / nullable)
// ─────────────────────────────────────────────────────────────

/**
 * Generate ALTER COLUMN statement(s) untuk modifikasi length / nullable.
 *
 * Modify ini bersifat dialect-specific. Type change TIDAK di-handle (defer).
 *
 * @param {Object} tableIR - { tableName, schemaName? }
 * @param {string} fieldName
 * @param {Object} sdfField - target shape (type, length, notnull, dll.)
 * @param {Object} options - { changeNullable: boolean, changeLength: boolean }
 * @param {Object} dialect - dialect adapter
 * @returns {string[]} array of ALTER statement (postgres bisa multiple,
 *                    mysql/oracle satu statement MODIFY)
 */
function emitModifyColumn(tableIR, fieldName, sdfField, options, dialect) {
    if (!tableIR || typeof tableIR.tableName !== 'string') {
        throw new Error('emitModifyColumn: tableIR with tableName is required');
    }
    if (typeof fieldName !== 'string' || fieldName === '') {
        throw new Error('emitModifyColumn: fieldName must be a non-empty string');
    }
    if (!sdfField || typeof sdfField !== 'object') {
        throw new Error(`emitModifyColumn: sdfField required for '${fieldName}'`);
    }
    const changeNullable = options && options.changeNullable === true;
    const changeLength = options && options.changeLength === true;
    if (!changeNullable && !changeLength) {
        return [];
    }

    if (dialect.name === 'sqlite') {
        throw new Error(
            `emitModifyColumn: sqlite does not support ALTER COLUMN. ` +
            `Filter modify operations in apply-engine before calling emitter.`
        );
    }

    const tableIdentifier = dialect.formatTableIdentifier(tableIR);
    const column = dialect.quoteIdentifier(fieldName);

    if (dialect.name === 'postgres') {
        const out = [];
        if (changeLength) {
            const typeFrag = dialect.mapType(sdfField);
            out.push(`ALTER TABLE ${tableIdentifier} ALTER COLUMN ${column} TYPE ${typeFrag}`);
        }
        if (changeNullable) {
            if (sdfField.notnull === true) {
                out.push(`ALTER TABLE ${tableIdentifier} ALTER COLUMN ${column} SET NOT NULL`);
            } else {
                out.push(`ALTER TABLE ${tableIdentifier} ALTER COLUMN ${column} DROP NOT NULL`);
            }
        }
        return out;
    }

    if (dialect.name === 'mysql') {
        const typeFrag = dialect.mapType(sdfField);
        const parts = [column, typeFrag];
        if (sdfField.notnull === true) parts.push('NOT NULL');
        else parts.push('NULL');
        return [`ALTER TABLE ${tableIdentifier} MODIFY COLUMN ${parts.join(' ')}`];
    }

    if (dialect.name === 'oracle') {
        const typeFrag = dialect.mapType(sdfField);
        const parts = [column, typeFrag];
        if (sdfField.notnull === true) parts.push('NOT NULL');
        else parts.push('NULL');
        return [`ALTER TABLE ${tableIdentifier} MODIFY (${parts.join(' ')})`];
    }

    throw new Error(`emitModifyColumn: unsupported dialect '${dialect.name}'`);
}

// ─────────────────────────────────────────────────────────────
// CREATE / DROP INDEX
// ─────────────────────────────────────────────────────────────

function deriveIndexName(tableName, columns, dialect) {
    const composite = Array.isArray(columns) && columns.length > 1;
    const suffix = composite
        ? deriveCompositeShortName(columns, dialect.maxIdentifierLength)
        : columns[0];
    return generateConstraintName('idx', tableName, suffix, dialect.maxIdentifierLength);
}

function emitCreateIndex(tableIR, columns, dialect) {
    if (!tableIR || typeof tableIR.tableName !== 'string') {
        throw new Error('emitCreateIndex: tableIR with tableName is required');
    }
    if (!Array.isArray(columns) || columns.length === 0) {
        throw new Error('emitCreateIndex: columns must be a non-empty array');
    }
    const tableIdentifier = dialect.formatTableIdentifier(tableIR);
    const name = deriveIndexName(tableIR.tableName, columns, dialect);
    const colList = columns.map((c) => dialect.quoteIdentifier(c)).join(', ');
    return `CREATE INDEX ${dialect.quoteIdentifier(name)} ON ${tableIdentifier} (${colList})`;
}

function emitDropIndex(tableIR, columns, dialect, options) {
    if (!tableIR || typeof tableIR.tableName !== 'string') {
        throw new Error('emitDropIndex: tableIR with tableName is required');
    }
    if (!Array.isArray(columns) || columns.length === 0) {
        throw new Error('emitDropIndex: columns must be a non-empty array');
    }
    const explicitName = options && typeof options.name === 'string' && options.name.length > 0
        ? options.name
        : null;
    const name = explicitName || deriveIndexName(tableIR.tableName, columns, dialect);
    const quotedName = dialect.quoteIdentifier(name);
    const tableIdentifier = dialect.formatTableIdentifier(tableIR);

    switch (dialect.name) {
        case 'postgres':
        case 'sqlite':
            // PG/SQLite: DROP INDEX adalah top-level object, tidak butuh ON clause.
            return `DROP INDEX ${quotedName}`;
        case 'mysql':
            // MySQL: DROP INDEX adalah operasi pada table, butuh ON.
            return `DROP INDEX ${quotedName} ON ${tableIdentifier}`;
        case 'oracle':
            // Oracle: DROP INDEX adalah top-level object.
            return `DROP INDEX ${quotedName}`;
        default:
            throw new Error(`emitDropIndex: unsupported dialect '${dialect.name}'`);
    }
}

// ─────────────────────────────────────────────────────────────
// ADD / DROP UNIQUE CONSTRAINT
// ─────────────────────────────────────────────────────────────

function deriveUniqueConstraintName(tableName, columns, dialect) {
    const suffix = columns.length > 1
        ? deriveCompositeShortName(columns, dialect.maxIdentifierLength)
        : columns[0];
    return generateConstraintName('uq', tableName, suffix, dialect.maxIdentifierLength);
}

function emitAddUnique(tableIR, columns, dialect) {
    if (!tableIR || typeof tableIR.tableName !== 'string') {
        throw new Error('emitAddUnique: tableIR with tableName is required');
    }
    if (!Array.isArray(columns) || columns.length === 0) {
        throw new Error('emitAddUnique: columns must be a non-empty array');
    }
    const tableIdentifier = dialect.formatTableIdentifier(tableIR);
    const name = deriveUniqueConstraintName(tableIR.tableName, columns, dialect);
    const colList = columns.map((c) => dialect.quoteIdentifier(c)).join(', ');
    return `ALTER TABLE ${tableIdentifier} ADD CONSTRAINT ${dialect.quoteIdentifier(name)} UNIQUE (${colList})`;
}

function emitDropUnique(tableIR, columns, dialect, options) {
    if (!tableIR || typeof tableIR.tableName !== 'string') {
        throw new Error('emitDropUnique: tableIR with tableName is required');
    }
    if (!Array.isArray(columns) || columns.length === 0) {
        throw new Error('emitDropUnique: columns must be a non-empty array');
    }
    const explicitName = options && typeof options.name === 'string' && options.name.length > 0
        ? options.name
        : null;
    const name = explicitName || deriveUniqueConstraintName(tableIR.tableName, columns, dialect);
    const tableIdentifier = dialect.formatTableIdentifier(tableIR);

    if (dialect.name === 'mysql') {
        // MySQL menyimpan UNIQUE sebagai index, syntax DROP INDEX adalah idiomatic.
        return `ALTER TABLE ${tableIdentifier} DROP INDEX ${dialect.quoteIdentifier(name)}`;
    }
    return `ALTER TABLE ${tableIdentifier} DROP CONSTRAINT ${dialect.quoteIdentifier(name)}`;
}

// ─────────────────────────────────────────────────────────────
// ADD / DROP FOREIGN KEY
// ─────────────────────────────────────────────────────────────

// Cross-model-validator augments rel with `_targetSchemaName` + `_targetTableName`.
// Apply path tidak selalu menjalankan validator, jadi fallback dengan split
// `rel.target` di '.' bila tersedia, lalu treat sebagai schema-less target.
function resolveTargetIr(rel) {
    if (rel && rel._targetTableName) {
        return { schemaName: rel._targetSchemaName || null, tableName: rel._targetTableName };
    }
    const target = rel && rel.target;
    if (typeof target === 'string' && target.includes('.')) {
        const idx = target.indexOf('.');
        return { schemaName: target.slice(0, idx), tableName: target.slice(idx + 1) };
    }
    return { schemaName: null, tableName: target };
}

function deriveForeignKeyConstraintName(tableName, relName, dialect, explicitName) {
    if (typeof explicitName === 'string' && explicitName !== '') {
        return explicitName;
    }
    return generateConstraintName('fk', tableName, relName, dialect.maxIdentifierLength);
}

function emitAddForeignKey(tableIR, relName, rel, dialect) {
    if (!tableIR || typeof tableIR.tableName !== 'string') {
        throw new Error('emitAddForeignKey: tableIR with tableName is required');
    }
    if (typeof relName !== 'string' || relName === '') {
        throw new Error('emitAddForeignKey: relName must be a non-empty string');
    }
    if (!rel || typeof rel !== 'object') {
        throw new Error(`emitAddForeignKey: rel definition required for '${relName}'`);
    }
    if (typeof rel.localKey !== 'string' || rel.localKey === '') {
        throw new Error(`emitAddForeignKey: rel.localKey required for '${relName}'`);
    }
    if (typeof rel.references !== 'string' || rel.references === '') {
        throw new Error(`emitAddForeignKey: rel.references required for '${relName}'`);
    }
    if (dialect.name === 'sqlite') {
        throw new Error(
            `emitAddForeignKey: sqlite does not support ALTER TABLE ADD CONSTRAINT FOREIGN KEY ` +
            `(requires table rebuild). Filter FK operations in apply-engine before calling emitter.`
        );
    }

    const tableIdentifier = dialect.formatTableIdentifier(tableIR);
    const constraintName = deriveForeignKeyConstraintName(tableIR.tableName, relName, dialect);
    const local = dialect.quoteIdentifier(rel.localKey);
    const target = dialect.formatTableIdentifier(resolveTargetIr(rel));
    const ref = dialect.quoteIdentifier(rel.references);

    let stmt = `ALTER TABLE ${tableIdentifier} ADD CONSTRAINT ${dialect.quoteIdentifier(constraintName)} `
             + `FOREIGN KEY (${local}) REFERENCES ${target} (${ref})`;
    if (rel.onDelete) stmt += ` ON DELETE ${dialect.formatReferentialAction(rel.onDelete)}`;
    if (rel.onUpdate) stmt += ` ON UPDATE ${dialect.formatReferentialAction(rel.onUpdate)}`;
    return stmt;
}

function emitDropForeignKey(tableIR, relName, dialect, options) {
    if (!tableIR || typeof tableIR.tableName !== 'string') {
        throw new Error('emitDropForeignKey: tableIR with tableName is required');
    }
    if (typeof relName !== 'string' || relName === '') {
        throw new Error('emitDropForeignKey: relName must be a non-empty string');
    }
    if (dialect.name === 'sqlite') {
        throw new Error(
            `emitDropForeignKey: sqlite does not support ALTER TABLE DROP CONSTRAINT FOREIGN KEY ` +
            `(requires table rebuild). Filter FK operations in apply-engine before calling emitter.`
        );
    }

    const explicitName = options && typeof options.name === 'string' && options.name.length > 0
        ? options.name
        : null;
    const constraintName = deriveForeignKeyConstraintName(tableIR.tableName, relName, dialect, explicitName);
    const tableIdentifier = dialect.formatTableIdentifier(tableIR);

    if (dialect.name === 'mysql') {
        // MySQL: DROP FOREIGN KEY syntax (DROP CONSTRAINT FK hanya supported MySQL 8.0.19+)
        return `ALTER TABLE ${tableIdentifier} DROP FOREIGN KEY ${dialect.quoteIdentifier(constraintName)}`;
    }
    return `ALTER TABLE ${tableIdentifier} DROP CONSTRAINT ${dialect.quoteIdentifier(constraintName)}`;
}

module.exports = {
    emitAddColumn,
    emitDropColumn,
    emitModifyColumn,
    emitCreateIndex,
    emitDropIndex,
    emitAddUnique,
    emitDropUnique,
    emitAddForeignKey,
    emitDropForeignKey,
    deriveIndexName,
    deriveUniqueConstraintName,
    deriveForeignKeyConstraintName
};
