/**
 * Skill File Generator
 *
 * Generates repo-specific SKILL.md files from detected Leiden communities.
 * Each significant community becomes a skill that describes a functional area
 * of the codebase, including key files, entry points, execution flows, and
 * cross-community connections.
 */
import fs from 'fs/promises';
import path from 'path';
import { estimateTokens } from './compress-stats.js';
/**
 * Supported skill targets. Project-relative output paths mirror each editor's
 * convention: Claude / Cursor use `skills/`, OpenCode uses `skill/` (singular)
 * to match its global config layout, Codex uses `skills/`. The trailing
 * `generated/` segment isolates auto-generated skills from human-authored ones.
 */
export const SKILL_TARGETS = ['claude', 'cursor', 'opencode', 'codex'];
const SKILL_OUTPUT_DIRS = {
    claude: ['.claude', 'skills', 'generated'],
    cursor: ['.cursor', 'skills', 'generated'],
    opencode: ['.opencode', 'skill', 'generated'],
    codex: ['.codex', 'skills', 'generated'],
};
// ============================================================================
// MAIN EXPORT
// ============================================================================
/**
 * @brief Generate repo-specific skill files from detected communities
 * @param {string} repoPath - Absolute path to the repository root
 * @param {string} projectName - Human-readable project name
 * @param {PipelineResult} pipelineResult - In-memory pipeline data with communities, processes, graph
 * @param {SkillTarget[]} targets - Editor targets to emit to. Defaults to ['claude'].
 * @returns {Promise<{ skills: GeneratedSkillInfo[], outputPath: string, outputPaths: string[] }>}
 *          `outputPath` is the Claude path (or first target) for backwards compat;
 *          `outputPaths` lists every directory written to.
 */
export const generateSkillFiles = async (repoPath, projectName, pipelineResult, targets = ['claude']) => {
    const { communityResult, processResult, graph } = pipelineResult;
    // Resolve all output dirs once. The "primary" path is Claude (if requested)
    // or the first target — kept for AGENTS.md / CLAUDE.md generators that link
    // to skill files relative to .claude/.
    const effectiveTargets = targets.length > 0 ? targets : ['claude'];
    const outputDirs = effectiveTargets.map((t) => path.join(repoPath, ...SKILL_OUTPUT_DIRS[t]));
    const primaryDir = effectiveTargets.includes('claude')
        ? path.join(repoPath, ...SKILL_OUTPUT_DIRS.claude)
        : outputDirs[0];
    if (!communityResult || !communityResult.memberships.length) {
        console.log('\n  Skills: no communities detected, skipping skill generation');
        return { skills: [], outputPath: primaryDir, outputPaths: outputDirs };
    }
    console.log('\n  Generating repo-specific skills...');
    // Step 1: Build communities from memberships (not the filtered communities array).
    // The community processor skips singletons from its communities array but memberships
    // include ALL assignments. For repos with sparse CALLS edges, the communities array
    // can be empty while memberships still has useful groupings.
    const communities = communityResult.communities.length > 0
        ? communityResult.communities
        : buildCommunitiesFromMemberships(communityResult.memberships, graph, repoPath);
    const aggregated = aggregateCommunities(communities);
    // Step 2: Filter to significant communities
    // Keep communities with >= 3 symbols after aggregation.
    const significant = aggregated
        .filter((c) => c.symbolCount >= 3)
        .sort((a, b) => b.symbolCount - a.symbolCount)
        .slice(0, 20);
    if (significant.length === 0) {
        console.log('\n  Skills: no significant communities found (all below 3-symbol threshold)');
        return { skills: [], outputPath: primaryDir, outputPaths: outputDirs };
    }
    // Step 3: Build lookup maps
    const membershipsByComm = buildMembershipMap(communityResult.memberships);
    const nodeIdToCommunityLabel = buildNodeCommunityLabelMap(communityResult.memberships, communities);
    // Step 4: Clear and recreate every output directory we'll write to
    for (const dir of outputDirs) {
        try {
            await fs.rm(dir, { recursive: true, force: true });
        }
        catch {
            /* may not exist */
        }
        await fs.mkdir(dir, { recursive: true });
    }
    // Step 5: Generate skill files
    const skills = [];
    const usedNames = new Set();
    for (const community of significant) {
        // Gather member symbols
        const members = gatherMembers(community.rawIds, membershipsByComm, graph);
        if (members.length === 0)
            continue;
        // Gather file info
        const files = gatherFiles(members, repoPath);
        // Gather entry points
        const entryPoints = gatherEntryPoints(members);
        // Gather execution flows
        const flows = gatherFlows(community.rawIds, processResult?.processes || []);
        // Gather cross-community connections
        const connections = gatherCrossConnections(community.rawIds, community.label, membershipsByComm, nodeIdToCommunityLabel, graph);
        // Generate kebab name
        const kebabName = toKebabName(community.label, usedNames);
        usedNames.add(kebabName);
        // Generate SKILL.md content
        const content = renderSkillMarkdown(community, projectName, members, files, entryPoints, flows, connections, kebabName);
        // Write the same SKILL.md to each requested editor target
        for (const dir of outputDirs) {
            const skillDir = path.join(dir, kebabName);
            await fs.mkdir(skillDir, { recursive: true });
            await fs.writeFile(path.join(skillDir, 'SKILL.md'), content, 'utf-8');
        }
        const skillTokens = estimateTokens(content);
        const info = {
            name: kebabName,
            label: community.label,
            symbolCount: community.symbolCount,
            fileCount: files.length,
        };
        skills.push(info);
        // Show the @codragraph/compress headline number per skill: how many
        // tokens of distilled context this community boils down to.
        console.log(`    \u2713 ${community.label} (${community.symbolCount} symbols, ${files.length} files) ` +
            `\u2192 ~${skillTokens.toLocaleString()} tokens`);
    }
    const targetSummary = effectiveTargets.join(', ');
    console.log(`\n  ${skills.length} skills generated \u2192 ${targetSummary}`);
    return { skills, outputPath: primaryDir, outputPaths: outputDirs };
};
// ============================================================================
// FALLBACK COMMUNITY BUILDER
// ============================================================================
/**
 * @brief Build CommunityNode-like objects from raw memberships when the community
 *        processor's communities array is empty (all singletons were filtered out)
 * @param {CommunityMembership[]} memberships - All node-to-community assignments
 * @param {KnowledgeGraph} graph - The knowledge graph for resolving node metadata
 * @param {string} repoPath - Repository root for path normalization
 * @returns {CommunityNode[]} Synthetic community nodes built from membership data
 */
