<?php

/**
 * Rate resolution lives in rate_support.php - ONE definition, shared by
 * reservation_form.php, calendar.php, move_booking.php and anything else
 * that prices a night. It used to be duplicated here, in rate_support.php
 * AND in reservation_form.php, all three guarded by function_exists(), so
 * whichever file happened to be included first silently won. The three
 * copies had drifted: different signatures (5 vs 6 args) and different
 * defaults for an invalid meal plan ('' vs 'HB').
 */
require_once __DIR__ . '/rate_support.php';

function calculate_nightly_person_rate(
    int $adults,
    int $kids,
    float $sharingRate,
    float $singleRate,
    float $singleSupplement,
    float $childPercent = 50.0
): array {
    $adults = max(0, $adults);
    $kids = max(0, $kids);
    $childPercent = max(0, $childPercent);

    if ($adults <= 1) {
        $adultTotal = $singleRate > 0
            ? $singleRate
            : ($sharingRate + $singleSupplement);

        $appliedType = $singleRate > 0
            ? 'single_rate'
            : 'sharing_plus_single_supplement';
    } else {
        $adultTotal = $adults * $sharingRate;
        $appliedType = 'sharing';
    }

    $childRate = $sharingRate * ($childPercent / 100);
    $childTotal = $kids * $childRate;
    $nightTotal = $adultTotal + $childTotal;

    return [
        'adult_total'        => round($adultTotal, 2),
        'child_total'        => round($childTotal, 2),
        'night_total'        => round($nightTotal, 2),
        'applied_rate_type'  => $appliedType,
        'child_rate_percent' => round($childPercent, 2),
    ];
}

