<?php
/**
 * apply_rate.php
 *
 * Shared endpoint for the "Apply Rate" feature. Called via AJAX from both
 * reservation_form.php and reservation_list.php.
 *
 * Actions (POST 'ar_action'):
 *   - list_plans   : return all rate plans (for the picker)
 *   - preview      : given order_id + rate_plan_id + scope, return the nights
 *                    that would change and the recomputed totals (no writes)
 *   - apply        : persist the chosen rate to the selected nights, ensure a
 *                    master folio exists, rebuild folio room charges
 *
 * Scope:
 *   - 'all'      : whole stay
 *   - 'selected' : only the dates passed in 'dates' (JSON array of Y-m-d)
 *
 * Pricing:
 *   - By default each night is recomputed from the plan via
 *     calculate_room_night_total() using the booking's rooming allocation.
 *   - If 'flat_total' is provided (per-night flat amount) it is used as the
 *     night total instead of the recomputed value (staff manual edit).
 *
 * Depends on functions.php (fetch_rate_plan, calculate helpers) already being
 * loaded by bootstrap.php. Reuses the reservation form's folio helpers when
 * available, otherwise falls back to a local master-folio ensure.
 */

require_once __DIR__ . '/../../includes/bootstrap.php';
require_login();

header('Content-Type: application/json');

/* ------------------------------------------------------------------ helpers */

