<?php
/**
 * pu_scheduler_lib.php
 * =====================================================================
 * SINGLE SOURCE OF TRUTH for PU scheduling logic — the PU equivalent of
 * cnc_scheduler_lib.php, for the same reason: recalculateMachineDatesforpu()
 * existed three times (batch script, admin ajax, tablet ajax) with two
 * different, subtly wrong filters, and the "virtual machine ID" encoding
 * (physical_machine_id * 10 + table_id) was reimplemented independently
 * in five places, two different ways.
 *
 * KEY ARCHITECTURAL DIFFERENCE FROM CNC:
 * PU has no dual-table concept. pu_machine_table.machine_id is itself a
 * COMPOSITE id (physical_machine_id * 10 + table_id) — each table of a
 * physical PU station is scheduled as if it were an independent machine.
 * v_machine_id holds the real physical machine id separately, used only
 * for grouping/lookup purposes (dashboard queries, breakdown status).
 * There is therefore no overlap-resolution engine here at all: unlike
 * CNC's table1/table2, PU's "table 1" and "table 2" never interact in
 * scheduling — they ARE independent machines from the scheduler's POV.
 *
 * BUG FIXES APPLIED HERE (see chat writeup for full detail):
 *   1. recalculateMachineDatesforpu() now filters WHERE finished = 0,
 *      consistently, in the one place it's now defined. Two of the three
 *      old copies filtered WHERE end_date >= today instead, which had it
 *      backwards in two ways: a task whose stored end_date had already
 *      slipped into the past got PERMANENTLY excluded from every future
 *      recalculation (since the filter used the very value the function
 *      exists to fix), while a FINISHED task whose end_date still looked
 *      current stayed in the walk, consuming real capacity for a job
 *      that's done.
 *   2. pu_encodeVirtualMachineId() / pu_decodeVirtualMachineId() are now
 *      the one place the physical_id*10+table_id arithmetic lives. One
 *      call site (insert_schedule_break_pu, schedule_break_all_tables_pu)
 *      built the composite via STRING CONCATENATION (intval($machineId
 *      . $tableId)) instead of the arithmetic every other site used —
 *      these only produce the same result while table_id stays a single
 *      digit, and silently diverge the moment a physical machine gets a
 *      10th table.
 *   3. pu_isMachineInBreakdown() is now the one place breakdown status is
 *      checked, backed by pu_machine_breakdown_log — the table the shop
 *      floor tablet already used. The admin Gantt page's start/stop
 *      breakdown endpoints now mirror into this table too (previously
 *      they only wrote to the shared machine_breakdown table, which the
 *      tablet dashboard never read — the same two-systems-that-never-
 *      talk bug found and fixed in the CNC module).
 * =====================================================================
 */

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

/* =====================================================================
 * VIRTUAL MACHINE ID ENCODING
 * ---------------------------------------------------------------------
 * pu_machine_table.machine_id = physical_machine_id * 10 + table_id.
 * KNOWN LIMITATION (pre-existing, inherited, not introduced here): this
 * scheme assumes table_id is always a single digit (1-9). A physical
 * machine with a 10th table would collide with a different physical
 * machine's table 0. Fixing this properly means widening the encoding
 * (e.g. physical_id * 100 + table_id, or a real composite key / separate
 * scheduling-unit table) — a schema-level decision, flagged here rather
 * than silently changed, since every existing row's machine_id would
 * need a migration to match a new encoding.
 * ===================================================================== */

function pu_encodeVirtualMachineId($physicalMachineId, $tableId) {
    return (intval($physicalMachineId) * 10) + intval($tableId);
}

function pu_decodeVirtualMachineId($compositeMachineId) {
    $compositeMachineId = intval($compositeMachineId);
    return [
        'machine_id' => intval($compositeMachineId / 10),
        'table_id'   => $compositeMachineId % 10,
    ];
}

/* =====================================================================
 * SHIFT / CALENDAR PRIMITIVES (identical approach to cnc_scheduler_lib.php)
 * ===================================================================== */

function pu_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.
 */
function pu_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,
    ];
}

/**
 * Re-walks every unfinished row for a (composite) PU machine, in priority
 * order, computing start_date/end_date/shift_ids/remaing_hours from
 * total_hours.
 *
 * FIX (#1): filters WHERE finished = 0, matching the batch script's
 * always-correct copy — not WHERE end_date >= today, which the two ajax
 * copies used and which had it backwards (see file header).
 */