if (!function_exists('rebuild_order_nightly_prices')) {
function rebuild_order_nightly_prices(PDO $pdo, int $orderId): array
{
    $stmt = $pdo->prepare("
        SELECT id, check_in, check_out, adults, kids, room_type_id, room_id, agent_id, meal_plan, totalrooms
        FROM orders
        WHERE id = :id
        LIMIT 1
    ");
    $stmt->execute([':id' => $orderId]);
    $order = $stmt->fetch(PDO::FETCH_ASSOC);

    if (!$order) {
        throw new RuntimeException('Order not found');
    }

    $checkIn  = strtotime((string)$order['check_in']);
    $checkOut = strtotime((string)$order['check_out']);

    if (!$checkIn || !$checkOut || $checkOut <= $checkIn) {
        throw new RuntimeException('Invalid stay dates');
    }

    $pdo->prepare("DELETE FROM rel_orders_prices WHERE order_id = :order_id")
        ->execute([':order_id' => $orderId]);

    $insert = $pdo->prepare("
        INSERT INTO rel_orders_prices
        (
            order_id, date, price, additional_person, additional_person_price, total, room_id,
            rate_plan_id, adults_count, kids_count, sharing_rate, single_rate,
            single_supplement, child_rate_percent, adult_total, child_total, applied_rate_type
        )
        VALUES
        (
            :order_id, :date, :price, :additional_person, :additional_person_price, :total, :room_id,
            :rate_plan_id, :adults_count, :kids_count, :sharing_rate, :single_rate,
            :single_supplement, :child_rate_percent, :adult_total, :child_total, :applied_rate_type
        )
    ");

    $cursor = $checkIn;
    $grandTotal = 0.0;
    $nightCount = 0;

    while ($cursor < $checkOut) {
        $stayDate = date('Y-m-d', $cursor);

        $plan = fetch_rate_plan(
            $pdo,
            (int)$order['room_type_id'],
            !empty($order['agent_id']) ? (int)$order['agent_id'] : null,
            $stayDate,
            (string)($order['meal_plan'] ?? ''),
            max(1, (int)($order['totalrooms'] ?? 1))
        );

        if (!$plan) {
            // No active rate plan for this night: hold the room at zero price
            // instead of blocking. Staff can reprice later.
            $plan = [
                'id'                 => 0,
                'title'              => 'No rate plan (held at 0)',
                'currency'           => '',
                'sharing_rate'       => 0.0,
                'single_rate'        => 0.0,
                'single_supplement'  => 0.0,
                'child_rate_percent' => 0.0,
                'rate_source'        => 'zero_hold',
            ];
        }

        $calc = calculate_nightly_person_rate(
            (int)$order['adults'],
            (int)$order['kids'],
            (float)$plan['sharing_rate'],
            (float)$plan['single_rate'],
            (float)$plan['single_supplement'],
            (float)$plan['child_rate_percent']
        );

        $insert->execute([
            ':order_id'                => $orderId,
            ':date'                    => $stayDate,
            ':price'                   => (float)$plan['sharing_rate'],
            ':additional_person'       => max(0, (int)$order['adults'] - 1),
            ':additional_person_price' => $calc['child_total'],
            ':total'                   => $calc['night_total'],
            ':room_id'                 => !empty($order['room_id']) ? (int)$order['room_id'] : 0,
            ':rate_plan_id'            => (int)$plan['id'],
            ':adults_count'            => (int)$order['adults'],
            ':kids_count'              => (int)$order['kids'],
            ':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'],
            ':adult_total'             => $calc['adult_total'],
            ':child_total'             => $calc['child_total'],
            ':applied_rate_type'       => $calc['applied_rate_type'],
        ]);

        $grandTotal += $calc['night_total'];
        $nightCount++;
        $cursor = strtotime('+1 day', $cursor);
    }

    $upd = $pdo->prepare("
        UPDATE orders
        SET nights = :nights,
            base_price = :base_price,
            amount = :amount,
            totalamount = :totalamount
        WHERE id = :id
    ");
    $upd->execute([
        ':nights'      => $nightCount,
        ':base_price'  => round($grandTotal, 2),
        ':amount'      => round($grandTotal, 2),
        ':totalamount' => round($grandTotal, 2),
        ':id'          => $orderId,
    ]);

    return [
        'nights' => $nightCount,
        'total'  => round($grandTotal, 2),
    ];
}
}

if (!function_exists('reprice_order_for_date_change')) {
    /**
     * Reprice a booking after its stay dates change (earlier arrival, shift,
     * backward-extend, or reduction). Used by move_booking.php once the rooming
     * rows and orders header have been updated to the new range.
     *
     * $mode:
     *   'reprice'  - rebuild EVERY night from the rate plan (fetch_rate_plan).
     *   'preserve' - keep the price of nights that already existed, insert new
     *                nights (front/tail) priced from the plan, and drop rows for
     *                nights no longer in range.
     *
     * Always rebuilds master-folio room charges and recomputes folio totals.
     * Returns a summary including any overpayment (payments > new charges).
     *
     * NOTE: run INSIDE the caller's transaction. Throws on hard failure so the
     * caller can roll the whole date change back.
     */
    function reprice_order_for_date_change(PDO $pdo, int $orderId, string $mode = 'reprice'): array
    {
        $mode = ($mode === 'preserve') ? 'preserve' : 'reprice';

        $stmt = $pdo->prepare("
            SELECT id, check_in, check_out, adults, kids, room_type_id, room_id, agent_id, meal_plan, totalrooms, property_id
            FROM orders WHERE id = :id LIMIT 1");
        $stmt->execute([':id' => $orderId]);
        $order = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$order) throw new RuntimeException('Order not found while repricing.');

        $checkIn  = strtotime(substr((string)$order['check_in'], 0, 10));
        $checkOut = strtotime(substr((string)$order['check_out'], 0, 10));
        if (!$checkIn || !$checkOut || $checkOut <= $checkIn) {
            throw new RuntimeException('Invalid stay dates while repricing.');
        }

        // Build the set of dates the new stay should contain.
        $wanted = [];
        for ($c = $checkIn; $c < $checkOut; $c = strtotime('+1 day', $c)) {
            $wanted[date('Y-m-d', $c)] = true;
        }

        if ($mode === 'reprice') {
            // Simplest correct path: full rebuild from the plan.
            $res = rebuild_order_nightly_prices($pdo, $orderId);
            $newNights = (int)$res['nights'];
            $newTotal  = (float)$res['total'];
            $added = []; $removed = [];
        } else {
            // Preserve existing nights; fill gaps; drop out-of-range.
            $stmt = $pdo->prepare("SELECT date FROM rel_orders_prices WHERE order_id = :id");
            $stmt->execute([':id' => $orderId]);
            $existing = [];
            foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $r) {
                $existing[substr((string)$r['date'], 0, 10)] = true;
            }

            // Remove nights no longer in range.
            $removed = [];
            foreach (array_keys($existing) as $d) {
                if (!isset($wanted[$d])) $removed[] = $d;
            }
            if ($removed) {
                $in = implode(',', array_fill(0, count($removed), '?'));
                $del = $pdo->prepare("DELETE FROM rel_orders_prices WHERE order_id = ? AND date IN ($in)");
                $del->execute(array_merge([$orderId], $removed));
            }

            // Insert nights newly in range, priced from the plan.
            $added = [];
            foreach (array_keys($wanted) as $d) {
                if (isset($existing[$d])) continue;
                $added[] = $d;
            }

            if ($added) {
                $insert = $pdo->prepare("
                    INSERT INTO rel_orders_prices
                    (
                        order_id, date, price, additional_person, additional_person_price, total, room_id,
                        rate_plan_id, adults_count, kids_count, sharing_rate, single_rate,
                        single_supplement, child_rate_percent, adult_total, child_total, applied_rate_type
                    )
                    VALUES
                    (
                        :order_id, :date, :price, :additional_person, :additional_person_price, :total, :room_id,
                        :rate_plan_id, :adults_count, :kids_count, :sharing_rate, :single_rate,
                        :single_supplement, :child_rate_percent, :adult_total, :child_total, :applied_rate_type
                    )
                ");
                foreach ($added as $stayDate) {
                    $plan = fetch_rate_plan(
                        $pdo,
                        (int)$order['room_type_id'],
                        !empty($order['agent_id']) ? (int)$order['agent_id'] : null,
                        $stayDate,
                        (string)($order['meal_plan'] ?? ''),
                        max(1, (int)($order['totalrooms'] ?? 1))
                    );
                    if (!$plan) {
                        throw new RuntimeException('No active rate plan found for new night ' . $stayDate . '. Try the "reprice" option, or add a rate for that date.');
                    }
                    $calc = calculate_nightly_person_rate(
                        (int)$order['adults'],
                        (int)$order['kids'],
                        (float)$plan['sharing_rate'],
                        (float)$plan['single_rate'],
                        (float)$plan['single_supplement'],
                        (float)$plan['child_rate_percent']
                    );
                    $insert->execute([
                        ':order_id'                => $orderId,
                        ':date'                    => $stayDate,
                        ':price'                   => (float)$plan['sharing_rate'],
                        ':additional_person'       => max(0, (int)$order['adults'] - 1),
                        ':additional_person_price' => $calc['child_total'],
                        ':total'                   => $calc['night_total'],
                        ':room_id'                 => !empty($order['room_id']) ? (int)$order['room_id'] : 0,
                        ':rate_plan_id'            => (int)$plan['id'],
                        ':adults_count'            => (int)$order['adults'],
                        ':kids_count'              => (int)$order['kids'],
                        ':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'],
                        ':adult_total'             => $calc['adult_total'],
                        ':child_total'             => $calc['child_total'],
                        ':applied_rate_type'       => $calc['applied_rate_type'],
                    ]);
                }
            }

            // Recompute header totals from the surviving + new rows.
            $sum = $pdo->prepare("SELECT COUNT(*) AS n, COALESCE(SUM(total),0) AS t FROM rel_orders_prices WHERE order_id = :id");
            $sum->execute([':id' => $orderId]);
            $agg = $sum->fetch(PDO::FETCH_ASSOC) ?: ['n' => 0, 't' => 0];
            $newNights = (int)$agg['n'];
            $newTotal  = round((float)$agg['t'], 2);

            $pdo->prepare("
                UPDATE orders
                SET nights = :nights, base_price = :bp, amount = :amt, totalamount = :tot
                WHERE id = :id
            ")->execute([
                ':nights' => $newNights,
                ':bp'     => $newTotal,
                ':amt'    => $newTotal,
                ':tot'    => $newTotal,
                ':id'     => $orderId,
            ]);
        }

        // Rebuild the master-folio room charges from the new price rows.
        if (function_exists('rf_rebuild_master_folio_room_charges_from_prices')) {
            rf_rebuild_master_folio_room_charges_from_prices($pdo, $orderId);
        } else {
            reprice_rebuild_folio_room_charges_fallback($pdo, $orderId);
        }

        // Compute payments vs new charges for overpayment detection.
        $payStmt = $pdo->prepare("
            SELECT COALESCE(SUM(COALESCE(credit,0)),0) AS payments,
                   COALESCE(SUM(COALESCE(debit,0)),0)  AS charges
            FROM folio_transactions
            WHERE order_id = :id AND COALESCE(is_voided,0) = 0");
        $payStmt->execute([':id' => $orderId]);
        $pay = $payStmt->fetch(PDO::FETCH_ASSOC) ?: ['payments' => 0, 'charges' => 0];
        $payments = round((float)$pay['payments'], 2);
        $charges  = round((float)$pay['charges'], 2);
        $balance  = round($charges - $payments, 2);
        $overpaid = $balance < 0 ? round(-$balance, 2) : 0.0;

        return [
            'mode'         => $mode,
            'nights'       => $newNights,
            'total'        => $newTotal,
            'added'        => $added,
            'removed'      => $removed,
            'charges'      => $charges,
            'payments'     => $payments,
            'balance'      => $balance,
            'overpaid'     => $overpaid,
            'is_overpaid'  => $overpaid > 0,
        ];
    }
}

if (!function_exists('reprice_rebuild_folio_room_charges_fallback')) {
    /**
     * Minimal folio room-charge rebuild used only if the reservation form's
     * helper is not loaded in this request. Keeps extras/payments intact.
     */
    function reprice_rebuild_folio_room_charges_fallback(PDO $pdo, int $orderId): void
    {
        // Ensure a master folio exists.
        $stmt = $pdo->prepare("SELECT id FROM folios WHERE order_id = :id AND folio_type = 'master' LIMIT 1");
        $stmt->execute([':id' => $orderId]);
        $folioId = (int)($stmt->fetchColumn() ?: 0);
        if ($folioId <= 0) {
            $o = $pdo->prepare("SELECT property_id, order_no FROM orders WHERE id = :id LIMIT 1");
            $o->execute([':id' => $orderId]);
            $orow = $o->fetch(PDO::FETCH_ASSOC) ?: [];
            $pdo->prepare("
                INSERT INTO folios (property_id, order_id, folio_no, folio_name, folio_type, status, total_charges, total_payments, balance)
                VALUES (:p, :o, :fno, :fname, 'master', 1, 0, 0, 0)
            ")->execute([
                ':p'     => (int)($orow['property_id'] ?? 0),
                ':o'     => $orderId,
                ':fno'   => 'FOL-M-' . $orderId,
                ':fname' => 'Master Folio - ' . ($orow['order_no'] ?? ('Booking #' . $orderId)),
            ]);
            $folioId = (int)$pdo->lastInsertId();
        }

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

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

        $rows = $pdo->prepare("SELECT date, total FROM rel_orders_prices WHERE order_id = :id ORDER BY date ASC");
        $rows->execute([':id' => $orderId]);
        $ins = $pdo->prepare("
            INSERT INTO folio_transactions
                (property_id, folio_id, order_id, tx_date, tx_type, description, debit, credit, source_table, source_id)
            VALUES
                (:p, :f, :o, :d, 'room_charge', :desc, :debit, 0, 'rel_orders_prices', 0)");
        foreach ($rows->fetchAll(PDO::FETCH_ASSOC) as $n) {
            $amt = round((float)($n['total'] ?? 0), 2);
            if ($amt <= 0) continue;
            $d = substr((string)$n['date'], 0, 10);
            $ins->execute([
                ':p' => $propertyId, ':f' => $folioId, ':o' => $orderId,
                ':d' => $d, ':desc' => 'Accommodation charge for ' . $d, ':debit' => $amt,
            ]);
        }

        // Recompute folio totals.
        $agg = $pdo->prepare("
            SELECT COALESCE(SUM(COALESCE(debit,0)),0) AS c, COALESCE(SUM(COALESCE(credit,0)),0) AS p
            FROM folio_transactions WHERE order_id = :id AND COALESCE(is_voided,0) = 0");
        $agg->execute([':id' => $orderId]);
        $a = $agg->fetch(PDO::FETCH_ASSOC) ?: ['c' => 0, 'p' => 0];
        $pdo->prepare("
            UPDATE folios SET total_charges = :c, total_payments = :p, balance = :b WHERE id = :f
        ")->execute([
            ':c' => round((float)$a['c'], 2),
            ':p' => round((float)$a['p'], 2),
            ':b' => round((float)$a['c'] - (float)$a['p'], 2),
            ':f' => $folioId,
        ]);
    }
}
function e($value)
{
    return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
}

function fetch_value(PDO $pdo, $sql, array $params = [])
{
    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);
    return $stmt->fetchColumn();
}