const buildCommunitiesFromMemberships = (memberships, graph, _repoPath) => {
    // Group memberships by communityId
    const groups = new Map();
    for (const m of memberships) {
        const arr = groups.get(m.communityId);
        if (arr) {
            arr.push(m.nodeId);
        }
        else {
            groups.set(m.communityId, [m.nodeId]);
        }
    }
    const communities = [];
    for (const [commId, nodeIds] of groups) {
        // Derive a heuristic label from the most common parent directory
        const folderCounts = new Map();
        for (const nodeId of nodeIds) {
            const node = graph.getNode(nodeId);
            if (!node?.properties.filePath)
                continue;
            const normalized = node.properties.filePath.replace(/\\/g, '/');
            const parts = normalized.split('/').filter(Boolean);
            if (parts.length >= 2) {
                const folder = parts[parts.length - 2];
                if (!['src', 'lib', 'core', 'utils', 'common', 'shared', 'helpers'].includes(folder.toLowerCase())) {
                    folderCounts.set(folder, (folderCounts.get(folder) || 0) + 1);
                }
            }
        }
        let bestFolder = '';
        let bestCount = 0;
        for (const [folder, count] of folderCounts) {
            if (count > bestCount) {
                bestCount = count;
                bestFolder = folder;
            }
        }
        const label = bestFolder
            ? bestFolder.charAt(0).toUpperCase() + bestFolder.slice(1)
            : `Cluster_${commId.replace('comm_', '')}`;
        // Compute cohesion as internal-edge ratio (matches backend calculateCohesion).
        // For each member node, count edges that stay inside the community vs total.
        const nodeSet = new Set(nodeIds);
        let internalEdges = 0;
        let totalEdges = 0;
        graph.forEachRelationship((rel) => {
            if (nodeSet.has(rel.sourceId)) {
                totalEdges++;
                if (nodeSet.has(rel.targetId))
                    internalEdges++;
            }
        });
        const cohesion = totalEdges > 0 ? Math.min(1.0, internalEdges / totalEdges) : 1.0;
        communities.push({
            id: commId,
            label,
            heuristicLabel: label,
            cohesion,
            symbolCount: nodeIds.length,
        });
    }
    return communities.sort((a, b) => b.symbolCount - a.symbolCount);
};
// ============================================================================
// AGGREGATION
// ============================================================================
/**
 * @brief Aggregate raw Leiden communities by heuristicLabel
 * @param {CommunityNode[]} communities - Raw community nodes from Leiden detection
 * @returns {AggregatedCommunity[]} Aggregated communities grouped by label
 */
