<?php
require('fpdf.php');

if (session_status() === PHP_SESSION_NONE) {
    session_start();
}

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

$orderId   = (int)($_GET['order_id'] ?? 0);
$bankParam = isset($_GET['bank']) ? $_GET['bank'] : 1;
$folioScopeTitle = 'MASTER FOLIO PROFORMA';
$folioScopeSuffix = 'Master_Folio';

if ($orderId <= 0) {
    exit('Missing order id');
}

/* -----------------------------
   Helpers
----------------------------- */
function pdf_txt($text)
{
    $text = (string)$text;
    return iconv('UTF-8', 'ISO-8859-1//TRANSLIT', $text);
}

function money_fmt_pdf($amount)
{
    return number_format((float)$amount, 2);
}

function safe_date_fmt_pdf($date, $format = 'd M Y')
{
    if (!$date) return '';
    $ts = strtotime((string)$date);
    return $ts ? date($format, $ts) : (string)$date;
}

function proforma_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];
}


function proforma_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];
}

function proforma_breakdown_label(string $txType, string $sourceTable, string $categoryName = ''): string
{
    $categoryName = trim($categoryName);
    if ($categoryName !== '') {
        return $categoryName;
    }

    $txType = strtolower(trim($txType));
    $sourceTable = strtolower(trim($sourceTable));

    if (in_array($txType, ['room_charge', 'accommodation', 'accommodation_charge', 'room', 'room_night', 'nightly'], true)
        || in_array($sourceTable, ['rel_orders_prices'], true)) {
        return 'Accommodation';
    }
    if (in_array($sourceTable, ['restaurant_menu_items'], true)) {
        if (in_array($txType, ['bar', 'drinks', 'drink'], true)) return 'Bar';
        if (in_array($txType, ['spar', 'spa'], true)) return 'Spa';
        return 'Restaurant';
    }
    if (in_array($sourceTable, ['inventory_items'], true)) {
        return 'Curio';
    }
    if (in_array($sourceTable, ['tblservices', 'services'], true)) {
        return 'Services';
    }

    $label = ucwords(str_replace('_', ' ', $txType));
    return $label !== '' ? $label : 'Other Charges';
}

function first_existing_column(PDO $pdo, string $table, array $columns): string
{
    foreach ($columns as $column) {
        if (proforma_column_exists($pdo, $table, $column)) {
            return $column;
        }
    }

    return '';
}

function build_room_mix_for_order(PDO $pdo, int $orderId, string $fallbackRoomType = '', int $fallbackRooms = 1): array
{
    $stmt = $pdo->prepare("
        SELECT
            rmg.room_type_id,
            rt.title AS room_type_title,
            COUNT(*) AS qty_rooms,
            SUM(COALESCE(rmg.adults, 0)) AS adults_total,
            SUM(COALESCE(rmg.children, 0)) AS children_total
        FROM order_rooming rmg
        LEFT JOIN room_types rt ON rt.id = rmg.room_type_id
        WHERE rmg.order_id = :order_id
        GROUP BY rmg.room_type_id, rt.title
        ORDER BY rt.title ASC, rmg.room_type_id ASC
    ");
    $stmt->execute([':order_id' => $orderId]);
    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];

    $parts = [];
    $detailRows = [];

    foreach ($rows as $row) {
        $title = trim((string)($row['room_type_title'] ?? ''));
        if ($title === '') {
            $title = 'Room Type #' . (int)($row['room_type_id'] ?? 0);
        }

        $qtyRooms = (int)($row['qty_rooms'] ?? 0);
        $adults = (int)($row['adults_total'] ?? 0);
        $children = (int)($row['children_total'] ?? 0);

        if ($qtyRooms <= 0) {
            continue;
        }

        $parts[] = $title . ' x ' . $qtyRooms;
        $detailRows[] = [
            'title' => $title,
            'qty_rooms' => $qtyRooms,
            'adults' => $adults,
            'children' => $children,
        ];
    }

    if (!$parts) {
        $fallbackRoomType = trim($fallbackRoomType);
        if ($fallbackRoomType === '') {
            $fallbackRoomType = 'Not set';
        }
        $fallbackRooms = max(1, $fallbackRooms);

        $parts[] = $fallbackRoomType . ' x ' . $fallbackRooms;
        $detailRows[] = [
            'title' => $fallbackRoomType,
            'qty_rooms' => $fallbackRooms,
            'adults' => 0,
            'children' => 0,
        ];
    }

    return [
        'summary' => implode(', ', $parts),
        'rows' => $detailRows,
    ];
}


function build_allocated_rooms_for_order(PDO $pdo, int $orderId): array
{
    $stmt = $pdo->prepare("
        SELECT
            rmg.room_no AS slot_no,
            rmg.room_id,
            rmg.room_type_id,
            rmg.adults,
            rmg.children,
            r.room_no AS actual_room_no,
            rt.title AS room_type_title
        FROM order_rooming rmg
        LEFT JOIN rooms r ON r.id = rmg.room_id
        LEFT JOIN room_types rt ON rt.id = rmg.room_type_id
        WHERE rmg.order_id = :order_id
        ORDER BY rmg.room_no ASC, rmg.id ASC
    ");
    $stmt->execute([':order_id' => $orderId]);
    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];

    $roomNumbers = [];
    $roomSummary = [];
    $detailRows = [];

    foreach ($rows as $row) {
        $slotNo = (int)($row['slot_no'] ?? 0);
        $roomNo = trim((string)($row['actual_room_no'] ?? ''));
        $roomType = trim((string)($row['room_type_title'] ?? ''));
        $adults = (int)($row['adults'] ?? 0);
        $children = (int)($row['children'] ?? 0);

        if ($roomNo !== '') {
            $roomNumbers[] = $roomNo;
            $label = $roomNo;
        } else {
            $label = 'Unassigned';
            if ($slotNo > 0) {
                $label .= ' Slot ' . $slotNo;
            }
        }

        if ($roomType !== '') {
            $label .= ' (' . $roomType . ')';
        }

        $paxParts = [];
        if ($adults > 0) $paxParts[] = $adults . 'A';
        if ($children > 0) $paxParts[] = $children . 'C';
        if ($paxParts) {
            $label .= ' - ' . implode('/', $paxParts);
        }

        $roomSummary[] = $label;
        $detailRows[] = [
            'slot_no' => $slotNo,
            'room_no' => $roomNo,
            'room_type' => $roomType,
            'adults' => $adults,
            'children' => $children,
        ];
    }

    return [
        'numbers' => $roomNumbers ? implode(', ', array_unique($roomNumbers)) : 'Not allocated',
        'summary' => $roomSummary ? implode('; ', $roomSummary) : 'No allocated rooms found',
        'rows' => $detailRows,
    ];
}

/* -----------------------------
   Booking / property query
----------------------------- */
$kids03Column = first_existing_column($pdo, 'orders', [
    'kids_0_3',
    'kids03',
    'kids_0_to_3',
    'children_0_3',
    'children03',
    'children_0_to_3',
    'infants',
    'infant',
    'babies',
    'toddlers'
]);

$resNotesColumn = first_existing_column($pdo, 'orders', [
    'res_notes',
    'reservation_notes',
    'reservation_note',
    'booking_notes',
    'booking_note',
    'notes',
    'note',
    'remarks',
    'special_requests',
    'special_request',
    'internal_notes'
]);