if (!function_exists('ar_column_exists')) {
    function ar_column_exists(PDO $pdo, string $table, string $column): bool
    {
        static $cache = [];
        $key = $table . '.' . $column;
        if (isset($cache[$key])) return $cache[$key];
        try {
            $stmt = $pdo->prepare("
                SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
                WHERE TABLE_SCHEMA = DATABASE()
                  AND TABLE_NAME = :t
                  AND COLUMN_NAME = :c");
            $stmt->execute([':t' => $table, ':c' => $column]);
            $cache[$key] = ((int)$stmt->fetchColumn() === 1);
        } catch (Throwable $e) {
            $cache[$key] = false;
        }
        return $cache[$key];
    }
}

if (!function_exists('ar_ensure_master_folio')) {
    /**
     * Ensure a master folio exists for the order. Reuses the reservation
     * form helper when it is loaded, otherwise creates one locally.
     */
    function ar_ensure_master_folio(PDO $pdo, int $orderId): int
    {
        if (function_exists('rf_get_or_create_master_folio')) {
            return rf_get_or_create_master_folio($pdo, $orderId);
        }

        $stmt = $pdo->prepare("SELECT id FROM folios WHERE order_id = :order_id AND folio_type = 'master' LIMIT 1");
        $stmt->execute([':order_id' => $orderId]);
        $existing = $stmt->fetchColumn();
        if ($existing) {
            return (int)$existing;
        }

        $stmt = $pdo->prepare("SELECT property_id, order_no FROM orders WHERE id = :order_id LIMIT 1");
        $stmt->execute([':order_id' => $orderId]);
        $o = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$o) {
            throw new RuntimeException('Booking not found while creating master folio.');
        }
        $propertyId = (int)$o['property_id'];
        $folioNo = 'FOL-M-' . $orderId;
        $folioName = 'Master Folio - ' . ($o['order_no'] ?: ('Booking #' . $orderId));

        $stmt = $pdo->prepare("
            INSERT INTO folios
                (property_id, order_id, folio_no, folio_name, folio_type, status, total_charges, total_payments, balance)
            VALUES
                (:property_id, :order_id, :folio_no, :folio_name, 'master', 1, 0, 0, 0)");
        $stmt->execute([
            ':property_id' => $propertyId,
            ':order_id'    => $orderId,
            ':folio_no'    => $folioNo,
            ':folio_name'  => $folioName,
        ]);
        return (int)$pdo->lastInsertId();
    }
}

if (!function_exists('ar_rebuild_folio_room_charges')) {
    function ar_rebuild_folio_room_charges(PDO $pdo, int $orderId): void
    {
        // Prefer the reservation form's helper when it is loaded (same behaviour).
        if (function_exists('rf_rebuild_master_folio_room_charges_from_prices')) {
            rf_rebuild_master_folio_room_charges_from_prices($pdo, $orderId);
            return;
        }

        // Standalone rebuild (e.g. when called from the reservation list).
        // Removes only system-generated room charges, leaving extras/payments.
        $folioId = ar_ensure_master_folio($pdo, $orderId);

        $stmt = $pdo->prepare("SELECT property_id FROM orders WHERE id = :id LIMIT 1");
        $stmt->execute([':id' => $orderId]);
        $propertyId = (int)($stmt->fetchColumn() ?: 0);

        $pdo->prepare("
            DELETE FROM folio_transactions
            WHERE order_id = :order_id
              AND folio_id = :folio_id
              AND LOWER(COALESCE(tx_type, '')) = 'room_charge'
              AND LOWER(COALESCE(source_table, '')) = 'rel_orders_prices'
        ")->execute([':order_id' => $orderId, ':folio_id' => $folioId]);

        $hasTitle = ar_column_exists($pdo, 'rel_orders_prices', 'rate_plan_title');
        $sel = "date, total"
             . ($hasTitle ? ", rate_plan_title" : ", '' AS rate_plan_title");
        $stmt = $pdo->prepare("
            SELECT {$sel}
            FROM rel_orders_prices
            WHERE order_id = :order_id
            ORDER BY date ASC");
        $stmt->execute([':order_id' => $orderId]);
        $nights = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];

        $insert = $pdo->prepare("
            INSERT INTO folio_transactions
                (property_id, folio_id, order_id, tx_date, tx_type, description, debit, credit, source_table, source_id)
            VALUES
                (:property_id, :folio_id, :order_id, :tx_date, 'room_charge', :description, :debit, 0, 'rel_orders_prices', 0)");

        foreach ($nights as $n) {
            $amount = round((float)($n['total'] ?? 0), 2);
            if ($amount <= 0) continue;
            $stayDate = (string)($n['date'] ?? '');
            if ($stayDate === '' || strtotime($stayDate) === false) $stayDate = date('Y-m-d');
            $desc = 'Accommodation charge for ' . $stayDate;
            $planTitle = trim((string)($n['rate_plan_title'] ?? ''));
            if ($planTitle !== '') $desc .= ' - ' . $planTitle;
            $insert->execute([
                ':property_id' => $propertyId,
                ':folio_id'    => $folioId,
                ':order_id'    => $orderId,
                ':tx_date'     => $stayDate,
                ':description' => $desc,
                ':debit'       => $amount,
            ]);
        }
    }
}

if (!function_exists('ar_recalc_folio')) {
    function ar_recalc_folio(PDO $pdo, int $orderId): void
    {
        // Recompute folio totals from transactions. Prefer form helper.
        if (function_exists('rf_recalculate_folio_totals')) {
            $stmt = $pdo->prepare("SELECT id FROM folios WHERE order_id = :order_id AND folio_type = 'master' LIMIT 1");
            $stmt->execute([':order_id' => $orderId]);
            $fid = (int)($stmt->fetchColumn() ?: 0);
            if ($fid > 0) { rf_recalculate_folio_totals($pdo, $fid); return; }
        }

        // Standalone recompute.
        $stmt = $pdo->prepare("
            SELECT folio_id,
                   COALESCE(SUM(COALESCE(debit,0)),0)  AS charges,
                   COALESCE(SUM(COALESCE(credit,0)),0) AS payments
            FROM folio_transactions
            WHERE order_id = :order_id
              AND COALESCE(is_voided,0) = 0
            GROUP BY folio_id");
        $stmt->execute([':order_id' => $orderId]);
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
        $upd = $pdo->prepare("
            UPDATE folios
            SET total_charges = :charges,
                total_payments = :payments,
                balance = :balance
            WHERE id = :folio_id");
        foreach ($rows as $r) {
            $charges = (float)$r['charges'];
            $payments = (float)$r['payments'];
            $upd->execute([
                ':charges'  => $charges,
                ':payments' => $payments,
                ':balance'  => round($charges - $payments, 2),
                ':folio_id' => (int)$r['folio_id'],
            ]);
        }
    }
}

/**
 * Load the booking header plus its rooming allocation for recomputation.
 */
if (!function_exists('ar_load_order')) {
    function ar_load_order(PDO $pdo, int $orderId): array
    {
        $stmt = $pdo->prepare("
            SELECT id, property_id, check_in, check_out, adults, kids, totalrooms,
                   room_type_id, agent_id, meal_plan
            FROM orders
            WHERE id = :id
            LIMIT 1");
        $stmt->execute([':id' => $orderId]);
        $order = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$order) {
            throw new RuntimeException('Booking not found.');
        }

        $stmt = $pdo->prepare("
            SELECT room_no, room_type_id, adults, children
            FROM order_rooming
            WHERE order_id = :order_id
            ORDER BY room_no ASC, id ASC");
        $stmt->execute([':order_id' => $orderId]);
        $rooming = $stmt->fetchAll(PDO::FETCH_ASSOC);

        if (!$rooming && function_exists('allocate_guests_to_rooms')) {
            $alloc = allocate_guests_to_rooms(
                (int)$order['adults'],
                (int)$order['kids'],
                max(1, (int)$order['totalrooms'])
            );
            $rooming = [];
            foreach ($alloc as $idx => $r) {
                $rooming[] = [
                    'room_no'      => $idx + 1,
                    'room_type_id' => (int)$order['room_type_id'],
                    'adults'       => (int)$r['adults'],
                    'children'     => (int)$r['children'],
                ];
            }
        }

        $order['_rooming'] = $rooming ?: [];
        return $order;
    }
}

/**
 * Compute one night's total for a given plan across the rooming allocation.
 */
if (!function_exists('ar_night_total_from_plan')) {
    function ar_night_total_from_plan(array $plan, array $rooming): array
    {
        $adultTotal = 0.0;
        $childTotal = 0.0;
        $total = 0.0;
        $types = [];

        foreach ($rooming as $slot) {
            if (function_exists('calculate_room_night_total')) {
                $calc = calculate_room_night_total(
                    (int)($slot['adults'] ?? 0),
                    (int)($slot['children'] ?? 0),
                    (float)$plan['sharing_rate'],
                    (float)$plan['single_rate'],
                    (float)$plan['single_supplement'],
                    (float)$plan['child_rate_percent']
                );
                $adultTotal += (float)($calc['adult_total'] ?? 0);
                $childTotal += (float)($calc['child_total'] ?? 0);
                $total += (float)($calc['room_total'] ?? $calc['night_total'] ?? 0);
                if (!empty($calc['applied_rate_type']) && $calc['applied_rate_type'] !== 'empty_room') {
                    $types[] = $calc['applied_rate_type'];
                }
            } else {
                // Minimal fallback if the shared calculator is unavailable.
                $a = (int)($slot['adults'] ?? 0);
                $c = (int)($slot['children'] ?? 0);
                $roomAdult = ($a <= 1)
                    ? ((float)$plan['single_rate'] > 0 ? (float)$plan['single_rate'] : (float)$plan['sharing_rate'] + (float)$plan['single_supplement'])
                    : $a * (float)$plan['sharing_rate'];
                $roomChild = $c * ((float)$plan['sharing_rate'] * ((float)$plan['child_rate_percent'] / 100));
                $adultTotal += $roomAdult;
                $childTotal += $roomChild;
                $total += $roomAdult + $roomChild;
                $types[] = ($a <= 1) ? 'single' : 'sharing';
            }
        }

        return [
            'adult_total'       => round($adultTotal, 2),
            'child_total'       => round($childTotal, 2),
            'night_total'       => round($total, 2),
            'applied_rate_type' => $types ? implode('+', array_unique($types)) : 'sharing',
        ];
    }
}

/* --------------------------------------------------------------- dispatch */

if (!function_exists('ar_folio_snapshot')) {
    /**
     * Current master-folio state for the order: existing room-charge total (from
     * system-generated rel_orders_prices rows), other charges (extras etc.),
     * payments, and resulting balance. Read-only.
     */
    function ar_folio_snapshot(PDO $pdo, int $orderId): array
    {
        $stmt = $pdo->prepare("SELECT id FROM folios WHERE order_id = :order_id AND folio_type = 'master' LIMIT 1");
        $stmt->execute([':order_id' => $orderId]);
        $folioId = (int)($stmt->fetchColumn() ?: 0);

        $exists = $folioId > 0;
        $roomCharges = 0.0;
        $otherCharges = 0.0;
        $payments = 0.0;

        if ($exists) {
            // Room charges we generate (will be replaced by Apply).
            $stmt = $pdo->prepare("
                SELECT COALESCE(SUM(COALESCE(debit,0)),0)
                FROM folio_transactions
                WHERE order_id = :order_id
                  AND folio_id = :folio_id
                  AND COALESCE(is_voided,0) = 0
                  AND LOWER(COALESCE(tx_type,'')) = 'room_charge'
                  AND LOWER(COALESCE(source_table,'')) = 'rel_orders_prices'");
            $stmt->execute([':order_id' => $orderId, ':folio_id' => $folioId]);
            $roomCharges = (float)$stmt->fetchColumn();

            // Everything else that is a debit (extras, manual charges).
            $stmt = $pdo->prepare("
                SELECT COALESCE(SUM(COALESCE(debit,0)),0)
                FROM folio_transactions
                WHERE order_id = :order_id
                  AND folio_id = :folio_id
                  AND COALESCE(is_voided,0) = 0
                  AND NOT (LOWER(COALESCE(tx_type,'')) = 'room_charge'
                           AND LOWER(COALESCE(source_table,'')) = 'rel_orders_prices')");
            $stmt->execute([':order_id' => $orderId, ':folio_id' => $folioId]);
            $otherCharges = (float)$stmt->fetchColumn();

            $stmt = $pdo->prepare("
                SELECT COALESCE(SUM(COALESCE(credit,0)),0)
                FROM folio_transactions
                WHERE order_id = :order_id
                  AND folio_id = :folio_id
                  AND COALESCE(is_voided,0) = 0");
            $stmt->execute([':order_id' => $orderId, ':folio_id' => $folioId]);
            $payments = (float)$stmt->fetchColumn();
        }

        $charges = round($roomCharges + $otherCharges, 2);
        return [
            'folio_exists'  => $exists,
            'room_charges'  => round($roomCharges, 2),
            'other_charges' => round($otherCharges, 2),
            'total_charges' => $charges,
            'payments'      => round($payments, 2),
            'balance'       => round($charges - $payments, 2),
        ];
    }
}

$pdo = $pdo ?? ($GLOBALS['pdo'] ?? null);
if (!($pdo instanceof PDO)) {
    echo json_encode(['ok' => false, 'error' => 'Database connection unavailable.']);
    exit;
}

$action = (string)($_POST['ar_action'] ?? $_GET['ar_action'] ?? '');

try {
    if ($action === 'list_plans') {
        /* All rate plans, active first, with a friendly label. */
        $hasMeal = ar_column_exists($pdo, 'rate_plans', 'meal_plan');
        $hasCode = ar_column_exists($pdo, 'rate_plans', 'code');
        $cols = "id, title, status"
              . ($hasMeal ? ", meal_plan" : ", '' AS meal_plan")
              . ($hasCode ? ", code" : ", '' AS code");
        $stmt = $pdo->query("SELECT {$cols} FROM rate_plans ORDER BY status DESC, title ASC");
        $plans = $stmt->fetchAll(PDO::FETCH_ASSOC);
        echo json_encode(['ok' => true, 'plans' => $plans]);
        exit;
    }

    if ($action === 'preview' || $action === 'apply') {
        $orderId    = (int)($_POST['order_id'] ?? 0);
        $ratePlanId = (int)($_POST['rate_plan_id'] ?? 0);
        $scope      = (string)($_POST['scope'] ?? 'all');
        $flatTotal  = ($_POST['flat_total'] ?? '') !== '' ? (float)$_POST['flat_total'] : null;
        $reason     = trim((string)($_POST['reason'] ?? ''));

        $selectedDates = [];
        if ($scope === 'selected') {
            $raw = (string)($_POST['dates'] ?? '[]');
            $decoded = json_decode($raw, true);
            if (is_array($decoded)) {
                foreach ($decoded as $d) {
                    $d = trim((string)$d);
                    if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $d)) {
                        $selectedDates[$d] = true;
                    }
                }
            }
            if (!$selectedDates) {
                throw new RuntimeException('No valid dates selected.');
            }
        }

        if ($orderId <= 0 || $ratePlanId <= 0) {
            throw new RuntimeException('Missing booking or rate plan.');
        }

        $order = ar_load_order($pdo, $orderId);
        $rooming = $order['_rooming'];

        /* Load the chosen plan's price rows for this room type. We resolve the
           plan's price per night by date window, so seasonal plans still work. */
        $roomTypeId = (int)$order['room_type_id'];

        // Pull every physical price row. Multi-room bookings normally have one
        // rel_orders_prices row per room per date, so DATE is not unique.
        $stmt = $pdo->prepare("\n            SELECT id, date, total\n            FROM rel_orders_prices\n            WHERE order_id = :order_id\n            ORDER BY date ASC, id ASC");
        $stmt->execute([':order_id' => $orderId]);
        $existingRows = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];

        $rowsByDate = [];
        $existingTotals = [];
        foreach ($existingRows as $r) {
            $d = (string)$r['date'];
            if ($d === '') continue;
            if (!isset($rowsByDate[$d])) $rowsByDate[$d] = [];
            $rowsByDate[$d][] = $r;
            $existingTotals[$d] = ($existingTotals[$d] ?? 0.0) + (float)($r['total'] ?? 0);
        }
        $existingDates = array_keys($rowsByDate);

        if (!$existingDates) {
            throw new RuntimeException('This booking has no nightly price rows to update. Save the booking first.');
        }

        /* For each date in scope, look up the plan price valid on that night. */
        $priceStmt = $pdo->prepare("\n            SELECT pp.sharing_rate, pp.single_rate, pp.single_supplement,\n                   pp.child_rate_percent, pp.currency,\n                   rp.id AS rate_plan_id, rp.title AS rate_plan_title\n            FROM rate_plan_prices pp\n            INNER JOIN rate_plans rp ON rp.id = pp.rate_plan_id\n            WHERE pp.rate_plan_id = :rate_plan_id\n              AND pp.room_type_id = :room_type_id\n              AND pp.status = 1\n              AND pp.valid_from <= :d1\n              AND pp.valid_to   >= :d2\n            ORDER BY DATEDIFF(pp.valid_to, pp.valid_from) ASC, pp.id DESC\n            LIMIT 1");

        $changes = [];
        $missing = [];
        foreach ($existingDates as $date) {
            if ($scope === 'selected' && !isset($selectedDates[$date])) continue;

            $priceStmt->execute([
                ':rate_plan_id' => $ratePlanId,
                ':room_type_id' => $roomTypeId,
                ':d1' => $date,
                ':d2' => $date,
            ]);
            $plan = $priceStmt->fetch(PDO::FETCH_ASSOC);
            if (!$plan) {
                $missing[] = $date;
                continue;
            }

            $dateRows = $rowsByDate[$date];
            $rowCount = count($dateRows);
            $rowUpdates = [];
            $adultTotal = 0.0;
            $childTotal = 0.0;
            $nightTotal = 0.0;
            $types = [];

            if ($flatTotal !== null) {
                // flat_total is an exact BOOKING total for the night. Split it over
                // the physical room rows while preserving the exact rounded total.
                $allocated = 0.0;
                foreach ($dateRows as $i => $priceRow) {
                    $rowAmount = ($i === $rowCount - 1)
                        ? round($flatTotal - $allocated, 2)
                        : round($flatTotal / max(1, $rowCount), 2);
                    $allocated += $rowAmount;
                    $rowUpdates[] = [
                        'row_id' => (int)$priceRow['id'],
                        'adult_total' => $rowAmount,
                        'child_total' => 0.0,
                        'total' => $rowAmount,
                        'applied_rate_type' => 'manual_flat',
                    ];
                }
                $adultTotal = round($flatTotal, 2);
                $nightTotal = round($flatTotal, 2);
                $types[] = 'manual_flat';
            } else {
                // Price each room separately. Never copy the whole-booking nightly
                // total into every row for the same date.
                foreach ($dateRows as $i => $priceRow) {
                    $slot = $rooming[$i] ?? ['adults' => 0, 'children' => 0];
                    $calc = calculate_room_night_total(
                        (int)($slot['adults'] ?? 0),
                        (int)($slot['children'] ?? 0),
                        (float)$plan['sharing_rate'],
                        (float)$plan['single_rate'],
                        (float)$plan['single_supplement'],
                        (float)$plan['child_rate_percent']
                    );
                    $ra = (float)($calc['adult_total'] ?? 0);
                    $rc = (float)($calc['child_total'] ?? 0);
                    $rt = (float)($calc['room_total'] ?? 0);
                    $type = (string)($calc['applied_rate_type'] ?? 'sharing');
                    $rowUpdates[] = [
                        'row_id' => (int)$priceRow['id'],
                        'adult_total' => $ra,
                        'child_total' => $rc,
                        'total' => $rt,
                        'applied_rate_type' => $type,
                    ];
                    $adultTotal += $ra;
                    $childTotal += $rc;
                    $nightTotal += $rt;
                    if ($type !== 'empty_room') $types[] = $type;
                }

                // If rooming has more rooms than price rows, preserve the full
                // booking amount by adding unmatched room calculations to the final
                // existing row rather than silently dropping them.
                for ($i = $rowCount; $i < count($rooming); $i++) {
                    $slot = $rooming[$i];
                    $calc = calculate_room_night_total(
                        (int)($slot['adults'] ?? 0),
                        (int)($slot['children'] ?? 0),
                        (float)$plan['sharing_rate'],
                        (float)$plan['single_rate'],
                        (float)$plan['single_supplement'],
                        (float)$plan['child_rate_percent']
                    );
                    $ra = (float)($calc['adult_total'] ?? 0);
                    $rc = (float)($calc['child_total'] ?? 0);
                    $rt = (float)($calc['room_total'] ?? 0);
                    $adultTotal += $ra;
                    $childTotal += $rc;
                    $nightTotal += $rt;
                    if ($rowUpdates) {
                        $last = count($rowUpdates) - 1;
                        $rowUpdates[$last]['adult_total'] += $ra;
                        $rowUpdates[$last]['child_total'] += $rc;
                        $rowUpdates[$last]['total'] += $rt;
                    }
                }
            }

            $changes[] = [
                'date' => $date,
                'sharing_rate' => (float)$plan['sharing_rate'],
                'single_rate' => (float)$plan['single_rate'],
                'single_supplement' => (float)$plan['single_supplement'],
                'child_rate_percent' => (float)$plan['child_rate_percent'],
                'currency' => (string)($plan['currency'] ?? ''),
                'rate_plan_id' => (int)$plan['rate_plan_id'],
                'rate_plan_title' => (string)$plan['rate_plan_title'],
                'adult_total' => round($adultTotal, 2),
                'child_total' => round($childTotal, 2),
                'night_total' => round($nightTotal, 2),
                'applied_rate_type' => $types ? implode('+', array_unique($types)) : 'sharing',
                '_row_updates' => $rowUpdates,
            ];
        }

        if (!$changes) {
            throw new RuntimeException($missing
                ? 'The selected plan has no price for these dates: ' . implode(', ', $missing)
                : 'Nothing to update.');
        }

        if ($action === 'preview') {
            $newTotal = 0.0;
            $changedByDate = [];
            $publicChanges = [];
            foreach ($changes as $c) {
                $newTotal += $c['night_total'];
                $changedByDate[$c['date']] = $c['night_total'];
                unset($c['_row_updates']);
                $publicChanges[] = $c;
            }

            $projectedRoomCharges = 0.0;
            foreach ($existingTotals as $d => $t) {
                $projectedRoomCharges += isset($changedByDate[$d]) ? $changedByDate[$d] : $t;
            }
            $projectedRoomCharges = round($projectedRoomCharges, 2);

            $folioBefore = ar_folio_snapshot($pdo, $orderId);
            $afterCharges = round($projectedRoomCharges + $folioBefore['other_charges'], 2);
            $folioAfter = [
                'folio_exists' => true,
                'room_charges' => $projectedRoomCharges,
                'other_charges' => $folioBefore['other_charges'],
                'total_charges' => $afterCharges,
                'payments' => $folioBefore['payments'],
                'balance' => round($afterCharges - $folioBefore['payments'], 2),
            ];

            echo json_encode([
                'ok' => true,
                'changes' => $publicChanges,
                'missing' => array_values(array_unique($missing)),
                'new_total' => round($newTotal, 2),
                'count' => count($changes),
                'folio_before' => $folioBefore,
                'folio_after' => $folioAfter,
            ]);
            exit;
        }

        /* action === 'apply': update each physical price row by its unique id. */
        $hasOrig = ar_column_exists($pdo, 'rel_orders_prices', 'original_total');
        $hasOverridden = ar_column_exists($pdo, 'rel_orders_prices', 'rate_overridden');
        $hasApplied = ar_column_exists($pdo, 'rel_orders_prices', 'applied_rate_type');
        $hasTitle = ar_column_exists($pdo, 'rel_orders_prices', 'rate_plan_title');
        $hasCurrency = ar_column_exists($pdo, 'rel_orders_prices', 'rate_plan_currency');
        $hasApplyReason = ar_column_exists($pdo, 'rel_orders_prices', 'override_reason');

        $set = [
            "rate_plan_id = :rate_plan_id", "sharing_rate = :sharing_rate",
            "single_rate = :single_rate", "single_supplement = :single_supplement",
            "child_rate_percent = :child_rate_percent", "adult_total = :adult_total",
            "child_total = :child_total", "total = :total", "price = :price",
        ];
        if ($hasTitle) $set[] = "rate_plan_title = :rate_plan_title";
        if ($hasCurrency) $set[] = "rate_plan_currency = :rate_plan_currency";
        if ($hasApplied) $set[] = "applied_rate_type = :applied_rate_type";
        if ($flatTotal !== null && $hasOverridden) {
            $set[] = "rate_overridden = 1";
            if ($hasOrig) $set[] = "original_total = :original_total";
            if ($hasApplyReason) $set[] = "override_reason = :override_reason";
        } elseif ($hasOverridden) {
            $set[] = "rate_overridden = 0";
        }

        $upd = $pdo->prepare("UPDATE rel_orders_prices SET " . implode(', ', $set) . " WHERE id = :row_id AND order_id = :order_id");

        $pdo->beginTransaction();
        try {
            $applied = 0;
            $grand = 0.0;
            foreach ($changes as $c) {
                foreach ($c['_row_updates'] as $ru) {
                    $params = [
                        ':rate_plan_id' => $c['rate_plan_id'],
                        ':sharing_rate' => $c['sharing_rate'],
                        ':single_rate' => $c['single_rate'],
                        ':single_supplement' => $c['single_supplement'],
                        ':child_rate_percent' => $c['child_rate_percent'],
                        ':adult_total' => round((float)$ru['adult_total'], 2),
                        ':child_total' => round((float)$ru['child_total'], 2),
                        ':total' => round((float)$ru['total'], 2),
                        ':price' => round((float)$ru['total'], 2),
                        ':row_id' => (int)$ru['row_id'],
                        ':order_id' => $orderId,
                    ];
                    if ($hasTitle) $params[':rate_plan_title'] = $c['rate_plan_title'];
                    if ($hasCurrency) $params[':rate_plan_currency'] = $c['currency'];
                    if ($hasApplied) $params[':applied_rate_type'] = $ru['applied_rate_type'];
                    if ($flatTotal !== null && $hasOverridden) {
                        if ($hasOrig) $params[':original_total'] = round((float)$ru['total'], 2);
                        if ($hasApplyReason) $params[':override_reason'] = $reason !== '' ? $reason : 'Manual rate applied';
                    }
                    $upd->execute($params);
                    $applied++;
                }
                $grand += $c['night_total'];
            }

            ar_ensure_master_folio($pdo, $orderId);
            ar_rebuild_folio_room_charges($pdo, $orderId);
            ar_recalc_folio($pdo, $orderId);
            $pdo->commit();
        } catch (Throwable $e) {
            $pdo->rollBack();
            throw $e;
        }

        $folioAfter = ar_folio_snapshot($pdo, $orderId);

        echo json_encode([
            'ok'          => true,
            'applied'     => $applied,
            'missing'     => $missing,
            'new_total'   => round($grand, 2),
            'folio_after' => $folioAfter,
            'message'     => count($changes) . ' night(s) updated across ' . $applied . ' room price row(s).'
                        . ($missing ? ' Skipped (no plan price): ' . implode(', ', $missing) : ''),
        ]);
        exit;
    }

    throw new RuntimeException('Unknown action.');
} catch (Throwable $e) {
    echo json_encode(['ok' => false, 'error' => $e->getMessage()]);
    exit;
}
