"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RelationshipResolver = void 0;
exports.collectTablesFromHierarchy = collectTablesFromHierarchy;
exports.getRootTables = getRootTables;
/**
 * Collects every table name in a relationship hierarchy (the root plus all
 * descendants, recursively).
 */
function collectTablesFromHierarchy(hierarchy) {
    const result = new Set();
    for (const [rootTable, node] of Object.entries(hierarchy)) {
        result.add(rootTable);
        collectDescendantTables(node.relationships, result);
    }
    return result;
}
function collectDescendantTables(relationships, result) {
    if (!relationships) {
        return;
    }
    for (const [table, node] of Object.entries(relationships)) {
        result.add(table);
        collectDescendantTables(node.relationships, result);
    }
}
/**
 * Returns root tables for a plugin — tables from getTables() that are not
 * marked as descendants in any relationship.
 */
function getRootTables(plugin) {
    const allTables = plugin.getTables();
    const relationships = plugin.getRelationships();
    const descendants = new Set();
    for (const rels of Object.values(relationships)) {
        for (const [table, config] of Object.entries(rels)) {
            if (config.descendant) {
                descendants.add(table);
            }
        }
    }
    const roots = new Set();
    for (const table of allTables) {
        if (!descendants.has(table)) {
            roots.add(table);
        }
    }
    return roots;
}
/**
 * Resolves and manages table relationships spread across plugins.
 * Builds hierarchical relationship trees, detects cycles, and identifies root tables.
 */