function app_setting(PDO $pdo, $field, $default = '')
{
    static $settings;
    if ($settings === null) {
        try {
            $settings = $pdo->query("SELECT * FROM settings ORDER BY id ASC LIMIT 1")->fetch();
        } catch (Exception $e) {
            $settings = [];
        }
    }
    return isset($settings[$field]) && $settings[$field] !== '' ? $settings[$field] : $default;
}

function money($amount, $currency = '$')
{
    return $currency . number_format((float)$amount, 2);
}

function display_date($value)
{
    if (!$value || $value === '0000-00-00' || $value === '0000-00-00 00:00:00') {
        return '-';
    }
    $ts = strtotime($value);
    return $ts ? date('d M Y', $ts) : $value;
}

function display_datetime($value)
{
    if (!$value || $value === '0000-00-00 00:00:00') {
        return '-';
    }
    $ts = strtotime($value);
    return $ts ? date('d M Y H:i', $ts) : $value;
}

function badge_for_status($status)
{
    $status = (int)$status;
    switch ($status) {
        case 1: return ['Confirmed', 'success'];
        case 2: return ['Pending', 'warning'];
        case 3: return ['Checked In', 'primary'];
        case 4: return ['Checked Out', 'secondary'];
        case 0: return ['Cancelled', 'danger'];
        default: return ['Unknown', 'dark'];
    }
}

