/**
 * derive-graph.ts
 *
 * Pure client-side derivation: domain Maps → React Flow nodes + edges.
 * All edges are derived from domain relationships (repositoryPath matching,
 * parentNodeId) — never stored independently.
 */
/**
 * Derives React Flow nodes and edges from domain Maps.
 *
 * - Feature nodes without a parentNodeId → repo→feature edge (matched by repositoryPath)
 * - Feature nodes without a parentNodeId but with no matching repo → virtual repo node
 * - Feature nodes with a parentNodeId → dependency edge (parentNodeId→nodeId)
 * - Pending feature nodes (creating state) from pendingMap → same rules, no action callbacks
 * - Callbacks are injected into node data via closures
 */
export function deriveGraph(featureMap, repoMap, pendingMap, callbacks, applicationMap) {
    const nodes = [];
    const edges = [];
    // Normalize path separators so Windows backslash paths match forward-slash paths
    const normalizePath = (p) => p.replace(/\\/g, '/');
    // Build a lookup: repositoryPath → repoNodeId
    const repoByPath = new Map();
    for (const [nodeId, entry] of repoMap) {
        if (entry.data.repositoryPath) {
            repoByPath.set(normalizePath(entry.data.repositoryPath), nodeId);
        }
    }
    // Build a lookup: application domain UUID → applicationNodeId. Used to
    // resolve a feature's `applicationId` (a domain UUID, not a node id)
    // to its rendered ApplicationNode on the canvas.
    const appByDomainId = new Map();
    if (applicationMap) {
        for (const [nodeId, entry] of applicationMap) {
            if (entry.data.id) {
                appByDomainId.set(entry.data.id, nodeId);
            }
        }
    }
    // Virtual repo nodes created for orphaned features (not in repoMap)
    const virtualRepos = new Map(); // path → virtualNodeId
    function getOrCreateVirtualRepo(repositoryPath) {
        if (virtualRepos.has(repositoryPath)) {
            return virtualRepos.get(repositoryPath);
        }
        const virtualId = `virtual-repo-${repositoryPath}`;
        const repoName = repositoryPath.split('/').filter(Boolean).at(-1) ?? repositoryPath;
        nodes.push({
            id: virtualId,
            type: 'repositoryNode',
            position: { x: 0, y: 0 },
            data: { name: repoName, repositoryPath },
        });
        virtualRepos.set(repositoryPath, virtualId);
        return virtualId;
    }
    // Add repo nodes (real ones) — sorted by createdAt for stable vertical order.
    // The server-side layoutWithDagre may reorder the node array (connected vs
    // disconnected split), so we cannot rely on Map insertion order alone.
    // Use Infinity fallback so repos without createdAt sort last (appear at bottom).
    const sortedRepoEntries = [...repoMap.entries()].sort((a, b) => {
        const aRaw = a[1].data.createdAt;
        const bRaw = b[1].data.createdAt;
        const aTime = typeof aRaw === 'number' && !Number.isNaN(aRaw) ? aRaw : Infinity;
        const bTime = typeof bRaw === 'number' && !Number.isNaN(bRaw) ? bRaw : Infinity;
        return aTime - bTime;
    });
    for (const [nodeId, entry] of sortedRepoEntries) {
        const data = {
            ...entry.data,
            ...(callbacks?.onRepositoryAdd && {
                onAdd: () => callbacks.onRepositoryAdd(nodeId),
            }),
            ...(callbacks?.onRepositoryClick && {
                onClick: () => callbacks.onRepositoryClick(nodeId),
            }),
            ...(callbacks?.onRepositoryDelete && {
                onDelete: callbacks.onRepositoryDelete,
            }),
        };
        nodes.push({
            id: nodeId,
            type: 'repositoryNode',
            position: { x: 0, y: 0 },
            data,
        });
    }
    // Process feature entries (both real and pending).
    // Skip pendingMap entries that already exist in featureMap (reconcile may not
    // have cleaned them up yet if the AI-generated name differs from the original).
    const allFeatureEntries = [
        ...[...featureMap.entries()].map(([id, e]) => [id, e, false]),
        ...[...pendingMap.entries()]
            .filter(([id]) => !featureMap.has(id))
            .map(([id, e]) => [id, e, true]),
    ];
    for (const [nodeId, entry, isPending] of allFeatureEntries) {
        const isCreating = entry.data.state === 'creating' || isPending;
        const data = {
            ...entry.data,
            // Callbacks only for non-creating nodes
            ...(!isCreating &&
                callbacks?.onNodeAction && {
                onAction: () => callbacks.onNodeAction(nodeId),
            }),
            ...(!isCreating &&
                callbacks?.onNodeSettings && {
                onSettings: () => callbacks.onNodeSettings(nodeId),
            }),
            ...(!isCreating &&
                callbacks?.onFeatureDelete && {
                onDelete: callbacks.onFeatureDelete,
            }),
            ...(!isCreating &&
                entry.data.state === 'error' &&
                callbacks?.onRetryFeature && {
                onRetry: callbacks.onRetryFeature,
            }),
            ...(!isCreating &&
                entry.data.state === 'pending' &&
                callbacks?.onStartFeature && {
                onStart: callbacks.onStartFeature,
            }),
            ...(!isCreating &&
                (entry.data.state === 'running' || entry.data.state === 'action-required') &&
                callbacks?.onStopFeature && {
                onStop: callbacks.onStopFeature,
            }),
            ...(!isCreating &&
                entry.data.state !== 'archived' &&
                entry.data.state !== 'deleting' &&
                callbacks?.onArchiveFeature && {
                onArchive: callbacks.onArchiveFeature,
            }),
            ...(!isCreating &&
                entry.data.state === 'archived' &&
                callbacks?.onUnarchiveFeature && {
                onUnarchive: callbacks.onUnarchiveFeature,
            }),
        };
        nodes.push({
            id: nodeId,
            type: 'featureNode',
            position: { x: 0, y: 0 },
            data,
        });
        // Edge derivation
        // Priority order:
        //   1) parentNodeId → feature→feature dep edge (existing behavior)
        //   2) applicationId matching a known ApplicationNode → app→feature dep edge
        //   3) repositoryPath → repo→feature edge (existing fallback)
        // If parentNodeId references a feature that's not visible (e.g. archived/filtered),
        // fall through so the child reconnects to its application or repository.
        const appNodeId = entry.applicationId ? appByDomainId.get(entry.applicationId) : undefined;
        if (entry.parentNodeId &&
            (featureMap.has(entry.parentNodeId) || pendingMap.has(entry.parentNodeId))) {
            // Dependency edge (parent→child feature)
            edges.push({
                id: `dep-${entry.parentNodeId}-${nodeId}`,
                source: entry.parentNodeId,
                target: nodeId,
                type: 'dependencyEdge',
            });
        }
        else if (appNodeId) {
            // Application→feature dep edge (SDD-mode features launched from an app)
            edges.push({
                id: `dep-${appNodeId}-${nodeId}`,
                source: appNodeId,
                target: nodeId,
                type: 'dependencyEdge',
            });
        }
        else {
            // Repo→feature edge (matched by repositoryPath)
            const repositoryPath = normalizePath(entry.data.repositoryPath);
            const repoNodeId = repoByPath.get(repositoryPath) ?? getOrCreateVirtualRepo(repositoryPath);
            edges.push({
                id: `edge-${repoNodeId}-${nodeId}`,
                source: repoNodeId,
                target: nodeId,
                style: { strokeDasharray: '5 5' },
            });
        }
    }
    // Emit ApplicationNodes for every workspace application. Features whose
    // `applicationId` matches an entry above already have an app→feature edge;
    // applications without any child SDD feature simply render as standalone
    // nodes (mirrors how repository nodes appear with no children).
    if (applicationMap) {
        for (const [nodeId, entry] of applicationMap) {
            const data = {
                ...entry.data,
                ...(callbacks?.onApplicationClick && {
                    onClick: () => callbacks.onApplicationClick(entry.data.id),
                }),
                ...(callbacks?.onApplicationDelete && {
                    onDelete: callbacks.onApplicationDelete,
                }),
                ...(callbacks?.onApplicationCreateSddFeature && {
                    onCreateSddFeature: callbacks.onApplicationCreateSddFeature,
                }),
            };
            nodes.push({
                id: nodeId,
                type: 'applicationNode',
                position: { x: 0, y: 0 },
                data,
            });
        }
    }
    // Build set of feature node IDs that have children (are dependency edge sources)
    const parentNodeIds = new Set();
    for (const edge of edges) {
        if (edge.type === 'dependencyEdge') {
            parentNodeIds.add(edge.source);
        }
    }
    // Inject showHandles and hasChildren based on edges
    const hasEdges = edges.length > 0;
    for (const node of nodes) {
        const data = node.data;
        data.showHandles = hasEdges;
        if (node.type === 'featureNode') {
            data.hasChildren = parentNodeIds.has(node.id);
        }
    }
    return { nodes, edges };
}
