<?php
/**
 * vac_scheduler_lib.php
 * =====================================================================
 * SINGLE SOURCE OF TRUTH for VAC scheduling logic — the VAC equivalent
 * of cnc_scheduler_lib.php / pu_scheduler_lib.php.
 *
 * ARCHITECTURE NOTE: VAC has no dual-table concept (like PU) and no
 * composite machine-id encoding (like PU) — vac_machine_table.machine_id
 * is a plain physical machine id throughout. VAC's distinguishing
 * feature is SETTING-INSTRUCTION-BASED SETUP HOURS: a task needs 2 hours
 * of setup if its setting_instruction differs from the previous task in
 * the priority queue, 1 hour if it matches, and 0 hours if production has
 * already started on it (evidenced by rows in vac_part_count_log) or if
 * it was already scheduled to have started before today.
 *
 * BUG FIXES APPLIED HERE:
 *
 *   1. recalculateMachineDatesforvac() existed 3 times (batch script,
 *      admin ajax, tablet ajax). The TABLET copy had already been fixed
 *      once (its own comment says so: "FIX: check production-started
 *      against manuf_id, not id") to check
 *      vac_part_count_log.vac_machine_table_id (which stores manuf_id)
 *      against the task's manuf_id. The other two copies were NEVER
 *      given that fix — they checked against the task's real row id
 *      instead, which almost never matches. Consequence: every time the
 *      daily batch job or an admin-side action (drag, split, priority
 *      edit) triggered a reschedule, "has production started" was
 *      evaluated as false for nearly every task, silently re-charging
 *      1-2 hours of setup time to jobs that were already running —
 *      while the tablet's own reschedule calls correctly charged zero.
 *      Depending on which of the three paths last touched a task, its
 *      total_hours (and therefore its end date, its UPH, everything
 *      downstream) would flip between two different values. Fixed by
 *      having exactly one copy of this function, using the tablet's
 *      already-correct manuf_id-based check.
 *
 *   2. The batch script's copy had one additional rule the other two
 *      didn't ("if this task's own start_date has already passed, no
 *      setup needed — it must already be running"). This is a
 *      reasonable extra safety net (a task scheduled to have started
 *      already probably has, even if a part-count row hasn't landed
 *      yet), so it's kept in this single canonical version rather than
 *      dropped — but now it applies consistently regardless of which
 *      endpoint triggers the recalculation, instead of only when the
 *      batch job happens to run.
 *
 *   3. select_order_for_vac (in ajax_actions_for_vac.php) had its own,
 *      4th, inline copy of the same shift-walking primitive used to
 *      schedule a single newly-inserted task. Consolidated into
 *      vac_scheduleNewTaskInline() here.
 *
 *   4. Planned/target UPH was computed as quantity / total_hours
 *      everywhere it was shown (dashboard, get_live_uph_vac) — the same
 *      problem already fixed in the PU module: total_hours drifts with
 *      setup-hour reassignment, breakdown-triggered splits, and manual
 *      splits, none of which reflect how fast the route itself actually
 *      runs. vac_calculateBasicUph() computes a stable, route-derived
 *      number instead (operation_time / parts_per_operation from
 *      route_details / set_route_details), independent of quantity.
 *
 *   5. TWO DIFFERENT "actual UPH" ALGORITHMS were disagreeing with each
 *      other. dashboard_vac.php's calculateAndStoreActualUPH() (moved
 *      here as vac_calculateAndStoreActualUPH()) is a CORRECT,
 *      session-based calculation: first 'start' to now/last 'finish',
 *      minus every pause/break/breakdown gap taken from the real
 *      activity log. It ran once at page load. But get_live_uph_vac's
 *      30-second poll used a completely different, BROKEN calculation:
 *      `$liveUph = round($okUnits / ($windowMinutes / 60), 2)` — since
 *      window_minutes is always sent as 60, that denominator is a
 *      hard-coded constant of exactly 1.0, not real elapsed time. The
 *      poll was displaying the raw count-in-last-hour mislabeled as a
 *      rate, and OVERWRITING the correct page-load value with it every
 *      30 seconds. The two numbers would visibly disagree and flip back
 *      and forth. Fixed by making vac_calculateAndStoreActualUPH() the
 *      one algorithm, called both at page render AND on every poll —
 *      there is no second implementation left to disagree with it.
 *
 *   6. Breakdown status: the admin Gantt page's start/stop-breakdown
 *      endpoints wrote only to the shared machine_breakdown table; the
 *      shop-floor dashboard's breakdown check reads only
 *      vac_machine_breakdown_log. Same two-systems-that-never-talk bug
 *      already found and fixed in CNC and PU. vac_isMachineInBreakdown()
 *      is now the one check, and the admin-side endpoints mirror into
 *      vac_machine_breakdown_log so the tablet sees it immediately.
 * =====================================================================
 */