const aggregateCommunities = (communities) => {
    const groups = new Map();
    for (const c of communities) {
        const label = c.heuristicLabel || c.label || 'Unknown';
        const symbols = c.symbolCount || 0;
        const cohesion = c.cohesion || 0;
        const existing = groups.get(label);
        if (!existing) {
            groups.set(label, {
                rawIds: [c.id],
                totalSymbols: symbols,
                weightedCohesion: cohesion * symbols,
            });
        }
        else {
            existing.rawIds.push(c.id);
            existing.totalSymbols += symbols;
            existing.weightedCohesion += cohesion * symbols;
        }
    }
    return Array.from(groups.entries()).map(([label, g]) => ({
        label,
        rawIds: g.rawIds,
        symbolCount: g.totalSymbols,
        cohesion: g.totalSymbols > 0 ? g.weightedCohesion / g.totalSymbols : 0,
    }));
};
// ============================================================================
// LOOKUP MAP BUILDERS
// ============================================================================
/**
 * @brief Build a map from communityId to member nodeIds
 * @param {CommunityMembership[]} memberships - All membership records
 * @returns {Map<string, string[]>} Map of communityId -> nodeId[]
 */
const buildMembershipMap = (memberships) => {
    const map = new Map();
    for (const m of memberships) {
        const arr = map.get(m.communityId);
        if (arr) {
            arr.push(m.nodeId);
        }
        else {
            map.set(m.communityId, [m.nodeId]);
        }
    }
    return map;
};
/**
 * @brief Build a map from nodeId to aggregated community label
 * @param {CommunityMembership[]} memberships - All membership records
 * @param {CommunityNode[]} communities - Community nodes with labels
 * @returns {Map<string, string>} Map of nodeId -> community label
 */
const buildNodeCommunityLabelMap = (memberships, communities) => {
    const commIdToLabel = new Map();
    for (const c of communities) {
        commIdToLabel.set(c.id, c.heuristicLabel || c.label || 'Unknown');
    }
    const map = new Map();
    for (const m of memberships) {
        const label = commIdToLabel.get(m.communityId);
        if (label) {
            map.set(m.nodeId, label);
        }
    }
    return map;
};
// ============================================================================
// DATA GATHERING
// ============================================================================
/**
 * @brief Gather member symbols for an aggregated community
 * @param {string[]} rawIds - Raw community IDs belonging to this aggregated community
 * @param {Map<string, string[]>} membershipsByComm - communityId -> nodeIds
 * @param {KnowledgeGraph} graph - The knowledge graph
 * @returns {MemberSymbol[]} Array of member symbol information
 */
const gatherMembers = (rawIds, membershipsByComm, graph) => {
    const seen = new Set();
    const members = [];
    for (const commId of rawIds) {
        const nodeIds = membershipsByComm.get(commId) || [];
        for (const nodeId of nodeIds) {
            if (seen.has(nodeId))
                continue;
            seen.add(nodeId);
            const node = graph.getNode(nodeId);
            if (!node)
                continue;
            members.push({
                id: node.id,
                name: node.properties.name,
                label: node.label,
                filePath: node.properties.filePath || '',
                startLine: node.properties.startLine || 0,
                isExported: node.properties.isExported === true,
            });
        }
    }
    return members;
};
/**
 * @brief Gather deduplicated file info with per-file symbol names
 * @param {MemberSymbol[]} members - Member symbols
 * @param {string} repoPath - Repository root for relative path computation
 * @returns {FileInfo[]} Sorted by symbol count descending
 */
const gatherFiles = (members, repoPath) => {
    const fileMap = new Map();
    for (const m of members) {
        if (!m.filePath)
            continue;
        const rel = toRelativePath(m.filePath, repoPath);
        const arr = fileMap.get(rel);
        if (arr) {
            arr.push(m.name);
        }
        else {
            fileMap.set(rel, [m.name]);
        }
    }
    return Array.from(fileMap.entries())
        .map(([relativePath, symbols]) => ({ relativePath, symbols }))
        .sort((a, b) => b.symbols.length - a.symbols.length);
};
/**
 * @brief Gather exported entry points prioritized by type
 * @param {MemberSymbol[]} members - Member symbols
 * @returns {MemberSymbol[]} Exported symbols sorted by type priority
 */
const gatherEntryPoints = (members) => {
    const typePriority = {
        Function: 0,
        Class: 1,
        Method: 2,
        Interface: 3,
    };
    return members
        .filter((m) => m.isExported)
        .sort((a, b) => {
        const pa = typePriority[a.label] ?? 99;
        const pb = typePriority[b.label] ?? 99;
        return pa - pb;
    });
};
/**
 * @brief Gather execution flows touching this community
 * @param {string[]} rawIds - Raw community IDs for this aggregated community
 * @param {ProcessNode[]} processes - All detected processes
 * @returns {ProcessNode[]} Processes whose communities intersect rawIds, sorted by stepCount
 */
