import { readJson, writeJson, projectDataDir } from './base-store.js';
import { migrateLegacyDataDir } from './migrations/cwd-to-absolute.js';
import { withFileLock } from './file-mutex.js';
import { discoverProjectDataFile } from './data-dir-discovery.js';
import { computeHealthScore } from '../engine/spec-health-scorer.js';
import { loadSpecContent } from '../engine/validation-loop.js';
// ---------------------------------------------------------------------------
// SPEC-720: Guard error — direct status write forbidden
// ---------------------------------------------------------------------------
/**
 * Thrown when code tries to write `status` via `updateSpec` directly.
 * All status transitions must go through `handleUpdateStatus` → `transitionSpec`.
 */
export class DirectStatusWriteForbiddenError extends Error {
    constructor(specId, attemptedStatus, caller) {
        super(`Direct status write to ${specId} (status=${attemptedStatus}) forbidden. ` +
            `Caller "${caller}" must invoke handleUpdateStatus instead. ` +
            `See SPEC-720.`);
        this.name = 'DirectStatusWriteForbiddenError';
    }
}
// --- internal helpers ---
function specsFile(projectId) {
    return `${projectDataDir(projectId)}/specs.json`;
}
/** Normalize legacy specs: map huPath→specPath, fichaTecnicaPath→technicalPath */
function normalizeSpec(raw) {
    // The raw object is parsed JSON; we perform field migration before treating it as Spec.
    // Direct cast via intermediate `any` is necessary because Record<string,unknown> does
    // not structurally overlap with the specific Spec interface.
    const spec = raw;
    // Lazy migration: legacy field names → modern field names
    if (!spec.specPath && spec.huPath) {
        spec.specPath = spec.huPath;
    }
    if (!spec.technicalPath && spec.fichaTecnicaPath) {
        spec.technicalPath = spec.fichaTecnicaPath;
    }
    // SPEC-601: Lazy UUID migration — generate if missing
    spec.uuid ??= crypto.randomUUID();
    return spec;
}
// Write-through in-memory cache: projectId → Spec[].
// Populated on first read; invalidated (updated) on every mutation.
// Eliminates repeated disk I/O for a local single-user MCP server.
const specsCache = new Map();
/**
 * Load all specs — from cache on subsequent calls, disk only on first access.
 *
 * Cwd-tolerant fallback: when the cwd-relative `specs.json` is missing or empty,
 * walk up from cwd and consult the registered projectPath before giving up.
 * Writes always go through the cwd-relative path (specsFile) — only reads
 * fall back, so divergent on-disk locations remain visible to the caller.
 */
async function loadAll(projectId) {
    const cached = specsCache.get(projectId);
    if (cached !== undefined) {
        return cached;
    }
    // SPEC-1007: migrate legacy <cwd>/data/projects/{id} to absolute home once.
    await migrateLegacyDataDir(projectId);
    const directFile = specsFile(projectId);
    let raw = await readJson(directFile, []);
    if (raw.length === 0) {
        const discovered = await discoverProjectDataFile(projectId, 'specs.json');
        if (discovered !== null && discovered !== directFile) {
            raw = await readJson(discovered, []);
        }
    }
    const specs = raw.map(normalizeSpec);
    specsCache.set(projectId, specs);
    return specs;
}
async function saveAll(projectId, specs) {
    await writeJson(specsFile(projectId), specs);
    specsCache.set(projectId, specs);
}
/** Evict the cache for a project (e.g. after external file changes). */
export function invalidateSpecsCache(projectId) {
    specsCache.delete(projectId);
}
// --- public API ---
/**
 * List every spec for a project.
 */
export async function listSpecs(projectId) {
    return loadAll(projectId);
}
/**
 * Get a single spec by ID. Returns `null` when not found.
 */
export async function getSpec(projectId, specId) {
    const specs = await loadAll(projectId);
    return specs.find((s) => s.id === specId) ?? null;
}
/** SPEC-601: Lookup a spec by its globally unique UUID. */
export async function getSpecByUuid(projectId, uuid) {
    const specs = await loadAll(projectId);
    return specs.find((s) => s.uuid === uuid) ?? null;
}
/**
 * Create a new spec. Throws if a spec with the same ID already exists.
 */
export async function createSpec(projectId, spec) {
    return withFileLock(specsFile(projectId), async () => {
        const specs = await loadAll(projectId);
        if (specs.some((s) => s.id === spec.id)) {
            throw new Error(`Spec "${spec.id}" already exists in project "${projectId}"`);
        }
        const { criteria } = await loadSpecContent(spec);
        const specWithScore = {
            ...spec,
            healthScore: computeHealthScore(spec, criteria.length > 0 ? criteria : undefined).score,
        };
        specs.push(specWithScore);
        await saveAll(projectId, specs);
        return specWithScore;
    });
}
/**
 * Internal implementation — updates any Spec field including status.
 * NOT exported from the barrel. Called only by `__internalSetStatus` and itself.
 * All external callers must use `updateSpec` (non-status) or `transitionSpec` (status).
 */
