<?php
declare(strict_types=1);

// ─── Stage action definitions ──────────────────────────────
function stageActionDefinitions(): array
{
    return [
        'maintenance_review' => [
            'label' => 'Approve Complaint',
            'description' => 'Maintenance office reviews the complaint and records an approval comment.',
            'stage_to' => STAGE_MAINTENANCE_REVIEW,
            'status_to' => STATUS_APPROVED,
            'comment_field' => 'maintenance_comment',
            'requires_comment' => true,
            'comment_label' => 'Maintenance Office Comment',
            'button_tone' => 'primary'
        ],
        'reject_complaint' => [
            'label' => 'Reject Complaint',
            'description' => 'Maintenance office rejects the complaint and closes it without a work order.',
            'stage_to' => STAGE_CANCELLED,
            'status_to' => STATUS_REJECTED,
            'comment_field' => 'maintenance_comment',
            'requires_comment' => true,
            'comment_label' => 'Maintenance Office Comment',
            'button_tone' => 'danger'
        ],
        'raise_work_order' => [
            'label' => 'Raise Work Order',
            'description' => 'Create the work order and route the vehicle to the selected workshop.',
            'stage_to' => STAGE_WORK_ORDER_RAISED,
            'status_to' => 'Job Order Created',
            'comment_field' => 'maintenance_comment',
            'append_comment' => true,
            'comment_label' => 'Maintenance Office Comment',
            'requires_line_items' => true,
            'button_tone' => 'primary'
        ],
        'send_rfq' => [
            'label' => 'Send RFQ',
            'description' => 'Send request for quote to the selected workshop and track RFQ status.',
            'stage_to' => STAGE_RFQ_SENT,
            'status_to' => STATUS_IN_PROGRESS,
            'requires_workshop' => true,
            'workshop_label' => 'RFQ Workshop',
            'button_tone' => 'warning'
        ],
        'submit_quote' => [
            'label' => 'Send Quote',
            'description' => 'Record the workshop quote after RFQ is sent.',
            'stage_to' => STAGE_QUOTE_SUBMITTED,
            'status_to' => STATUS_IN_PROGRESS,
            'amount_field' => 'quote_amount',
            'requires_amount' => true,
            'requires_workshop' => true,
            'workshop_label' => 'Quoted Workshop',
            'allow_hod_override' => true,
            'button_tone' => 'warning'
        ],
        'vet_quote' => [
            'label' => 'Vet Selected Quote',
            'description' => 'Review comparable quotes, confirm the selected workshop, and record the vetted amount.',
            'stage_to' => STAGE_QUOTE_VETTED,
            'status_to' => STATUS_IN_PROGRESS,
            'amount_field' => 'vetted_amount',
            'amount_label' => 'Vetted Amount',
            'amount_help' => 'Use the selected quote amount, or adjust it after technical and price review.',
            'comment_label' => 'Vetting Note',
            'comment_help' => 'Record any clarification, correction, or HOD override context.',
            'requires_amount' => true,
            'requires_final_workshop' => true,
            'allow_hod_override' => true,
            'button_tone' => 'warning'
        ],
        'raise_job_order' => [
            'label' => 'Approve Final Amount',
            'description' => 'Compare vendor quotes and approve the final amount for the selected workshop.',
            'stage_to' => STAGE_JOB_ORDER_APPROVED,
            'status_to' => STATUS_APPROVED,
            'amount_field' => 'approved_amount',
            'amount_label' => 'Approved Amount',
            'amount_help' => 'Use the vetted/selected quote amount, or adjust the final approved amount.',
            'comment_label' => 'Approval Note',
            'comment_help' => 'Record why this quote and amount were approved.',
            'requires_amount' => true,
            'requires_final_workshop' => true,
            'button_tone' => 'success'
        ],
        'approve_job_order' => [
            'label' => 'Approve Final Amount',
            'description' => 'Compare vendor quotes and approve the final amount for the selected workshop job order.',
            'stage_to' => STAGE_JOB_ORDER_APPROVED,
            'status_to' => STATUS_APPROVED,
            'amount_field' => 'approved_amount',
            'amount_label' => 'Approved Amount',
            'amount_help' => 'Use the vetted/selected quote amount, or adjust the final approved amount.',
            'comment_label' => 'Approval Note',
            'comment_help' => 'Record why this quote and amount were approved.',
            'requires_amount' => true,
            'requires_final_workshop' => true,
            'button_tone' => 'success'
        ],
        'return_to_workshop' => [
            'label' => 'Return to Workshop',
            'description' => 'Reopen the job and send the vehicle back to the workshop for rework.',
            'stage_to' => STAGE_WORKSHOP_IN_PROGRESS,
            'status_to' => STATUS_IN_PROGRESS,
            'date_field' => 'workshop_received_date',
            'button_tone' => 'warning',
            'comment_field' => 'maintenance_comment',
            'comment_label' => 'Rework Note',
            'comment_help' => 'Describe what needs rework.'
        ],
        'start_workshop' => [
            'label' => 'Send Job Order',
            'description' => 'Send the approved job order to the workshop and mark the vehicle as received.',
            'stage_to' => STAGE_WORKSHOP_IN_PROGRESS,
            'status_to' => STATUS_IN_PROGRESS,
            'date_field' => 'workshop_received_date',
            'button_tone' => 'primary'
        ],
        'complete_work' => [
            'label' => 'Mark Work Done',
            'description' => 'Workshop confirms the repair is completed and ready for inspection.',
            'stage_to' => STAGE_INSPECTION,
            'status_to' => STATUS_COMPLETED,
            'date_field' => 'work_completed_date',
            'button_tone' => 'info'
        ],
        'confirm_work' => [
            'label' => 'Confirm Work Done',
            'description' => 'Inspect the vehicle and confirm the workshop completed the repair satisfactorily.',
            'stage_to' => STAGE_INTERNAL_CONTROL,
            'status_to' => STATUS_COMPLETED,
            'date_field' => 'inspection_date',
            'comment_field' => 'inspection_comment',
            'extra' => ['inspection_status' => 'Confirmed'],
            'requires_comment' => true,
            'button_tone' => 'success'
        ],
        'reject_work' => [
            'label' => 'Reject Work Done',
            'description' => 'Inspection failed and the vehicle is returned to workshop for rework.',
            'stage_to' => STAGE_WORKSHOP_IN_PROGRESS,
            'status_to' => STATUS_IN_PROGRESS,
            'date_field' => 'inspection_date',
            'comment_field' => 'inspection_comment',
            'extra' => ['inspection_status' => 'Rejected'],
            'requires_comment' => true,
            'button_tone' => 'danger'
        ],
        'validate_internal_control' => [
            'label' => 'Validate Internal Control',
            'description' => 'Internal control validates the completed maintenance before closure.',
            'stage_to' => STAGE_CLOSED,
            'status_to' => STATUS_CLOSED,
            'comment_field' => 'internal_control_comment',
            'extra' => ['internal_control_status' => 'Validated'],
            'requires_comment' => true,
            'button_tone' => 'success'
        ],
        'close_job' => [
            'label' => 'Close Job Order',
            'description' => 'Close the job manually after all workflow checks are complete.',
            'stage_to' => STAGE_CLOSED,
            'status_to' => STATUS_CLOSED,
            'comment_field' => 'internal_control_comment',
            'extra' => ['internal_control_status' => 'Validated'],
            'button_tone' => 'success'
        ],
        'raise_requisition' => [
            'label' => 'Raise Requisition',
            'description' => 'Generate the workshop requisition for the approved maintenance cost.',
            'stage_to' => STAGE_REQUISITION_RAISED,
            'status_to' => STAGE_REQUISITION_RAISED,
            'extra' => ['requisition_status' => 'Raised'],
            'button_tone' => 'primary'
        ],
        'approve_requisition' => [
            'label' => 'Approve Requisition',
            'description' => 'Approve the workshop requisition for payment processing.',
            'stage_to' => STAGE_REQUISITION_APPROVED,
            'status_to' => STAGE_REQUISITION_APPROVED,
            'extra' => ['requisition_status' => 'Approved'],
            'button_tone' => 'success'
        ],
        'reject_requisition' => [
            'label' => 'Reject Requisition',
            'description' => 'Reject the workshop requisition.',
            'stage_to' => STAGE_REQUISITION_REJECTED,
            'status_to' => STAGE_REQUISITION_REJECTED,
            'comment_field' => 'maintenance_comment',
            'requires_comment' => true,
            'comment_label' => 'Rejection Reason',
            'extra' => ['requisition_status' => 'Rejected'],
            'button_tone' => 'danger'
        ],
        'mark_paid' => [
            'label' => 'Make Payment',
            'description' => 'Record that payment was made for this requisition.',
            'stage_to' => STAGE_PAYMENT_MADE,
            'status_to' => STAGE_PAYMENT_MADE,
            'extra' => ['requisition_status' => 'Paid'],
            'button_tone' => 'dark'
        ],
        'mark_not_paid' => [
            'label' => 'Payment Not Made',
            'description' => 'Record that payment was not made for this requisition.',
            'stage_to' => STAGE_PAYMENT_NOT_MADE,
            'status_to' => 'Not Paid',
            'extra' => ['requisition_status' => 'Not Paid'],
            'requires_comment' => true,
            'comment_label' => 'Reason',
            'button_tone' => 'danger'
        ],
        'rate_job_order' => [
            'label' => 'Rate Job Order',
            'description' => 'Capture a post-completion assessment for the workshop and the quality of repair.',
            'stage_to' => STAGE_PAYMENT_MADE,
            'status_to' => STAGE_PAYMENT_MADE,
            'comment_field' => 'rating_comment',
            'requires_comment' => true,
            'button_tone' => 'secondary'
        ],
        'suspend_job' => [
            'label' => 'Suspend Job Order',
            'description' => 'Temporarily pause the job while awaiting a decision or dependency.',
            'stage_to' => STAGE_SUSPENDED,
            'status_to' => STAGE_SUSPENDED,
            'requires_comment' => true,
            'button_tone' => 'secondary'
        ],
        'cancel_job' => [
            'label' => 'Cancel Job Order',
            'description' => 'Cancel the maintenance job completely.',
            'stage_to' => STAGE_CANCELLED,
            'status_to' => STAGE_CANCELLED,
            'requires_comment' => true,
            'button_tone' => 'danger'
        ]
    ];
}

