/**
 * Pool Storage — JSON file persistence for Task Pool
 *
 * Handles reading and writing the task pool state to disk using
 * atomic writes (temp-file + fsync + rename) and operation locks
 * to prevent corruption on crash or concurrent access.
 *
 * @module services/task-pool/pool-storage
 */
import * as path from 'path';
import { ensureDir, safeReadJson, atomicWriteJson, } from '../../utils/file-io.utils.js';
import { LoggerService } from '../core/logger.service.js';
import { getCrewlyHomePath } from '../core/crewly-home.utils.js';
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/** Default directory name under ~/.crewly/ for pool data. */
const POOL_DIR_NAME = 'task-pool';
/** File name for the pool data. */
const POOL_FILE_NAME = 'pool.json';
/** Debounce interval in ms for write batching. */
const WRITE_DEBOUNCE_MS = 500;
// ---------------------------------------------------------------------------
// PoolStorage
// ---------------------------------------------------------------------------
/**
 * Persists task pool state to a JSON file with atomic writes.
 *
 * Uses the same patterns as the rest of the Crewly backend:
 * - {@link atomicWriteJson} for crash-safe writes
 * - {@link modifyJsonFile} for locked read-modify-write cycles
 * - Debounced flushing to reduce disk I/O under burst writes
 */