function payment_badge($status)
{
    $status = (int)$status;
    switch ($status) {
        case 1: return ['Paid', 'success'];
        case 2: return ['Pending', 'warning'];
        case 0: return ['Unpaid', 'danger'];
        default: return ['Unknown', 'dark'];
    }
}

function reservation_currency_symbol($currency)
{
    $currency = trim((string)$currency);
    if ($currency === '' || strtoupper($currency) === 'USD' || $currency === '1') {
        return '$';
    }
    return $currency . ' ';
}

function reservation_nights($checkIn, $checkOut)
{
    if (!$checkIn || !$checkOut) {
        return 0;
    }
    try {
        $start = new DateTime($checkIn);
        $end = new DateTime($checkOut);
        return max(0, (int)$start->diff($end)->days);
    } catch (Exception $e) {
        return 0;
    }
}

function build_order_no()
{
    return (string)time();
}

function detect_settings_name_field(PDO $pdo)
{
    static $field = null;
    if ($field !== null) {
        return $field;
    }
    try {
        $cols = $pdo->query("SHOW COLUMNS FROM settings")->fetchAll(PDO::FETCH_COLUMN);
        foreach (['app_name', 'name', 'company_name', 'title'] as $candidate) {
            if (in_array($candidate, $cols, true)) {
                $field = $candidate;
                return $field;
            }
        }
    } catch (Exception $e) {
    }
    $field = 'name';
    return $field;
}

