/**
 * src/server/routes/specs.ts — Spec CRUD endpoints for Planu HTTP API
 *
 * SPEC-610: REST endpoints for spec lifecycle management.
 * Imports from src/core/index.ts (transport-agnostic engine layer).
 *
 * Endpoints:
 *   POST   /api/specs              → create spec
 *   GET    /api/specs              → list specs (query: ?status=draft)
 *   GET    /api/specs/:id          → get spec details
 *   PATCH  /api/specs/:id/status   → update status
 *   POST   /api/specs/:id/validate → validate spec
 */
import { sendOk, sendBadRequest, sendNotFound, sendInternalError, readBody } from '../response.js';
import { hashProjectPath, specStore } from '../../storage/index.js';
import { generateSpecId, slugify } from '../../tools/create-spec-helpers.js';
import { checkSpecReadiness } from '../../core/index.js';
import { validateSpec } from '../../engine/validator.js';
import { handleUpdateStatus } from '../../tools/update-status/index.js';
const VALID_STATUSES = [
    'draft',
    'review',
    'approved',
    'implementing',
    'done',
    'discarded',
];
/** Resolve projectPath from the request Host header or fallback to process.cwd(). */
function getProjectPath(ctx) {
    // The server is started with a projectPath; it is injected via the request context
    // by storing it on the server options. We access it via a well-known header set by
    // the server, or fall back to process.cwd() for single-project deployments.
    const headerPath = ctx.req.headers['x-planu-project-path'];
    if (typeof headerPath === 'string' && headerPath.length > 0) {
        return headerPath;
    }
    return process.cwd();
}
/**
 * POST /api/specs
 * Body: { title: string; description: string }
 */
export async function handleCreateSpec(ctx) {
    let body;
    try {
        body = await readBody(ctx.req);
    }
    catch {
        sendBadRequest(ctx.res, 'Invalid JSON body');
        return;
    }
    if (typeof body !== 'object' || body === null) {
        sendBadRequest(ctx.res, 'Request body must be a JSON object');
        return;
    }
    const { title, description } = body;
    if (typeof title !== 'string' || title.trim().length === 0) {
        sendBadRequest(ctx.res, 'Field "title" is required and must be a non-empty string');
        return;
    }
    if (typeof description !== 'string' || description.trim().length === 0) {
        sendBadRequest(ctx.res, 'Field "description" is required and must be a non-empty string');
        return;
    }
    const projectPath = getProjectPath(ctx);
    const projectId = hashProjectPath(projectPath);
    try {
        const existingSpecs = await specStore.listSpecs(projectId);
        const specId = generateSpecId(existingSpecs);
        const now = new Date().toISOString();
        const slug = slugify(title.trim());
        const spec = {
            id: specId,
            uuid: crypto.randomUUID(),
            title: title.trim(),
            slug,
            type: 'feature',
            scope: 'feature',
            status: 'draft',
            difficulty: 2,
            risk: 'low',
            projectId,
            createdAt: now,
            updatedAt: now,
            specPath: `planu/specs/${specId}-${slug}/spec.md`,
            technicalPath: `planu/specs/${specId}-${slug}/spec.md`,
            estimation: {
                devHours: 4,
                reviewHours: 1,
                totalCostUsd: 0,
                recommendedModel: 'sonnet',
                tokensOpus: 0,
                tokensSonnet: 0,
                apiCostUsd: 0,
                hourlyRate: 0,
                humanCostUsd: 0,
                tokenOptimization: {
                    mode: 'light-llm',
                    reasoning: 'Default for new specs',
                    estimatedTokens: 0,
                    savings: '0%',
                },
            },
            actuals: null,
            target: 'fullstack',
            tags: [],
            dependencies: [],
            blockedBy: [],
            gitBranch: `feat/${specId.toLowerCase()}-${slug}`,
            impactAnalysis: null,
        };
        const created = await specStore.createSpec(projectId, spec);
        sendOk(ctx.res, created, 201);
    }
    catch (err) {
        const message = err instanceof Error ? err.message : 'Failed to create spec';
        sendInternalError(ctx.res, message);
    }
}
/**
 * GET /api/specs
 * Query: ?status=draft (optional)
 */
