// File: src/scheduling/cpm.js
// CPM Algorithm — Forward/Backward Pass, Float, Critical Path
// Exclusive-end convention: EF = ES + duration (working days)
// Supports: FS, SS, FF, SF with lag

function toDays(duration, unit) {
    switch (unit) {
        case 'hour': return duration / 8;
        case 'week': return duration * 5;
        default:     return duration;
    }
}

function topoSort(taskIds, bySucc) {
    const inDeg   = {};
    const succsOf = {};
    taskIds.forEach(id => { inDeg[id] = 0; succsOf[id] = []; });

    for (const [succId, deps] of Object.entries(bySucc)) {
        const sid = Number(succId);
        if (!(sid in inDeg)) continue;
        for (const dep of deps) {
            const pid = dep.predecessorId ?? dep.predecessor_id;
            if (!(pid in inDeg)) continue;
            inDeg[sid]++;
            succsOf[pid].push(sid);
        }
    }

    const queue = taskIds.filter(id => inDeg[id] === 0);
    const order = [];

    while (queue.length) {
        const id = queue.shift();
        order.push(id);
        for (const sid of [...new Set(succsOf[id])]) {
            if (--inDeg[sid] === 0) queue.push(sid);
        }
    }

    // Cycle fallback
    const inOrder = new Set(order);
    for (const id of taskIds) {
        if (!inOrder.has(id)) order.push(id);
    }
    return order;
}

/**
 * Run CPM on tasks + dependencies.
 *
 * @param {Array}  tasks        [{id, duration, duration_unit?}]
 * @param {Array}  dependencies [{predecessorId|predecessor_id, successorId|successor_id, type, lag}]
 * @returns {Object}            keyed by taskId: { ES, EF, LS, LF, TF, FF, isCritical }
 */
export function computeCpm(tasks, dependencies) {
    if (!tasks?.length) return {};

    const taskIds = tasks.map(t => t.id);
    const dur = {};
    tasks.forEach(t => {
        dur[t.id] = toDays(t.duration ?? 0, t.duration_unit ?? t.durationUnit ?? 'day');
    });

    // Index deps
    const bySucc = {};
    const byPred = {};
    taskIds.forEach(id => { bySucc[id] = []; byPred[id] = []; });

    for (const dep of (dependencies ?? [])) {
        const pid = dep.predecessorId ?? dep.predecessor_id;
        const sid = dep.successorId   ?? dep.successor_id;
        if (!taskIds.includes(pid) || !taskIds.includes(sid)) continue;
        bySucc[sid].push(dep);
        byPred[pid].push(dep);
    }

    const order = topoSort(taskIds, bySucc);

    // ── Forward pass ──────────────────────────────────────────
    const ES = {}, EF = {};
    for (const id of order) {
        const d = dur[id];
        let es = 0;
        for (const dep of bySucc[id]) {
            const pid = dep.predecessorId ?? dep.predecessor_id;
            const lag = dep.lag ?? 0;
            switch (dep.type ?? 'FS') {
                case 'FS': es = Math.max(es, (EF[pid] ?? 0) + lag);         break;
                case 'SS': es = Math.max(es, (ES[pid] ?? 0) + lag);         break;
                case 'FF': es = Math.max(es, (EF[pid] ?? 0) + lag - d);    break;
                case 'SF': es = Math.max(es, (ES[pid] ?? 0) + lag - d);    break;
            }
        }
        ES[id] = Math.max(0, es);
        EF[id] = ES[id] + d;
    }

    const projEnd = taskIds.length ? Math.max(...Object.values(EF)) : 0;

    // ── Backward pass ─────────────────────────────────────────
    const LS = {}, LF = {};
    for (const id of [...order].reverse()) {
        const d = dur[id];
        let lf = projEnd;
        for (const dep of byPred[id]) {
            const sid = dep.successorId ?? dep.successor_id;
            const lag = dep.lag ?? 0;
            switch (dep.type ?? 'FS') {
                case 'FS': lf = Math.min(lf, (LS[sid] ?? projEnd) - lag);         break;
                case 'SS': lf = Math.min(lf, (LS[sid] ?? projEnd) - lag + d);    break;
                case 'FF': lf = Math.min(lf, (LF[sid] ?? projEnd) - lag);         break;
                case 'SF': lf = Math.min(lf, (LF[sid] ?? projEnd) - lag + d);    break;
            }
        }
        LF[id] = lf;
        LS[id] = lf - d;
    }

    // ── Float & critical path ─────────────────────────────────
    const results = {};
    for (const id of taskIds) {
        const tf = Math.round((LS[id] - ES[id]) * 1000) / 1000;

        // Free float from FS successors
        let ff = tf;
        let hasFSSucc = false;
        for (const dep of byPred[id]) {
            if ((dep.type ?? 'FS') !== 'FS') continue;
            const sid  = dep.successorId ?? dep.successor_id;
            const lag  = dep.lag ?? 0;
            const cand = (ES[sid] ?? projEnd) - lag - EF[id];
            if (!hasFSSucc || cand < ff) { ff = cand; hasFSSucc = true; }
        }
        ff = Math.max(0, Math.round(ff * 1000) / 1000);

        results[id] = {
            ES: ES[id],
            EF: EF[id],
            LS: LS[id],
            LF: LF[id],
            TF: Math.max(0, tf),
            FF: ff,
            isCritical: tf <= 0.001,
        };
    }
    return results;
}