function room_is_available(PDO $pdo, $roomId, $checkIn, $checkOut, $excludeOrderId = 0)
{
    $sql = "SELECT COUNT(*)
            FROM orders
            WHERE room_id = :room_id
              AND status <> 0
              AND id <> :exclude_id
              AND check_in < :check_out
              AND check_out > :check_in";
    $stmt = $pdo->prepare($sql);
    $stmt->execute([
        ':room_id' => (int)$roomId,
        ':exclude_id' => (int)$excludeOrderId,
        ':check_in' => $checkIn,
        ':check_out' => $checkOut,
    ]);
    return ((int)$stmt->fetchColumn()) === 0;
}

function sync_order_price_rows(PDO $pdo, $orderId, $checkIn, $checkOut, $roomId, $roomTypeId, $guestLabel, $pricePerNight)
{
    $pdo->prepare("DELETE FROM rel_orders_prices WHERE order_id = :id")->execute([':id' => (int)$orderId]);
    $start = new DateTime($checkIn);
    $end = new DateTime($checkOut);
    if ($end <= $start) {
        return;
    }
    $stmt = $pdo->prepare("INSERT INTO rel_orders_prices (order_id, date, guest, room_id, price, room_type_id)
                           VALUES (:order_id, :date, :guest, :room_id, :price, :room_type_id)");
    $cursor = clone $start;
    while ($cursor < $end) {
        $stmt->execute([
            ':order_id' => (int)$orderId,
            ':date' => $cursor->format('Y-m-d'),
            ':guest' => $guestLabel,
            ':room_id' => (int)$roomId,
            ':price' => (float)$pricePerNight,
            ':room_type_id' => (int)$roomTypeId,
        ]);
        $cursor->modify('+1 day');
    }
}

function order_room_label(array $row)
{
    if (!empty($row['room_no'])) {
        return 'Room ' . $row['room_no'];
    }
    return 'Unassigned';
}