// ─── Stage action dispatcher ───────────────────────────────
function handleStageAction(PDO $pdo): void
{
    $request = validateStageActionRequest($pdo);
    $job = $request['job'];
    $definition = $request['definition'];
    $actionCode = $request['action_code'];
    $comment = $request['comment'];
    $amount = $request['amount'];
    $lineItems = $request['line_items'];
    $lineItemsTotal = $request['line_items_total'];
    $selectedWorkshopId = $request['workshop_id'];
    $selectedWorkshopName = $request['workshop_name'];

    $result = resolveStageActionUpdate($pdo, $job, $definition, $actionCode, $comment, $amount, $lineItems, $lineItemsTotal, $selectedWorkshopId, $selectedWorkshopName);
    $update = $result['update'] ?? [];
    $lineItems = $result['line_items'] ?? $lineItems;
    $lineItemsTotal = $result['line_items_total'] ?? $lineItemsTotal;

    // For send_rfq, if the job is already at the target stage, skip the redundant
    // action log, email, and audit — the RFQ record was already created above.
    if (strtolower($actionCode) === 'send_rfq' && ($job['workflow_stage'] ?? '') === ($definition['stage_to'] ?? '')) {
        respond(['status' => 'success']);
    }

    applyStageActionUpdate($pdo, $job, $update, $definition, $actionCode, $comment, $amount, $lineItems);

    // If this action created/sent RFQs, attempt to send the invite email to the workshop using DB email
    if (strtolower($actionCode) === 'send_rfq' && (string)($_POST['skip_invite_email'] ?? '') !== '1') {
        try {
            $jobId = (int)($job['id'] ?? 0);
            sendRfqInviteToWorkshop($pdo, $jobId, $selectedWorkshopId);
        } catch (Throwable $e) {
            error_log('RFQ invite send error: ' . $e->getMessage());
        }
    }
    respond(['status' => 'success']);
}