export class PoolStorage {
    filePath;
    logger;
    /** In-memory cache — authoritative after first load. */
    cache = null;
    /** Debounce timer handle for batching writes. */
    flushTimer = null;
    /** Whether a flush is pending. */
    dirty = false;
    constructor(options) {
        const dataDir = options?.dataDir ??
            path.join(getCrewlyHomePath(), POOL_DIR_NAME);
        this.filePath = path.join(dataDir, POOL_FILE_NAME);
        this.logger = LoggerService.getInstance().createComponentLogger('PoolStorage');
    }
    // -----------------------------------------------------------------------
    // Public API
    // -----------------------------------------------------------------------
    /**
     * Loads pool data from disk (or returns cached copy).
     *
     * **F-CYCLE7-3 (2026-05-07):** Normalizes legacy / partial on-disk
     * shapes — if a previous write produced a `pool.json` missing the
     * `workItems` or `claims` arrays (root cause of the post-restart
     * `Cannot read properties of undefined (reading 'filter')` storm in
     * `ReconcilerDataProvider`), we coerce the missing slots to `[]`
     * before caching so every downstream consumer can rely on the
     * `PoolData` invariant.
     *
     * @returns Current pool data with `workItems` and `claims` guaranteed to be arrays
     */
    async load() {
        if (this.cache)
            return this.cache;
        await ensureDir(path.dirname(this.filePath));
        const raw = await safeReadJson(this.filePath, createEmptyPoolData(), this.logger);
        // Defensive normalization. `safeReadJson` round-trips whatever
        // shape is on disk, so a partial / pre-migration file can produce
        // `data.workItems === undefined`. Repair to canonical shape once,
        // here, instead of every read site.
        const normalized = {
            workItems: Array.isArray(raw?.workItems) ? raw.workItems : [],
            claims: Array.isArray(raw?.claims) ? raw.claims : [],
            lastUpdatedAt: raw?.lastUpdatedAt ?? new Date(0).toISOString(),
        };
        this.cache = normalized;
        return normalized;
    }
    /**
     * Returns all work items currently in the pool.
     *
     * Always returns an array — `load()` normalizes a missing
     * `workItems` field to `[]` (F-CYCLE7-3).
     *
     * @returns Array of WorkItems
     */
    async getWorkItems() {
        const data = await this.load();
        return data.workItems ?? [];
    }
    /**
     * Returns all claims.
     *
     * Always returns an array — `load()` normalizes a missing `claims`
     * field to `[]` (F-CYCLE7-3).
     *
     * @returns Array of TaskClaims
     */
    async getClaims() {
        const data = await this.load();
        return data.claims ?? [];
    }
    /**
     * Finds a work item by ID.
     *
     * @param workItemId - The work item ID
     * @returns The work item, or undefined
     */
    async findWorkItem(workItemId) {
        const data = await this.load();
        return data.workItems.find((wi) => wi.id === workItemId);
    }
    /**
     * Finds a claim by work item ID (active claims only).
     *
     * @param workItemId - The work item ID
     * @returns The active claim, or undefined
     */
    async findActiveClaimByWorkItem(workItemId) {
        const data = await this.load();
        return data.claims.find((c) => c.workItemId === workItemId && c.status === 'active');
    }
    /**
     * Finds a claim by agent ID (active claims only).
     *
     * @param agentId - The agent session name
     * @returns The active claim, or undefined
     */
    async findActiveClaimByAgent(agentId) {
        const data = await this.load();
        return data.claims.find((c) => c.agentId === agentId && c.status === 'active');
    }
    /**
     * Adds a work item to the pool.
     *
     * @param workItem - The work item to add
     */
    async addWorkItem(workItem) {
        const data = await this.load();
        data.workItems.push(workItem);
        this.markDirty();
    }
    /**
     * Updates a work item in-place.
     *
     * @param workItemId - ID of the work item to update
     * @param updater - Function that mutates the work item
     * @returns True if item was found and updated
     */
    async updateWorkItem(workItemId, updater) {
        const data = await this.load();
        const item = data.workItems.find((wi) => wi.id === workItemId);
        if (!item)
            return false;
        updater(item);
        this.markDirty();
        return true;
    }
    /**
     * Removes a work item from the pool.
     *
     * @param workItemId - ID of the work item to remove
     * @returns True if item was found and removed
     */
    async removeWorkItem(workItemId) {
        const data = await this.load();
        const idx = data.workItems.findIndex((wi) => wi.id === workItemId);
        if (idx === -1)
            return false;
        data.workItems.splice(idx, 1);
        this.markDirty();
        return true;
    }
    /**
     * Adds a claim to storage.
     *
     * @param claim - The claim to add
     */
    async addClaim(claim) {
        const data = await this.load();
        data.claims.push(claim);
        this.markDirty();
    }
    /**
     * Updates a claim in-place.
     *
     * @param claimId - ID of the claim to update
     * @param updater - Function that mutates the claim
     * @returns True if claim was found and updated
     */
    async updateClaim(claimId, updater) {
        const data = await this.load();
        const claim = data.claims.find((c) => c.id === claimId);
        if (!claim)
            return false;
        updater(claim);
        this.markDirty();
        return true;
    }
    /**
     * Forces an immediate flush to disk (bypasses debounce).
     * Call this during graceful shutdown.
     */
    async flush() {
        if (this.flushTimer) {
            clearTimeout(this.flushTimer);
            this.flushTimer = null;
        }
        if (!this.dirty || !this.cache)
            return;
        this.cache.lastUpdatedAt = new Date().toISOString();
        await atomicWriteJson(this.filePath, this.cache);
        this.dirty = false;
        this.logger.debug?.('Pool data flushed to disk', { path: this.filePath });
    }
    /**
     * Clears all pool data (useful for testing).
     */
    async clear() {
        this.cache = createEmptyPoolData();
        this.dirty = true;
        await this.flush();
    }
    /**
     * Cleanup: cancels pending debounce timer and flushes.
     */
    async destroy() {
        await this.flush();
    }
    // -----------------------------------------------------------------------
    // Internal
    // -----------------------------------------------------------------------
    /**
     * Marks cache as dirty and schedules a debounced flush.
     */
    markDirty() {
        this.dirty = true;
        if (this.flushTimer)
            return; // already scheduled
        this.flushTimer = setTimeout(() => {
            this.flushTimer = null;
            this.flush().catch((err) => {
                this.logger.warn('Failed to flush pool data', { error: String(err) });
            });
        }, WRITE_DEBOUNCE_MS);
    }
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
 * Creates an empty pool data structure.
 *
 * @returns Empty PoolData
 */
export function createEmptyPoolData() {
    return {
        workItems: [],
        claims: [],
        lastUpdatedAt: new Date().toISOString(),
    };
}
//# sourceMappingURL=pool-storage.js.map