/**
 * Enhanced Model with Relationships
 * Full ORM capabilities with eager/lazy loading
 */
import { DEFAULTS } from '../config/constants.js';
import { ErrorFactory } from '../exceptions/ZintrustError.js';
import { useDatabase } from './Database.js';
import { QueryBuilder } from './QueryBuilder.js';
import { BelongsTo, BelongsToMany, HasMany, HasManyThrough, HasOne, HasOneThrough, MorphMany, MorphOne, MorphTo, } from './Relationships.js';
const getRelatedTableName = (relatedModel) => {
    if (typeof relatedModel.getTable === 'function') {
        return relatedModel.getTable();
    }
    throw ErrorFactory.createConfigError('Related model does not provide a table name');
};
/**
 * Cast attribute value based on config
 */
const castAttribute = (config, key, value) => {
    const castType = config.casts ? config.casts[key] : undefined;
    if (castType === undefined)
        return value;
    switch (castType) {
        case 'boolean':
            return value === true || value === 1 || value === '1';
        case 'integer':
            return Number.parseInt(String(value), 10);
        case 'bigint': {
            // Native BigInt if supported, otherwise string
            try {
                return BigInt(value);
            }
            catch {
                return String(value);
            }
        }
        case 'uuid':
            return String(value);
        case 'float':
            return Number.parseFloat(String(value));
        case 'date':
            return new Date(String(value)).toISOString().split('T')[0];
        case 'datetime':
            return new Date(String(value)).toISOString();
        case 'json':
            return typeof value === 'string' ? JSON.parse(value) : value;
        default:
            return value;
    }
};
/**
 * Fill attributes based on fillable config
 */