const gatherFlows = (rawIds, processes) => {
    const rawIdSet = new Set(rawIds);
    return processes
        .filter((proc) => proc.communities.some((cid) => rawIdSet.has(cid)))
        .sort((a, b) => b.stepCount - a.stepCount);
};
/**
 * @brief Gather cross-community call connections
 * @param {string[]} rawIds - Raw community IDs for this aggregated community
 * @param {string} ownLabel - This community's aggregated label
 * @param {Map<string, string[]>} membershipsByComm - communityId -> nodeIds
 * @param {Map<string, string>} nodeIdToCommunityLabel - nodeId -> community label
 * @param {KnowledgeGraph} graph - The knowledge graph
 * @returns {CrossConnection[]} Aggregated cross-community connections sorted by count
 */
const gatherCrossConnections = (rawIds, ownLabel, membershipsByComm, nodeIdToCommunityLabel, graph) => {
    // Collect all node IDs in this aggregated community
    const ownNodeIds = new Set();
    for (const commId of rawIds) {
        const nodeIds = membershipsByComm.get(commId) || [];
        for (const nid of nodeIds) {
            ownNodeIds.add(nid);
        }
    }
    // Count outgoing CALLS to nodes in different communities
    const targetCounts = new Map();
    graph.forEachRelationship((rel) => {
        if (rel.type !== 'CALLS')
            return;
        if (!ownNodeIds.has(rel.sourceId))
            return;
        if (ownNodeIds.has(rel.targetId))
            return; // same community
        const targetLabel = nodeIdToCommunityLabel.get(rel.targetId);
        if (!targetLabel || targetLabel === ownLabel)
            return;
        targetCounts.set(targetLabel, (targetCounts.get(targetLabel) || 0) + 1);
    });
    return Array.from(targetCounts.entries())
        .map(([targetLabel, count]) => ({ targetLabel, count }))
        .sort((a, b) => b.count - a.count);
};
// ============================================================================
// MARKDOWN RENDERING
// ============================================================================
/**
 * @brief Render SKILL.md content for a single community
 * @param {AggregatedCommunity} community - The aggregated community data
 * @param {string} projectName - Project name for the description
 * @param {MemberSymbol[]} members - All member symbols
 * @param {FileInfo[]} files - File info with symbol names
 * @param {MemberSymbol[]} entryPoints - Exported entry point symbols
 * @param {ProcessNode[]} flows - Execution flows touching this community
 * @param {CrossConnection[]} connections - Cross-community connections
 * @param {string} kebabName - Kebab-case name for the skill
 * @returns {string} Full SKILL.md content
 */
