import path from "node:path";
import { appendEvent } from "../observability/index.js";
import { runDir } from "../run-state/index.js";
import { createId, ensureDir, nowIso, readJsonFile, sanitizeText, withFileLock, writeJsonFile } from "../../shared/index.js";
import { LAB_REPRESENTATIVE_AGENT } from "../../agents/identity.js";
import { missingActiveWorkspaceLeases, workspacePathWithin } from "./enforcement.js";
export function workspaceRoot(store, runId) {
    return path.join(runDir(store, runId), "workspace");
}
export function workspaceIntentsPath(store, runId) {
    return path.join(workspaceRoot(store, runId), "intents.json");
}
export function workspaceLeasesPath(store, runId) {
    return path.join(workspaceRoot(store, runId), "leases.json");
}
export function workspaceConflictsPath(store, runId) {
    return path.join(workspaceRoot(store, runId), "conflicts.json");
}
export function workspaceDiffsPath(store, runId) {
    return path.join(workspaceRoot(store, runId), "diffs.json");
}
export function workspaceMergesPath(store, runId) {
    return path.join(workspaceRoot(store, runId), "merges.json");
}
export async function listWorkspaceIntents(store, runId) {
    await ensureWorkspaceState(store, runId);
    return (await readJsonFile(workspaceIntentsPath(store, runId))) ?? [];
}
export async function listWorkspaceLeases(store, runId) {
    await ensureWorkspaceState(store, runId);
    return ((await readJsonFile(workspaceLeasesPath(store, runId))) ?? []).map(normalizeLease);
}
export async function listWorkspaceConflicts(store, runId) {
    await ensureWorkspaceState(store, runId);
    return ((await readJsonFile(workspaceConflictsPath(store, runId))) ?? []).map(normalizeConflict);
}
export async function listWorkspaceWorktreeMerges(store, runId) {
    await ensureWorkspaceState(store, runId);
    return ((await readJsonFile(workspaceMergesPath(store, runId))) ?? []).map(normalizeWorktreeMerge);
}
export async function unresolvedWorkspaceConflicts(store, runId) {
    return (await listWorkspaceConflicts(store, runId)).filter((conflict) => conflict.status === "open");
}
export async function declareWorkspaceIntent(store, runId, input) {
    await ensureWorkspaceState(store, runId);
    const file = workspaceIntentsPath(store, runId);
    const intent = await withFileLock(file, async () => {
        const intents = (await readJsonFile(file)) ?? [];
        const next = {
            intent_id: createId("workspace_intent"),
            run_id: runId,
            actor: sanitizeText(input.actor),
            paths: input.paths.map(normalizeWorkspacePath).filter(Boolean),
            mode: input.mode,
            reason: sanitizeText(input.reason ?? ""),
            created_at: nowIso(),
        };
        intents.push(next);
        await writeJsonFile(file, intents);
        return next;
    });
    await appendEvent(store, runId, {
        type: "workspace_intent_declared",
        actor: intent.actor,
        summary: `${intent.mode} intent for ${intent.paths.join(", ")}`,
        metadata: { intent_id: intent.intent_id, paths: intent.paths, mode: intent.mode },
    });
    return intent;
}
export async function acquireWorkspaceLease(store, runId, input) {
    await ensureWorkspaceState(store, runId);
    const leasesFile = workspaceLeasesPath(store, runId);
    const conflictsFile = workspaceConflictsPath(store, runId);
    const result = await withFileLock(leasesFile, async () => {
        const leases = ((await readJsonFile(leasesFile)) ?? []).map(normalizeLease);
        const conflicts = ((await readJsonFile(conflictsFile)) ?? []).map(normalizeConflict);
        const timestamp = nowIso();
        const leaseBase = {
            lease_id: createId("workspace_lease"),
            run_id: runId,
            owner: sanitizeText(input.owner),
            path: normalizeWorkspacePath(input.path),
            mode: input.mode,
            status: "active",
            task_id: input.task_id,
            expected_base_hash: input.expected_base_hash,
            actual_base_hash: input.actual_base_hash,
            created_at: timestamp,
            updated_at: timestamp,
            expires_at: input.expires_at,
        };
        const staleBase = input.expected_base_hash && input.actual_base_hash && input.expected_base_hash !== input.actual_base_hash;
        const overlap = staleBase ? undefined : findBlockingLease(leases, leaseBase);
        if (!staleBase && !overlap) {
            leases.push(leaseBase);
            await writeJsonFile(leasesFile, leases);
            return { granted: true, lease: leaseBase };
        }
        const conflict = staleBase
            ? createWorkspaceConflict(runId, "stale_base", leaseBase, [], `Stale base hash for ${leaseBase.path}: expected ${input.expected_base_hash}, actual ${input.actual_base_hash}.`)
            : createWorkspaceConflict(runId, "lease_overlap", leaseBase, [overlap], `${leaseBase.owner} cannot acquire ${leaseBase.mode} lease for ${leaseBase.path}; overlaps active ${overlap.mode} lease held by ${overlap.owner}.`);
        const blockedLease = {
            ...leaseBase,
            status: "blocked",
            conflict_id: conflict.conflict_id,
        };
        leases.push(blockedLease);
        conflicts.push(conflict);
        await writeJsonFile(leasesFile, leases);
        await writeJsonFile(conflictsFile, conflicts);
        return { granted: false, lease: blockedLease, conflict };
    });
    await appendEvent(store, runId, {
        type: result.granted ? "workspace_lease_acquired" : "workspace_lease_blocked",
        actor: result.lease.owner,
        summary: result.granted
            ? `${result.lease.mode} lease acquired for ${result.lease.path}`
            : `${result.lease.mode} lease blocked for ${result.lease.path}: ${result.conflict?.summary ?? "conflict"}`,
        related_task_id: result.lease.task_id,
        metadata: { lease: result.lease, conflict: result.conflict },
    });
    return result;
}
export async function refreshWorkspaceLease(store, runId, leaseId, input = {}) {
    await ensureWorkspaceState(store, runId);
    const file = workspaceLeasesPath(store, runId);
    const lease = await withFileLock(file, async () => {
        const leases = ((await readJsonFile(file)) ?? []).map(normalizeLease);
        const index = leases.findIndex((item) => item.lease_id === leaseId);
        if (index < 0)
            throw new Error(`workspace lease not found: ${leaseId}`);
        const current = leases[index];
        if (input.owner && current.owner !== input.owner)
            throw new Error(`workspace lease is owned by ${current.owner}`);
        const next = { ...current, expires_at: input.expires_at ?? current.expires_at, updated_at: nowIso() };
        leases[index] = next;
        await writeJsonFile(file, leases);
        return next;
    });
    await appendEvent(store, runId, {
        type: "workspace_lease_refreshed",
        actor: lease.owner,
        summary: `${lease.mode} lease refreshed for ${lease.path}`,
        metadata: { lease_id: lease.lease_id },
    });
    return lease;
}
export async function releaseWorkspaceLease(store, runId, leaseId, input = {}) {
    await ensureWorkspaceState(store, runId);
    const file = workspaceLeasesPath(store, runId);
    const lease = await withFileLock(file, async () => {
        const leases = ((await readJsonFile(file)) ?? []).map(normalizeLease);
        const index = leases.findIndex((item) => item.lease_id === leaseId);
        if (index < 0)
            throw new Error(`workspace lease not found: ${leaseId}`);
        const current = leases[index];
        if (input.owner && current.owner !== input.owner)
            throw new Error(`workspace lease is owned by ${current.owner}`);
        const timestamp = nowIso();
        const next = { ...current, status: "released", updated_at: timestamp, released_at: timestamp };
        leases[index] = next;
        await writeJsonFile(file, leases);
        return next;
    });
    await appendEvent(store, runId, {
        type: "workspace_lease_released",
        actor: lease.owner,
        summary: `${lease.mode} lease released for ${lease.path}`,
        metadata: { lease_id: lease.lease_id },
    });
    return lease;
}
export async function recordWorkspaceConflict(store, runId, input) {
    await ensureWorkspaceState(store, runId);
    const file = workspaceConflictsPath(store, runId);
    const conflict = await withFileLock(file, async () => {
        const conflicts = ((await readJsonFile(file)) ?? []).map(normalizeConflict);
        const next = {
            conflict_id: createId("workspace_conflict"),
            run_id: runId,
            kind: input.kind,
            status: "open",
            path: normalizeWorkspacePath(input.path),
            actors: input.actors.map(sanitizeText),
            lease_ids: input.lease_ids ?? [],
            expected_base_hash: input.expected_base_hash,
            actual_base_hash: input.actual_base_hash,
            summary: sanitizeText(input.summary),
            created_at: nowIso(),
        };
        conflicts.push(next);
        await writeJsonFile(file, conflicts);
        return next;
    });
    await appendEvent(store, runId, {
        type: "workspace_conflict_recorded",
        actor: conflict.actors[0] ?? store.config.default_run_agent ?? LAB_REPRESENTATIVE_AGENT,
        summary: conflict.summary,
        metadata: { conflict },
    });
    return conflict;
}
export async function resolveWorkspaceConflict(store, runId, conflictId, input) {
    await ensureWorkspaceState(store, runId);
    const file = workspaceConflictsPath(store, runId);
    const conflict = await withFileLock(file, async () => {
        const conflicts = ((await readJsonFile(file)) ?? []).map(normalizeConflict);
        const index = conflicts.findIndex((item) => item.conflict_id === conflictId);
        if (index < 0)
            throw new Error(`workspace conflict not found: ${conflictId}`);
        const current = conflicts[index];
        const next = {
            ...current,
            status: "resolved",
            resolved_at: nowIso(),
            resolution: sanitizeText(input.resolution),
            resolution_evidence_refs: input.evidence_refs ?? current.resolution_evidence_refs ?? [],
            meeting_id: input.meeting_id ?? current.meeting_id,
        };
        conflicts[index] = next;
        await writeJsonFile(file, conflicts);
        return next;
    });
    await appendEvent(store, runId, {
        type: "workspace_conflict_resolved",
        actor: input.actor,
        summary: `${conflict.path}: ${conflict.resolution ?? "resolved"}`,
        metadata: { conflict_id: conflict.conflict_id },
    });
    return conflict;
}
export async function recordWorkspaceWorktreeMerge(store, runId, input) {
    await ensureWorkspaceState(store, runId);
    const actor = sanitizeText(input.actor);
    const mergeOwner = sanitizeText(input.merge_owner ?? input.actor);
    const lead = store.config.default_run_agent ?? LAB_REPRESENTATIVE_AGENT;
    if (actor !== lead && actor !== LAB_REPRESENTATIVE_AGENT && actor !== mergeOwner) {
        throw new Error("worktree merge can be recorded only by Lab-Representative or the designated merge owner");
    }
    const status = input.status ?? "applied";
    if (status === "applied" && !input.main_diff_ref) {
        throw new Error("applied worktree merge requires main_diff_ref");
    }
    const targetPaths = input.target_paths.map(normalizeWorkspacePath).filter(Boolean);
    if (!targetPaths.length)
        throw new Error("worktree merge requires at least one target path");
    const leases = await listWorkspaceLeases(store, runId);
    const missing = status === "applied"
        ? missingActiveWorkspaceLeases({
            paths: targetPaths.map((targetPath) => ({ path: targetPath, access: "write", source: "worktree_merge.target_paths" })),
            leases,
            owner: actor,
            leaseIds: input.lease_ids,
        })
        : [];
    if (missing.length) {
        throw new Error(`worktree merge requires active main-workspace write leases for: ${missing.map((entry) => entry.path).join(", ")}`);
    }
    const allowedLeaseIds = new Set(input.lease_ids ?? []);
    const appliedLeaseIds = status === "applied"
        ? leases
            .filter((lease) => lease.status === "active" && lease.owner === actor && (!allowedLeaseIds.size || allowedLeaseIds.has(lease.lease_id)) && targetPaths.some((targetPath) => workspacePathWithin(targetPath, lease.path)))
            .map((lease) => lease.lease_id)
        : input.lease_ids ?? [];
    const file = workspaceMergesPath(store, runId);
    const merge = await withFileLock(file, async () => {
        const merges = ((await readJsonFile(file)) ?? []).map(normalizeWorktreeMerge);
        const mergeId = createId("workspace_merge");
        const next = {
            merge_id: mergeId,
            merge_ref: `workspace/merges.json#${mergeId}`,
            run_id: runId,
            status,
            actor,
            merge_owner: mergeOwner,
            subplan_id: input.subplan_id,
            worktree_path: normalizeWorkspacePath(input.worktree_path),
            target_paths: targetPaths,
            worktree_diff_ref: sanitizeText(input.worktree_diff_ref),
            main_diff_ref: input.main_diff_ref ? sanitizeText(input.main_diff_ref) : undefined,
            lease_ids: unique(appliedLeaseIds),
            rejected_paths: unique((input.rejected_paths ?? []).map(normalizeWorkspacePath).filter(Boolean)),
            summary: sanitizeText(input.summary),
            evidence_refs: input.evidence_refs ?? [],
            created_at: nowIso(),
        };
        merges.push(next);
        await writeJsonFile(file, merges);
        return next;
    });
    await appendEvent(store, runId, {
        type: merge.status === "applied" ? "workspace_worktree_merge_applied" : "workspace_worktree_merge_rejected",
        actor: merge.actor,
        summary: `${merge.subplan_id ?? "worktree"}: ${merge.summary}`,
        metadata: { merge },
    });
    return merge;
}
export async function ensureWorkspaceState(store, runId) {
    await ensureDir(workspaceRoot(store, runId));
    await Promise.all([
        ensureJsonArray(workspaceIntentsPath(store, runId)),
        ensureJsonArray(workspaceLeasesPath(store, runId)),
        ensureJsonArray(workspaceConflictsPath(store, runId)),
        ensureJsonArray(workspaceDiffsPath(store, runId)),
        ensureJsonArray(workspaceMergesPath(store, runId)),
    ]);
}
async function ensureJsonArray(file) {
    const existing = await readJsonFile(file);
    if (Array.isArray(existing))
        return;
    await writeJsonFile(file, []);
}
function findBlockingLease(leases, requested) {
    return leases.find((lease) => lease.status === "active" && leasesConflict(lease, requested));
}
function leasesConflict(active, requested) {
    if (!pathsOverlap(active.path, requested.path))
        return false;
    if (active.mode === "append" && requested.mode === "append")
        return false;
    if (active.mode === "read" && requested.mode === "read")
        return false;
    if (requested.mode === "append" && active.mode === "read")
        return false;
    if (active.mode === "append" && requested.mode === "read")
        return false;
    return active.mode === "write" || requested.mode === "write";
}
function createWorkspaceConflict(runId, kind, requested, blockers, summary) {
    return {
        conflict_id: createId("workspace_conflict"),
        run_id: runId,
        kind,
        status: "open",
        path: requested.path,
        actors: [requested.owner, ...blockers.map((lease) => lease.owner)],
        lease_ids: [requested.lease_id, ...blockers.map((lease) => lease.lease_id)],
        expected_base_hash: requested.expected_base_hash,
        actual_base_hash: requested.actual_base_hash,
        summary: sanitizeText(summary),
        created_at: nowIso(),
    };
}
function pathsOverlap(left, right) {
    const a = normalizeWorkspacePath(left);
    const b = normalizeWorkspacePath(right);
    if (!a || !b)
        return false;
    return a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`);
}
function normalizeWorkspacePath(value) {
    const normalized = path.posix.normalize(sanitizeText(value).replaceAll("\\", "/").replace(/\/+/g, "/"));
    return normalized === "." ? "" : normalized.replace(/\/$/, "");
}
function normalizeLease(lease) {
    return {
        ...lease,
        path: normalizeWorkspacePath(lease.path),
        status: lease.status ?? "active",
    };
}
function normalizeConflict(conflict) {
    return {
        ...conflict,
        path: normalizeWorkspacePath(conflict.path),
        status: conflict.status ?? "open",
        actors: conflict.actors ?? [],
        lease_ids: conflict.lease_ids ?? [],
        resolution_evidence_refs: conflict.resolution_evidence_refs ?? [],
    };
}
function normalizeWorktreeMerge(merge) {
    return {
        ...merge,
        merge_ref: merge.merge_ref ?? `workspace/merges.json#${merge.merge_id}`,
        status: merge.status ?? "applied",
        worktree_path: normalizeWorkspacePath(merge.worktree_path),
        target_paths: (merge.target_paths ?? []).map(normalizeWorkspacePath).filter(Boolean),
        lease_ids: merge.lease_ids ?? [],
        rejected_paths: (merge.rejected_paths ?? []).map(normalizeWorkspacePath).filter(Boolean),
        evidence_refs: merge.evidence_refs ?? [],
    };
}
function unique(values) {
    return [...new Set(values)];
}
//# sourceMappingURL=store.js.map