const fillAttributes = (config, attrs, newAttrs) => {
    for (const [key, value] of Object.entries(newAttrs)) {
        if (config.fillable.length === 0 || config.fillable.includes(key)) {
            const mutator = config.mutators?.[key];
            const nextValue = mutator ? mutator(value, attrs) : value;
            attrs[key] = castAttribute(config, key, nextValue);
        }
    }
};
const applyAccessor = (config, key, attrs) => {
    const raw = attrs[key];
    const accessor = config.accessors?.[key];
    return accessor ? accessor(raw, attrs) : raw;
};
const runObservers = async (config, hook, model) => {
    const observers = config.observers;
    if (observers === undefined || observers.length === 0)
        return;
    // Run "before" hooks sequentially to ensure safety and consistent state changes.
    // This allows observers to modify the model or throw errors to cancel the operation.
    const isBeforeHook = ['saving', 'creating', 'updating', 'deleting'].includes(hook);
    if (isBeforeHook) {
        for (const observer of observers) {
            const fn = observer[hook];
            if (typeof fn === 'function') {
                // eslint-disable-next-line no-await-in-loop
                await fn(model);
            }
        }
        return;
    }
    // Run "after" hooks in parallel for better performance.
    // These are typically side effects (logging, notifications) that don't depend on each other.
    await Promise.all(observers.map(async (observer) => {
        const fn = observer[hook];
        return typeof fn === 'function' ? Promise.resolve(fn(model)) : Promise.resolve();
    }));
};
const createModelJSON = (config, attrs) => {
    const json = {};
    for (const [key, value] of Object.entries(attrs)) {
        if (config.hidden.includes(key))
            continue;
        // Serialize BigInt as string to keep JSON stable
        if (typeof value === 'bigint') {
            json[key] = String(value);
            continue;
        }
        // Convert Dates to ISO strings for JSON
        if (value instanceof Date) {
            json[key] = value.toISOString();
            continue;
        }
        json[key] = value;
    }
    return json;
};
const createHasOneFactory = (config) => (relatedModel, foreignKey) => HasOne.create(relatedModel, foreignKey ?? `${config.table.slice(0, -1)}_id`, 'id');
const createHasManyFactory = (config) => (relatedModel, foreignKey) => HasMany.create(relatedModel, foreignKey ?? `${config.table.slice(0, -1)}_id`, 'id');
const createBelongsToFactory = () => (relatedModel, foreignKey) => {
    const relatedTable = getRelatedTableName(relatedModel);
    return BelongsTo.create(relatedModel, foreignKey ?? `${relatedTable.slice(0, -1)}_id`, 'id');
};
const createBelongsToManyFactory = (config) => (relatedModel, throughTable, foreignKey, relatedKey) => {
    const relatedTable = getRelatedTableName(relatedModel);
    const table = throughTable ?? [config.table, relatedTable].sort((a, b) => a.localeCompare(b)).join('_');
    return BelongsToMany.create(relatedModel, table, foreignKey ?? `${config.table.slice(0, -1)}_id`, relatedKey ?? `${relatedTable.slice(0, -1)}_id`);
};
const createMorphOneFactory = () => (relatedModel, morphName, morphType, morphId, localKey) => MorphOne.create(relatedModel, morphName, morphType, morphId, localKey);
const createMorphManyFactory = () => (relatedModel, morphName, morphType, morphId, localKey) => MorphMany.create(relatedModel, morphName, morphType, morphId, localKey);
const createMorphToFactory = () => (morphName, morphMap, morphType, morphId) => MorphTo.create(morphName, morphMap, morphType, morphId);
const createHasOneThroughFactory = () => (relatedModel, through, foreignKey, throughForeignKey, localKey, secondLocalKey) => HasOneThrough.create(relatedModel, through, foreignKey, throughForeignKey, localKey, secondLocalKey);
const createHasManyThroughFactory = () => (relatedModel, through, foreignKey, throughForeignKey, localKey, secondLocalKey) => HasManyThrough.create(relatedModel, through, foreignKey, throughForeignKey, localKey, secondLocalKey);
const createModelRelationships = (config) => {
    return {
        hasOne: createHasOneFactory(config),
        hasMany: createHasManyFactory(config),
        belongsTo: createBelongsToFactory(),
        belongsToMany: createBelongsToManyFactory(config),
        morphOne: createMorphOneFactory(),
        morphMany: createMorphManyFactory(),
        morphTo: createMorphToFactory(),
        hasOneThrough: createHasOneThroughFactory(),
        hasManyThrough: createHasManyThroughFactory(),
    };
};
const performModelSave = async (model, config, attrs, getDb, context) => {
    const db = getDb();
    if (db === undefined)
        throw ErrorFactory.createDatabaseError('Database not initialized');
    const isCreate = context.isExists === false;
    await runObservers(config, 'saving', model);
    await runObservers(config, isCreate ? 'creating' : 'updating', model);
    if (config.timestamps ?? false) {
        attrs['created_at'] = attrs['created_at'] ?? new Date().toISOString();
        attrs['updated_at'] = new Date().toISOString();
    }
    context.setExists(true);
    context.updateOriginal({ ...attrs });
    context.clearDirty();
    await runObservers(config, isCreate ? 'created' : 'updated', model);
    await runObservers(config, 'saved', model);
    return true;
};
const performModelDelete = async (model, config, getDb, isExists) => {
    const db = getDb();
    if (!isExists || db === undefined)
        return false;
    await runObservers(config, 'deleting', model);
    await runObservers(config, 'deleted', model);
    return true;
};
/**
 * Create a new model instance
 */