$kids03SelectSql = $kids03Column !== ''
    ? "COALESCE(o.`" . str_replace('`', '``', $kids03Column) . "`, 0) AS kids_0_3_count,"
    : "0 AS kids_0_3_count,";

$resNotesSelectSql = $resNotesColumn !== ''
    ? "COALESCE(o.`" . str_replace('`', '``', $resNotesColumn) . "`, '') AS reservation_notes,"
    : "'' AS reservation_notes,";

$stmt = $pdo->prepare("
    SELECT
        o.*,
        {$kids03SelectSql}
        {$resNotesSelectSql}
        g.firstname,
        g.lastname,
        rt.title AS room_type,
        p.name AS property_name,
        p.code AS property_code,
        p.currency_code AS property_currency_code,
        CONCAT(COALESCE(a.firstname,''), ' ', COALESCE(a.lastname,'')) AS agent_name
    FROM orders o
    LEFT JOIN guests g ON g.id = o.guest_id
    LEFT JOIN room_types rt ON rt.id = o.room_type_id
    LEFT JOIN properties p ON p.id = o.property_id
    LEFT JOIN agents a ON a.id = o.agent_id
    WHERE o.id = :id
    LIMIT 1
");
$stmt->execute([':id' => $orderId]);
$order = $stmt->fetch(PDO::FETCH_ASSOC);

if (!$order) {
    exit('Booking not found');
}

$propertyId = (int)($order['property_id'] ?? 0);

$guestName = trim(($order['firstname'] ?? '') . ' ' . ($order['lastname'] ?? ''));
if ($guestName === '') $guestName = 'Guest';

$agentName = trim((string)($order['agent_name'] ?? ''));
if ($agentName === '') $agentName = 'Direct';

$currencyCode = trim((string)($order['property_currency_code'] ?? 'USD'));
if ($currencyCode === '') {
    $currencyCode = 'USD';
}

$currency  = $currencyCode;
$symbolMap = [
    'USD' => '$',
    'EUR' => '€',
    'GBP' => '£',
    'ZAR' => 'R',
    'ZWG' => 'ZWG ',
    'ZWL' => 'ZWL ',
    'BWP' => 'P',
];
$symbol = $symbolMap[$currencyCode] ?? ($currencyCode . ' ');

$roomMix = build_room_mix_for_order(
    $pdo,
    $orderId,
    (string)($order['room_type'] ?? ''),
    (int)($order['totalrooms'] ?? 1)
);
$roomMixSummary = (string)($roomMix['summary'] ?? '');
$roomMixRows = $roomMix['rows'] ?? [];

$allocatedRoomInfo = build_allocated_rooms_for_order($pdo, $orderId);
$allocatedRoomNumbersText = (string)($allocatedRoomInfo['numbers'] ?? 'Not allocated');
$allocatedRoomSummaryText = (string)($allocatedRoomInfo['summary'] ?? 'No allocated rooms found');

$kids03Count = (int)($order['kids_0_3_count'] ?? 0);
$reservationNotes = trim((string)($order['reservation_notes'] ?? ''));
$reservationNotes = preg_replace('/\s+/', ' ', $reservationNotes);

/* -----------------------------
   Booking folio totals
----------------------------- */
/*
   Proforma totals must be calculated from live folio_transactions,
   not stale folio totals.

   Important:
   - Voided payments must NOT increase charges.
   - Original voided payments must NOT count as payments.
   - Separate reversal/void rows must reduce payments, not add invoice value.
*/
$stmt = $pdo->prepare("
    SELECT
        COALESCE(SUM(
            CASE
                WHEN COALESCE(ft.is_voided, 0) = 0
                     AND COALESCE(ft.debit, 0) > 0
                     AND LOWER(COALESCE(ft.tx_type, '')) NOT IN (
                        'payment_void','void_payment','payment_reversal',
                        'reversal','refund','payment_refund'
                     )
                     AND LOWER(COALESCE(ft.description, '')) NOT LIKE '%void%'
                     AND LOWER(COALESCE(ft.description, '')) NOT LIKE '%reversal%'
                     AND LOWER(COALESCE(ft.description, '')) NOT LIKE '%refund%'
                THEN COALESCE(ft.debit, 0)
                ELSE 0
            END
        ), 0) AS total_charges,

        /*
           Gross payments received:
           This intentionally includes original payment rows even if later voided,
           so the summary can show Payment 100 and Void 100 separately.
        */
        COALESCE(SUM(
            CASE
                WHEN (
                        LOWER(COALESCE(ft.tx_type, '')) IN (
                            'payment','receipt','deposit','payment_received',
                            'folio_payment','guest_payment'
                        )
                        OR LOWER(COALESCE(ft.source_table, '')) IN (
                            'payments','payment','receipts','receipt'
                        )
                     )
                     AND COALESCE(ft.credit, 0) > 0
                THEN COALESCE(ft.credit, 0)
                ELSE 0
            END
        ), 0) AS gross_payments,

        /*
           Voided/reversed payments:
           Supports original payment row marked is_voided=1 and separate reversal rows.
        */
        COALESCE(SUM(
            CASE
                WHEN COALESCE(ft.is_voided, 0) = 1
                     AND COALESCE(ft.credit, 0) > 0
                THEN COALESCE(ft.credit, 0)

                WHEN COALESCE(ft.is_voided, 0) = 0
                     AND (
                        LOWER(COALESCE(ft.tx_type, '')) IN (
                            'payment_void','void_payment','payment_reversal',
                            'reversal','refund','payment_refund'
                        )
                        OR LOWER(COALESCE(ft.description, '')) LIKE '%void%'
                        OR LOWER(COALESCE(ft.description, '')) LIKE '%reversal%'
                        OR LOWER(COALESCE(ft.description, '')) LIKE '%refund%'
                     )
                     AND COALESCE(ft.debit, 0) > 0
                THEN COALESCE(ft.debit, 0)

                WHEN COALESCE(ft.is_voided, 0) = 0
                     AND (
                        LOWER(COALESCE(ft.tx_type, '')) IN (
                            'payment_void','void_payment','payment_reversal',
                            'reversal','refund','payment_refund'
                        )
                        OR LOWER(COALESCE(ft.description, '')) LIKE '%void%'
                        OR LOWER(COALESCE(ft.description, '')) LIKE '%reversal%'
                        OR LOWER(COALESCE(ft.description, '')) LIKE '%refund%'
                     )
                     AND COALESCE(ft.credit, 0) < 0
                THEN ABS(COALESCE(ft.credit, 0))

                ELSE 0
            END
        ), 0) AS voided_payments
    FROM folio_transactions ft
    INNER JOIN folios fscope ON fscope.id = ft.folio_id
    WHERE ft.order_id = :order_id
      AND fscope.folio_type = 'master'
");
$stmt->execute([':order_id' => $orderId]);
$folioTotals = $stmt->fetch(PDO::FETCH_ASSOC);

$totalCharges   = (float)($folioTotals['total_charges'] ?? 0);
$grossPayments  = (float)($folioTotals['gross_payments'] ?? 0);
$voidedPayments = (float)($folioTotals['voided_payments'] ?? 0);

/*
   Display payments gross, display void separately, but calculate balance using net payments.
   Example: charges 350, payment 100, void 100 => balance 350.
*/
$totalPayments = $grossPayments;
$netPayments   = max(0, $grossPayments - $voidedPayments);
$totalBalance  = max(0, $totalCharges - $netPayments);

/* 
   Accommodation total:
   Proforma must always include accommodation, even if folio_transactions
   currently only contain extras. Prefer nightly prices because they include
   any nightly/component overrides. Fallback to orders.totalamount.
*/
$stmt = $pdo->prepare("
    SELECT COALESCE(SUM(total), 0)
    FROM rel_orders_prices
    WHERE order_id = :order_id
");
$stmt->execute([':order_id' => $orderId]);
$accommodationTotal = (float)($stmt->fetchColumn() ?: 0);

$bookingTotal = (float)($order['totalamount'] ?? 0);
if ($accommodationTotal <= 0 && $bookingTotal > 0) {
    $accommodationTotal = $bookingTotal;
}

$folioChargeTotal = $totalCharges;
$hasAccommodationPosted = false;

/* -----------------------------
   Representative folio
   (for displaying a folio context only)
----------------------------- */
$stmt = $pdo->prepare("
    SELECT *
    FROM folios
    WHERE order_id = :order_id
      AND folio_type = 'master'
    ORDER BY id ASC
    LIMIT 1
");
$stmt->execute([':order_id' => $orderId]);
$displayFolio = $stmt->fetch(PDO::FETCH_ASSOC);

$displayFolioNo = (string)($displayFolio['folio_no'] ?? '');
$displayFolioName = trim((string)($displayFolio['folio_name'] ?? 'Booking Folios'));
if ($displayFolioName === '') {
    $displayFolioName = 'Booking Folios';
}

/* -----------------------------
   Company profile
   Source: lodge_settings first, fallback to old tblmailinglist.
   Layout stays unchanged; only data source is changed.
----------------------------- */
$company = [
    'CompanyName'     => defined('APP_NAME') ? APP_NAME : 'RoomZ Lodge',
    'LegalName'       => '',
    'Postal'          => '',
    'Street'          => '',
    'City'            => '',
    'StateRegion'     => '',
    'Country'         => '',
    'Email'           => '',
    'Phone'           => '',
    'AltPhone'        => '',
    'Website'         => '',
    'VATNO'           => '',
    'TIN'             => '',
    'CompanyRegNo'    => '',
    'Motto'           => 'Reservations • Folios • Payments',
    'ProformaPrefix'  => 'PF',
    'CurrencyCode'    => '',
    'LogoPath'        => '',
    'PaymentTerms'    => '',
    'Terms'           => '',
    'Footer'          => ''
];

try {
    $loadedCompany = false;

    if (proforma_table_exists($pdo, 'lodge_settings')) {
        try {
            $stmtLodge = $pdo->query("SELECT * FROM lodge_settings ORDER BY id ASC LIMIT 1");
            $lodge = $stmtLodge ? $stmtLodge->fetch(PDO::FETCH_ASSOC) : false;

            if ($lodge) {
                $company['CompanyName']    = trim((string)($lodge['lodge_name'] ?? '')) !== '' ? (string)$lodge['lodge_name'] : $company['CompanyName'];
                $company['LegalName']      = (string)($lodge['legal_name'] ?? '');
                $company['Motto']          = trim((string)($lodge['tag_line'] ?? '')) !== '' ? (string)$lodge['tag_line'] : $company['Motto'];
                $company['Email']          = (string)($lodge['email'] ?? '');
                $company['Phone']          = (string)($lodge['phone'] ?? '');
                $company['AltPhone']       = (string)($lodge['alt_phone'] ?? '');
                $company['Website']        = (string)($lodge['website'] ?? '');
                $company['Street']         = trim((string)($lodge['address_line1'] ?? ''));
                $company['Postal']         = trim((string)($lodge['address_line2'] ?? ''));
                $company['City']           = trim((string)($lodge['city'] ?? ''));
                $company['StateRegion']    = trim((string)($lodge['state_region'] ?? ''));
                $company['Country']        = trim((string)($lodge['country'] ?? ''));
                $company['VATNO']          = (string)($lodge['vat_number'] ?? '');
                $company['TIN']            = (string)($lodge['tin_number'] ?? '');
                $company['CompanyRegNo']   = (string)($lodge['company_reg_number'] ?? '');
                $company['ProformaPrefix'] = trim((string)($lodge['invoice_prefix'] ?? '')) !== '' ? (string)$lodge['invoice_prefix'] : $company['ProformaPrefix'];
                $company['CurrencyCode']   = (string)($lodge['currency_code'] ?? '');
                $company['LogoPath']       = (string)($lodge['logo_path'] ?? '');
                $company['PaymentTerms']   = (string)($lodge['payment_terms'] ?? '');
                $company['Terms']          = (string)($lodge['terms_and_conditions'] ?? '');
                $company['Footer']         = (string)($lodge['invoice_footer'] ?? '');

                $loadedCompany = true;
            }
        } catch (Throwable $e) {
            $loadedCompany = false;
        }
    }

    if (!$loadedCompany) {
        $address = false;

        try {
            $stmtCompany = $pdo->prepare("
                SELECT *
                FROM tblmailinglist
                WHERE property_id = :property_id
                ORDER BY ID DESC
                LIMIT 1
            ");
            $stmtCompany->execute([':property_id' => $propertyId]);
            $address = $stmtCompany->fetch(PDO::FETCH_ASSOC);
        } catch (Throwable $e) {
            $address = false;
        }

        if (!$address) {
            try {
                $q = $pdo->query("SELECT * FROM tblmailinglist WHERE ID = 2 LIMIT 1");
                $address = $q ? $q->fetch(PDO::FETCH_ASSOC) : false;
            } catch (Throwable $e) {
                $address = false;
            }
        }

        if ($address) {
            $company['CompanyName']    = $address['CompanyName'] ?: $company['CompanyName'];
            $company['Postal']         = $address['Postal'] ?? '';
            $company['Street']         = $address['Street'] ?? '';
            $company['City']           = $address['City'] ?? '';
            $company['Email']          = $address['Email'] ?? '';
            $company['Phone']          = $address['Phone'] ?? '';
            $company['Website']        = $address['Website'] ?? '';
            $company['VATNO']          = $address['VATNO'] ?? '';
            $company['TIN']            = $address['TIN'] ?? '';
            $company['Motto']          = $address['Motto'] ?: $company['Motto'];
            $company['ProformaPrefix'] = $address['ProformaPrefix'] ?: $company['ProformaPrefix'];
        }
    }
} catch (Throwable $e) {
    // keep defaults
}

$propertyDisplay = trim((string)($order['property_name'] ?? ''));
if ($propertyDisplay !== '' && stripos($company['CompanyName'], $propertyDisplay) === false) {
    $company['Motto'] = $propertyDisplay . ' • ' . $company['Motto'];
}

/* -----------------------------
   Charge lines across booking
----------------------------- */
$stmt = $pdo->prepare("
    SELECT
        ft.*, 
        f.folio_name,
        f.folio_type,
        r.room_no
    FROM folio_transactions ft
    INNER JOIN folios f ON f.id = ft.folio_id
    LEFT JOIN rooms r ON r.id = f.room_id
    WHERE ft.order_id = :order_id
      AND f.folio_type = 'master'
      AND COALESCE(ft.is_voided, 0) = 0
      AND ft.debit > 0
      AND LOWER(COALESCE(ft.tx_type, '')) NOT IN (
            'payment_void','void_payment','payment_reversal',
            'reversal','refund','payment_refund'
      )
      AND LOWER(COALESCE(ft.description, '')) NOT LIKE '%void%'
      AND LOWER(COALESCE(ft.description, '')) NOT LIKE '%reversal%'
      AND LOWER(COALESCE(ft.description, '')) NOT LIKE '%refund%'
    ORDER BY ft.tx_date ASC, ft.id ASC
");
$stmt->execute([':order_id' => $orderId]);
$lines = $stmt->fetchAll(PDO::FETCH_ASSOC);

/*
   New Extras posting support:
   Extras now create a linked extra_charge_items row for each folio transaction.
   Use that table for clearer proforma descriptions and reporting categories,
   but keep totals based on folio_transactions so accounting stays unchanged.
*/
$linkedExtraItemsByTx = [];
if (proforma_table_exists($pdo, 'extra_charge_items')) {
    try {
        $stmtExtraItems = $pdo->prepare("
            SELECT
                eci.*,
                ft.id AS folio_transaction_id,
                ft.tx_date,
                ft.tx_type,
                ft.debit AS transaction_debit,
                ft.source_table AS transaction_source_table,
                ft.source_id AS transaction_source_id,
                f.folio_name,
                f.folio_type,
                r.room_no
            FROM extra_charge_items eci
            INNER JOIN folio_transactions ft ON ft.id = eci.folio_transaction_id
            INNER JOIN folios f ON f.id = ft.folio_id
            LEFT JOIN rooms r ON r.id = f.room_id
            WHERE ft.order_id = :order_id
              AND f.folio_type = 'master'
              AND COALESCE(ft.is_voided, 0) = 0
              AND ft.debit > 0
            ORDER BY ft.tx_date ASC, ft.id ASC, eci.id ASC
        ");
        $stmtExtraItems->execute([':order_id' => $orderId]);
        foreach ($stmtExtraItems->fetchAll(PDO::FETCH_ASSOC) as $extraItemRow) {
            $txId = (int)($extraItemRow['folio_transaction_id'] ?? 0);
            if ($txId <= 0) {
                continue;
            }
            if (!isset($linkedExtraItemsByTx[$txId])) {
                $linkedExtraItemsByTx[$txId] = [];
            }
            $linkedExtraItemsByTx[$txId][] = $extraItemRow;
        }
    } catch (Throwable $e) {
        $linkedExtraItemsByTx = [];
    }
}

if ($linkedExtraItemsByTx) {
    $expandedLines = [];
    foreach ($lines as $line) {
        $txId = (int)($line['id'] ?? 0);
        if ($txId > 0 && !empty($linkedExtraItemsByTx[$txId])) {
            foreach ($linkedExtraItemsByTx[$txId] as $itemRow) {
                $itemName = trim((string)($itemRow['item_name'] ?? ''));
                if ($itemName === '') {
                    $itemName = trim((string)($line['description'] ?? 'Extra Item'));
                }

                $itemCode = trim((string)($itemRow['item_code'] ?? ''));
                $categoryName = trim((string)($itemRow['category_name'] ?? ''));
                $qty = (float)($itemRow['qty'] ?? 0);
                $unitPrice = (float)($itemRow['unit_price'] ?? 0);
                $lineTotal = (float)($itemRow['line_total'] ?? 0);

                $cleanDescription = '';
                if ($categoryName !== '') {
                    $cleanDescription .= $categoryName . ' - ';
                }
                $cleanDescription .= $itemName;
                if ($itemCode !== '') {
                    $cleanDescription .= ' [' . $itemCode . ']';
                }
                if (abs($qty) > 0.00001) {
                    $cleanDescription .= ' x ' . money_fmt_pdf($qty);
                    if (abs($unitPrice) > 0.00001) {
                        $cleanDescription .= ' @ ' . money_fmt_pdf($unitPrice);
                    }
                }

                $newLine = $line;
                $newLine['description'] = $cleanDescription;
                $newLine['debit'] = abs($lineTotal) > 0.00001 ? $lineTotal : (float)($line['debit'] ?? 0);
                $newLine['source_table'] = (string)($itemRow['source_table'] ?? ($line['source_table'] ?? ''));
                $newLine['source_id'] = (int)($itemRow['source_id'] ?? ($line['source_id'] ?? 0));
                $newLine['breakdown_category'] = proforma_breakdown_label(
                    (string)($line['tx_type'] ?? ''),
                    (string)($newLine['source_table'] ?? ''),
                    $categoryName
                );
                $expandedLines[] = $newLine;
            }
        } else {
            $line['breakdown_category'] = proforma_breakdown_label(
                (string)($line['tx_type'] ?? ''),
                (string)($line['source_table'] ?? ''),
                ''
            );
            $expandedLines[] = $line;
        }
    }
    $lines = $expandedLines;
} else {
    foreach ($lines as &$line) {
        $line['breakdown_category'] = proforma_breakdown_label(
            (string)($line['tx_type'] ?? ''),
            (string)($line['source_table'] ?? ''),
            ''
        );
    }
    unset($line);
}

$chargeBreakdown = [];
foreach ($lines as $line) {
    $label = trim((string)($line['breakdown_category'] ?? ''));
    if ($label === '') {
        $label = proforma_breakdown_label((string)($line['tx_type'] ?? ''), (string)($line['source_table'] ?? ''), '');
    }
    $amount = (float)($line['debit'] ?? 0);
    if ($amount <= 0) {
        continue;
    }
    if (!isset($chargeBreakdown[$label])) {
        $chargeBreakdown[$label] = 0.0;
    }
    $chargeBreakdown[$label] += $amount;
}

/*
   Detect whether accommodation is already posted as a folio transaction.
   This avoids duplicating accommodation if your folio posting already includes it.
*/
foreach ($lines as $line) {
    $txType = strtolower(trim((string)($line['tx_type'] ?? '')));
    $sourceTable = strtolower(trim((string)($line['source_table'] ?? '')));
    $desc = strtolower(trim((string)($line['description'] ?? '')));

    $isExtra = in_array($txType, ['extra', 'extras', 'extra_charge', 'addon', 'add_on', 'transfer', 'restaurant', 'minibar', 'laundry', 'park_fee'], true)
        || in_array($sourceTable, ['tblservices', 'services', 'restaurant_menu_items', 'inventory_items', 'extra_charge_items', 'extras', 'order_extras', 'booking_extras', 'reservation_extras', 'order_extra_items', 'manual_extra'], true);

    $isAccommodation = in_array($txType, ['accommodation', 'room', 'room_charge', 'accommodation_charge', 'nightly', 'room_night'], true)
        || strpos($desc, 'accommodation') !== false
        || strpos($desc, 'room charge') !== false
        || strpos($desc, 'night') !== false;

    if ($isAccommodation && !$isExtra) {
        $hasAccommodationPosted = true;
        break;
    }
}

/*
   If accommodation is not yet posted to the folio, add a provisional
   accommodation line before extras. This fixes proforma showing extras only.
*/
if (!$hasAccommodationPosted && $accommodationTotal > 0) {
    $fallbackDate = !empty($order['ordered_on']) ? $order['ordered_on'] : $order['check_in'];
    $fallbackDesc = 'Accommodation charges for booking ' . ($order['order_no'] ?: $orderId);

    if ($roomMixSummary !== '') {
        $fallbackDesc .= ' (' . $roomMixSummary . ')';
    }
    if ($allocatedRoomNumbersText !== '' && $allocatedRoomNumbersText !== 'Not allocated') {
        $fallbackDesc .= ' - Rooms: ' . $allocatedRoomNumbersText;
    }

    array_unshift($lines, [
        'tx_date'      => $fallbackDate,
        'description'  => $fallbackDesc,
        'debit'        => $accommodationTotal,
        'folio_type'   => 'master',
        'room_no'      => '',
        'folio_name'   => 'Booking Folio',
        'tx_type'      => 'accommodation',
        'source_table' => 'rel_orders_prices',
        'breakdown_category' => 'Accommodation'
    ]);

    if (!isset($chargeBreakdown['Accommodation'])) {
        $chargeBreakdown['Accommodation'] = 0.0;
    }
    $chargeBreakdown['Accommodation'] += $accommodationTotal;
}

if (!$hasAccommodationPosted && $accommodationTotal > 0) {
    $totalCharges = $accommodationTotal + $folioChargeTotal;
} elseif ($totalCharges <= 0 && $accommodationTotal > 0) {
    $totalCharges = $accommodationTotal;
}

$netPayments = max(0, $totalPayments - $voidedPayments);
$totalBalance = max(0, $totalCharges - $netPayments);

/* -----------------------------
   Bank details
   Source: lodge_settings first, fallback to old tblbankdetails.
----------------------------- */
$bankId = is_numeric($bankParam) ? (int)$bankParam : 1;
if ($bankId <= 0) $bankId = 1;

$bankDetails = [
    'SwiftCode'   => '',
    'BranchCode'  => '',
    'Branch'      => '',
    'Bank'        => '',
    'AccountNo'   => '',
    'AccountName' => ''
];

try {
    $loadedBank = false;

    if (proforma_table_exists($pdo, 'lodge_settings')) {
        try {
            $stmtLodgeBank = $pdo->query("SELECT bank_name, bank_account_name, bank_account_no, bank_branch, bank_swift FROM lodge_settings ORDER BY id ASC LIMIT 1");
            $lodgeBank = $stmtLodgeBank ? $stmtLodgeBank->fetch(PDO::FETCH_ASSOC) : false;

            if ($lodgeBank) {
                $bankDetails['Bank']        = (string)($lodgeBank['bank_name'] ?? '');
                $bankDetails['AccountName'] = (string)($lodgeBank['bank_account_name'] ?? '');
                $bankDetails['AccountNo']   = (string)($lodgeBank['bank_account_no'] ?? '');
                $bankDetails['Branch']      = (string)($lodgeBank['bank_branch'] ?? '');
                $bankDetails['SwiftCode']   = (string)($lodgeBank['bank_swift'] ?? '');

                $loadedBank = trim($bankDetails['Bank'] . $bankDetails['AccountName'] . $bankDetails['AccountNo']) !== '';
            }
        } catch (Throwable $e) {
            $loadedBank = false;
        }
    }

    if (!$loadedBank) {
        $rowbank = false;

        try {
            $stmtBank = $pdo->prepare("
                SELECT *
                FROM tblbankdetails
                WHERE property_id = :property_id
                  AND ID = :id
                LIMIT 1
            ");
            $stmtBank->execute([
                ':property_id' => $propertyId,
                ':id'          => $bankId,
            ]);
            $rowbank = $stmtBank->fetch(PDO::FETCH_ASSOC);
        } catch (Throwable $e) {
            $rowbank = false;
        }

        if (!$rowbank) {
            try {
                $stmtBank = $pdo->prepare("
                    SELECT *
                    FROM tblbankdetails
                    WHERE property_id = :property_id
                    ORDER BY ID ASC
                    LIMIT 1
                ");
                $stmtBank->execute([':property_id' => $propertyId]);
                $rowbank = $stmtBank->fetch(PDO::FETCH_ASSOC);
            } catch (Throwable $e) {
                $rowbank = false;
            }
        }

        if (!$rowbank) {
            try {
                $stmtBank = $pdo->prepare("SELECT * FROM tblbankdetails WHERE ID = :id LIMIT 1");
                $stmtBank->execute([':id' => $bankId]);
                $rowbank = $stmtBank->fetch(PDO::FETCH_ASSOC);
            } catch (Throwable $e) {
                $rowbank = false;
            }
        }

        if ($rowbank) {
            $bankDetails['SwiftCode']   = $rowbank['SwiftCode'] ?? '';
            $bankDetails['BranchCode']  = $rowbank['BranchCode'] ?? '';
            $bankDetails['Branch']      = $rowbank['Branch'] ?? '';
            $bankDetails['Bank']        = $rowbank['Bank'] ?? '';
            $bankDetails['AccountNo']   = $rowbank['AccountNo'] ?? '';
            $bankDetails['AccountName'] = $rowbank['AccountName'] ?? '';
        }
    }
} catch (Throwable $e) {
    // keep defaults
}

/* -----------------------------
   PDF - Kaufeld-style layout structure
   Layout only: top document bar, company hero, 3 summary cards,
   item table, bank/totals grid, notes/footer.
----------------------------- */
class PDF extends FPDF
{
    public $company = [];
    public $docTitle = 'PROFORMA INVOICE';
    public $docRef = '';

    function RoundedRect($x, $y, $w, $h, $r, $style = '')
    {
        $k = $this->k;
        $hp = $this->h;
        if ($style == 'F') {
            $op = 'f';
        } elseif ($style == 'FD' || $style == 'DF') {
            $op = 'B';
        } else {
            $op = 'S';
        }
        $MyArc = 4 / 3 * (sqrt(2) - 1);
        $this->_out(sprintf('%.2F %.2F m', ($x + $r) * $k, ($hp - $y) * $k));
        $xc = $x + $w - $r;
        $yc = $y + $r;
        $this->_out(sprintf('%.2F %.2F l', $xc * $k, ($hp - $y) * $k));
        $this->_Arc($xc + $r * $MyArc, $yc - $r, $xc + $r, $yc - $r * $MyArc, $xc + $r, $yc);
        $xc = $x + $w - $r;
        $yc = $y + $h - $r;
        $this->_out(sprintf('%.2F %.2F l', ($x + $w) * $k, ($hp - $yc) * $k));
        $this->_Arc($xc + $r, $yc + $r * $MyArc, $xc + $r * $MyArc, $yc + $r, $xc, $yc + $r);
        $xc = $x + $r;
        $yc = $y + $h - $r;
        $this->_out(sprintf('%.2F %.2F l', $xc * $k, ($hp - ($y + $h)) * $k));
        $this->_Arc($xc - $r * $MyArc, $yc + $r, $xc - $r, $yc + $r * $MyArc, $xc - $r, $yc);
        $xc = $x + $r;
        $yc = $y + $r;
        $this->_out(sprintf('%.2F %.2F l', $x * $k, ($hp - $yc) * $k));
        $this->_Arc($xc - $r, $yc - $r * $MyArc, $xc - $r * $MyArc, $yc - $r, $xc, $yc - $r);
        $this->_out($op);
    }

    function _Arc($x1, $y1, $x2, $y2, $x3, $y3)
    {
        $h = $this->h;
        $this->_out(sprintf('%.2F %.2F %.2F %.2F %.2F %.2F c ',
            $x1 * $this->k, ($h - $y1) * $this->k,
            $x2 * $this->k, ($h - $y2) * $this->k,
            $x3 * $this->k, ($h - $y3) * $this->k));
    }

    function Header()
    {
        // Intentionally blank. The document header is drawn manually on page 1
        // to keep the layout close to the supplied reference.
    }

    function Footer()
    {
        $this->SetY(-15);
        $this->SetDrawColor(196, 205, 214);
        $this->Line(10, $this->GetY(), 200, $this->GetY());
        $this->SetY(-11);
        $this->SetFont('Arial', '', 7.5);
        $this->SetTextColor(45, 55, 70);
        $this->Cell(95, 5, 'Prepared By: Reservations Department', 0, 0, 'L');
        $this->Cell(55, 5, 'Generated On: ' . date('d-M-Y H:i'), 0, 0, 'C');
        $this->Cell(40, 5, 'Page ' . $this->PageNo() . ' of {nb}', 0, 0, 'R');
    }

    function docTopBar(string $title, string $ref): void
    {
        $this->SetFillColor(36, 50, 66);
        $this->SetDrawColor(36, 50, 66);
        $this->RoundedRect(10, 8, 190, 12, 3, 'F');
        $this->SetTextColor(255, 255, 255);
        $this->SetFont('Arial', 'B', 14);
        $this->SetXY(18, 10.5);
        $this->Cell(90, 6, pdf_txt($title), 0, 0, 'L');
        $this->SetFont('Arial', 'B', 10);
        $this->SetXY(115, 10.8);
        $this->Cell(78, 6, pdf_txt('Ref: ' . $ref), 0, 0, 'R');
    }

    function card($x, $y, $w, $h, $title = '', $icon = ''): void
    {
        $this->SetFillColor(255, 255, 255);
        $this->SetDrawColor(157, 169, 184);
        $this->RoundedRect($x, $y, $w, $h, 3, 'D');
        if ($title !== '') {
            $this->SetFont('Arial', 'B', 8.5);
            $this->SetTextColor(18, 28, 43);
            $this->SetXY($x + 6, $y + 5);
            $this->Cell($w - 12, 5, pdf_txt($title), 0, 0, 'L');
            $this->SetDrawColor(222, 226, 232);
            $this->Line($x, $y + 13, $x + $w, $y + 13);
        }
    }

    function labelValue($x, $y, $label, $value, $w = 60, $labelW = 24, $h = 6): void
    {
        $this->SetXY($x, $y);
        $this->SetFont('Arial', 'B', 8);
        $this->SetTextColor(18, 28, 43);
        $this->Cell($labelW, $h, pdf_txt($label), 0, 0, 'L');
        $this->SetFont('Arial', '', 8);
        $this->Cell($w - $labelW, $h, pdf_txt($value), 0, 0, 'R');
    }

    function addItemsHeader(): void
    {
        $this->SetFillColor(36, 50, 66);
        $this->SetTextColor(255, 255, 255);
        $this->SetFont('Arial', 'B', 8.5);
        $this->SetX(10);
        $this->Cell(9, 8, '#', 0, 0, 'C', true);
        $this->Cell(23, 8, 'DATE', 0, 0, 'L', true);
        $this->Cell(98, 8, 'DESCRIPTION', 0, 0, 'L', true);
        $this->Cell(27, 8, 'FOLIO', 0, 0, 'L', true);
        $this->Cell(33, 8, 'AMOUNT', 0, 1, 'R', true);
    }

    function checkPageSpace(float $needed): void
    {
        if ($this->GetY() + $needed > 266) {
            $this->AddPage();
            $this->SetY(18);
            $this->addItemsHeader();
        }
    }
}

$proformaPrefix = trim((string)($company['ProformaPrefix'] ?? 'PF'));
if ($proformaPrefix === '') {
    $proformaPrefix = 'PF';
}
if (!empty($order['property_code'])) {
    $proformaPrefix .= '-' . strtoupper((string)$order['property_code']);
}
$proformaNo = $proformaPrefix . '-' . $orderId;

$pdf = new PDF();
$pdf->company  = $company;
$pdf->docTitle = $folioScopeTitle;
$pdf->docRef   = $proformaNo;
$pdf->AliasNbPages();
$pdf->AddPage();
$pdf->SetAutoPageBreak(true, 18);

/* -----------------------------
   Top document bar + company hero
----------------------------- */
$pdf->docTopBar($folioScopeTitle, $proformaNo);

$pdf->SetFillColor(255, 255, 255);
$pdf->SetDrawColor(157, 169, 184);
$pdf->RoundedRect(10, 20, 190, 48, 3, 'D');

$companyName = (string)($company['CompanyName'] ?? 'Company');
$pdf->SetTextColor(18, 28, 43);
$pdf->SetFont('Arial', 'B', 14);
$pdf->SetXY(16, 27);
$pdf->Cell(95, 6, pdf_txt($companyName), 0, 1, 'L');

$pdf->SetFont('Arial', 'B', 8.5);
$pdf->SetX(16);
$pdf->Cell(95, 5, pdf_txt($companyName), 0, 1, 'L');

$pdf->SetFont('Arial', 'I', 8.7);
$pdf->SetTextColor(90, 90, 90);
$pdf->SetX(16);
$pdf->Cell(95, 5, pdf_txt(trim((string)($company['Motto'] ?? 'Reservations | Folios | Payments'))), 0, 1, 'L');
$pdf->SetTextColor(18, 28, 43);
$pdf->Ln(3);
$pdf->SetX(16);
$pdf->Cell(95, 5, pdf_txt(trim((string)($company['Street'] ?? ''))), 0, 1, 'L');
$pdf->SetX(16);
$companyAddressLine2 = trim(implode(' ', array_filter([
    (string)($company['Postal'] ?? ''),
    (string)($company['City'] ?? ''),
    (string)($company['StateRegion'] ?? ''),
    (string)($company['Country'] ?? '')
], static function ($v) { return trim((string)$v) !== ''; })));
$pdf->Cell(95, 5, pdf_txt($companyAddressLine2), 0, 1, 'L');
$pdf->SetX(16);
$companyTelText = (string)($company['Phone'] ?? '');
if (trim((string)($company['AltPhone'] ?? '')) !== '') {
    $companyTelText .= ' / ' . (string)$company['AltPhone'];
}
$pdf->Cell(95, 5, pdf_txt('Email: ' . (string)($company['Email'] ?? '') . '   Tel: ' . $companyTelText), 0, 1, 'L');
$pdf->SetX(16);
$companyTaxLineParts = [];
if (trim((string)($company['TIN'] ?? '')) !== '') {
    $companyTaxLineParts[] = 'TIN: ' . (string)$company['TIN'];
}
if (trim((string)($company['VATNO'] ?? '')) !== '') {
    $companyTaxLineParts[] = 'VAT: ' . (string)$company['VATNO'];
}
if (trim((string)($company['CompanyRegNo'] ?? '')) !== '') {
    $companyTaxLineParts[] = 'Reg No: ' . (string)$company['CompanyRegNo'];
}
$pdf->Cell(95, 5, pdf_txt(implode('   |   ', $companyTaxLineParts)), 0, 1, 'L');

$logo = __DIR__ . '/att.png';
$lodgeLogoPath = trim((string)($company['LogoPath'] ?? ''));
if ($lodgeLogoPath !== '') {
    $candidateLogo = $lodgeLogoPath;
    if (!preg_match('/^([A-Za-z]:)?[\\\/]/', $candidateLogo)) {
        $candidateLogo = __DIR__ . '/../../' . ltrim($candidateLogo, '/');
    }
    if (file_exists($candidateLogo)) {
        $logo = $candidateLogo;
    }
}
if (file_exists($logo)) {
    $pdf->Image($logo, 128, 31, 58);
}

/* -----------------------------
   Client + wide booking summary cards
   Final polish: remove duplicated "From" card because company details
   already appear in the hero header. Summary gets more width and cleaner rows.
----------------------------- */
$propertyLabel = trim((string)($order['property_name'] ?? ''));
if (!empty($order['property_code'])) {
    $propertyLabel .= ($propertyLabel !== '' ? ' (' . $order['property_code'] . ')' : $order['property_code']);
}

$cardY = 75;
$clientW = 70;
$summaryX0 = 85;
$summaryW = 115;

/* CLIENT CARD */
$pdf->card(10, $cardY, $clientW, 61, 'CLIENT / PROFORMA TO');
$pdf->SetTextColor(18, 28, 43);
$pdf->SetFont('Arial', 'B', 8.5);
$pdf->SetXY(14, $cardY + 17);
$pdf->MultiCell($clientW - 8, 4.7, pdf_txt($agentName), 0, 'L');

$pdf->SetFont('Arial', '', 7.8);
$pdf->SetXY(14, $cardY + 29);
$pdf->MultiCell($clientW - 8, 4.4, pdf_txt('Guest: ' . $guestName), 0, 'L');
$pdf->SetXY(14, $cardY + 38);
$pdf->MultiCell($clientW - 8, 4.4, pdf_txt('Property: ' . ($propertyLabel !== '' ? $propertyLabel : 'Not set')), 0, 'L');
$pdf->SetXY(14, $cardY + 47);
$pdf->SetFont('Arial', '', 7.8);
$pdf->MultiCell($clientW - 8, 4.2, pdf_txt('Room(s): ' . $allocatedRoomNumbersText), 0, 'L');
$pdf->SetXY(14, $cardY + 55);
$pdf->SetFont('Arial', 'B', 7.8);
$pdf->Cell($clientW - 8, 4.5, pdf_txt('Currency: ' . $currency), 0, 1, 'L');

/* WIDE BOOKING SUMMARY CARD */
$pdf->card($summaryX0, $cardY, $summaryW, 61, 'BOOKING SUMMARY');

$paxText = (int)($order['totalrooms'] ?? 1) . ' rooms | ' . (int)($order['adults'] ?? 0) . 'A / ' . (int)($order['kids'] ?? 0) . 'C';
if ($kids03Count > 0) {
    $paxText .= ' / ' . $kids03Count . ' kids 0-3yrs';
}

$leftX = $summaryX0 + 6;
$rightX = $summaryX0 + 61;
$rowY = $cardY + 16;

$summaryLeft = [
    ['Proforma No.', $proformaNo],
    ['Proforma Date', safe_date_fmt_pdf(date('Y-m-d'), 'd-M-Y')],
    ['Booking Ref', (string)($order['order_no'] ?: $orderId)],
    ['Room(s)', $allocatedRoomNumbersText],
];
$summaryRight = [
    ['Check In', safe_date_fmt_pdf($order['check_in'], 'd-M-Y')],
    ['Check Out', safe_date_fmt_pdf($order['check_out'], 'd-M-Y')],
    ['Folio No.', $displayFolioNo !== '' ? $displayFolioNo : '-'],
    ['Guest', $guestName],
];

for ($i = 0; $i < 4; $i++) {
    $y = $rowY + ($i * 7.5);
    $pdf->SetDrawColor(232, 236, 241);
    if ($i > 0) {
        $pdf->Line($summaryX0 + 4, $y - 1.5, $summaryX0 + $summaryW - 4, $y - 1.5);
    }

    $pdf->SetXY($leftX, $y);
    $pdf->SetFont('Arial', 'B', 7.5);
    $pdf->SetTextColor(18, 28, 43);
    $pdf->Cell(25, 5, pdf_txt($summaryLeft[$i][0]), 0, 0, 'L');
    $pdf->SetFont('Arial', '', 7.5);
    $pdf->Cell(25, 5, pdf_txt($summaryLeft[$i][1]), 0, 0, 'R');

    $pdf->SetXY($rightX, $y);
    $pdf->SetFont('Arial', 'B', 7.5);
    $pdf->Cell(22, 5, pdf_txt($summaryRight[$i][0]), 0, 0, 'L');
    $pdf->SetFont('Arial', '', 7.5);
    $pdf->Cell(28, 5, pdf_txt($summaryRight[$i][1]), 0, 0, 'R');
}

/* Highlighted pax strip inside summary */
$pdf->SetFillColor(245, 247, 249);
$pdf->SetDrawColor(222, 226, 232);
$pdf->RoundedRect($summaryX0 + 5, $cardY + 50, $summaryW - 10, 8, 2, 'DF');
$pdf->SetXY($summaryX0 + 8, $cardY + 52);
$pdf->SetFont('Arial', 'B', 7.8);
$pdf->SetTextColor(18, 28, 43);
$pdf->Cell(18, 4, 'Pax', 0, 0, 'L');
$pdf->SetFont('Arial', '', 7.5);
$pdf->Cell($summaryW - 34, 4, pdf_txt($paxText), 0, 1, 'R');

/* -----------------------------
   Room mix mini row
----------------------------- */
$pdf->SetY(141);
$pdf->SetFont('Arial', 'B', 8.5);
$pdf->SetTextColor(18, 28, 43);
$pdf->Cell(25, 5, 'Room No(s):', 0, 0, 'L');
$pdf->SetFont('Arial', '', 8.3);
$pdf->MultiCell(165, 5, pdf_txt($allocatedRoomSummaryText), 0, 'L');

$pdf->SetFont('Arial', 'B', 8.5);
$pdf->SetTextColor(18, 28, 43);
$pdf->Cell(25, 5, 'Room Mix:', 0, 0, 'L');
$pdf->SetFont('Arial', '', 8.3);
$pdf->MultiCell(165, 5, pdf_txt($roomMixSummary !== '' ? $roomMixSummary : 'Not set'), 0, 'L');

if ($kids03Count > 0) {
    $pdf->SetFont('Arial', 'I', 8);
    $pdf->Cell(190, 5, pdf_txt('Kids 0-3yrs booked: ' . $kids03Count), 0, 1, 'L');
}

/* -----------------------------
   Items table
----------------------------- */
$pdf->Ln(2);
$pdf->addItemsHeader();
$pdf->SetTextColor(18, 28, 43);
$pdf->SetFont('Arial', '', 8);

$idx = 1;
$fill = false;
foreach ($lines as $line) {
    $pdf->checkPageSpace(13);

    $dateTxt = safe_date_fmt_pdf($line['tx_date'], 'd-M-y');
    $descTxt = (string)($line['description'] ?? '');
    $amt = (float)($line['debit'] ?? 0);

    $folioShort = 'Master';
    if (($line['folio_type'] ?? '') === 'room') {
        $folioShort = 'Room';
        if (!empty($line['room_no'])) {
            $folioShort .= ' ' . $line['room_no'];
        }
    } elseif (($line['folio_type'] ?? '') === 'guest') {
        $folioShort = 'Guest';
    }

    $rowY = $pdf->GetY();
    $descLines = max(1, ceil(strlen($descTxt) / 62));
    $rowH = max(9, min(20, $descLines * 4.5));
    $pdf->SetFillColor($fill ? 248 : 255, $fill ? 250 : 255, $fill ? 252 : 255);
    $pdf->SetDrawColor(222, 226, 232);

    $pdf->SetX(10);
    $pdf->Cell(9, $rowH, (string)$idx, 1, 0, 'C', true);
    $pdf->Cell(23, $rowH, pdf_txt($dateTxt), 1, 0, 'L', true);
    $xDesc = $pdf->GetX();
    $pdf->MultiCell(98, 4.5, pdf_txt($descTxt), 1, 'L', true);
    $pdf->SetXY($xDesc + 98, $rowY);
    $pdf->Cell(27, $rowH, pdf_txt($folioShort), 1, 0, 'L', true);
    $pdf->Cell(33, $rowH, money_fmt_pdf($amt), 1, 1, 'R', true);

    $fill = !$fill;
    $idx++;
}

if (empty($lines)) {
    $pdf->SetX(10);
    $pdf->Cell(190, 10, 'No charge items found.', 1, 1, 'C');
}

/* -----------------------------
   Bank + totals grid
----------------------------- */
if ($pdf->GetY() > 205) {
    $pdf->AddPage();
    $pdf->SetY(18);
} else {
    $pdf->Ln(5);
}

$gridY = $pdf->GetY();
$breakdownPrintable = [];
foreach ($chargeBreakdown as $breakdownLabel => $breakdownAmount) {
    if (abs((float)$breakdownAmount) <= 0.00001) {
        continue;
    }
    $breakdownPrintable[] = [$breakdownLabel . ' (' . $currency . ')', money_fmt_pdf($breakdownAmount), false];
}
if (count($breakdownPrintable) > 8) {
    usort($breakdownPrintable, function($a, $b) {
        return ((float)str_replace(',', '', $b[1])) <=> ((float)str_replace(',', '', $a[1]));
    });
    $breakdownPrintable = array_slice($breakdownPrintable, 0, 8);
}
$summaryLineCount = count($breakdownPrintable) + 3 + ($voidedPayments > 0 ? 1 : 0);
$bankH = max(45, 18 + ($summaryLineCount * 6.5) + 14);
$pdf->card(10, $gridY, 92, $bankH, 'BANK DETAILS - ALL PAYMENTS TO BE MADE TO');
$pdf->SetFont('Arial', '', 8);
$pdf->SetTextColor(18, 28, 43);
$by = $gridY + 16;
$bankRows = [
    ['Account Name', (string)$bankDetails['AccountName']],
    ['Account Number', (string)$bankDetails['AccountNo']],
    ['Bank', (string)$bankDetails['Bank']],
    ['Branch', (string)$bankDetails['Branch']],
    ['Currency', $currency],
];
foreach ($bankRows as $br) {
    $pdf->SetXY(14, $by);
    $pdf->SetFont('Arial', 'B', 7.7);
    $pdf->Cell(34, 5.5, pdf_txt($br[0]), 0, 0, 'L');
    $pdf->SetFont('Arial', '', 7.7);
    $pdf->Cell(49, 5.5, pdf_txt($br[1]), 0, 1, 'L');
    $by += 6;
}

$pdf->card(106, $gridY, 94, $bankH, '');
$tx = 112;
$ty = $gridY + 7;
$pdf->SetFont('Arial', '', 8.2);
$pdf->SetTextColor(18, 28, 43);
$totRows = [];
if ($breakdownPrintable) {
    foreach ($breakdownPrintable as $brk) {
        $totRows[] = $brk;
    }
    $totRows[] = ['', '', false];
}
$totRows[] = ['Charges (' . $currency . ')', money_fmt_pdf($totalCharges), false];
$totRows[] = ['Payments (' . $currency . ')', money_fmt_pdf($totalPayments), false];
if ($voidedPayments > 0) {
    $totRows[] = ['Voided (' . $currency . ')', money_fmt_pdf($voidedPayments), false];
}
$totRows[] = ['Balance Due (' . $currency . ')', money_fmt_pdf($totalBalance), true];
foreach ($totRows as $tr) {
    if ($tr[0] === '' && $tr[1] === '') {
        $pdf->SetDrawColor(222, 226, 232);
        $pdf->Line($tx, $ty + 2, $tx + 80, $ty + 2);
        $ty += 4;
        continue;
    }
    $pdf->SetXY($tx, $ty);
    $pdf->SetFont('Arial', $tr[2] ? 'B' : '', $tr[2] ? 8.2 : 7.8);
    $pdf->Cell(45, 6, pdf_txt($tr[0]), 0, 0, 'L');
    $pdf->Cell(35, 6, pdf_txt($tr[1]), 0, 1, 'R');
    $ty += 6.2;
}
$pdf->SetFillColor(36, 50, 66);
$pdf->RoundedRect(106, $gridY + $bankH - 12, 94, 12, 2, 'F');
$pdf->SetTextColor(255, 255, 255);
$pdf->SetFont('Arial', 'B', 11);
$pdf->SetXY(112, $gridY + $bankH - 9.5);
$pdf->Cell(38, 7, 'TOTAL DUE', 0, 0, 'L');
$pdf->SetFont('Arial', 'B', 12);
$pdf->Cell(44, 7, pdf_txt($currency . ' ' . money_fmt_pdf($totalBalance)), 0, 1, 'R');

/* -----------------------------
   Notes card
   Dynamic height so notes never overflow the rounded rectangle.
----------------------------- */
$notesText = "Please make payment to the account details provided.\nKindly use the proforma number as payment reference.";
if (trim((string)($company['PaymentTerms'] ?? '')) !== '') {
    $notesText .= "\nPayment Terms: " . trim((string)$company['PaymentTerms']);
}
if ($reservationNotes !== '') {
    $notesText .= "\nReservation Notes: " . $reservationNotes;
}
if (trim((string)($company['Footer'] ?? '')) !== '') {
    $notesText .= "\n" . trim((string)$company['Footer']);
}

$notesText = trim((string)$notesText);
$notesLineWidthChars = 98;
$notesLines = 0;
foreach (preg_split('/\r\n|\r|\n/', $notesText) as $noteLine) {
    $noteLine = trim((string)$noteLine);
    $notesLines += max(1, (int)ceil(strlen($noteLine) / $notesLineWidthChars));
}

$notesLineHeight = 4.6;
$notesTopPadding = 16;
$notesBottomPadding = 7;
$notesH = max(24, $notesTopPadding + ($notesLines * $notesLineHeight) + $notesBottomPadding);
$notesH = min($notesH, 235);

$notesY = $gridY + $bankH + 6;
if ($notesY + $notesH > 270) {
    $pdf->AddPage();
    $notesY = 20;
}

if ($notesY + $notesH > 270) {
    $notesH = 270 - $notesY;
}

$pdf->card(10, $notesY, 190, $notesH, 'NOTES');
$pdf->SetXY(14, $notesY + 16);
$pdf->SetFont('Arial', '', 8);
$pdf->SetTextColor(18, 28, 43);
$pdf->MultiCell(182, $notesLineHeight, pdf_txt($notesText), 0, 'L');

$pdf->Output($folioScopeSuffix . '_Proforma_' . ($order['order_no'] ?: $orderId) . '.pdf', 'I');
exit;
