// tools/create-spec/spec-builder.ts — Builds the Spec object, diagrams, and scope filters.
// Extracted from create-spec.ts to keep file sizes within limits.
import { hashProjectPath, knowledgeStore, specStore, globalStore } from '../../storage/index.js';
import { getCurrentTier } from '../license-gate.js';
import { checkLimits } from '../../engine/license-validator.js';
import { inferDifficulty, calculateEstimation } from '../../engine/estimator.js';
import { generateDiagrams } from '../../engine/mermaid-generator.js';
import { analyzeSplit, formatSplitSuggestion } from '../../engine/spec-splitter.js';
import { getScopeFilters } from '../../engine/scope-filter.js';
import { isLegacyLocation, getSpecFileNames } from '../../engine/planu-config-writer.js';
import { slugify, generateSpecId, generateBranchName } from '../create-spec-helpers.js';
import { shouldSuggestMcpLayer, buildMcpLayerSection } from './mcp-layer-adapter.js';
import { buildLegalProfile, shouldInjectLegal, generateLegalSection, generateLegalCriteria, } from '../../engine/legal-compliance/index.js';
import { join, resolve } from 'node:path';
import { readdir, readFile } from 'node:fs/promises';
/** Builds the Spec object, calculates estimation, generates diagrams, and scope filters. */
export async function buildSpecContext(params) {
    const { title, description, projectPath, type = 'feature', scope = 'feature', target = 'fullstack', tags = [], feature, } = params;
    const projectId = hashProjectPath(projectPath);
    const knowledge = await knowledgeStore.getKnowledge(projectId);
    const existingSpecs = await specStore.listSpecs(projectId);
    // License: check active spec limit (exclude completed specs)
    const activeSpecs = existingSpecs.filter((s) => s.status !== 'done');
    const tier = await getCurrentTier();
    const specLimit = checkLimits(tier, activeSpecs.length, 'maxActiveSpecs');
    /* v8 ignore start -- license limit requires paid tier test env */
    if (!specLimit.allowed) {
        return { ok: false, errorMessage: specLimit.message };
    }
    /* v8 ignore stop */
    // Generate spec ID: combine store IDs + filesystem IDs for global max
    const fsSpecIds = await scanFilesystemSpecIds(projectPath);
    const allSpecs = [...existingSpecs, ...fsSpecIds.map((id) => ({ id }))];
    const specId = generateSpecId(allSpecs);
    const slug = slugify(title);
    // Check for duplicate title
    const duplicate = existingSpecs.find((s) => slugify(s.title) === slug);
    // Infer difficulty and calculate estimation
    const difficulty = inferDifficulty(type, scope);
    const globalConfig = await globalStore.getGlobalConfig();
    const estimation = calculateEstimation(type, scope, difficulty, globalConfig);
    // Determine risk level from difficulty and scope
    const risk = difficulty >= 4 || scope === 'architectural'
        ? 'high'
        : difficulty >= 3 || scope === 'cross-module'
            ? 'medium'
            : 'low';
    // Build spec location with naming convention detection
    const specLocation = knowledge?.specLocation ?? 'planu/specs';
    const naming = isLegacyLocation(specLocation) ? 'legacy' : 'modern';
    const fileNames = getSpecFileNames(naming);
    // Flat structure — no subcategories (feature stored as tag instead)
    // Always resolve to absolute path to prevent CWD-relative writes
    const specDir = resolve(join(projectPath, specLocation, `${specId}-${slug}`));
    // Safety: abort if the specDir already exists (ID collision)
    try {
        await readdir(specDir);
        return {
            ok: false,
            errorMessage: `Directory already exists: ${specDir}. Spec ID ${specId} collides with filesystem. Delete the conflicting directory or check planu.json specLocation.`,
        };
    }
    catch {
        // Expected: directory doesn't exist yet — proceed
    }
    const specPath = join(specDir, fileNames.spec);
    const technicalPath = join(specDir, fileNames.technical);
    const progressPath = join(specDir, fileNames.progress);
    // Generate git branch name
    const gitBranch = generateBranchName(specId, slug, type);
    // Build the Spec object
    const now = new Date().toISOString();
    const spec = {
        id: specId,
        title,
        slug,
        feature: feature ?? undefined,
        type,
        scope,
        status: 'draft',
        difficulty,
        risk,
        projectId,
        createdAt: now,
        updatedAt: now,
        specPath,
        technicalPath,
        progressPath,
        estimation,
        actuals: null,
        target,
        tags: feature ? [...new Set([...tags, slugify(feature)])] : tags,
        dependencies: [],
        blockedBy: [],
        gitBranch,
        impactAnalysis: null,
    };
    // Detect context flags for diagrams and filters
    const { detectContextFlags } = await import('../create-spec-tech.js');
    const contextFlags = detectContextFlags(spec, knowledge);
    // Generate Mermaid diagrams
    const diagrams = generateDiagrams(spec, knowledge, contextFlags);
    // Analyze if spec should be split
    const splitSuggestion = analyzeSplit({
        criteriaCount: 4,
        moduleCount: scope === 'cross-module' ? 5 : scope === 'architectural' ? 6 : 2,
        estimatedHours: estimation.devHours,
        description,
        type,
        scope,
        target,
    }, specId);
    // Get scope filter configuration
    const scopeFilters = getScopeFilters(scope, {
        hasDatabase: contextFlags.hasDatabase,
        hasUi: contextFlags.hasUi,
    });
    // SPEC-131: Detect legal compliance requirements
    let legalSection = null;
    let legalCriteria = [];
    if (knowledge) {
        const legalProfile = buildLegalProfile(knowledge);
        if (shouldInjectLegal(legalProfile)) {
            legalSection = generateLegalSection(legalProfile, description);
            legalCriteria = generateLegalCriteria(legalProfile, description);
        }
    }
    // SPEC-132: Detect MCP server layer opportunity
    let mcpLayerSection = null;
    if (knowledge && shouldSuggestMcpLayer(knowledge)) {
        mcpLayerSection = buildMcpLayerSection(knowledge, description);
    }
    return {
        ok: true,
        context: {
            spec,
            specDir,
            specPath,
            technicalPath,
            progressPath,
            diagrams,
            estimation,
            splitSuggestion,
            scopeFilters,
            duplicate,
            projectId,
            legalSection,
            legalCriteria,
            mcpLayerSection,
        },
    };
}
/** Builds the split suggestion result object if applicable. */
export function buildSplitResult(splitSuggestion, experienceLevel) {
    if (!splitSuggestion) {
        return undefined;
    }
    const level = experienceLevel ?? 'intermediate';
    return {
        shouldSplit: true,
        reason: splitSuggestion.reason,
        subSpecs: splitSuggestion.suggestedSpecs,
        formatted: formatSplitSuggestion(splitSuggestion, level === 'beginner' ? 'beginner' : 'expert'),
    };
}
/* v8 ignore start -- filesystem scanning is best-effort, tested via integration */
/** Scan filesystem for SPEC-XXX directories to find max ID globally. */
async function scanFilesystemSpecIds(projectPath) {
    const ids = [];
    try {
        const configPath = join(projectPath, 'planu.json');
        let specLocation = 'planu/specs';
        try {
            const config = JSON.parse(await readFile(configPath, 'utf-8'));
            specLocation = config.specLocation ?? specLocation;
        }
        catch {
            /* no planu.json — use default */
        }
        const specsRoot = join(projectPath, specLocation);
        await scanSpecDir(specsRoot, ids);
        // Also scan one level deeper for subcategory folders (e.g., planu/specs/category/SPEC-xxx/)
        const topEntries = await readdir(specsRoot, { withFileTypes: true }).catch(() => []);
        for (const top of topEntries) {
            if (top.isDirectory() && !/^SPEC-\d+/.test(top.name)) {
                await scanSpecDir(join(specsRoot, top.name), ids);
            }
        }
        // Also check legacy location
        const legacyRoot = join(projectPath, 'docs/sdd/specs');
        const legacyEntries = await readdir(legacyRoot, { withFileTypes: true }).catch(() => []);
        for (const entry of legacyEntries) {
            const match = /^(SPEC-\d+)/.exec(entry.name);
            if (match?.[1]) {
                ids.push(match[1]);
            }
        }
    }
    catch (err) {
        /* filesystem scan is best-effort — log for diagnostics */
        console.error('[planu] scanFilesystemSpecIds failed:', err.message);
    }
    return ids;
}
/** Scan a single directory for SPEC-XXX folders or slug folders with spec.md. */
async function scanSpecDir(dir, ids) {
    const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
    for (const entry of entries) {
        if (!entry.isDirectory()) {
            continue;
        }
        // 1. Try directory name (e.g. SPEC-042-feature-name/)
        const dirMatch = /^(SPEC-\d+)/.exec(entry.name);
        if (dirMatch?.[1]) {
            ids.push(dirMatch[1]);
            continue;
        }
        // 2. Fallback: read spec.md for ID (slug-only dirs)
        try {
            const specFile = join(dir, entry.name, 'spec.md');
            const content = await readFile(specFile, 'utf-8');
            const first200 = content.slice(0, 200);
            // Try heading: # SPEC-001
            const headingMatch = /^#\s+(SPEC-\d+)/m.exec(first200);
            if (headingMatch?.[1]) {
                ids.push(headingMatch[1]);
            }
            else {
                // Try YAML frontmatter: id: SPEC-001
                const yamlMatch = /^id:\s*(SPEC-\d+)/m.exec(first200);
                if (yamlMatch?.[1]) {
                    ids.push(yamlMatch[1]);
                }
            }
        }
        catch {
            /* no spec.md or unreadable — skip */
        }
    }
}
/* v8 ignore stop */
//# sourceMappingURL=spec-builder.js.map