const renderSkillMarkdown = (community, projectName, members, files, entryPoints, flows, connections, kebabName) => {
    const cohesionPct = Math.round(community.cohesion * 100);
    // Dominant directory: most common top-level directory
    const dominantDir = getDominantDirectory(files);
    // Top symbol names for "When to Use"
    const topNames = entryPoints.slice(0, 3).map((e) => e.name);
    if (topNames.length === 0) {
        // Fallback to any members
        topNames.push(...members.slice(0, 3).map((m) => m.name));
    }
    const lines = [];
    // Frontmatter
    lines.push('---');
    lines.push(`name: ${kebabName}`);
    lines.push(`description: "Skill for the ${community.label} area of ${projectName}. ${community.symbolCount} symbols across ${files.length} files."`);
    lines.push('---');
    lines.push('');
    // Title
    lines.push(`# ${community.label}`);
    lines.push('');
    lines.push(`${community.symbolCount} symbols | ${files.length} files | Cohesion: ${cohesionPct}%`);
    lines.push('');
    // When to Use
    lines.push('## When to Use');
    lines.push('');
    if (dominantDir) {
        lines.push(`- Working with code in \`${dominantDir}/\``);
    }
    if (topNames.length > 0) {
        lines.push(`- Understanding how ${topNames.join(', ')} work`);
    }
    lines.push(`- Modifying ${community.label.toLowerCase()}-related functionality`);
    lines.push('');
    // Key Files (top 10)
    lines.push('## Key Files');
    lines.push('');
    lines.push('| File | Symbols |');
    lines.push('|------|---------|');
    for (const f of files.slice(0, 10)) {
        const symbolList = f.symbols.slice(0, 5).join(', ');
        const suffix = f.symbols.length > 5 ? ` (+${f.symbols.length - 5})` : '';
        lines.push(`| \`${f.relativePath}\` | ${symbolList}${suffix} |`);
    }
    lines.push('');
    // Entry Points (top 5)
    if (entryPoints.length > 0) {
        lines.push('## Entry Points');
        lines.push('');
        lines.push('Start here when exploring this area:');
        lines.push('');
        for (const ep of entryPoints.slice(0, 5)) {
            lines.push(`- **\`${ep.name}\`** (${ep.label}) \u2014 \`${ep.filePath}:${ep.startLine}\``);
        }
        lines.push('');
    }
    // Key Symbols (top 20, exported first, then by type)
    lines.push('## Key Symbols');
    lines.push('');
    lines.push('| Symbol | Type | File | Line |');
    lines.push('|--------|------|------|------|');
    const sortedMembers = [...members].sort((a, b) => {
        if (a.isExported !== b.isExported)
            return a.isExported ? -1 : 1;
        return a.label.localeCompare(b.label);
    });
    for (const m of sortedMembers.slice(0, 20)) {
        lines.push(`| \`${m.name}\` | ${m.label} | \`${m.filePath}\` | ${m.startLine} |`);
    }
    lines.push('');
    // Execution Flows
    if (flows.length > 0) {
        lines.push('## Execution Flows');
        lines.push('');
        lines.push('| Flow | Type | Steps |');
        lines.push('|------|------|-------|');
        for (const f of flows.slice(0, 10)) {
            lines.push(`| \`${f.heuristicLabel}\` | ${f.processType} | ${f.stepCount} |`);
        }
        lines.push('');
    }
    // Connected Areas
    if (connections.length > 0) {
        lines.push('## Connected Areas');
        lines.push('');
        lines.push('| Area | Connections |');
        lines.push('|------|-------------|');
        for (const c of connections.slice(0, 8)) {
            lines.push(`| ${c.targetLabel} | ${c.count} calls |`);
        }
        lines.push('');
    }
    // How to Explore
    const firstEntry = entryPoints.length > 0
        ? entryPoints[0].name
        : members.length > 0
            ? members[0].name
            : community.label;
    lines.push('## How to Explore');
    lines.push('');
    lines.push(`1. \`codragraph_context({name: "${firstEntry}"})\` \u2014 see callers and callees`);
    lines.push(`2. \`codragraph_query({query: "${community.label.toLowerCase()}"})\` \u2014 find related execution flows`);
    lines.push('3. Read key files listed above for implementation details');
    lines.push('');
    return lines.join('\n');
};
// ============================================================================
// UTILITY HELPERS
// ============================================================================
/**
 * @brief Convert a community label to a kebab-case directory name
 * @param {string} label - The community label
 * @param {Set<string>} usedNames - Already-used names for collision detection
 * @returns {string} Unique kebab-case name capped at 50 characters
 */
const toKebabName = (label, usedNames) => {
    let name = label
        .toLowerCase()
        .replace(/[^a-z0-9]+/g, '-')
        .replace(/^-+|-+$/g, '')
        .slice(0, 50);
    if (!name)
        name = 'skill';
    let candidate = name;
    let counter = 2;
    while (usedNames.has(candidate)) {
        candidate = `${name}-${counter}`;
        counter++;
    }
    return candidate;
};
/**
 * @brief Convert an absolute or repo-relative file path to a clean relative path
 * @param {string} filePath - The file path from the graph node
 * @param {string} repoPath - Repository root path
 * @returns {string} Relative path using forward slashes
 */
const toRelativePath = (filePath, repoPath) => {
    // Normalize to forward slashes for cross-platform consistency
    const normalizedFile = filePath.replace(/\\/g, '/');
    const normalizedRepo = repoPath.replace(/\\/g, '/');
    if (normalizedFile.startsWith(normalizedRepo)) {
        return normalizedFile.slice(normalizedRepo.length).replace(/^\//, '');
    }
    // Already relative or different root
    return normalizedFile.replace(/^\//, '');
};
/**
 * @brief Find the dominant (most common) top-level directory across files
 * @param {FileInfo[]} files - File info entries
 * @returns {string | null} Most common directory or null
 */
const getDominantDirectory = (files) => {
    const dirCounts = new Map();
    for (const f of files) {
        const parts = f.relativePath.split('/');
        if (parts.length >= 2) {
            const dir = parts[0];
            dirCounts.set(dir, (dirCounts.get(dir) || 0) + f.symbols.length);
        }
    }
    let best = null;
    let bestCount = 0;
    for (const [dir, count] of dirCounts) {
        if (count > bestCount) {
            bestCount = count;
            best = dir;
        }
    }
    return best;
};