async function __internalUpdateSpec(projectId, specId, updates) {
    return withFileLock(specsFile(projectId), async () => {
        const specs = await loadAll(projectId);
        const idx = specs.findIndex((s) => s.id === specId);
        if (idx === -1) {
            throw new Error(`Spec "${specId}" not found in project "${projectId}"`);
        }
        const existing = specs[idx];
        if (!existing) {
            throw new Error(`Spec "${specId}" not found in project "${projectId}"`);
        }
        const now = new Date().toISOString();
        const incomingStatus = updates.status;
        let updatedStatusHistory = existing.statusHistory;
        if (incomingStatus !== undefined && incomingStatus !== existing.status) {
            const entry = { status: incomingStatus, changedAt: now };
            updatedStatusHistory = [...(existing.statusHistory ?? []), entry];
        }
        const merged = {
            ...existing,
            ...updates,
            id: specId, // never allow id override
            updatedAt: now,
            statusHistory: updatedStatusHistory,
        };
        const { criteria } = await loadSpecContent(merged);
        const updated = {
            ...merged,
            healthScore: computeHealthScore(merged, criteria.length > 0 ? criteria : undefined).score,
        };
        specs[idx] = updated;
        await saveAll(projectId, specs);
        return updated;
    });
}
/**
 * SPEC-720: Internal-only status write. NOT exported from the barrel.
 * Importable exclusively from `src/engine/spec-state-machine/transition-spec.ts`.
 * All status transitions must flow through `transitionSpec` → `__internalSetStatus`.
 */
export async function __internalSetStatus(projectId, specId, status) {
    return __internalUpdateSpec(projectId, specId, { status });
}
/**
 * Replace an existing spec entirely (non-status fields only).
 * Throws `DirectStatusWriteForbiddenError` if `updates` contains a `status` field.
 * To change status, call `handleUpdateStatus` from the tool layer.
 */
export async function updateSpec(projectId, specId, updates) {
    // SPEC-720: Guard — reject any attempt to write status via this path
    if ('status' in updates) {
        throw new DirectStatusWriteForbiddenError(specId, String(updates.status), new Error().stack?.split('\n')[2]?.trim() ?? 'unknown');
    }
    return __internalUpdateSpec(projectId, specId, updates);
}
/**
 * @deprecated SPEC-720: updateSpecStatus is removed from the public API.
 * Use `handleUpdateStatus` from `src/tools/update-status/index.ts` instead.
 * This stub remains only to produce a descriptive compile error for any
 * remaining callers until they are migrated.
 * @throws {DirectStatusWriteForbiddenError} Always.
 */
// eslint-disable-next-line @typescript-eslint/require-await -- always throws, async signature required for API compatibility
export async function updateSpecStatus(_projectId, specId, status) {
    throw new DirectStatusWriteForbiddenError(specId, status, 'updateSpecStatus (deprecated — use handleUpdateStatus, see SPEC-720)');
}
/**
 * Delete a spec by ID. Returns `true` if removed, `false` if not found.
 */
export async function deleteSpec(projectId, specId) {
    return withFileLock(specsFile(projectId), async () => {
        const specs = await loadAll(projectId);
        const idx = specs.findIndex((s) => s.id === specId);
        if (idx === -1) {
            return false;
        }
        specs.splice(idx, 1);
        await saveAll(projectId, specs);
        return true;
    });
}
/**
 * Find specs matching a predicate.
 */
export async function findSpecs(projectId, predicate) {
    const specs = await loadAll(projectId);
    return specs.filter(predicate);
}
/**
 * Count specs for a project, optionally filtered by status.
 */
export async function countSpecs(projectId, status) {
    const specs = await loadAll(projectId);
    if (status) {
        return specs.filter((s) => s.status === status).length;
    }
    return specs.length;
}
// --- DoR/DoD persistence ---
function dorFile(projectId, specId) {
    return `${projectDataDir(projectId)}/specs/${specId}/dor.json`;
}
function dodFile(projectId, specId) {
    return `${projectDataDir(projectId)}/specs/${specId}/dod.json`;
}
/**
 * Persist a DoR checklist for a spec.
 */
export async function saveDor(projectId, specId, dor) {
    await withFileLock(dorFile(projectId, specId), () => writeJson(dorFile(projectId, specId), dor));
}
/**
 * Load the persisted DoR checklist for a spec. Returns null if not found.
 */
export async function loadDor(projectId, specId) {
    return readJson(dorFile(projectId, specId), null);
}
/**
 * Persist a DoD checklist for a spec.
 */
export async function saveDoD(projectId, specId, dod) {
    await withFileLock(dodFile(projectId, specId), () => writeJson(dodFile(projectId, specId), dod));
}
/**
 * Load the persisted DoD checklist for a spec. Returns null if not found.
 */
export async function loadDoD(projectId, specId) {
    return readJson(dodFile(projectId, specId), null);
}
// --- Version persistence ---
function versionsFile(projectId, specId) {
    return `${projectDataDir(projectId)}/specs/${specId}/versions.json`;
}
/**
 * Append a new SpecVersion to the version history for a spec.
 */
export async function saveVersion(projectId, specId, version) {
    await withFileLock(versionsFile(projectId, specId), async () => {
        const existing = await loadVersions(projectId, specId);
        existing.push(version);
        await writeJson(versionsFile(projectId, specId), existing);
    });
}
/**
 * Load all persisted versions for a spec. Returns empty array if none exist.
 */
export async function loadVersions(projectId, specId) {
    return readJson(versionsFile(projectId, specId), []);
}
/**
 * Get the latest version number for a spec (0 if no versions exist).
 */
export async function getLatestVersion(projectId, specId) {
    const versions = await loadVersions(projectId, specId);
    if (versions.length === 0) {
        return 0;
    }
    return Math.max(...versions.map((v) => v.version));
}
// --- Summary persistence ---
function summaryFile(projectId, specId) {
    return `${projectDataDir(projectId)}/specs/${specId}/summary.json`;
}
/**
 * Persist a SpecSummary for a spec. Used by summarize_spec to cache results.
 */
export async function saveSummary(projectId, specId, summary) {
    await withFileLock(summaryFile(projectId, specId), () => writeJson(summaryFile(projectId, specId), summary));
}
/**
 * Load the persisted SpecSummary for a spec. Returns null if not found.
 */
export async function loadSummary(projectId, specId) {
    return readJson(summaryFile(projectId, specId), null);
}
//# sourceMappingURL=spec-store.js.map