if (defined('VAC_SCHEDULER_LIB_LOADED')) {
    return;
}
define('VAC_SCHEDULER_LIB_LOADED', true);

/* =====================================================================
 * SHIFT / CALENDAR PRIMITIVES
 * ===================================================================== */

function vac_getShiftsByDay($conn) {
    $shiftsByDay = [];
    $res = mysqli_query($conn, "SELECT * FROM working_hours");
    while ($row = mysqli_fetch_assoc($res)) {
        $shiftsByDay[(int)$row['day_shifts']][] = $row;
    }
    return $shiftsByDay;
}

/**
 * Walk shifts from $startFrom (or "today 00:00" if not given) allocating
 * $hoursToSchedule hours of work. Used both by the per-machine recalc
 * loop below and by select_order_for_vac's single-task insert (which
 * previously had its own, 4th, copy of this exact logic).
 */
function vac_scheduleNewTaskInline($conn, $hoursToSchedule, $shiftsByDay, $startFrom = null) {
    $currentDateTime = $startFrom !== null
        ? strtotime($startFrom)
        : strtotime(date('Y-m-d') . ' 00:00:00');

    $dateHoursMap   = [];
    $taskStartTime  = null;
    $taskEndTime    = null;
    $firstShiftUsed = null;
    $hoursRemaining = $hoursToSchedule;

    if ($hoursRemaining <= 0) {
        return [
            'start_date'      => date('Y-m-d H:i:s', $currentDateTime),
            'end_date'        => date('Y-m-d H:i:s', $currentDateTime),
            'shift_ids'       => null,
            'remaining_hours' => '',
        ];
    }

    while ($hoursRemaining > 0) {
        $currentDate = date('Y-m-d', $currentDateTime);
        $dayOfWeek   = (int) date('w', $currentDateTime);

        if (!isset($shiftsByDay[$dayOfWeek])) {
            $currentDateTime = strtotime($currentDate . ' +1 day');
            continue;
        }

        foreach ($shiftsByDay[$dayOfWeek] as $shift) {
            $shiftStart = strtotime($currentDate . ' ' . $shift['start_time']);
            $shiftEnd   = strtotime($currentDate . ' ' . $shift['end_time']);
            if ($shiftEnd <= $shiftStart) $shiftEnd = strtotime('+1 day', $shiftEnd);
            $shiftName  = $shift['shift_name'];

            if ($shiftEnd <= $currentDateTime) continue;

            $actualStart = max($currentDateTime, $shiftStart);
            $availableHours = max(0, $shiftEnd - $actualStart) / 3600;
            if ($availableHours <= 0) continue;

            $allocatable = min($hoursRemaining, $availableHours);

            if (!$taskStartTime) {
                $taskStartTime  = $actualStart;
                $firstShiftUsed = $shiftName;
            }

            $taskEndTime = $actualStart + ($allocatable * 3600);
            $hoursRemaining -= $allocatable;

            $dateOnly = date('Y-m-d', $actualStart);
            $dateHoursMap[$dateOnly] = ($dateHoursMap[$dateOnly] ?? 0) + $allocatable;

            $currentDateTime = $taskEndTime;
            if ($hoursRemaining <= 0) break;
        }

        if ($hoursRemaining > 0) {
            $currentDateTime = strtotime(date('Y-m-d', $currentDateTime) . ' +1 day 00:00:00');
        }
    }

    $remainingHoursStr = '';
    foreach ($dateHoursMap as $date => $hrs) {
        $remainingHoursStr .= $date . '=' . round($hrs, 2) . ',';
    }
    $remainingHoursStr = rtrim($remainingHoursStr, ',');

    return [
        'start_date'      => date('Y-m-d H:i:s', $taskStartTime),
        'end_date'        => date('Y-m-d H:i:s', $taskEndTime),
        'shift_ids'       => $firstShiftUsed,
        'remaining_hours' => $remainingHoursStr,
    ];
}