export async function handleListSpecs(ctx) {
    const statusFilter = ctx.searchParams.get('status');
    if (statusFilter !== null && !VALID_STATUSES.includes(statusFilter)) {
        sendBadRequest(ctx.res, `Invalid status "${statusFilter}". Valid values: ${VALID_STATUSES.join(', ')}`);
        return;
    }
    const projectPath = getProjectPath(ctx);
    const projectId = hashProjectPath(projectPath);
    try {
        let specs = await specStore.listSpecs(projectId);
        if (statusFilter !== null) {
            specs = specs.filter((s) => s.status === statusFilter);
        }
        sendOk(ctx.res, specs);
    }
    catch (err) {
        const message = err instanceof Error ? err.message : 'Failed to list specs';
        sendInternalError(ctx.res, message);
    }
}
/**
 * GET /api/specs/:id
 */
export async function handleGetSpec(ctx) {
    const specId = ctx.params.id;
    if (!specId) {
        sendBadRequest(ctx.res, 'Missing spec ID');
        return;
    }
    const projectPath = getProjectPath(ctx);
    const projectId = hashProjectPath(projectPath);
    try {
        const spec = await specStore.getSpec(projectId, specId);
        if (!spec) {
            sendNotFound(ctx.res, `Spec "${specId}" not found`);
            return;
        }
        sendOk(ctx.res, spec);
    }
    catch (err) {
        const message = err instanceof Error ? err.message : 'Failed to get spec';
        sendInternalError(ctx.res, message);
    }
}
/**
 * PATCH /api/specs/:id/status
 * Body: { status: SpecStatus }
 */
export async function handleUpdateSpecStatus(ctx) {
    const specId = ctx.params.id;
    if (!specId) {
        sendBadRequest(ctx.res, 'Missing spec ID');
        return;
    }
    let body;
    try {
        body = await readBody(ctx.req);
    }
    catch {
        sendBadRequest(ctx.res, 'Invalid JSON body');
        return;
    }
    if (typeof body !== 'object' || body === null) {
        sendBadRequest(ctx.res, 'Request body must be a JSON object');
        return;
    }
    const { status } = body;
    if (typeof status !== 'string' || !VALID_STATUSES.includes(status)) {
        sendBadRequest(ctx.res, `Field "status" is required. Valid values: ${VALID_STATUSES.join(', ')}`);
        return;
    }
    const projectPath = getProjectPath(ctx);
    const projectId = hashProjectPath(projectPath);
    try {
        const existing = await specStore.getSpec(projectId, specId);
        if (!existing) {
            sendNotFound(ctx.res, `Spec "${specId}" not found`);
            return;
        }
        // SPEC-720: Route through handleUpdateStatus so all gates run (replaces direct updateSpecStatus).
        // Clients may now receive 422 when gates block the transition.
        const result = await handleUpdateStatus({
            specId,
            status: status,
            projectId,
            projectPath,
            trigger: 'http',
            actor: 'http-api',
        });
        if (result.isError) {
            const reason = result.content[0]?.type === 'text' ? result.content[0].text : 'Gate blocked transition';
            // 422 Unprocessable Entity — gates blocked the transition
            ctx.res.writeHead(422, { 'Content-Type': 'application/json' });
            ctx.res.end(JSON.stringify({ error: reason }));
            return;
        }
        // Re-fetch updated spec to return current state
        const updated = await specStore.getSpec(projectId, specId);
        if (!updated) {
            sendNotFound(ctx.res, `Spec "${specId}" not found after update`);
            return;
        }
        sendOk(ctx.res, updated);
    }
    catch (err) {
        const message = err instanceof Error ? err.message : 'Failed to update spec status';
        sendInternalError(ctx.res, message);
    }
}
/**
 * POST /api/specs/:id/validate
 */
export async function handleValidateSpec(ctx) {
    const specId = ctx.params.id;
    if (!specId) {
        sendBadRequest(ctx.res, 'Missing spec ID');
        return;
    }
    const projectPath = getProjectPath(ctx);
    const projectId = hashProjectPath(projectPath);
    try {
        const spec = await specStore.getSpec(projectId, specId);
        if (!spec) {
            sendNotFound(ctx.res, `Spec "${specId}" not found`);
            return;
        }
        // Run readiness check (lightweight, no file I/O needed beyond spec content)
        const readiness = await checkSpecReadiness(spec, 'lenient');
        // Run full validation (criteria matching)
        const validation = await validateSpec(spec, projectPath);
        sendOk(ctx.res, {
            specId,
            ready: readiness.ready,
            score: validation.score,
            readinessScore: readiness.score,
            issues: [...readiness.issues.blockers, ...validation.missing],
            matches: validation.matches,
            qualityIssues: validation.qualityIssues.map((q) => q.message),
        });
    }
    catch (err) {
        const message = err instanceof Error ? err.message : 'Failed to validate spec';
        sendInternalError(ctx.res, message);
    }
}
//# sourceMappingURL=specs.js.map