// eslint-disable-next-line max-lines-per-function
export const createModel = (config, attributes = {}) => {
    const connection = config.connection ?? DEFAULTS.CONNECTION;
    const getDb = () => useDatabase(undefined, connection);
    const attrs = {};
    const relations = {}; // Store eager loaded relations
    let original = {};
    let isExists = false;
    const dirtyFields = new Set();
    fillAttributes(config, attrs, attributes);
    original = { ...attrs };
    const model = {
        fill: (newAttrs) => {
            fillAttributes(config, attrs, newAttrs);
            // Mark all filled fields as dirty
            for (const key of Object.keys(newAttrs)) {
                if (attrs[key] !== original[key]) {
                    dirtyFields.add(key);
                }
            }
            return model;
        },
        setAttribute: (key, value) => {
            const mutator = config.mutators?.[key];
            const nextValue = mutator ? mutator(value, attrs) : value;
            const castedValue = castAttribute(config, key, nextValue);
            attrs[key] = castedValue;
            // Track dirty field
            if (original[key] === castedValue) {
                dirtyFields.delete(key);
            }
            else {
                dirtyFields.add(key);
            }
            return model;
        },
        getAttribute: (key) => {
            // Check relations first if it's a relation name
            if (relations[key] !== undefined)
                return relations[key];
            return applyAccessor(config, key, attrs);
        },
        getAttributes: () => ({ ...attrs }),
        // Relationship helpers
        setRelation: (name, value) => {
            relations[name] = value;
        },
        getRelation: (name) => relations[name],
        // remove in production - use saveChanges pattern
        save: async () => performModelSave(model, config, attrs, getDb, {
            isExists,
            setExists: (v) => {
                isExists = v;
            },
            updateOriginal: (v) => {
                original = v;
            },
            clearDirty: () => dirtyFields.clear(),
        }),
        // remove in production - use delete pattern
        delete: async () => performModelDelete(model, config, getDb, isExists),
        // eslint-disable-next-line @typescript-eslint/require-await
        restore: async () => {
            if (config.softDeletes !== true || !isExists)
                return false;
            const deleteAtColumn = config.deleteAtColumn ?? 'deleted_at';
            attrs[deleteAtColumn] = null;
            dirtyFields.add(deleteAtColumn);
            return true;
        },
        forceDelete: async () => {
            if (!isExists)
                return false;
            await runObservers(config, 'deleting', model);
            await runObservers(config, 'deleted', model);
            return true;
        },
        isDeleted: () => {
            if (config.softDeletes !== true)
                return false;
            const deleteAtColumn = config.deleteAtColumn ?? 'deleted_at';
            const deletedValue = attrs[deleteAtColumn];
            return deletedValue !== null && deletedValue !== undefined;
        },
        toJSON: () => createModelJSON(config, attrs),
        isDirty: (key) => {
            if (key !== undefined) {
                return dirtyFields.has(key);
            }
            return dirtyFields.size > 0;
        },
        getTable: () => config.table,
        exists: () => isExists,
        setExists: (exists) => {
            isExists = exists;
        },
    };
    Object.assign(model, createModelRelationships(config));
    return model;
};
/**
 * Get a query builder for a table
 */
export const query = (table, connection) => {
    const db = useDatabase(undefined, connection ?? DEFAULTS.CONNECTION);
    return QueryBuilder.create(table, db);
};
const buildSoftDeleteOptions = (config) => {
    if (config.softDeletes !== true)
        return undefined;
    return { softDeleteColumn: 'deleted_at', softDeleteMode: 'exclude' };
};
/**
 * Find a model by ID
 */
export const find = async (config, id) => {
    const db = useDatabase(undefined, config.connection ?? DEFAULTS.CONNECTION);
    const builder = QueryBuilder.create(config.table, db, buildSoftDeleteOptions(config));
    builder.where('id', '=', String(id)).limit(1);
    const result = await builder.first();
    if (result === null)
        return null;
    const model = createModel(config, result);
    model.setExists(true);
    return model;
};
/**
 * Get all records for a model
 */
