// Oracle Database Dialect — extends AbstractSqlDialect
// Equivalent to org.hibernate.dialect.OracleDialect (Hibernate ORM 6.4)
// Driver: npm install oracledb
// Author: Dr Hamid MADANI drmdh@msn.com
import { AbstractSqlDialect } from './abstract-sql.dialect.js';
// ============================================================
// Type Mapping — DAL FieldType → Oracle column type
// ============================================================
const ORACLE_TYPE_MAP = {
    string: 'VARCHAR2(4000)',
    number: 'NUMBER',
    boolean: 'NUMBER(1)',
    date: 'TIMESTAMP',
    json: 'CLOB',
    array: 'CLOB',
};
// ============================================================
// OracleDialect
// ============================================================
class OracleDialect extends AbstractSqlDialect {
    dialectType = 'oracle';
    pool = null;
    oracledb = null;
    // --- Abstract implementations ---
    quoteIdentifier(name) {
        return `"${name}"`;
    }
    // Oracle uses :1, :2, ... bind variables
    getPlaceholder(index) {
        return `:${index}`;
    }
    fieldToSqlType(field) {
        return ORACLE_TYPE_MAP[field.type] || 'VARCHAR2(4000)';
    }
    getIdColumnType() {
        return 'VARCHAR2(36)';
    }
    getTableListQuery() {
        return "SELECT table_name as name FROM user_tables";
    }
    // --- Hooks ---
    // Oracle prior to 23c doesn't support IF NOT EXISTS
    supportsIfNotExists() { return false; }
    supportsReturning() { return false; }
    // Oracle: pass native Date objects — oracledb handles binding correctly
    serializeDate(value) {
        if (value === 'now')
            return new Date();
        if (value instanceof Date)
            return value;
        if (typeof value === 'string') {
            const parsed = new Date(value);
            if (!isNaN(parsed.getTime()))
                return parsed;
            return value;
        }
        return null;
    }
    // Oracle NUMBER(1): 1 = true, 0 = false
    serializeBoolean(v) { return v ? 1 : 0; }
    deserializeBoolean(v) {
        return v === 1 || v === true || v === '1';
    }
    /** Oracle LIKE is case-sensitive — use UPPER() for case-insensitive search */
    buildRegexCondition(col, flags) {
        if (flags?.includes('i')) {
            return `UPPER(${col}) LIKE UPPER(${this.nextPlaceholder()})`;
        }
        return `${col} LIKE ${this.nextPlaceholder()}`;
    }
    // Oracle uses OFFSET n ROWS FETCH FIRST m ROWS ONLY (12c+)
    buildLimitOffset(options) {
        if (!options?.limit && !options?.skip)
            return '';
        const offset = options.skip ?? 0;
        const limit = options.limit;
        let sql = ` OFFSET ${offset} ROWS`;
        if (limit)
            sql += ` FETCH FIRST ${limit} ROWS ONLY`;
        return sql;
    }
    // Oracle: use PL/SQL block to check existence before CREATE TABLE
    getCreateTablePrefix(tableName) {
        const q = this.quoteIdentifier(tableName);
        return `CREATE TABLE ${q}`;
    }
    getCreateIndexPrefix(indexName, unique) {
        const u = unique ? 'UNIQUE ' : '';
        return `CREATE ${u}INDEX ${this.quoteIdentifier(indexName)}`;
    }
    // --- Connection ---
    async doConnect(config) {
        try {
            const oracledb = await import(/* webpackIgnore: true */ 'oracledb');
            this.oracledb = oracledb.default || oracledb;
            this.oracledb.outFormat = this.oracledb.OUT_FORMAT_OBJECT;
            this.oracledb.autoCommit = true;
            // Parse oracle:// URI to extract user, password, connectString
            const poolOpts = {
                poolMax: config.poolSize ?? 10,
                poolMin: 2,
            };
            const uriMatch = config.uri.match(/^oracle:\/\/([^:]+):([^@]+)@(.+)$/);
            if (uriMatch) {
                poolOpts.user = uriMatch[1];
                poolOpts.password = uriMatch[2];
                poolOpts.connectString = uriMatch[3]; // host:port/service
            }
            else {
                poolOpts.connectString = config.uri;
            }
            this.pool = await this.oracledb.createPool(poolOpts);
        }
        catch (e) {
            throw new Error(`Oracle driver not found. Install it: npm install oracledb\n` +
                `Original error: ${e instanceof Error ? e.message : String(e)}`);
        }
    }
    async doDisconnect() {
        if (this.pool) {
            await this.pool.close(0);
            this.pool = null;
        }
    }
    async doTestConnection() {
        if (!this.pool)
            return false;
        const conn = await this.pool.getConnection();
        try {
            await conn.execute('SELECT 1 FROM DUAL');
            return true;
        }
        finally {
            await conn.close();
        }
    }
    // --- Query execution ---
    async doExecuteQuery(sql, params) {
        if (!this.pool)
            throw new Error('Oracle not connected. Call connect() first.');
        const conn = await this.pool.getConnection();
        try {
            const result = await conn.execute(sql, params);
            return result.rows ?? [];
        }
        finally {
            await conn.close();
        }
    }
    async doExecuteRun(sql, params) {
        if (!this.pool)
            throw new Error('Oracle not connected. Call connect() first.');
        const conn = await this.pool.getConnection();
        try {
            const result = await conn.execute(sql, params);
            return { changes: result.rowsAffected ?? 0 };
        }
        finally {
            await conn.close();
        }
    }
    // Override initSchema to handle Oracle's lack of IF NOT EXISTS
    async initSchema(schemas) {
        this.schemas = schemas;
        const strategy = this.config?.schemaStrategy ?? 'none';
        this.log('INIT_SCHEMA', `strategy=${strategy}`, { entities: schemas.map(s => s.name) });
        if (strategy === 'none')
            return;
        if (strategy === 'validate') {
            for (const schema of schemas) {
                const exists = await this.tableExists(schema.collection);
                if (!exists) {
                    throw new Error(`Schema validation failed: table "${schema.collection}" does not exist ` +
                        `(entity: ${schema.name}). Set schemaStrategy to "update" or "create".`);
                }
            }
            return;
        }
        // For 'update' strategy: create tables only if they don't exist
        for (const schema of schemas) {
            const exists = await this.tableExists(schema.collection);
            if (!exists) {
                const createSql = this.generateCreateTable(schema);
                this.log('DDL', schema.collection, createSql);
                await this.executeRun(createSql, []);
            }
            // Indexes: check existence before creating
            const indexStatements = this.generateIndexes(schema);
            for (const stmt of indexStatements) {
                try {
                    await this.executeRun(stmt, []);
                }
                catch {
                    // Index may already exist — ignore
                }
            }
        }
        // Junction tables
        for (const schema of schemas) {
            for (const [, rel] of Object.entries(schema.relations)) {
                if (rel.type === 'many-to-many' && rel.through) {
                    const exists = await this.tableExists(rel.through);
                    if (exists)
                        continue;
                    const targetSchema = schemas.find(s => s.name === rel.target);
                    if (!targetSchema)
                        continue;
                    const sourceKey = `${schema.name.toLowerCase()}Id`;
                    const targetKey = `${rel.target.toLowerCase()}Id`;
                    const q = (n) => this.quoteIdentifier(n);
                    const idType = this.getIdColumnType();
                    const ddl = `CREATE TABLE ${q(rel.through)} (
  ${q(sourceKey)} ${idType} NOT NULL,
  ${q(targetKey)} ${idType} NOT NULL,
  PRIMARY KEY (${q(sourceKey)}, ${q(targetKey)})
)`;
                    this.log('DDL_JUNCTION', rel.through, ddl);
                    await this.executeRun(ddl, []);
                }
            }
        }
    }
    // Oracle returns column names in UPPERCASE by default
    // Override count to handle CNT vs cnt
    async count(schema, filter) {
        this.resetParams();
        const where = this.translateFilter(filter, schema);
        const table = this.quoteIdentifier(schema.collection);
        const sql = `SELECT COUNT(*) as cnt FROM ${table} WHERE ${where.sql}`;
        this.log('COUNT', schema.collection, { sql, params: where.params });
        const rows = await this.executeQuery(sql, where.params);
        if (rows.length === 0)
            return 0;
        // Oracle returns uppercase column names: CNT or cnt depending on outFormat
        const row = rows[0];
        const val = row.CNT ?? row.cnt ?? row.COUNT ?? row['COUNT(*)'];
        return Number(val) || 0;
    }
    // Oracle uppercase column mapping: normalize row keys to schema field names
    deserializeRow(row, schema) {
        if (!row)
            return row;
        // Oracle returns UPPERCASE keys — normalize before calling parent deserialize
        const upperToField = { ID: 'id', CREATEDAT: 'createdAt', UPDATEDAT: 'updatedAt' };
        for (const fieldName of Object.keys(schema.fields)) {
            upperToField[fieldName.toUpperCase()] = fieldName;
        }
        for (const [relName] of Object.entries(schema.relations || {})) {
            upperToField[relName.toUpperCase()] = relName;
        }
        const normalized = {};
        for (const [key, value] of Object.entries(row)) {
            const mapped = upperToField[key] ?? upperToField[key.toUpperCase()] ?? key;
            normalized[mapped] = value;
        }
        return super.deserializeRow(normalized, schema);
    }
    getDialectLabel() { return 'Oracle'; }
}
// ============================================================
// Factory export
// ============================================================
export function createDialect() {
    return new OracleDialect();
}