class RelationshipResolver {
    project;
    descendantParents = new Map();
    parentDescendantRelations = new Map();
    pluginRoots = new Set();
    nodeIdCounter = 0;
    /**
     * Creates a new RelationshipResolver instance.
     * @param project - The project containing the table relationships to resolve
     */
    constructor(project) {
        this.project = project;
        this.init();
    }
    /**
     * Generates a unique node identifier for the relationship tree.
     * @returns A unique node ID string
     */
    generateNodeId() {
        return `node${++this.nodeIdCounter}`;
    }
    /**
     * Initializes the resolver by processing all relationships from project plugins.
     * Populates internal maps tracking parent-descendant relationships.
     * Also identifies plugin entry-point tables: tables with toShape that are not
     * descendants within that same plugin. These are treated as roots even if
     * another plugin claims them as descendants.
     */
    init() {
        const plugins = this.project.getPlugins();
        for (const plugin of plugins) {
            const searchableRelationships = plugin.getRelationships();
            const pluginRootTables = getRootTables(plugin);
            pluginRootTables.forEach((rootTable) => this.pluginRoots.add(rootTable));
            for (const [parent, relationships] of Object.entries(searchableRelationships)) {
                this.processRelationship(parent, relationships);
            }
        }
    }
    /**
     * Normalizes the 'via' field of a relationship into a consistent format.
     * @param via - The relationship via field (string, object, or regex)
     * @returns A normalized Via object with type and value
     */
    normalizeVia(via) {
        if (typeof via === 'string') {
            return { type: 'string', value: via };
        }
        else if (Array.isArray(via)) {
            return { type: 'regex', value: via };
        }
        else if (typeof via === 'object') {
            return { type: 'object', value: via };
        }
        else {
            return { type: 'unknown', value: via };
        }
    }
    isComposite(table) {
        return this.project.getPlugins().isComposite(table);
    }
    /**
     * Recursively processes relationships for a parent table.
     * Updates internal maps tracking descendant-parent relationships.
     * @param parent - The parent table name
     * @param relationships - The relationships configuration for the parent table
     */
    processRelationship(parent, relationships) {
        if (!relationships) {
            return;
        }
        const parents = this.descendantParents.get(parent) ?? new Set();
        this.descendantParents.set(parent, parents);
        for (const [descendant, relationship] of Object.entries(relationships)) {
            const parents = this.descendantParents.get(descendant) ?? new Set();
            this.descendantParents.set(descendant, parents);
            const isDescendant = relationship.descendant;
            if (isDescendant) {
                parents.add(parent);
                this.descendantParents.set(descendant, parents);
                const relations = this.parentDescendantRelations.get(parent) ?? new Map();
                relations.set(descendant, {
                    descendantTable: descendant,
                    via: this.normalizeVia(relationship.via),
                    inverse: relationship.inverse ?? false,
                    composite: this.isComposite(descendant),
                });
                this.parentDescendantRelations.set(parent, relations);
            }
            this.processRelationship(descendant, relationship.relationships);
        }
    }
    /**
     * Finds all root tables (tables with no parents) for a given table.
     * Uses depth-first search with cycle detection.
     * A table is considered a root if it has no parents in the merged graph,
     * or if it is an entry-point table declared by any plugin (appears in a
     * plugin's relationship graph but not as a descendant within that plugin).
     * @param table - The table to find roots for
     * @param visited - Set of already visited tables to prevent infinite loops
     * @returns Set of root table names
     */
    findAllRoots(table, visited = new Set()) {
        if (visited.has(table)) {
            return new Set();
        }
        if (this.isRoot(table)) {
            return new Set([table]);
        }
        visited.add(table);
        const allRoots = new Set();
        // A table can be both a root (declared as a plugin entry point) and a descendant of another plugin.
        // Example: sys_security_acl is a root for AclPlugin but also a descendant of NowAssistSkillPlugin.
        // So we mark it as root here but continue traversing to discover all relationships.
        if (this.pluginRoots.has(table)) {
            allRoots.add(table);
        }
        // Continue traversing upward to find roots from other plugins
        // biome-ignore lint/style/noNonNullAssertion: we ensure its present in isRoot
        const parents = this.descendantParents.get(table);
        for (const parent of parents) {
            this.findAllRoots(parent, visited).forEach((root) => allRoots.add(root));
        }
        return allRoots;
    }
    /**
     * Adds cyclic reference metadata to the relationship tree.
     * @param relationships - The relationship tree to annotate
     * @param cyclicRefs - Map of node IDs to their cyclic reference node IDs
     */
    addCyclicReferencesToTree(relationships, cyclicRefs) {
        if (!relationships) {
            return;
        }
        for (const [, relationship] of Object.entries(relationships)) {
            const refs = cyclicRefs.get(relationship.nodeId);
            if (refs) {
                relationship.cyclicReferences = refs;
            }
            this.addCyclicReferencesToTree(relationship.relationships, cyclicRefs);
        }
    }
    /**
     * Recursively builds the relationship tree for a table.
     * Detects and marks cyclic relationships during traversal.
     * @param table - The table to build relationships for
     * @param visitedPath - Map of visited tables to their node IDs in current path
     * @param cyclicRefs - Map collecting cyclic references by node ID
     * @returns The descendant relationships tree, or undefined if no relationships exist
     */
    buildRelationship(table, visitedPath = new Map(), cyclicRefs = new Map()) {
        const relations = this.parentDescendantRelations.get(table)?.values();
        if (!relations) {
            return undefined;
        }
        const relationship = {};
        for (const relation of relations) {
            const nodeId = this.generateNodeId();
            const cycleTargetNodeId = visitedPath.get(relation.descendantTable);
            const isCycle = cycleTargetNodeId !== undefined;
            if (isCycle) {
                const refs = cyclicRefs.get(cycleTargetNodeId) ?? [];
                refs.push(nodeId);
                cyclicRefs.set(cycleTargetNodeId, refs);
                relationship[relation.descendantTable] = {
                    nodeId,
                    table: relation.descendantTable,
                    via: relation.via,
                    inverse: relation.inverse,
                    composite: relation.composite,
                    cycleTargetNodeId,
                };
            }
            else {
                visitedPath.set(relation.descendantTable, nodeId);
                const associatedRelations = this.buildRelationship(relation.descendantTable, visitedPath, cyclicRefs);
                visitedPath.delete(relation.descendantTable);
                relationship[relation.descendantTable] = {
                    nodeId,
                    table: relation.descendantTable,
                    via: relation.via,
                    inverse: relation.inverse,
                    composite: relation.composite,
                    ...(associatedRelations && { relationships: associatedRelations }),
                };
            }
        }
        return relationship;
    }
    /**
     * Generates the complete relationship hierarchy for a given table.
     * @template T - The table name type
     * @param table - The root table to generate hierarchy for
     * @returns The complete relationship hierarchy including cyclic references
     */
    generateRelationshipHierarchy(table) {
        const rootNodeId = this.generateNodeId();
        const visitedPath = new Map([[table, rootNodeId]]);
        const cyclicRefs = new Map();
        const relationships = this.buildRelationship(table, visitedPath, cyclicRefs);
        this.addCyclicReferencesToTree(relationships, cyclicRefs);
        const rootCyclicRefs = cyclicRefs.get(rootNodeId);
        return {
            [table]: {
                nodeId: rootNodeId,
                table,
                composite: this.isComposite(table),
                ...(rootCyclicRefs && { cyclicReferences: rootCyclicRefs }),
                ...(relationships && { relationships }),
            },
        };
    }
    /**
     * Checks if a table is a root table (has no parent tables).
     * Assumption: Root table doesn't have any parent table even via cylic references.
     * @param table - The table name to check
     * @returns True if the table is a root, false otherwise
     */
    isRoot(table) {
        return !this.descendantParents.has(table) || this.descendantParents.get(table)?.size === 0;
    }
    /**
     * Validates that all 'via' fields in the hierarchy are supported types.
     * Currently rejects hierarchies containing unknown type relationships.
     * @param hierarchy - The relationship hierarchy to validate
     * @returns True if all via fields are supported, false otherwise
     */
    validateViaInHierarchy(hierarchy) {
        if (!hierarchy) {
            return true;
        }
        for (const [, relationship] of Object.entries(hierarchy)) {
            if (relationship.via.type === 'unknown') {
                return false;
            }
            if (relationship.relationships) {
                if (!this.validateViaInHierarchy(relationship.relationships)) {
                    return false;
                }
            }
        }
        return true;
    }
    /**
     * Resolves all possible relationship hierarchies for a given table.
     * Finds all root tables and generates hierarchies from each root.
     * Filters out hierarchies with unsupported relationship types.
     * @param table - The table to resolve relationships for
     * @returns Array of all valid relationship hierarchies
     */
    resolveRelationship(table) {
        const roots = this.findAllRoots(table);
        const allPossibleHierarchies = [];
        for (const root of roots) {
            const hierarchy = this.generateRelationshipHierarchy(root);
            if (!hierarchy) {
                continue;
            }
            if (!this.validateViaInHierarchy(hierarchy[root]?.relationships)) {
                continue; // skip unsupported hierarchies
            }
            allPossibleHierarchies.push(hierarchy);
        }
        return allPossibleHierarchies;
    }
    /**
     * Gets the plugin root tables for a given table by traversing upward.
     * Useful for warning users when they pass a non-root table.
     * @param table - The table to find plugin roots for
     * @returns Set of plugin root table names, empty if the table has no parents
     *          in the merged relationship graph (i.e. `isRoot()` is true for this table)
     */
    getPluginRootTables(table) {
        if (this.isRoot(table)) {
            return new Set();
        }
        return this.findAllRoots(table);
    }
    /**
     * Checks if a table is a plugin entry-point (declared as a root by any plugin).
     * Used to determine if a warning should be shown when transforming a non-root table.
     * @param table - The table name to check
     * @returns True if the table is a plugin root, false otherwise
     */
    isPluginRootTable(table) {
        return this.pluginRoots.has(table);
    }
}
exports.RelationshipResolver = RelationshipResolver;
//# sourceMappingURL=relationship-resolver.js.map