export const all = async (config) => {
    const db = useDatabase(undefined, config.connection ?? DEFAULTS.CONNECTION);
    const builder = QueryBuilder.create(config.table, db, buildSoftDeleteOptions(config));
    const results = await builder.get();
    return results.map((result) => {
        const model = createModel(config, result);
        model.setExists(true);
        return model;
    });
};
const bindUnboundMethods = (model, methods) => {
    const bound = {};
    for (const [name, method] of Object.entries(methods)) {
        bound[name] = (...args) => method(model, ...args);
    }
    return bound;
};
const extendModel = (model, methods) => {
    const extended = { ...model };
    for (const [name, method] of Object.entries(methods)) {
        extended[name] = method;
    }
    return extended;
};
const isRecord = (value) => {
    return typeof value === 'object' && value !== null && !Array.isArray(value);
};
const isRelationship = (value) => {
    if (!isRecord(value))
        return false;
    return 'type' in value && 'get' in value;
};
const createModelBuilder = (cfg) => {
    const db = useDatabase(undefined, cfg.connection ?? DEFAULTS.CONNECTION);
    return QueryBuilder.create(cfg.table, db, buildSoftDeleteOptions(cfg));
};
const createHydrator = (cfg, attach) => {
    return (attributes) => {
        const model = createModel(cfg, attributes);
        model.setExists(true);
        return attach(model);
    };
};
const createRelationMapping = (cfg, resolveMethods) => {
    const dummyModel = createModel(cfg);
    const methods = resolveMethods(dummyModel);
    const relationMapping = {};
    for (const [name, fn] of Object.entries(methods)) {
        try {
            const result = fn();
            if (isRelationship(result)) {
                relationMapping[name] = result;
            }
        }
        catch {
            // Not a relationship call or requires params
        }
    }
    return relationMapping;
};
const hydrateRows = (raw, hydrateModel) => {
    if (!Array.isArray(raw))
        return null;
    const rows = raw.filter((element) => isRecord(element));
    return rows.map((element) => hydrateModel(element));
};
const loadEagerRelations = async (eagerBuilder, models) => {
    const eagerLoads = typeof eagerBuilder.getEagerLoads === 'function' ? eagerBuilder.getEagerLoads() : undefined;
    const eagerLoadConstraints = typeof eagerBuilder.getEagerLoadConstraints === 'function'
        ? eagerBuilder.getEagerLoadConstraints()
        : undefined;
    if (!Array.isArray(eagerLoads) ||
        eagerLoads.length === 0 ||
        typeof eagerBuilder.load !== 'function' ||
        models.length === 0) {
        return;
    }
    await Promise.all(eagerLoads.map(async (relation) => {
        const constraint = eagerLoadConstraints?.[relation];
        await eagerBuilder.load?.(models, relation, constraint);
    }));
};
const loadEagerCounts = async (eagerBuilder, models) => {
    const eagerLoadCounts = typeof eagerBuilder.getEagerLoadCounts === 'function'
        ? eagerBuilder.getEagerLoadCounts()
        : undefined;
    if (!Array.isArray(eagerLoadCounts) ||
        eagerLoadCounts.length === 0 ||
        typeof eagerBuilder.loadCount !== 'function' ||
        models.length === 0) {
        return;
    }
    await Promise.all(eagerLoadCounts.map(async (relation) => eagerBuilder.loadCount?.(models, relation)));
};
const hydrateAndLoadRelations = async (raw, eagerBuilder, hydrateModel) => {
    const models = hydrateRows(raw, hydrateModel);
    if (!models)
        return raw;
    await loadEagerRelations(eagerBuilder, models);
    await loadEagerCounts(eagerBuilder, models);
    return models;
};
const wrapBuilderGetForEagerLoading = (builder, hydrateModel) => {
    const eagerBuilder = builder;
    const originalGet = eagerBuilder.get.bind(builder);
    eagerBuilder.get = async () => {
        const raw = await originalGet();
        return hydrateAndLoadRelations(raw, eagerBuilder, hydrateModel);
    };
    if (typeof eagerBuilder.paginate === 'function') {
        const originalPaginate = eagerBuilder.paginate.bind(builder);
        eagerBuilder.paginate = async (page, perPage, options) => {
            const result = await originalPaginate(page, perPage, options);
            if (!Array.isArray(result.items))
                return result;
            const models = await hydrateAndLoadRelations(result.items, eagerBuilder, hydrateModel);
            if (!Array.isArray(models))
                return result;
            return {
                ...result,
                items: models,
            };
        };
    }
};
const createQueryBuilderMethods = (cfg, hydrateModel) => {
    const wrappedBuilder = () => {
        const builder = createModelBuilder(cfg);
        wrapBuilderGetForEagerLoading(builder, hydrateModel);
        return builder;
    };
    return {
        query: () => wrappedBuilder(),
        paginate: async (page, perPage, options) => wrappedBuilder().paginate(page, perPage, options),
        where: (column, operator, value) => wrappedBuilder().where(column, operator, value),
        andWhere: (column, operator, value) => wrappedBuilder().andWhere(column, operator, value),
        orWhere: (column, operator, value) => wrappedBuilder().orWhere(column, operator, value),
        whereIn: (column, values) => wrappedBuilder().whereIn(column, values),
        whereNotIn: (column, values) => wrappedBuilder().whereNotIn(column, values),
        select: (...columns) => wrappedBuilder().select(...columns),
        selectAs: (column, alias) => wrappedBuilder().selectAs(column, alias),
        max: (column, alias) => wrappedBuilder().max(column, alias),
        join: (table, on) => wrappedBuilder().join(table, on),
        leftJoin: (table, on) => wrappedBuilder().leftJoin(table, on),
        orderBy: (column, direction) => wrappedBuilder().orderBy(column, direction),
        limit: (count) => wrappedBuilder().limit(count),
        offset: (count) => wrappedBuilder().offset(count),
        withTrashed: () => wrappedBuilder().withTrashed(),
        onlyTrashed: () => wrappedBuilder().onlyTrashed(),
        withoutTrashed: () => wrappedBuilder().withoutTrashed(),
        scope: (name, ...args) => {
            const fn = cfg.scopes?.[name];
            if (typeof fn !== 'function') {
                throw ErrorFactory.createConfigError(`Unknown query scope: ${name}`);
            }
            const builder = createModelBuilder(cfg);
            return fn(builder, ...args);
        },
        getTable: () => cfg.table,
    };
};
const createDefinedModelInternal = (cfg, methodsOrPlan, attach, resolveMethods) => {
    const relationMapping = createRelationMapping(cfg, resolveMethods);
    const hydrateModel = createHydrator(cfg, attach);
    return {
        create: (attributes = {}) => attach(createModel(cfg, attributes)),
        hydrate: (attributes) => hydrateModel(attributes),
        find: async (id) => {
            const model = await find(cfg, id);
            return model === null ? null : attach(model);
        },
        all: async () => {
            const models = await all(cfg);
            return models.map((m) => attach(m));
        },
        raw: async () => {
            const builder = createModelBuilder(cfg);
            return builder.get();
        },
        ...createQueryBuilderMethods(cfg, hydrateModel),
        db: (connection) => createDefinedModelInternal({ ...cfg, connection }, methodsOrPlan, attach, resolveMethods),
        hydrateWithRelations(attributes, related) {
            const model = hydrateModel(attributes);
            for (const [name, data] of Object.entries(related)) {
                const rel = relationMapping[name];
                if (rel === undefined)
                    continue;
                const relatedStatic = rel.related;
                const hydrate = relatedStatic.hydrate;
                if (typeof hydrate !== 'function')
                    continue;
                if (Array.isArray(data)) {
                    const relatedModels = data.filter((element) => isRecord(element)).map((d) => hydrate(d));
                    model.setRelation(name, relatedModels);
                    continue;
                }
                if (data !== null && data !== undefined && isRecord(data)) {
                    const relatedModel = hydrate(data);
                    model.setRelation(name, relatedModel);
                }
            }
            return model;
        },
    };
};
export function define(config, methodsOrPlan) {
    const plan = typeof methodsOrPlan === 'function' ? methodsOrPlan : undefined;
    const unboundMethods = typeof methodsOrPlan === 'function' ? undefined : methodsOrPlan;
    const resolveMethods = (model) => {
        return plan ? plan(model) : bindUnboundMethods(model, unboundMethods ?? {});
    };
    const attach = (model) => {
        const methods = resolveMethods(model);
        return extendModel(model, methods);
    };
    return createDefinedModelInternal(config, methodsOrPlan, attach, resolveMethods);
}
/**
 * Insert a single or multiple records into the database
 * Returns insert metadata including ID and affected rows
 */
const insert = async (config, values) => {
    const db = useDatabase(undefined, config.connection ?? DEFAULTS.CONNECTION);
    const builder = QueryBuilder.create(config.table, db, buildSoftDeleteOptions(config));
    return builder.insert(values);
};
/**
 * Batch insert multiple records (alias for insert with array)
 */
const bulkInsert = async (config, records) => {
    return insert(config, records);
};
/**
 * Model namespace - sealed namespace object grouping all model operations
 * Frozen to prevent accidental mutation
 *
 * @see FRAMEWORK_REFACTOR_FUNCTION_PATTERN.md for Pattern 2 details
 */
export const Model = Object.freeze({
    create: createModel,
    query,
    find,
    all,
    insert,
    bulkInsert,
    define,
});