/* =====================================================================
 * PER-MACHINE RESCHEDULE — the single canonical version
 * ===================================================================== */

/**
 * FIX (#1, #2): the one place recalculateMachineDatesforvac() is now
 * defined. Uses manuf_id (not the real cnc-table-style row id) to check
 * whether production has started, and applies the "already scheduled to
 * have started before today ⇒ no setup" safety net consistently.
 */
function vac_recalculateMachineDates($conn, $machineId) {
    set_time_limit(300);

    $machineId = intval($machineId);
    $shiftsByDay = vac_getShiftsByDay($conn);
    $currentDateStr = date('Y-m-d');

    // FIX (#1): started-task lookup keyed by manuf_id, matching how
    // vac_part_count_log.vac_machine_table_id is actually written
    // everywhere else in this module.
    $startedTasks = [];
    $logResult = mysqli_query($conn, "SELECT DISTINCT vac_machine_table_id FROM vac_part_count_log");
    while ($row = mysqli_fetch_assoc($logResult)) {
        $startedTasks[$row['vac_machine_table_id']] = true;
    }

    $taskResult = mysqli_query($conn, "
        SELECT
            v.id,
            v.manuf_id,
            v.work_order_id,
            v.part_id,
            v.is_break,
            v.is_breakdown_affected,
            v.breakdown_hours,
            v.start_date,
            v.total_hours_update AS hours_to_schedule,
            v.total_hours AS hours,
            v.setting_instruction
        FROM vac_machine_table v
        WHERE v.machine_id = $machineId
          AND (v.finished = 0 OR v.is_breakdown_affected = 1)
        ORDER BY v.priority
    ");

    $tasks = [];
    while ($row = mysqli_fetch_assoc($taskResult)) $tasks[] = $row;

    $currentDateTime = strtotime(date('Y-m-d') . ' 00:00:00');
    $previous_setting_instruction = null;

    foreach ($tasks as $task) {

        $manufId = $task['manuf_id'];
        $productionStarted = isset($startedTasks[$manufId]);

        $total_hours          = (float) $task['hours_to_schedule'];
        $setting_instruction  = $task['setting_instruction'];
        $start_date           = $task['start_date'];
        $is_break              = (int) $task['is_break'];
        $breakdown_hours       = (float) $task['breakdown_hours'];
        $setup_hours           = 0.00;

        if ($is_break === 1) {
            $total_hours = (float) $task['hours'];
            $setup_hours = 0.00;
        } else {
            if ($productionStarted) {
                $setup_hours = 0.00;
            } else {
                if ($previous_setting_instruction !== null) {
                    $setup_hours = (strcasecmp($previous_setting_instruction, $setting_instruction) === 0) ? 1.00 : 2.00;
                } else {
                    $setup_hours = 2.00;
                }

                // FIX (#2): the extra safety net previously only present
                // in the batch script's copy, now applied consistently
                // everywhere — a task already scheduled to have started
                // before today is treated as already running.
                if ($start_date && date('Y-m-d', strtotime($start_date)) < $currentDateStr) {
                    $setup_hours = 0.00;
                }
            }
            $total_hours = (float) ($total_hours + $setup_hours + $breakdown_hours);
        }

        $sched = vac_scheduleNewTaskInline($conn, $total_hours, $shiftsByDay, date('Y-m-d H:i:s', $currentDateTime));
        $startStr = $sched['start_date'];
        $endStr   = $sched['end_date'];
        $shiftId  = mysqli_real_escape_string($conn, (string) $sched['shift_ids']);
        $remStr   = mysqli_real_escape_string($conn, $sched['remaining_hours']);

        mysqli_query($conn, "
            UPDATE vac_machine_table SET
                start_date    = '$startStr',
                end_date      = '$endStr',
                shift_ids     = '$shiftId',
                remaing_hours = '$remStr',
                setup_hours   = $setup_hours,
                total_hours   = $total_hours
            WHERE id = {$task['id']}
        ");

        $currentDateTime = strtotime($endStr);
        $previous_setting_instruction = $setting_instruction;
    }
}

function vac_fullReschedule($conn, $machineId) {
    vac_recalculateMachineDates($conn, $machineId);
}

/* =====================================================================
 * BASIC UPH — route-derived, quantity-independent (same principle as
 * pu_calculateBasicUph() in the PU module).
 * ===================================================================== */

function vac_calculateBasicUph($conn, $partId, $setId = 0) {
    $partId = intval($partId);
    $setId  = intval($setId);

    if ($setId > 0) {
        $query = "SELECT operation_time, parts_per_operation FROM set_route_details WHERE set_id = $setId LIMIT 1";
    } else {
        $query = "SELECT operation_time, parts_per_operation FROM route_details WHERE item_id = $partId LIMIT 1";
    }
    $result = mysqli_query($conn, $query);
    if (!$result || mysqli_num_rows($result) == 0) return 0.0;

    $row = mysqli_fetch_assoc($result);

    if (!empty($row['operation_time'])) {
        [$h, $m, $s] = explode(':', $row['operation_time']);
        $operationTimeSeconds = ($h * 3600) + ($m * 60) + $s;
    } else {
        $operationTimeSeconds = 0;
    }

    $partsPerOperation = (!empty($row['parts_per_operation']) && $row['parts_per_operation'] > 0)
        ? (float) $row['parts_per_operation'] : 1.0;

    if ($operationTimeSeconds <= 0) return 0.0;

    $cycleSecondsPerPart = $operationTimeSeconds / $partsPerOperation;
    if ($cycleSecondsPerPart <= 0) return 0.0;

    return round(3600 / $cycleSecondsPerPart, 2);
}

/* =====================================================================
 * ACTUAL (LIVE) UPH — the ONE correct algorithm, used everywhere
 * ---------------------------------------------------------------------
 * FIX (#5): this is dashboard_vac.php's original calculateAndStoreActualUPH(),
 * moved here unchanged in its logic — it was already correct. The bug
 * was that get_live_uph_vac had a SECOND, different, broken
 * implementation. Now there is only this one, called from both places,
 * so they can never disagree with each other again.
 * ===================================================================== */

function vac_calculateAndStoreActualUPH($conn, $workOrderId, $partId, $machineId, $vacMachineTableId) {
    $workOrderId       = intval($workOrderId);
    $partId            = intval($partId);
    $vacMachineTableId = intval($vacMachineTableId);

    $countSql = "
        SELECT SUM(quantity) AS total
        FROM vac_part_count_log
        WHERE work_order_id = $workOrderId
          AND vac_machine_table_id = $vacMachineTableId
          AND part_id = $partId
          AND type = 'ok'
    ";
    $countResult = mysqli_query($conn, $countSql);
    $countRow    = mysqli_fetch_assoc($countResult);
    $totalMade   = floatval($countRow['total'] ?? 0);

    $activitySql = "SELECT
                    MIN(CASE WHEN action_type = 'start' THEN created_at END) as first_start,
                    MAX(CASE WHEN action_type = 'finish' THEN created_at END) as last_finish
                    FROM vac_machine_activity_log
                    WHERE work_order_id = $workOrderId
                    AND vac_machine_table_id = $vacMachineTableId
                    AND part_id = $partId";
    $activityRes = mysqli_query($conn, $activitySql);
    $actualUph   = 0;

    if ($activityRes && mysqli_num_rows($activityRes) > 0) {
        $activityRow = mysqli_fetch_assoc($activityRes);
        $firstStart  = $activityRow['first_start'];
        $lastFinish  = $activityRow['last_finish'];

        if ($firstStart) {
            $startTime    = strtotime($firstStart);
            $totalSeconds = $lastFinish ? (strtotime($lastFinish) - $startTime) : (time() - $startTime);

            $nonProductiveSql = "
                SELECT action_type, created_at
                FROM vac_machine_activity_log
                WHERE work_order_id = $workOrderId
                AND vac_machine_table_id = $vacMachineTableId
                AND part_id = $partId
                AND action_type IN ('start', 'pause', 'break', 'breakdown')
                ORDER BY created_at ASC
            ";
            $nonProdRes           = mysqli_query($conn, $nonProductiveSql);
            $nonProductiveSeconds = 0;
            $lastPauseStart       = null;

            if ($nonProdRes && mysqli_num_rows($nonProdRes) > 0) {
                while ($row = mysqli_fetch_assoc($nonProdRes)) {
                    $actionType = $row['action_type'];
                    $timestamp  = strtotime($row['created_at']);
                    if (in_array($actionType, ['pause', 'break', 'breakdown'])) {
                        $lastPauseStart = $timestamp;
                    } else if ($actionType === 'start' && $lastPauseStart !== null) {
                        $nonProductiveSeconds += $timestamp - $lastPauseStart;
                        $lastPauseStart = null;
                    }
                }
                if ($lastPauseStart !== null) {
                    $endPoint = $lastFinish ? strtotime($lastFinish) : time();
                    $nonProductiveSeconds += $endPoint - $lastPauseStart;
                }
            }

            $productiveSeconds = max(0, $totalSeconds - $nonProductiveSeconds);
            $totalHours        = $productiveSeconds / 3600;
            $actualUph         = ($totalHours > 0) ? round($totalMade / $totalHours, 2) : 0;
        }
    }

    $checkSql    = "SELECT id FROM vac_part_count_log
                    WHERE work_order_id = $workOrderId
                    AND vac_machine_table_id = $vacMachineTableId
                    AND part_id = $partId
                    LIMIT 1";
    $checkResult = mysqli_query($conn, $checkSql);
    if ($checkResult && mysqli_num_rows($checkResult) > 0) {
        $updateUphSql = "UPDATE vac_part_count_log
                         SET uph = $actualUph
                         WHERE work_order_id = $workOrderId
                         AND vac_machine_table_id = $vacMachineTableId
                         AND part_id = $partId";
        mysqli_query($conn, $updateUphSql);
    }

    return $actualUph;
}

/* =====================================================================
 * BREAKDOWN STATUS — single source of truth
 * ===================================================================== */

function vac_isMachineInBreakdown($conn, $machineId) {
    $machineId = intval($machineId);
    $res = mysqli_query($conn, "
        SELECT status FROM vac_machine_breakdown_log
        WHERE machine_id = $machineId
        ORDER BY created_at DESC, id DESC
        LIMIT 1
    ");
    if ($res && mysqli_num_rows($res) > 0) {
        return mysqli_fetch_assoc($res)['status'] === 'active';
    }
    return false;
}

function vac_markBreakdownAffectedWorkOrders($conn, $machineId, $breakdownStart) {
    $machineId = intval($machineId);
    $startEsc  = mysqli_real_escape_string($conn, $breakdownStart);

    $res = mysqli_query($conn, "
        SELECT id FROM vac_machine_table
        WHERE machine_id = $machineId
          AND is_break = 0
          AND start_date < '$startEsc'
          AND end_date   > '$startEsc'
    ");
    $count = 0;
    while ($row = mysqli_fetch_assoc($res)) {
        mysqli_query($conn, "UPDATE vac_machine_table SET is_breakdown_affected = 1 WHERE id = {$row['id']}");
        $count++;
    }
    return $count;
}
?>