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

$waitlistSupport = __DIR__ . '/waitlist_support.php';
if (file_exists($waitlistSupport)) {
    require_once $waitlistSupport;
}

if (!function_exists('cr_redirect')) {
    function cr_redirect(string $url): void
    {
        header('Location: ' . $url);
        exit;
    }
}

if (!function_exists('cr_fail')) {
    function cr_fail(string $message): void
    {
        cr_redirect('/rooms/modules/reservations/reservation_list.php?cancel_error=1&msg=' . urlencode($message));
    }
}

if (!function_exists('cr_column_exists')) {
    function cr_column_exists(PDO $pdo, string $table, string $column): bool
    {
        static $cache = [];
        $key = $table . '.' . $column;

        if (array_key_exists($key, $cache)) {
            return $cache[$key];
        }

        try {
            $stmt = $pdo->prepare("
                SELECT COUNT(*)
                FROM INFORMATION_SCHEMA.COLUMNS
                WHERE TABLE_SCHEMA = DATABASE()
                  AND TABLE_NAME = :table_name
                  AND COLUMN_NAME = :column_name
            ");
            $stmt->execute([
                ':table_name'  => $table,
                ':column_name' => $column,
            ]);
            $cache[$key] = ((int)$stmt->fetchColumn() > 0);
        } catch (Throwable $e) {
            $cache[$key] = false;
        }

        return $cache[$key];
    }
}

if (!function_exists('cr_table_exists')) {
    function cr_table_exists(PDO $pdo, string $table): bool
    {
        static $cache = [];

        if (array_key_exists($table, $cache)) {
            return $cache[$table];
        }

        try {
            $stmt = $pdo->prepare("
                SELECT COUNT(*)
                FROM INFORMATION_SCHEMA.TABLES
                WHERE TABLE_SCHEMA = DATABASE()
                  AND TABLE_NAME = :table_name
            ");
            $stmt->execute([':table_name' => $table]);
            $cache[$table] = ((int)$stmt->fetchColumn() > 0);
        } catch (Throwable $e) {
            $cache[$table] = false;
        }

        return $cache[$table];
    }
}

if (!function_exists('cr_money')) {
    function cr_money($amount): float
    {
        return round((float)$amount, 2);
    }
}

if (!function_exists('cr_get_current_user_id')) {
    function cr_get_current_user_id(): int
    {
        foreach (['user_id', 'id', 'userid'] as $key) {
            if (!empty($_SESSION[$key])) {
                return (int)$_SESSION[$key];
            }
        }
        return 0;
    }
}

if (!function_exists('cr_get_or_create_master_folio')) {
    function cr_get_or_create_master_folio(PDO $pdo, array $order): int
    {
        $stmt = $pdo->prepare("
            SELECT id
            FROM folios
            WHERE order_id = :order_id
              AND folio_type = 'master'
            ORDER BY id ASC
            LIMIT 1
        ");
        $stmt->execute([':order_id' => (int)$order['id']]);
        $folioId = (int)($stmt->fetchColumn() ?: 0);

        if ($folioId > 0) {
            return $folioId;
        }

        $folioNo = 'FOL-M-' . (int)$order['id'];
        $folioName = 'Master Folio - ' . (!empty($order['order_no']) ? $order['order_no'] : ('Booking #' . (int)$order['id']));

        $cols = ['property_id', 'order_id', 'folio_no', 'folio_name', 'folio_type', 'status', 'total_charges', 'total_payments', 'balance'];
        $vals = [':property_id', ':order_id', ':folio_no', ':folio_name', "'master'", '1', '0', '0', '0'];
        $params = [
            ':property_id' => (int)($order['property_id'] ?? 0),
            ':order_id'    => (int)$order['id'],
            ':folio_no'    => $folioNo,
            ':folio_name'  => $folioName,
        ];

        $sql = "INSERT INTO folios (" . implode(',', $cols) . ") VALUES (" . implode(',', $vals) . ")";
        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);

        return (int)$pdo->lastInsertId();
    }
}

if (!function_exists('cr_recalculate_folio_totals')) {
    function cr_recalculate_folio_totals(PDO $pdo, int $folioId): void
    {
        if ($folioId <= 0) {
            return;
        }

        $stmt = $pdo->prepare("
            SELECT
                COALESCE(SUM(debit), 0) AS debits,
                COALESCE(SUM(credit), 0) AS credits
            FROM folio_transactions
            WHERE folio_id = :folio_id
              AND COALESCE(is_voided, 0) = 0
        ");
        $stmt->execute([':folio_id' => $folioId]);
        $totals = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];

        $charges = cr_money($totals['debits'] ?? 0);
        $payments = cr_money($totals['credits'] ?? 0);
        $balance = cr_money($charges - $payments);

        $set = [
            'total_charges = :charges',
            'total_payments = :payments',
            'balance = :balance',
        ];

        if (cr_column_exists($pdo, 'folios', 'updated_at')) {
            $set[] = 'updated_at = NOW()';
        }

        $stmt = $pdo->prepare("
            UPDATE folios
            SET " . implode(', ', $set) . "
            WHERE id = :id
        ");
        $stmt->execute([
            ':charges'  => $charges,
            ':payments' => $payments,
            ':balance'  => $balance,
            ':id'       => $folioId,
        ]);
    }
}

if (!function_exists('cr_sync_order_payment_status')) {
    function cr_sync_order_payment_status(PDO $pdo, int $orderId): void
    {
        if ($orderId <= 0 || !cr_column_exists($pdo, 'orders', 'payment_status')) {
            return;
        }

        $stmt = $pdo->prepare("
            SELECT
                COALESCE(SUM(total_charges), 0) AS charges,
                COALESCE(SUM(total_payments), 0) AS payments,
                COALESCE(SUM(balance), 0) AS balance
            FROM folios
            WHERE order_id = :order_id
        ");
        $stmt->execute([':order_id' => $orderId]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC) ?: [];

        $charges = (float)($row['charges'] ?? 0);
        $payments = (float)($row['payments'] ?? 0);
        $balance = (float)($row['balance'] ?? 0);

        $paymentStatus = 0;
        if ($payments > 0.00001) {
            $paymentStatus = ($balance <= 0.00001 || $payments >= $charges) ? 2 : 1;
        }

        $stmt = $pdo->prepare("
            UPDATE orders
            SET payment_status = :payment_status
            WHERE id = :id
        ");
        $stmt->execute([
            ':payment_status' => $paymentStatus,
            ':id'             => $orderId,
        ]);
    }
}

if (!function_exists('cr_booking_total')) {
    function cr_booking_total(PDO $pdo, int $orderId, array $order): float
    {
        $stmt = $pdo->prepare("
            SELECT COALESCE(SUM(total), 0)
            FROM rel_orders_prices
            WHERE order_id = :order_id
        ");
        $stmt->execute([':order_id' => $orderId]);
        $roomTotal = (float)($stmt->fetchColumn() ?: 0);

        if ($roomTotal <= 0) {
            $roomTotal = (float)($order['totalamount'] ?? 0);
        }

        $stmt = $pdo->prepare("
            SELECT COALESCE(SUM(debit - credit), 0)
            FROM folio_transactions
            WHERE order_id = :order_id
              AND COALESCE(is_voided, 0) = 0
              AND (
                    LOWER(COALESCE(tx_type, '')) IN (
                        'extra','extras','extra_charge','addon','add_on','transfer',
                        'restaurant','minibar','laundry','park_fee','activity','service','bar','curio'
                    )
                    OR LOWER(COALESCE(source_table, '')) IN (
                        'tblservices','services','extras','order_extras','booking_extras',
                        'reservation_extras','order_extra_items','manual_extra'
                    )
                  )
        ");
        $stmt->execute([':order_id' => $orderId]);
        $extrasTotal = (float)($stmt->fetchColumn() ?: 0);

        return cr_money($roomTotal + max(0, $extrasTotal));
    }
}

if (!function_exists('cr_post_cancellation_fee')) {
    function cr_post_cancellation_fee(PDO $pdo, array $order, float $amount, string $reason): void
    {
        if ($amount <= 0) {
            return;
        }

        $orderId = (int)$order['id'];
        $folioId = cr_get_or_create_master_folio($pdo, $order);

        /*
           Avoid duplicate cancellation fee if user refreshes / resubmits.
        */
        $stmt = $pdo->prepare("
            SELECT COUNT(*)
            FROM folio_transactions
            WHERE order_id = :order_id
              AND folio_id = :folio_id
              AND LOWER(COALESCE(tx_type, '')) = 'cancellation_fee'
              AND COALESCE(is_voided, 0) = 0
        ");
        $stmt->execute([
            ':order_id' => $orderId,
            ':folio_id' => $folioId,
        ]);

        if ((int)$stmt->fetchColumn() > 0) {
            cr_recalculate_folio_totals($pdo, $folioId);
            return;
        }

        $description = '100% cancellation fee';
        if ($reason !== '') {
            $description .= ' - ' . $reason;
        }

        $cols = ['property_id', 'folio_id', 'order_id', 'tx_date', 'tx_type', 'description', 'debit', 'credit', 'source_table', 'source_id'];
        $vals = [':property_id', ':folio_id', ':order_id', 'NOW()', ':tx_type', ':description', ':debit', '0', ':source_table', ':source_id'];

        if (cr_column_exists($pdo, 'folio_transactions', 'created_by')) {
            $cols[] = 'created_by';
            $vals[] = ':created_by';
        }

        $params = [
            ':property_id'  => (int)($order['property_id'] ?? 0),
            ':folio_id'     => $folioId,
            ':order_id'     => $orderId,
            ':tx_type'      => 'cancellation_fee',
            ':description'  => $description,
            ':debit'        => cr_money($amount),
            ':source_table' => 'orders',
            ':source_id'    => $orderId,
        ];

        if (in_array(':created_by', $vals, true)) {
            $params[':created_by'] = cr_get_current_user_id();
        }

        $stmt = $pdo->prepare("
            INSERT INTO folio_transactions (" . implode(',', $cols) . ")
            VALUES (" . implode(',', $vals) . ")
        ");
        $stmt->execute($params);

        cr_recalculate_folio_totals($pdo, $folioId);
    }
}

if (!function_exists('cr_audit')) {
    /*
       Real-time availability push to Nightsbridge after a cancellation, so the
       freed room is re-opened on the channel within seconds instead of waiting
       for the 02:00 full flush. Without this, a cancelled room stays SOLD on
       Nightsbridge until the nightly sweep - an under-sell (lost booking), not
       an oversell. Same proven pattern as move_booking.php:
         - called strictly AFTER the cancel transaction has committed
         - only fires for the Nightsbridge-connected property
         - all failures swallowed - a slow/dead push can never break the
           cancellation itself; the nightly flush is the backstop.
    */
    function cr_nightsbridge_availability_push(PDO $pdo, int $propertyId, array $roomTypeIds): void
    {
        try {
            if (!function_exists('nb_run_inventory_push')) {
                $candidates = array(
                    __DIR__ . '/../../api/nightsbridge_inventory_push.php',
                    __DIR__ . '/../api/nightsbridge_inventory_push.php',
                    rtrim((string)(isset($_SERVER['DOCUMENT_ROOT']) ? $_SERVER['DOCUMENT_ROOT'] : ''), '/') . '/rooms/api/nightsbridge_inventory_push.php',
                );
                foreach ($candidates as $lib) {
                    if (is_file($lib)) {
                        if (!defined('NB_INVENTORY_PUSH_LIB')) {
                            define('NB_INVENTORY_PUSH_LIB', true);
                        }
                        require_once $lib;
                        break;
                    }
                }
            }
            if (!function_exists('nb_run_inventory_push')) {
                return; // library not deployed - the nightly flush covers it
            }
            if (defined('NIGHTSBRIDGE_PROPERTY_ID') && $propertyId !== (int)NIGHTSBRIDGE_PROPERTY_ID) {
                return;
            }

            $ids = array_values(array_unique(array_filter(array_map('intval', $roomTypeIds))));
            nb_run_inventory_push($pdo, $ids ?: null, 'cancel_reservation', false);
        } catch (Throwable $e) {
            // Intentionally silent - see docblock.
        }
    }
}

if (!function_exists('cr_audit')) {
    function cr_audit(PDO $pdo, int $orderId, string $action, string $details): void
    {
        try {
            if (cr_table_exists($pdo, 'reservation_audit_log')) {
                $stmt = $pdo->prepare("
                    INSERT INTO reservation_audit_log
                    (order_id, action, details, user_id, created_at)
                    VALUES
                    (:order_id, :action, :details, :user_id, NOW())
                ");
                $stmt->execute([
                    ':order_id' => $orderId,
                    ':action'   => $action,
                    ':details'  => $details,
                    ':user_id'  => cr_get_current_user_id(),
                ]);
                return;
            }

            if (cr_table_exists($pdo, 'audit_log')) {
                $stmt = $pdo->prepare("
                    INSERT INTO audit_log
                    (entity_type, entity_id, action, details, user_id, created_at)
                    VALUES
                    ('reservation', :entity_id, :action, :details, :user_id, NOW())
                ");
                $stmt->execute([
                    ':entity_id' => $orderId,
                    ':action'    => $action,
                    ':details'   => $details,
                    ':user_id'   => cr_get_current_user_id(),
                ]);
            }
        } catch (Throwable $e) {
            /*
               Audit failure should not block cancellation.
            */
        }
    }
}

try {
    if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST' && function_exists('verify_csrf')) {
        verify_csrf();
    }

    $orderId = 0;

    if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
        $orderId = (int)($_POST['order_id'] ?? $_POST['id'] ?? 0);
    } else {
        $orderId = (int)($_GET['order_id'] ?? $_GET['id'] ?? 0);
    }

    if ($orderId <= 0) {
        throw new RuntimeException('Invalid booking selected.');
    }

    $cancelMode = trim((string)($_POST['cancel_mode'] ?? $_GET['cancel_mode'] ?? 'cancel_only'));
    if (!in_array($cancelMode, ['cancel_only', 'cancel_and_charge_full'], true)) {
        $cancelMode = 'cancel_only';
    }

    $cancelReason = trim((string)($_POST['cancel_reason'] ?? $_GET['cancel_reason'] ?? ''));

    if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST' && $cancelReason === '') {
        throw new RuntimeException('Cancellation reason is required.');
    }

    $stmt = $pdo->prepare("
        SELECT *
        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.');
    }

    /*
       Capture the room type(s) this booking occupies BEFORE the transaction
       zeroes order_rooming.room_id below - we need them for the post-commit
       Nightsbridge push so the freed inventory re-opens on the channel. Use
       COALESCE(physical room type, rooming's own room_type_id) to match the
       push library's own occupancy logic, so it resolves whether or not a
       physical room was assigned.
    */
    $nbAffectedTypeIds = array();
    try {
        $rmgHasType = cr_column_exists($pdo, 'order_rooming', 'room_type_id');
        $rtidExpr = $rmgHasType
            ? 'COALESCE(r.room_type_id, rmg.room_type_id)'
            : 'r.room_type_id';
        $typeStmt = $pdo->prepare("
            SELECT DISTINCT {$rtidExpr} AS rtid
            FROM order_rooming rmg
            LEFT JOIN rooms r ON r.id = rmg.room_id
            WHERE rmg.order_id = :order_id
        ");
        $typeStmt->execute([':order_id' => $orderId]);
        foreach ($typeStmt->fetchAll(PDO::FETCH_COLUMN) as $rtid) {
            if ((int)$rtid > 0) {
                $nbAffectedTypeIds[] = (int)$rtid;
            }
        }
    } catch (Throwable $nbTypeErr) {
        // Non-fatal: fall back to a full push (null) if we can't resolve types.
        $nbAffectedTypeIds = array();
    }

    $pdo->beginTransaction();

    $set = ['status = 0'];
    $params = [':id' => $orderId];

    if (cr_column_exists($pdo, 'orders', 'cancelled_at')) {
        $set[] = 'cancelled_at = NOW()';
    }
    if (cr_column_exists($pdo, 'orders', 'cancelled_by')) {
        $set[] = 'cancelled_by = :cancelled_by';
        $params[':cancelled_by'] = cr_get_current_user_id();
    }
    if (cr_column_exists($pdo, 'orders', 'cancel_reason')) {
        $set[] = 'cancel_reason = :cancel_reason';
        $params[':cancel_reason'] = $cancelReason;
    }
    if (cr_column_exists($pdo, 'orders', 'cancellation_reason')) {
        $set[] = 'cancellation_reason = :cancellation_reason';
        $params[':cancellation_reason'] = $cancelReason;
    }
    if (cr_column_exists($pdo, 'orders', 'cancel_mode')) {
        $set[] = 'cancel_mode = :cancel_mode';
        $params[':cancel_mode'] = $cancelMode;
    }
    if (cr_column_exists($pdo, 'orders', 'updated_at')) {
        $set[] = 'updated_at = NOW()';
    }

    $stmt = $pdo->prepare("
        UPDATE orders
        SET " . implode(', ', $set) . "
        WHERE id = :id
    ");
    $stmt->execute($params);

    /*
       Keep rooming rows for history, but release assigned physical rooms
       if your schema supports room_id. Availability is mainly released because
       orders.status = 0 is excluded by availability checks.
    */
    if (cr_column_exists($pdo, 'order_rooming', 'room_id')) {
        $stmt = $pdo->prepare("
            UPDATE order_rooming
            SET room_id = 0
            WHERE order_id = :order_id
        ");
        $stmt->execute([':order_id' => $orderId]);
    }

    if ($cancelMode === 'cancel_and_charge_full') {
        $bookingTotal = cr_booking_total($pdo, $orderId, $order);
        cr_post_cancellation_fee($pdo, $order, $bookingTotal, $cancelReason);
    }

    /*
       Recalculate all folios for this order.
    */
    $stmt = $pdo->prepare("
        SELECT id
        FROM folios
        WHERE order_id = :order_id
    ");
    $stmt->execute([':order_id' => $orderId]);
    $folioIds = $stmt->fetchAll(PDO::FETCH_COLUMN) ?: [];

    foreach ($folioIds as $folioId) {
        cr_recalculate_folio_totals($pdo, (int)$folioId);
    }

    cr_sync_order_payment_status($pdo, $orderId);

    cr_audit(
        $pdo,
        $orderId,
        'cancel_reservation',
        'Mode: ' . $cancelMode . '; Reason: ' . $cancelReason
    );

    $pdo->commit();

    /*
       Real-time Nightsbridge push AFTER commit - re-opens the freed room on the
       channel within seconds instead of waiting for the 02:00 nightly flush.
       Fully guarded and silent: a slow or failed push can never affect the
       cancellation, which is already committed. The nightly flush is the backstop.
    */
    cr_nightsbridge_availability_push(
        $pdo,
        (int)($order['property_id'] ?? 0),
        $nbAffectedTypeIds
    );

    /*
       Refresh waitlist after transaction. This should not break cancellation.
    */
    try {
        if (function_exists('refresh_waitlist_notifications')) {
            refresh_waitlist_notifications($pdo);
        }
    } catch (Throwable $e) {
        // ignore waitlist refresh errors
    }

    cr_redirect('/rooms/modules/reservations/reservation_list.php?cancelled=1');
} catch (Throwable $e) {
    if (isset($pdo) && $pdo instanceof PDO && $pdo->inTransaction()) {
        $pdo->rollBack();
    }

    cr_fail($e->getMessage());
}