function handleTimeline(PDO $pdo): void
{
    $jobId = (int)($_GET['job_id'] ?? 0);
    if ($jobId <= 0) {
        respond(['status' => 'error', 'message' => 'Invalid maintenance job ID: ' . $_GET['job_id']]);
    }

    try {
        $stmt = $pdo->prepare("
            SELECT
                mja.*,
                u.name AS acted_by_name
            FROM maintenance_job_actions mja
            LEFT JOIN users u ON u.id = mja.acted_by
            WHERE mja.maintenance_job_id = ?
              AND mja.deleted_at IS NULL
            ORDER BY mja.acted_at DESC, mja.id DESC
        ");
        $stmt->execute([$jobId]);
        $actions = $stmt->fetchAll(PDO::FETCH_ASSOC);

        respond(['status' => 'success', 'data' => $actions]);
    } catch (Exception $e) {
        respond(['status' => 'error', 'message' => 'Database error: ' . $e->getMessage()]);
    }
}

function advanceMaintenanceRfqStage(PDO $pdo, array $job, string $targetStage, string $targetStatus, string $actionCode, string $comment): void
{
    $jobId = (int)($job['id'] ?? 0);
    $currentStage = (string)($job['workflow_stage'] ?? '');
    if ($jobId <= 0 || $currentStage === '' || $currentStage === $targetStage) {
        return;
    }

    $currentIndex = array_search($currentStage, MAINTENANCE_STAGE_ORDER, true);
    $targetIndex = array_search($targetStage, MAINTENANCE_STAGE_ORDER, true);
    if ($currentIndex === false || $targetIndex === false || $currentIndex > $targetIndex) {
        return;
    }

    validateStageTransition($pdo, $currentStage, $targetStage);

    $stmt = $pdo->prepare("
        UPDATE maintenance_jobs
        SET workflow_stage = :workflow_stage,
            status = :status
        WHERE id = :id
          AND deleted_at IS NULL
    ");
    $stmt->execute([
        'workflow_stage' => $targetStage,
        'status' => $targetStatus,
        'id' => $jobId
    ]);

    logMaintenanceAction(
        $pdo,
        $jobId,
        $actionCode,
        $targetStage,
        $currentStage,
        $targetStage,
        null,
        $comment
    );
    logJobStageHistory($pdo, $jobId, $currentStage, $targetStage, $actionCode, $comment);

    audit(
        $pdo,
        (int)($_SESSION['user_id'] ?? 0),
        'update',
        'maintenance_job',
        $jobId,
        'Maintenance RFQ stage advanced',
        $job,
        ['workflow_stage' => $targetStage, 'status' => $targetStatus]
    );
    sendMaintenanceStageEmailNotifications(
        $pdo,
        $jobId,
        $job,
        ['workflow_stage' => $targetStage, 'status' => $targetStatus],
        ['label' => $targetStage, 'stage_to' => $targetStage, 'status_to' => $targetStatus],
        $actionCode,
        $comment,
        null
    );
}

// ─── Request validation ────────────────────────────────────
function validateStageActionRequest(PDO $pdo): array
{
    enforceMaintenanceApprovalPermission();

    $jobId = (int)($_POST['job_id'] ?? 0);
    $actionCode = trim((string)($_POST['action_code'] ?? ''));
    $comment = normalizeNullableString($_POST['comment'] ?? null);
    $amount = normalizeNullableDecimal($_POST['amount'] ?? null);
    $selectedWorkshopId = normalizeNullableInt($_POST['workshop_id'] ?? null);

    if ($jobId <= 0 || $actionCode === '') {
        respond(['status' => 'error', 'message' => 'Job and action are required']);
    }

    $job = fetchMaintenanceJob($pdo, $jobId);
    if (!$job) {
        respond(['status' => 'error', 'message' => 'Maintenance job not found']);
    }
    enforceStageRolePermission($pdo, (string)($job['workflow_stage'] ?? ''));

    $definition = stageActionDefinitions()[$actionCode] ?? null;
    if (!$definition) {
        respond(['status' => 'error', 'message' => 'Invalid stage action']);
    }
    if (!empty($definition['requires_comment']) && $comment === null) {
        respond(['status' => 'error', 'message' => ($definition['comment_label'] ?? 'Comment') . ' is required']);
    }

    $lineItems = [];
    $lineItemsTotal = null;
    if (!empty($definition['requires_line_items'])) {
        ensureMaintenanceLineItemsTable($pdo);
        $lineItems = parseMaintenanceLineItems($_POST['line_items'] ?? '');
        if (!$lineItems) {
            respond(['status' => 'error', 'message' => 'At least one work line item is required']);
        }
        $lineItemsTotal = calculateMaintenanceLineItemsTotal($lineItems);
        $amount = number_format($lineItemsTotal, 2, '.', '');
    }
    if (!empty($definition['requires_amount']) && empty($definition['requires_line_items']) && ($amount === null || (float)$amount <= 0)) {
        respond(['status' => 'error', 'message' => ($definition['amount_label'] ?? 'Amount') . ' must be greater than 0']);
    }

    $lineItemActions = ['send_rfq', 'submit_quote', 'vet_quote', 'raise_job_order', 'approve_job_order'];
    if (in_array($actionCode, $lineItemActions, true)) {
        enforceMaintenanceJobHasLineItems($pdo, $jobId, 'Work order line items are required before RFQ, quote capture, vetting, or final approval.');
    }
    if (in_array($actionCode, ['vet_quote', 'raise_job_order', 'approve_job_order'], true)) {
        enforceSelectedFinalQuoteHasLineItems($pdo, $jobId, 'The selected final workshop quote must have line items before vetting or approval.');
    }

    // Require requisition to be approved before payment actions
    if (in_array($actionCode, ['mark_paid', 'mark_not_paid'], true)) {
        $reqStatus = isset($job['requisition_status']) ? trim((string)$job['requisition_status']) : '';
        if (strcasecmp($reqStatus, 'Approved') !== 0) {
            respond(['status' => 'error', 'message' => 'Requisition must be Approved before recording payment status.']);
        }
    }

    // Ensure a work order is created before sending the job order to the workshop
    if ($actionCode === 'start_workshop') {
        $rfqStats = getRfqStats($pdo, $jobId);
        if (empty($rfqStats['vendors']) || (int)$rfqStats['vendors'] === 0) {
            respond(['status' => 'error', 'message' => 'Raise RFQ (invite vendors) before sending the job order to the workshop.']);
        }

        $finalWorkshopName = fetchSelectedFinalWorkshopName($pdo, $jobId);
        if ($finalWorkshopName === null || $finalWorkshopName === '') {
            respond(['status' => 'error', 'message' => 'Select a final workshop before sending the job order to the workshop.']);
        }

        if (empty($job['work_order_number']) && $job['job_order_date'] === null) {
            respond(['status' => 'error', 'message' => 'Create work order before sending the job order to the workshop.']);
        }
    }

    validateStageTransition($pdo, (string)($job['workflow_stage'] ?? ''), (string)$definition['stage_to']);
    enforceSeparationOfDuties($pdo, $jobId, $actionCode);
    enforceRfqStagePolicy($pdo, $job, $actionCode, $definition);

    $selectedWorkshopName = null;
    if (!empty($definition['requires_workshop'])) {
        if ($selectedWorkshopId === null) {
            respond(['status' => 'error', 'message' => 'Workshop is required for this action']);
        }
        $selectedWorkshopName = fetchWorkshopNameById($pdo, $selectedWorkshopId);
        if ($selectedWorkshopName === null) {
            respond(['status' => 'error', 'message' => 'Selected workshop is invalid or inactive']);
        }
    }

    return [
        'job' => $job,
        'job_id' => $jobId,
        'definition' => $definition,
        'action_code' => $actionCode,
        'comment' => $comment,
        'amount' => $amount,
        'line_items' => $lineItems,
        'line_items_total' => $lineItemsTotal,
        'workshop_id' => $selectedWorkshopId,
        'workshop_name' => $selectedWorkshopName,
    ];
}

// ─── Update resolution and application ─────────────────────
function resolveStageActionUpdate(PDO $pdo, array $job, array $definition, string $actionCode, ?string $comment, ?string $amount, array $lineItems, ?float $lineItemsTotal, ?int $selectedWorkshopId, ?string $selectedWorkshopName): array
{
    $update = [
        'workflow_stage' => $definition['stage_to'],
        'status' => $definition['status_to']
    ];

    if (!empty($definition['requires_workshop']) && !empty($definition['updates_job_workshop'])) {
        $update['workshop'] = $selectedWorkshopName;
    }
    if (!empty($definition['requires_final_workshop'])) {
        $finalWorkshopName = fetchSelectedFinalWorkshopName($pdo, (int)$job['id']);
        if ($finalWorkshopName === null) {
            respond(['status' => 'error', 'message' => 'Select a final workshop before raising the job order']);
        }
        $update['workshop'] = $finalWorkshopName;
    }

    if (!empty($definition['date_field'])) {
        $update[$definition['date_field']] = date('Y-m-d');
    }
    if (!empty($definition['comment_field'])) {
        $commentField = (string)$definition['comment_field'];
        if (!empty($definition['append_comment'])) {
            if ($comment !== null) {
                $update[$commentField] = appendMaintenanceComment(
                    (string)($job[$commentField] ?? ''),
                    $comment,
                    (string)($definition['label'] ?? 'Maintenance Action')
                );
            }
        } else {
            $update[$commentField] = $comment;
        }
    }
    if (!empty($definition['amount_field'])) {
        $update[$definition['amount_field']] = $amount;
    }
    if (!empty($definition['extra'])) {
        foreach ($definition['extra'] as $field => $value) {
            $update[$field] = $value;
        }
    }

    $jobId = (int)$job['id'];

    if ($actionCode === 'raise_work_order') {
        if (empty($job['work_order_number'])) {
            $update['work_order_number'] = generateWorkOrderNumber($pdo, $jobId);
        }
        if ($job['job_order_date'] === null) {
            $update['job_order_date'] = date('Y-m-d');
        }
        // If no explicit line items were provided, attempt to populate from the selected final workshop quote
        if (empty($lineItems) || $lineItemsTotal === null) {
            $quoteId = fetchSelectedFinalQuoteId($pdo, $jobId);
            if ($quoteId !== null) {
                try {
                    $stmtQ = $pdo->prepare("SELECT quote_amount FROM workshop_quotes WHERE id = ? AND deleted_at IS NULL LIMIT 1");
                    $stmtQ->execute([$quoteId]);
                    $quoteAmount = $stmtQ->fetchColumn();

                    $stmtItems = $pdo->prepare(
                        "SELECT item_description AS description, quoted_cost AS cost FROM workshop_quote_line_items WHERE quote_id = ? AND deleted_at IS NULL ORDER BY sort_order ASC, id ASC"
                    );
                    $stmtItems->execute([$quoteId]);
                    $fetchedItems = $stmtItems->fetchAll(PDO::FETCH_ASSOC);

                    $prepared = [];
                    $sum = 0.0;
                    foreach ($fetchedItems as $it) {
                        $costVal = number_format((float)($it['cost'] ?? 0), 2, '.', '');
                        $prepared[] = ['description' => (string)($it['description'] ?? ''), 'cost' => $costVal];
                        $sum += (float)$costVal;
                    }

                    if ($prepared) {
                        $lineItems = $prepared;
                        $lineItemsTotal = number_format($sum, 2, '.', '');
                    }
                    if ($quoteAmount !== null && $quoteAmount !== false && $quoteAmount !== '') {
                        $update['cost_of_repair'] = $quoteAmount;
                    } elseif ($lineItemsTotal !== null) {
                        $update['cost_of_repair'] = $lineItemsTotal;
                    }
                } catch (Throwable) {
                    // ignore quote fetch errors; fallback to provided values
                }
            } else {
                if ($lineItemsTotal !== null) {
                    $update['cost_of_repair'] = $lineItemsTotal;
                }
            }
        } else {
            if ($lineItemsTotal !== null) {
                $update['cost_of_repair'] = $lineItemsTotal;
            }
        }
    }
    if ($actionCode === 'send_rfq') {
        createOrUpdateRfqForJob($pdo, $jobId, $job, $selectedWorkshopId);
        $update['is_rfq'] = 1;
    }
    if ($actionCode === 'submit_quote' && $selectedWorkshopId !== null) {
        createOrUpdateRfqForJob($pdo, $jobId, $job, $selectedWorkshopId, 'quoted');
    }
    if (in_array($actionCode, ['raise_job_order', 'approve_job_order'], true) && $job['job_order_date'] === null) {
        $update['job_order_date'] = date('Y-m-d');
    }
    if ($actionCode === 'close_job') {
        $update['closure_date'] = date('Y-m-d');
    }
    if ($actionCode === 'complete_work') {
        $update['workshop_days'] = calculateWorkshopDays($job['workshop_received_date'] ?? null, date('Y-m-d'));
    }

    return [
        'update' => $update,
        'line_items' => $lineItems,
        'line_items_total' => $lineItemsTotal
    ];
}

function applyStageActionUpdate(PDO $pdo, array $job, array $update, array $definition, string $actionCode, ?string $comment, ?string $amount, array $lineItems): void
{
    $jobId = (int)$job['id'];

    $setParts = [];
    foreach (array_keys($update) as $column) {
        $setParts[] = $column . ' = :' . $column;
    }
    $update['id'] = $jobId;

    $stmt = $pdo->prepare("
        UPDATE maintenance_jobs
        SET " . implode(', ', $setParts) . "
        WHERE id = :id
          AND deleted_at IS NULL
    ");
    $stmt->execute($update);

    if ($lineItems) {
        saveMaintenanceLineItems($pdo, $jobId, $lineItems);
    }

    logMaintenanceAction(
        $pdo,
        $jobId,
        $actionCode,
        $definition['label'],
        $job['workflow_stage'] ?? null,
        $definition['stage_to'],
        $amount,
        $comment
    );
    logJobStageHistory($pdo, $jobId, (string)($job['workflow_stage'] ?? ''), (string)$definition['stage_to'], $actionCode, $comment);

    audit($pdo, (int)$_SESSION['user_id'], 'update', 'maintenance_job', $jobId, 'Maintenance stage action applied', $job, $update);
    sendMaintenanceStageEmailNotifications($pdo, $jobId, $job, $update, $definition, $actionCode, $comment, $amount);
}

// ─── Email notifications ───────────────────────────────────
function sendMaintenanceStageEmailNotifications(PDO $pdo, int $jobId, array $previousJob, array $update, array $definition, string $actionCode, ?string $comment, ?string $amount): void
{
    try {
        $stageTo = (string)($update['workflow_stage'] ?? ($definition['stage_to'] ?? ''));
        if ($stageTo === '') {
            return;
        }

        $roles = maintenanceStageAlertRoles($stageTo);
        if (!$roles) {
            return;
        }

        $context = fetchMaintenanceEmailContext($pdo, $jobId);
        $plate = (string)($context['plate_number'] ?? ('Job #' . $jobId));
        $subject = 'Maintenance stage update: ' . $stageTo . ' - ' . $plate;
        $message = buildMaintenanceStageAlertMessage($context, $previousJob, $update, $definition, $actionCode, $comment, $amount);

        alertUsersByRoles($pdo, $roles, $subject, $message, [
            'event' => 'maintenance.stage_update',
            'type' => 'maintenance',
            'url' => 'maintenance.php',
            'reference_type' => 'maintenance_job',
            'reference_id' => $jobId,
            'channels' => ['in_app', 'email', 'sms']
        ]);

        // Additionally send the job order email directly to the selected workshop when sending the job order
        if (strtolower($actionCode) === 'start_workshop' || strtolower($stageTo) === 'workshop in progress') {
            $workshopName = (string)($previousJob['workshop'] ?? $update['workshop'] ?? '');
            if ($workshopName === '') {
                $workshopName = fetchSelectedFinalWorkshopName($pdo, $jobId) ?? '';
            }
            if ($workshopName !== '') {
                try {
                    $stmt = $pdo->prepare("SELECT email, contact_person FROM workshops WHERE name = :name AND deleted_at IS NULL LIMIT 1");
                    $stmt->execute(['name' => $workshopName]);
                    $ws = $stmt->fetch(PDO::FETCH_ASSOC);
                    $workshopEmail = $ws['email'] ?? '';
                    $contactPerson = $ws['contact_person'] ?? '';
                    if ($workshopEmail && filter_var($workshopEmail, FILTER_VALIDATE_EMAIL)) {
                        $jobSubject = 'Job Order: ' . ($previousJob['work_order_number'] ?? ('Job #' . $jobId)) . ' - ' . $plate;
                        $jobBody = buildMaintenanceStageEmailBody($context, $previousJob, $update, $definition, $actionCode, $comment, $amount);
                        $sent = false;
                        try {
                            $sent = (bool)sendMail($workshopEmail, $jobSubject, $jobBody, ['to_name' => $contactPerson ?? null]);
                        } catch (Throwable $e) {
                            $sent = false;
                        }

                        alertLogDelivery($pdo, [
                            'event_key' => 'maintenance.job_order_sent',
                            'channel' => 'email',
                            'user_id' => 0,
                            'recipient' => (string)$workshopEmail,
                            'subject' => $jobSubject,
                            'message' => strip_tags($jobBody),
                            'status' => $sent ? 'sent' : 'failed',
                            'provider_response' => null,
                            'payload_json' => json_encode(['job_id' => $jobId, 'workshop' => $workshopName]),
                            'reference_type' => 'maintenance_job',
                            'reference_id' => $jobId,
                        ]);
                    }
                } catch (Throwable) {
                    // Ignore workshop email failures — do not block the workflow
                }
            }
        }
    } catch (Throwable) {
        // Alert delivery must never block maintenance workflow progression.
    }
}

function maintenanceStageAlertRoles(string $stageTo): array
{
    $roles = MAINTENANCE_STAGE_ROLE_MATRIX[$stageTo] ?? [];
    $roles[] = 'Admin';
    return array_values(array_unique(array_filter($roles)));
}

function fetchMaintenanceEmailContext(PDO $pdo, int $jobId): array
{
    $stmt = $pdo->prepare("
        SELECT
            mj.*,
            v.plate_number,
            CONCAT_WS(' ', NULLIF(v.vehicle_model, ''), NULLIF(v.vehicle_type, '')) AS vehicle_name,
            COALESCE(d.full_name, CONCAT_WS(' ', d.first_name, d.surname), 'Unassigned') AS driver_name
        FROM maintenance_jobs mj
        INNER JOIN vehicles v ON v.id = mj.vehicle_id
        LEFT JOIN drivers d ON d.id = mj.driver_id
        WHERE mj.id = ?
          AND mj.deleted_at IS NULL
        LIMIT 1
    ");
    $stmt->execute([$jobId]);
    $row = $stmt->fetch(PDO::FETCH_ASSOC);

    return $row ?: ['id' => $jobId];
}

function buildMaintenanceStageEmailBody(array $context, array $previousJob, array $update, array $definition, string $actionCode, ?string $comment, ?string $amount): string
{
    $stageFrom = htmlspecialchars((string)($previousJob['workflow_stage'] ?? '-'), ENT_QUOTES, 'UTF-8');
    $stageTo = htmlspecialchars((string)($update['workflow_stage'] ?? ($definition['stage_to'] ?? '-')), ENT_QUOTES, 'UTF-8');
    $statusTo = htmlspecialchars((string)($update['status'] ?? ($definition['status_to'] ?? '-')), ENT_QUOTES, 'UTF-8');
    $plate = htmlspecialchars((string)($context['plate_number'] ?? '-'), ENT_QUOTES, 'UTF-8');
    $vehicle = htmlspecialchars((string)($context['vehicle_name'] ?? '-'), ENT_QUOTES, 'UTF-8');
    $driver = htmlspecialchars((string)($context['driver_name'] ?? '-'), ENT_QUOTES, 'UTF-8');
    $title = htmlspecialchars((string)($context['complaint_title'] ?? $context['nature_of_fault'] ?? 'Maintenance job'), ENT_QUOTES, 'UTF-8');
    $actionLabel = htmlspecialchars((string)($definition['label'] ?? $actionCode), ENT_QUOTES, 'UTF-8');
    $commentHtml = htmlspecialchars((string)($comment ?? '-'), ENT_QUOTES, 'UTF-8');
    $amountHtml = $amount !== null ? htmlspecialchars(number_format((float)$amount, 2), ENT_QUOTES, 'UTF-8') : '-';

    return "
        <div style=\"font-family: Arial, sans-serif; color: #0f172a;\">
            <h2 style=\"margin: 0 0 12px;\">Maintenance Stage Updated</h2>
            <p style=\"margin: 0 0 16px;\">A maintenance job has moved from <strong>{$stageFrom}</strong> to <strong>{$stageTo}</strong>.</p>
            <table style=\"border-collapse: collapse; width: 100%; max-width: 680px;\">
                <tr><td style=\"padding: 8px; border: 1px solid #e5e7eb; font-weight: bold;\">Action</td><td style=\"padding: 8px; border: 1px solid #e5e7eb;\">{$actionLabel}</td></tr>
                <tr><td style=\"padding: 8px; border: 1px solid #e5e7eb; font-weight: bold;\">Status</td><td style=\"padding: 8px; border: 1px solid #e5e7eb;\">{$statusTo}</td></tr>
                <tr><td style=\"padding: 8px; border: 1px solid #e5e7eb; font-weight: bold;\">Vehicle</td><td style=\"padding: 8px; border: 1px solid #e5e7eb;\">{$plate} - {$vehicle}</td></tr>
                <tr><td style=\"padding: 8px; border: 1px solid #e5e7eb; font-weight: bold;\">Driver</td><td style=\"padding: 8px; border: 1px solid #e5e7eb;\">{$driver}</td></tr>
                <tr><td style=\"padding: 8px; border: 1px solid #e5e7eb; font-weight: bold;\">Issue</td><td style=\"padding: 8px; border: 1px solid #e5e7eb;\">{$title}</td></tr>
                <tr><td style=\"padding: 8px; border: 1px solid #e5e7eb; font-weight: bold;\">Amount</td><td style=\"padding: 8px; border: 1px solid #e5e7eb;\">{$amountHtml}</td></tr>
                <tr><td style=\"padding: 8px; border: 1px solid #e5e7eb; font-weight: bold;\">Comment</td><td style=\"padding: 8px; border: 1px solid #e5e7eb;\">{$commentHtml}</td></tr>
            </table>
            <p style=\"margin-top: 16px; color: #64748b;\">Open the fleet system to review and take the next action.</p>
        </div>
    ";
}

function buildMaintenanceStageAlertMessage(array $context, array $previousJob, array $update, array $definition, string $actionCode, ?string $comment, ?string $amount): string
{
    $stageFrom = (string)($previousJob['workflow_stage'] ?? '-');
    $stageTo = (string)($update['workflow_stage'] ?? ($definition['stage_to'] ?? '-'));
    $statusTo = (string)($update['status'] ?? ($definition['status_to'] ?? '-'));
    $plate = (string)($context['plate_number'] ?? '-');
    $vehicle = (string)($context['vehicle_name'] ?? '-');
    $driver = (string)($context['driver_name'] ?? '-');
    $title = (string)($context['complaint_title'] ?? $context['nature_of_fault'] ?? 'Maintenance job');
    $actionLabel = (string)($definition['label'] ?? $actionCode);
    $amountText = $amount !== null ? number_format((float)$amount, 2) : '-';
    $commentText = $comment ?: '-';

    return "A maintenance job moved from {$stageFrom} to {$stageTo}.\n"
        . "Action: {$actionLabel}\n"
        . "Status: {$statusTo}\n"
        . "Vehicle: {$plate} - {$vehicle}\n"
        . "Driver: {$driver}\n"
        . "Issue: {$title}\n"
        . "Amount: {$amountText}\n"
        . "Comment: {$commentText}";
}

// ─── Logging ───────────────────────────────────────────────
function logMaintenanceAction(
    PDO $pdo,
    int $jobId,
    string $actionCode,
    string $label,
    ?string $stageFrom,
    ?string $stageTo,
    ?string $amount,
    ?string $comment
): void {
    $stmt = $pdo->prepare("
        INSERT INTO maintenance_job_actions (
            maintenance_job_id,
            action_code,
            action_label,
            stage_from,
            stage_to,
            amount,
            comment,
            acted_by,
            acted_at
        ) VALUES (
            :maintenance_job_id,
            :action_code,
            :action_label,
            :stage_from,
            :stage_to,
            :amount,
            :comment,
            :acted_by,
            NOW()
        )
    ");
    $stmt->execute([
        'maintenance_job_id' => $jobId,
        'action_code' => $actionCode,
        'action_label' => $label,
        'stage_from' => $stageFrom,
        'stage_to' => $stageTo,
        'amount' => $amount,
        'comment' => $comment,
        'acted_by' => (int)($_SESSION['user_id'] ?? 0)
    ]);
}

function logJobStageHistory(PDO $pdo, int $jobId, ?string $fromStage, ?string $toStage, string $actionCode, ?string $comment): void
{
    try {
        $stmt = $pdo->prepare("
            INSERT INTO job_stage_history (
                maintenance_job_id,
                stage_from,
                stage_to,
                action_code,
                comment,
                changed_by,
                changed_at
            ) VALUES (
                :maintenance_job_id,
                :stage_from,
                :stage_to,
                :action_code,
                :comment,
                :changed_by,
                NOW()
            )
        ");
        $stmt->execute([
            'maintenance_job_id' => $jobId,
            'stage_from' => $fromStage,
            'stage_to' => $toStage,
            'action_code' => $actionCode,
            'comment' => $comment,
            'changed_by' => (int)($_SESSION['user_id'] ?? 0)
        ]);
    } catch (Throwable) {
        // Backward compatibility: history table may not exist yet.
    }
}

// ─── Permission enforcement ────────────────────────────────
function enforceStageRolePermission(PDO $pdo, string $stage): void
{
    if (isAdmin()) {
        return;
    }

    $currentRole = trim((string)($_SESSION['role_name'] ?? ''));
    if (checkRolePermissionPerStage($pdo, $stage, $currentRole)) {
        return;
    }

    respond([
        'status' => 'error',
        'message' => 'Role "' . ($currentRole !== '' ? $currentRole : 'Unknown') . '" is not allowed to perform actions at stage "' . $stage . '".'
    ]);
}

function checkRolePermissionPerStage(PDO $pdo, string $stage, string $roleName): bool
{
    if ($roleName === '') {
        return false;
    }

    try {
        $stmt = $pdo->prepare("
            SELECT 1
            FROM role_stage_permissions rsp
            INNER JOIN roles r ON r.id = rsp.role_id
            INNER JOIN maintenance_stages ms ON ms.id = rsp.stage_id
            WHERE r.name = :role_name
              AND ms.stage_name = :stage_name
              AND rsp.can_act = 1
            LIMIT 1
        ");
        $stmt->execute([
            'role_name' => $roleName,
            'stage_name' => $stage
        ]);
        $row = $stmt->fetchColumn();
        if ($row !== false) {
            return true;
        }
    } catch (Throwable) {
        // Fallback to matrix
    }

    $matrix = maintenanceStageRoleMatrix();
    $allowedRoles = $matrix[$stage] ?? null;
    if ($allowedRoles === null) {
        return true;
    }

    foreach ($allowedRoles as $role) {
        if (strcasecmp($roleName, $role) === 0) {
            return true;
        }
    }

    return false;
}

function enforceSeparationOfDuties(PDO $pdo, int $jobId, string $actionCode): void
{
    if (isAdmin()) {
        return;
    }

    $currentUserId = (int)($_SESSION['user_id'] ?? 0);
    if ($currentUserId <= 0) {
        return;
    }

    $currentDomain = actionGovernanceDomain($actionCode);
    if ($currentDomain === 'none') {
        return;
    }

    $stmt = $pdo->prepare("
        SELECT DISTINCT action_code
        FROM maintenance_job_actions
        WHERE maintenance_job_id = :job_id
          AND acted_by = :user_id
          AND deleted_at IS NULL
    ");
    $stmt->execute(['job_id' => $jobId, 'user_id' => $currentUserId]);
    $codes = $stmt->fetchAll(PDO::FETCH_COLUMN);

    $seenDomains = [];
    foreach ($codes as $code) {
        $domain = actionGovernanceDomain((string)$code);
        if ($domain !== 'none') {
            $seenDomains[$domain] = true;
        }
    }

    foreach (array_keys($seenDomains) as $domain) {
        if ($domain !== $currentDomain) {
            respond([
                'status' => 'error',
                'message' => 'Separation of duties rule: same user cannot perform both "' . $domain . '" and "' . $currentDomain . '" controls on one job.'
            ]);
        }
    }
}

function actionGovernanceDomain(string $actionCode): string
{
    $request = ['complaint_logged', 'job_updated', 'raise_work_order', 'send_rfq', 'submit_quote', 'start_workshop', 'complete_work', 'reject_work'];
    $approval = ['maintenance_review', 'reject_complaint', 'vet_quote', 'raise_job_order', 'approve_job_order', 'confirm_work', 'suspend_job', 'cancel_job', 'raise_requisition', 'approve_requisition', 'mark_paid', 'mark_not_paid'];
    $audit = ['validate_internal_control', 'close_job'];

    if (in_array($actionCode, $request, true)) {
        return 'request';
    }
    if (in_array($actionCode, $approval, true)) {
        return 'approval';
    }
    if (in_array($actionCode, $audit, true)) {
        return 'audit';
    }
    return 'none';
}

// ─── Stage policy enforcement ──────────────────────────────
function enforceRfqStagePolicy(PDO $pdo, array $job, string $actionCode, array $definition): void
{
    if (!in_array($actionCode, ['submit_quote', 'vet_quote'], true)) {
        return;
    }

    $policy = maintenanceRfqPolicy();
    $stats = getRfqStats($pdo, (int)$job['id']);
    $overrideRequested = ((string)($_POST['hod_override'] ?? '0') === '1');
    $overrideReason = normalizeNullableString($_POST['hod_override_reason'] ?? null);

    if ($actionCode === 'submit_quote' && $stats['vendors'] < $policy['min_vendors']) {
        enforceOverrideOrBlock(
            $pdo,
            $job,
            $definition,
            $stats,
            $overrideRequested,
            $overrideReason,
            'Minimum RFQ vendor count not met',
            'RFQ requires at least ' . $policy['min_vendors'] . ' vendors before moving to Quote Submitted.'
        );
        return;
    }

    if ($actionCode === 'vet_quote' && $policy['require_min_quotes_for_vetting'] && $stats['quotes'] < $policy['min_quotes_for_vetting']) {
        enforceOverrideOrBlock(
            $pdo,
            $job,
            $definition,
            $stats,
            $overrideRequested,
            $overrideReason,
            'Minimum quote count not met for vetting',
            'At least ' . $policy['min_quotes_for_vetting'] . ' quotes are required before vetting.'
        );
    }
}

function enforceOverrideOrBlock(
    PDO $pdo,
    array $job,
    array $definition,
    array $stats,
    bool $overrideRequested,
    ?string $overrideReason,
    string $policyIssue,
    string $defaultMessage
): void {
    if (!$overrideRequested) {
        respond([
            'status' => 'error',
            'message' => $defaultMessage . ' Request HOD override to proceed.',
            'data' => ['rfq_vendors' => $stats['vendors'], 'quotes_received' => $stats['quotes']]
        ]);
    }

    if (!isHodOverrideActor()) {
        respond(['status' => 'error', 'message' => 'Only HOD (or Admin) can apply override for this RFQ rule.']);
    }

    if ($overrideReason === null) {
        respond(['status' => 'error', 'message' => 'HOD override reason is required.']);
    }

    $comment = 'HOD override approved. ' . $policyIssue . '. Reason: ' . $overrideReason .
        ' | Vendors: ' . $stats['vendors'] . ' | Quotes: ' . $stats['quotes'];

    logMaintenanceAction(
        $pdo,
        (int)$job['id'],
        'hod_override',
        'HOD Override',
        (string)($job['workflow_stage'] ?? ''),
        (string)($definition['stage_to'] ?? ''),
        null,
        $comment
    );

    audit(
        $pdo,
        (int)($_SESSION['user_id'] ?? 0),
        'override',
        'maintenance_job',
        (int)$job['id'],
        'HOD override applied for RFQ policy gate',
        null,
        [
            'policy_issue' => $policyIssue,
            'reason' => $overrideReason,
            'rfq_vendors' => $stats['vendors'],
            'quotes_received' => $stats['quotes'],
            'stage' => $job['workflow_stage'] ?? null
        ]
    );
}

function isHodOverrideActor(): bool
{
    if (isAdmin()) {
        return true;
    }
    $role = trim((string)($_SESSION['role_name'] ?? ''));
    return strcasecmp($role, 'HOD') === 0;
}

// ─── Stage transition validation ───────────────────────────
function validateStageTransition(PDO $pdo, string $fromStage, string $toStage): void
{
    if ($fromStage === '' || $toStage === '' || $fromStage === $toStage) {
        return;
    }

    $runtimeAllowedTransitions = [
        STAGE_WORK_ORDER_RAISED => [STAGE_WORKSHOP_IN_PROGRESS],
        STAGE_CLOSED => [STAGE_REQUISITION_RAISED],
        STAGE_REQUISITION_RAISED => [STAGE_REQUISITION_APPROVED, STAGE_REQUISITION_REJECTED, STAGE_WORKSHOP_IN_PROGRESS],
        STAGE_REQUISITION_APPROVED => [STAGE_PAYMENT_MADE, STAGE_PAYMENT_NOT_MADE, STAGE_WORKSHOP_IN_PROGRESS],
        STAGE_REQUISITION_REJECTED => [STAGE_WORKSHOP_IN_PROGRESS],
        STAGE_PAYMENT_MADE => [STAGE_PAYMENT_MADE, STAGE_WORKSHOP_IN_PROGRESS],
        STAGE_PAYMENT_NOT_MADE => [STAGE_PAYMENT_NOT_MADE, STAGE_WORKSHOP_IN_PROGRESS],
    ];
    if (in_array($toStage, $runtimeAllowedTransitions[$fromStage] ?? [], true)) {
        return;
    }

    // Direct transition from Closed to Workshop In Progress (for Reject Requisition action)
    if ($fromStage === STAGE_CLOSED && $toStage === STAGE_WORKSHOP_IN_PROGRESS) {
        return;
    }

    $allowed = allowedStageTransitions($pdo, $fromStage);
    if (!$allowed) {
        return;
    }

    foreach ($allowed as $stage) {
        if (strcasecmp($stage, $toStage) === 0) {
            return;
        }
    }

    respond([
        'status' => 'error',
        'message' => 'Invalid stage transition: ' . $fromStage . ' -> ' . $toStage
    ]);
}

function allowedStageTransitions(PDO $pdo, string $fromStage): array
{
    static $map = null;

    try {
        $stmt = $pdo->prepare("
            SELECT ms_to.stage_name
            FROM stage_transitions st
            INNER JOIN maintenance_stages ms_from ON ms_from.id = st.from_stage_id
            INNER JOIN maintenance_stages ms_to ON ms_to.id = st.to_stage_id
            WHERE ms_from.stage_name = :from_stage
              AND st.is_active = 1
        ");
        $stmt->execute(['from_stage' => $fromStage]);
        $rows = $stmt->fetchAll(PDO::FETCH_COLUMN);
        if ($rows) {
            return array_values(array_map('strval', $rows));
        }
    } catch (Throwable) {
        // Fallback to static map.
    }

    if ($map === null) {
        $map = MAINTENANCE_STAGE_TRANSITIONS;
    }

    return $map[$fromStage] ?? [];
}