function pu_recalculateMachineDates($conn, $compositeMachineId) {
    set_time_limit(300);

    $compositeMachineId = intval($compositeMachineId);
    $shiftsByDay = pu_getShiftsByDay($conn);

    $taskResult = mysqli_query($conn, "
        SELECT id, total_hours AS hours_to_schedule
        FROM pu_machine_table
        WHERE machine_id = $compositeMachineId
          AND finished   = 0
        ORDER BY priority
    ");

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

    $cursor = null;
    foreach ($tasks as $task) {
        $hours = floatval($task['hours_to_schedule']);
        $sched = pu_scheduleNewTaskInline($conn, $hours, $shiftsByDay, $cursor);

        $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 pu_machine_table SET
                start_date    = '$startStr',
                end_date      = '$endStr',
                shift_ids     = '$shiftId',
                remaing_hours = '$remStr'
            WHERE id = {$task['id']}
        ");

        $cursor = $endStr;
    }
}

/**
 * Thin wrapper kept for readability at call sites — PU has no overlap
 * engine or dual-table resync step to bundle in, so this is currently
 * just pu_recalculateMachineDates(), but centralising the call means any
 * future cross-cutting step (the way cnc_fullRescheduleMachine grew one)
 * has exactly one place to be added.
 */
function pu_fullReschedule($conn, $compositeMachineId) {
    pu_recalculateMachineDates($conn, $compositeMachineId);
}

/* =====================================================================
 * BREAKDOWN STATUS — SINGLE SOURCE OF TRUTH
 * ---------------------------------------------------------------------
 * Same fix as CNC: the admin Gantt page and the shop-floor tablet used
 * two different tables to answer "is this machine in breakdown" and
 * never told each other. pu_machine_breakdown_log (physical machine id)
 * is now the single source of truth; admin-side start/stop mirrors into
 * it so the tablet's dashboard sees it immediately, and vice versa.
 * ===================================================================== */

function pu_isMachineInBreakdown($conn, $physicalMachineId) {
    $physicalMachineId = intval($physicalMachineId);
    $res = mysqli_query($conn, "
        SELECT status FROM pu_machine_breakdown_log
        WHERE machine_id = $physicalMachineId
        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;
}

/**
 * Marks pu_machine_table rows overlapping a breakdown window as
 * is_breakdown_affected = 1, scoped by physical machine (mirrors the
 * equivalent CNC helper; same "no per-breakdown-event scoping on the
 * task row" limitation noted there applies here too).
 */
function pu_markBreakdownAffectedWorkOrders($conn, $physicalMachineId, $breakdownStart) {
    $physicalMachineId = intval($physicalMachineId);
    $startEsc = mysqli_real_escape_string($conn, $breakdownStart);

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

/**
 * All composite (virtual) machine_ids currently in use for a physical
 * machine — i.e. one per table that has ever had a task scheduled on it.
 * Used when a breakdown or a cross-table admin action needs to reschedule
 * every table of a physical machine, not just the one table_id it was
 * told about.
 */
function pu_getActiveVirtualMachineIds($conn, $physicalMachineId) {
    $physicalMachineId = intval($physicalMachineId);
    $res = mysqli_query($conn, "
        SELECT DISTINCT machine_id FROM pu_machine_table
        WHERE v_machine_id = $physicalMachineId AND finished = 0
    ");
    $ids = [];
    while ($row = mysqli_fetch_assoc($res)) $ids[] = intval($row['machine_id']);
    return $ids;
}

/**
 * Reschedules every table of a physical machine (all of its composite
 * machine_ids), used by breakdown handling where the caller only knows
 * the physical machine id.
 */
function pu_fullReschedulePhysicalMachine($conn, $physicalMachineId) {
    foreach (pu_getActiveVirtualMachineIds($conn, $physicalMachineId) as $vmid) {
        pu_fullReschedule($conn, $vmid);
    }
}

/* =====================================================================
 * BASIC UPH — the single source of truth for "planned / target UPH"
 * ---------------------------------------------------------------------
 * Deliberately computed ONLY from route_details / set_route_details
 * (operation_time, parts_per_operation) — never from quantity or
 * total_hours. Those drift: total_hours gets mutated by setup_hours,
 * breakdown adjustments, and splits, none of which change how fast the
 * route itself actually runs. quantity / total_hours therefore isn't a
 * stable "planned rate" — it's a number that moves every time a work
 * order gets split or delayed, even though nothing about the route
 * changed. This is the fix: one function, called everywhere a
 * planned/target/original UPH is needed, that always answers "how many
 * parts per hour does this route run at" and nothing else.
 * ===================================================================== */

function pu_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;

    // Cycle time per single part, in seconds — the same quantity-
    // independent unit the pasted route_time calculation is built from
    // (operation_time / parts_per_operation), just not then multiplied
    // by a quantity. UPH is simply how many of those cycles fit in an
    // hour.
    $cycleSecondsPerPart = $operationTimeSeconds / $partsPerOperation;
    if ($cycleSecondsPerPart <= 0) return 0.0;

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



function pu_splitTask($conn, $taskId, $keepQty, $splitQty, $keepOverrides = [], $splitOverrides = []) {
    $taskId = intval($taskId);

    $res = mysqli_query($conn, "SELECT * FROM pu_machine_table WHERE id = $taskId LIMIT 1");
    if (!$res || mysqli_num_rows($res) == 0) return false;
    $task = mysqli_fetch_assoc($res);

    $currentQty = floatval($task['quantity']);
    $setupHours = floatval($task['setup_hours']);
    $totalHours = floatval($task['total_hours']);
    $manufId    = isset($task['manuf_id']) ? intval($task['manuf_id']) : 0;

    if ($keepQty < 0 || $splitQty <= 0 || ($keepQty + $splitQty) > $currentQty + 0.0001) {
        return false;
    }

    $hoursPerUnit = $currentQty > 0 ? max(0, ($totalHours - $setupHours)) / $currentQty : 0;
    $keepHours  = ($keepQty  * $hoursPerUnit) + $setupHours;
    $splitHours = ($splitQty * $hoursPerUnit) + $setupHours;

    $newManufId = null;
    if ($manufId > 0) {
        $manufRes = mysqli_query($conn, "SELECT * FROM manufacture_machine_table WHERE id = $manufId LIMIT 1");
        if ($manufRes && mysqli_num_rows($manufRes) > 0) {
            $origManuf = mysqli_fetch_assoc($manufRes);
            $manufQty  = floatval($origManuf['quantity']);
            $manufHrs  = floatval($origManuf['total_hours']);
            $manufHoursPerUnit = $manufQty > 0 ? ($manufHrs / $manufQty) : 0;

            $manufKeepHours  = $keepQty  * $manufHoursPerUnit;
            $manufSplitHours = $splitQty * $manufHoursPerUnit;

            mysqli_query($conn, "
                UPDATE manufacture_machine_table
                SET quantity = $keepQty, total_hours = $manufKeepHours
                WHERE id = $manufId
            ");

            $cols = []; $vals = [];
            foreach ($origManuf as $col => $val) {
                if ($col === 'id') continue;
                $cols[] = "`$col`";
                if ($col === 'quantity')      $vals[] = $splitQty;
                elseif ($col === 'total_hours') $vals[] = $manufSplitHours;
                elseif (is_null($val))        $vals[] = 'NULL';
                else $vals[] = "'" . mysqli_real_escape_string($conn, $val) . "'";
            }
            mysqli_query($conn, "INSERT INTO manufacture_machine_table (" . implode(',', $cols) . ") VALUES (" . implode(',', $vals) . ")");
            $newManufId = mysqli_insert_id($conn);
        }
    }

    $keepFinished = isset($keepOverrides['finished']) ? intval($keepOverrides['finished']) : intval($task['finished']);
    mysqli_query($conn, "
        UPDATE pu_machine_table SET
            quantity            = $keepQty,
            total_hours         = $keepHours,
            total_hours_update  = " . ($keepHours - $setupHours) . ",
            finished             = $keepFinished
        WHERE id = $taskId
    ");

    $machineId = intval($task['machine_id']);
    $priority  = intval($task['priority']);
    mysqli_query($conn, "
        UPDATE pu_machine_table SET priority = priority + 1
        WHERE machine_id = $machineId AND priority > $priority AND id != $taskId
    ");

    $manufIdForInsert = $newManufId !== null ? intval($newManufId) : ($manufId > 0 ? $manufId : 'NULL');
    $splitFinished = isset($splitOverrides['finished']) ? intval($splitOverrides['finished']) : 0;

    mysqli_query($conn, "
        INSERT INTO pu_machine_table (
            machine_id, manuf_id, v_machine_id, table_id, work_order_id, part_id,
            quantity, product_name, total_hours, total_hours_update, setup_hours,
            priority, set_id, cycle, shift_ids, start_date, end_date, is_break, finished
        ) VALUES (
            $machineId, $manufIdForInsert, {$task['v_machine_id']}, {$task['table_id']},
            {$task['work_order_id']}, {$task['part_id']},
            $splitQty, '" . mysqli_real_escape_string($conn, $task['product_name']) . "',
            $splitHours, " . ($splitHours - $setupHours) . ", $setupHours,
            " . ($priority + 1) . ", " . intval($task['set_id']) . ", " . intval($task['cycle']) . ",
            '" . mysqli_real_escape_string($conn, $task['shift_ids']) . "',
            '{$task['start_date']}', '{$task['end_date']}', 0, $splitFinished
        )
    ");
    $newTaskId = mysqli_insert_id($conn);

    return [
        'original_hours' => $keepHours,
        'split_hours'    => $splitHours,
        'new_manuf_id'   => $newManufId,
        'new_task_id'    => $newTaskId,
    ];
}
?>