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

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

require_once __DIR__ . '/../../includes/bootstrap.php';

/*
 * Outlook add-in authentication bridge.
 *
 * Normal browser requests still require the standard RoomZ login.
 * Only a valid, unexpired add-in session tied to this exact booking
 * can bypass require_login() for PDF generation.
 */
$addinAuthenticated = false;
$addinToken = trim((string)($_GET['addin_token'] ?? ''));
$addinOrderId = (int)($_GET['order_id'] ?? 0);

if ($addinToken !== '' && $addinOrderId > 0) {
    try {
        require_once __DIR__ . '/../communications/lib.php';

        $stmtAddin = $pdo->prepare("
            SELECT id, user_id, order_id
            FROM communication_addin_sessions
            WHERE session_hash = :session_hash
              AND order_id = :order_id
              AND revoked_at IS NULL
              AND expires_at > NOW()
            LIMIT 1
        ");
        $stmtAddin->execute([
            ':session_hash' => comm_hash_token($addinToken),
            ':order_id'     => $addinOrderId,
        ]);

        $addinSession = $stmtAddin->fetch(PDO::FETCH_ASSOC);
        $addinAuthenticated = (bool)$addinSession;

        if ($addinAuthenticated) {
            try {
                $pdo->prepare("
                    UPDATE communication_addin_sessions
                    SET last_used_at = NOW()
                    WHERE id = :id
                ")->execute([
                    ':id' => (int)$addinSession['id'],
                ]);
            } catch (Throwable $ignored) {
                // PDF generation should not fail only because audit timestamp update failed.
            }
        }
    } catch (Throwable $addinError) {
        error_log(
            'Proforma Outlook authentication failed: '
            . $addinError->getMessage()
        );
        $addinAuthenticated = false;
    }
}

if (!$addinAuthenticated) {
    require_login();
}

$orderId   = (int)($_GET['order_id'] ?? 0);
$bankParam = isset($_GET['bank']) ? $_GET['bank'] : 1;

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 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,
            COALESCE(NULLIF(r.room_no, ''), NULLIF(CAST(rmg.room_id AS CHAR), ''), '') AS actual_room_no,
            rt.title AS room_type_title,
            rmg.adults,
            rmg.children
        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) ?: [];

    $allocated = [];
    $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'] ?? ''));

        if ($roomNo === '' || $roomNo === '0') {
            $roomNo = 'Unassigned';
        }

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

        $allocated[] = $label;
        $detailRows[] = [
            'slot_no'   => $slotNo,
            'room_no'   => $roomNo,
            'room_type' => $roomType,
            'adults'    => (int)($row['adults'] ?? 0),
            'children'  => (int)($row['children'] ?? 0),
        ];
    }

    return [
        'summary' => $allocated ? implode(', ', $allocated) : 'Unassigned',
        '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'] ?? [];

$allocatedRooms = build_allocated_rooms_for_order($pdo, $orderId);
$allocatedRoomSummary = (string)($allocatedRooms['summary'] ?? 'Unassigned');
$allocatedRoomRows = $allocatedRooms['rows'] ?? [];

/* -----------------------------------------------------------------
   Group bookings jumble the header when every room number is listed
   individually (e.g. "1 (Double Room), 2 (Double Room), ...").
   For anything above the threshold, collapse the header display to
   room types and quantities ("Double Room x 11") using the room mix.
   The full per-room allocation is still available lower down / in the
   detail table for small bookings.
------------------------------------------------------------------ */
$GROUP_ROOM_THRESHOLD = 3; // bookings with more than this many rooms are treated as groups
$allocatedRoomCount = count($allocatedRoomRows);
$isGroupBooking = $allocatedRoomCount > $GROUP_ROOM_THRESHOLD;

// What the header cells should show for "Room(s)".
$headerRoomSummary = $allocatedRoomCount . ' rooms';

/* -----------------------------------------------------------------
   Real pax totals. The orders header columns (totalrooms/adults/kids)
   are unreliable for group bookings imported via bulk/channel paths,
   so derive rooms + adults + children from the rooming rows instead.
------------------------------------------------------------------ */
$paxRooms = 0;
$paxAdults = 0;
$paxChildren = 0;
foreach ($roomMixRows as $mixRow) {
    $paxRooms    += (int)($mixRow['qty_rooms'] ?? 0);
    $paxAdults   += (int)($mixRow['adults'] ?? 0);
    $paxChildren += (int)($mixRow['children'] ?? 0);
}
if ($paxRooms <= 0) {
    $paxRooms = (int)($order['totalrooms'] ?? 1);
}
if ($paxAdults <= 0 && $paxChildren <= 0) {
    $paxAdults   = (int)($order['adults'] ?? 0);
    $paxChildren = (int)($order['kids'] ?? 0);
}

$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
    WHERE ft.order_id = :order_id
");
$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
    ORDER BY
        CASE folio_type
            WHEN 'master' THEN 1
            WHEN 'room' THEN 2
            WHEN 'guest' THEN 3
            ELSE 9
        END,
        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, then old tblmailinglist fallback.
   Layout is unchanged below; only the data source is changed.
----------------------------- */
$company = [
    'CompanyName'     => defined('APP_NAME') ? APP_NAME : 'RoomZ Lodge',
    'LegalName'       => '',
    'Postal'          => '',
    'Street'          => '',
    'City'            => '',
    'Email'           => '',
    'Phone'           => '',
    'AltPhone'        => '',
    'Website'         => '',
    'VATNO'           => '',
    'TIN'             => '',
    'CompanyRegNo'    => '',
    'Motto'           => 'Reservations • Folios • Payments',
    'ProformaPrefix'  => 'PF',
    'LogoPath'        => '',
    'PaymentTerms'    => '',
    'Terms'           => '',
    'InvoiceFooter'   => '',
];

$lodgeSettings = false;

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

if ($lodgeSettings) {
    $cityParts = [];
    foreach (['city', 'state_region', 'country'] as $partCol) {
        $part = trim((string)($lodgeSettings[$partCol] ?? ''));
        if ($part !== '') {
            $cityParts[] = $part;
        }
    }

    $postalParts = [];
    foreach (['address_line2', 'postal_code'] as $partCol) {
        $part = trim((string)($lodgeSettings[$partCol] ?? ''));
        if ($part !== '') {
            $postalParts[] = $part;
        }
    }

    $phoneParts = [];
    foreach (['phone', 'alt_phone'] as $phoneCol) {
        $phone = trim((string)($lodgeSettings[$phoneCol] ?? ''));
        if ($phone !== '') {
            $phoneParts[] = $phone;
        }
    }

    $company['CompanyName']    = trim((string)($lodgeSettings['lodge_name'] ?? '')) ?: $company['CompanyName'];
    $company['LegalName']      = trim((string)($lodgeSettings['legal_name'] ?? ''));
    $company['Street']         = trim((string)($lodgeSettings['address_line1'] ?? ''));
    $company['Postal']         = implode(', ', $postalParts);
    $company['City']           = implode(', ', $cityParts);
    $company['Email']          = trim((string)($lodgeSettings['email'] ?? ''));
    $company['Phone']          = implode(' / ', $phoneParts);
    $company['AltPhone']       = trim((string)($lodgeSettings['alt_phone'] ?? ''));
    $company['Website']        = trim((string)($lodgeSettings['website'] ?? ''));
    $company['VATNO']          = trim((string)($lodgeSettings['vat_number'] ?? ''));
    $company['TIN']            = trim((string)($lodgeSettings['tin_number'] ?? ''));
    $company['CompanyRegNo']   = trim((string)($lodgeSettings['company_reg_number'] ?? ''));
    $company['Motto']          = trim((string)($lodgeSettings['tag_line'] ?? '')) ?: $company['Motto'];
    $company['LogoPath']       = trim((string)($lodgeSettings['logo_path'] ?? ''));
    $company['PaymentTerms']   = trim((string)($lodgeSettings['payment_terms'] ?? ''));
    $company['Terms']          = trim((string)($lodgeSettings['terms_and_conditions'] ?? ''));
    $company['InvoiceFooter']  = trim((string)($lodgeSettings['invoice_footer'] ?? ''));
} else {
    try {
        $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'] = $company['Motto'];
}

/* -----------------------------
   Charge lines across booking
----------------------------- */
$hasExtraChargeItems = proforma_table_exists($pdo, 'extra_charge_items');

$extraItemJoinSql = $hasExtraChargeItems ? "
    LEFT JOIN (
        SELECT
            folio_transaction_id,
            GROUP_CONCAT(
                CONCAT(
                    COALESCE(NULLIF(item_name,''), 'Item'),
                    CASE WHEN COALESCE(item_code,'') <> '' THEN CONCAT(' [', item_code, ']') ELSE '' END,
                    ' x ', TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(COALESCE(qty,0), 4))),
                    ' @ ', FORMAT(COALESCE(unit_price,0), 2)
                )
                ORDER BY id ASC SEPARATOR '\n'
            ) AS linked_item_description,
            GROUP_CONCAT(DISTINCT NULLIF(category_name,'') ORDER BY category_name SEPARATOR ', ') AS linked_category_name,
            SUM(COALESCE(line_total,0)) AS linked_line_total
        FROM extra_charge_items
        GROUP BY folio_transaction_id
    ) eci ON eci.folio_transaction_id = ft.id
" : "";

$extraItemSelectSql = $hasExtraChargeItems ? "
        eci.linked_item_description,
        eci.linked_category_name,
        eci.linked_line_total,
" : "
        NULL AS linked_item_description,
        NULL AS linked_category_name,
        NULL AS linked_line_total,
";

$printableDescriptionSql = $hasExtraChargeItems ? "
        CASE
            WHEN COALESCE(eci.linked_item_description, '') <> '' THEN eci.linked_item_description
            ELSE ft.description
        END AS printable_description
" : "
        ft.description AS printable_description
";

$stmt = $pdo->prepare("
    SELECT
        ft.*, 
        f.folio_name,
        f.folio_type,
        r.room_no,
        {$extraItemSelectSql}
        {$printableDescriptionSql}
    FROM folio_transactions ft
    INNER JOIN folios f ON f.id = ft.folio_id
    LEFT JOIN rooms r ON r.id = f.room_id
    {$extraItemJoinSql}
    WHERE ft.order_id = :order_id
      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);

/*
   Charge breakdown for the totals panel.
   New postings are grouped from extra_charge_items.category_name where present;
   older postings fall back to folio_transactions.tx_type/source_table.
*/
$chargeBreakdownRows = [];
try {
    if ($hasExtraChargeItems) {
        $stmtBreakdown = $pdo->prepare("
            SELECT
                CASE
                    WHEN COALESCE(eci.category_name,'') <> '' THEN eci.category_name
                    WHEN COALESCE(ft.tx_type,'') <> '' THEN REPLACE(ft.tx_type, '_', ' ')
                    ELSE 'Other Charges'
                END AS charge_group,
                SUM(CASE
                    WHEN eci.id IS NOT NULL THEN COALESCE(eci.line_total, 0)
                    ELSE COALESCE(ft.debit, 0) - COALESCE(ft.credit, 0)
                END) AS amount
            FROM folio_transactions ft
            LEFT JOIN extra_charge_items eci ON eci.folio_transaction_id = ft.id
            WHERE ft.order_id = :order_id
              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%'
            GROUP BY charge_group
            HAVING amount <> 0
            ORDER BY charge_group
        ");
    } else {
        $stmtBreakdown = $pdo->prepare("
            SELECT
                CASE
                    WHEN COALESCE(ft.tx_type,'') <> '' THEN REPLACE(ft.tx_type, '_', ' ')
                    ELSE 'Other Charges'
                END AS charge_group,
                SUM(COALESCE(ft.debit, 0) - COALESCE(ft.credit, 0)) AS amount
            FROM folio_transactions ft
            WHERE ft.order_id = :order_id
              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%'
            GROUP BY charge_group
            HAVING amount <> 0
            ORDER BY charge_group
        ");
    }
    $stmtBreakdown->execute([':order_id' => $orderId]);
    $chargeBreakdownRows = $stmtBreakdown->fetchAll(PDO::FETCH_ASSOC) ?: [];
} catch (Throwable $e) {
    $chargeBreakdownRows = [];
}

/*
   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', 'bar', 'spar', 'spa', 'curio', 'minibar', 'laundry', 'park_fee'], true)
        || in_array($sourceTable, ['tblservices', '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 . ')';
    }

    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'
    ]);
}

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, then old tblbankdetails fallback.
----------------------------- */
$bankId = is_numeric($bankParam) ? (int)$bankParam : 1;
if ($bankId <= 0) $bankId = 1;

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

try {
    if (!empty($lodgeSettings)) {
        $bankDetails['SwiftCode']   = trim((string)($lodgeSettings['bank_swift'] ?? ''));
        $bankDetails['BranchCode']  = '';
        $bankDetails['Branch']      = trim((string)($lodgeSettings['bank_branch'] ?? ''));
        $bankDetails['Bank']        = trim((string)($lodgeSettings['bank_name'] ?? ''));
        $bankDetails['AccountNo']   = trim((string)($lodgeSettings['bank_account_no'] ?? ''));
        $bankDetails['AccountName'] = trim((string)($lodgeSettings['bank_account_name'] ?? ''));
    }

    if ($bankDetails['Bank'] === '' && $bankDetails['AccountNo'] === '') {
        $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 = 'PROFORMA INVOICE';
$pdf->docRef   = $proformaNo;
$pdf->AliasNbPages();
$pdf->AddPage();
$pdf->SetAutoPageBreak(true, 18);

/* -----------------------------
   Top document bar + company hero
----------------------------- */
$pdf->docTopBar('PROFORMA INVOICE', $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', 'I', 8.5);
$pdf->SetTextColor(72, 82, 96);
$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->SetFont('Arial', '', 8.2);
$pdf->Ln(1);
$pdf->SetX(16);
$pdf->Cell(95, 4.6, pdf_txt(trim((string)($company['Street'] ?? ''))), 0, 1, 'L');
$pdf->SetX(16);
$pdf->Cell(95, 4.6, pdf_txt(trim((string)($company['Postal'] ?? '') . ' ' . (string)($company['City'] ?? ''))), 0, 1, 'L');
$pdf->SetX(16);
$pdf->Cell(95, 4.6, pdf_txt('Email: ' . (string)($company['Email'] ?? '') . '   Tel: ' . (string)($company['Phone'] ?? '')), 0, 1, 'L');

$statParts = [];
if (trim((string)($company['TIN'] ?? '')) !== '') {
    $statParts[] = 'TIN: ' . trim((string)$company['TIN']);
}
if (trim((string)($company['VATNO'] ?? '')) !== '') {
    $statParts[] = 'VAT: ' . trim((string)$company['VATNO']);
}
if (trim((string)($company['CompanyRegNo'] ?? '')) !== '') {
    $statParts[] = 'Reg No: ' . trim((string)$company['CompanyRegNo']);
}
if ($statParts) {
    $pdf->SetX(16);
    $pdf->Cell(95, 4.6, pdf_txt(implode('   |   ', $statParts)), 0, 1, 'L');
}

$logo = __DIR__ . '/att.png';
if (!empty($company['LogoPath'])) {
    $configuredLogo = (string)$company['LogoPath'];
    $logoCandidates = [];

    if (preg_match('/^https?:\/\//i', $configuredLogo)) {
        $logoCandidates[] = $configuredLogo;
    } else {
        $logoCandidates[] = $configuredLogo;
        $logoCandidates[] = __DIR__ . '/' . ltrim($configuredLogo, '/');
        $logoCandidates[] = dirname(__DIR__, 2) . '/' . ltrim($configuredLogo, '/');
        $logoCandidates[] = rtrim((string)($_SERVER['DOCUMENT_ROOT'] ?? ''), '/') . '/' . ltrim($configuredLogo, '/');
    }

    foreach ($logoCandidates as $candidateLogo) {
        if (preg_match('/^https?:\/\//i', $candidateLogo) || file_exists($candidateLogo)) {
            $logo = $candidateLogo;
            break;
        }
    }
}
if (file_exists($logo) || preg_match('/^https?:\/\//i', $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.4, pdf_txt('Room(s): ' . $headerRoomSummary), 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 = $paxRooms . ' rooms | ' . $paxAdults . 'A / ' . $paxChildren . '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)($orderId)],
    ['Folio No.', $displayFolioNo !== '' ? $displayFolioNo : '-'],
];
$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')],
    ['Room(s)', $headerRoomSummary],
    ['Guest', $guestName],
];

for ($i = 0; $i < 4; $i++) {
    $y = $rowY + ($i * 8);
    $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);

/* For group bookings, skip the long individual room list (it jumbles the
   layout) and show only the room-type mix. Small bookings keep both lines. */
if (!$isGroupBooking) {
    $pdf->SetFont('Arial', 'B', 8.5);
    $pdf->SetTextColor(18, 28, 43);
    $pdf->Cell(25, 5, 'Rooms:', 0, 0, 'L');
    $pdf->SetFont('Arial', '', 8.3);
    $pdf->MultiCell(165, 5, pdf_txt($allocatedRoomSummary !== '' ? $allocatedRoomSummary : 'Unassigned'), 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 ($isGroupBooking) {
    $pdf->SetFont('Arial', 'I', 8);
    $pdf->SetTextColor(90, 100, 115);
    $pdf->Cell(190, 5, pdf_txt('Group booking - ' . $allocatedRoomCount . ' rooms. Individual room allocation available on the rooming list.'), 0, 1, 'L');
    $pdf->SetTextColor(18, 28, 43);
}

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;
$rowLineH = 4.5; // must match the description MultiCell line height below
foreach ($lines as $line) {
    $dateTxt = safe_date_fmt_pdf($line['tx_date'], 'd-M-y');
    $descTxt = (string)($line['printable_description'] ?? $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';
    }

    // Row height tracks the actual wrapped description height (4.5mm per line
    // + ~1.5mm vertical padding) instead of a fixed 9mm floor, which was
    // leaving a large blank gap under every single-line row.
    $descLines = max(1, (int)ceil(strlen($descTxt) / 62));
    $rowH = min(20, ($descLines * $rowLineH) + 1.5);

    // Ensure the whole row fits on the current page before drawing it.
    $pdf->checkPageSpace($rowH + 1);

    $rowY = $pdf->GetY();
    $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();
    // Paint the description cell background (zebra) before the text.
    $pdf->Rect($xDesc, $rowY, 98, $rowH, 'F');
    // Vertically centre a short description within the row by padding the top.
    $descBlockH = $descLines * $rowLineH;
    $descPadTop = max(0, ($rowH - $descBlockH) / 2);
    $pdf->SetXY($xDesc, $rowY + $descPadTop);
    $pdf->MultiCell(98, $rowLineH, pdf_txt($descTxt), 0, 'L', false);
    // Draw the description cell border on top so padding doesn't distort it.
    $pdf->Rect($xDesc, $rowY, 98, $rowH);
    $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();
$breakdownDisplayRows = array_slice($chargeBreakdownRows, 0, 5);
$bankH = max(45, 48 + (count($breakdownDisplayRows) * 5));
$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);

if (!empty($breakdownDisplayRows)) {
    $pdf->SetXY($tx, $ty);
    $pdf->SetFont('Arial', 'B', 7.6);
    $pdf->Cell(80, 5, pdf_txt('Charge Breakdown'), 0, 1, 'L');
    $ty += 5;
    foreach ($breakdownDisplayRows as $bd) {
        $label = ucwords(strtolower(trim((string)($bd['charge_group'] ?? 'Other Charges'))));
        if ($label === '') $label = 'Other Charges';
        $pdf->SetXY($tx, $ty);
        $pdf->SetFont('Arial', '', 7.2);
        $pdf->Cell(45, 4.6, pdf_txt($label), 0, 0, 'L');
        $pdf->Cell(35, 4.6, pdf_txt(money_fmt_pdf((float)($bd['amount'] ?? 0))), 0, 1, 'R');
        $ty += 4.8;
    }
    if (count($chargeBreakdownRows) > count($breakdownDisplayRows)) {
        $pdf->SetXY($tx, $ty);
        $pdf->SetFont('Arial', 'I', 7);
        $pdf->Cell(80, 4.5, pdf_txt('More charge groups included in Charges total.'), 0, 1, 'L');
        $ty += 4.8;
    }
    $pdf->SetDrawColor(222, 226, 232);
    $pdf->Line(112, $ty + 1, 194, $ty + 1);
    $ty += 4;
}

$totRows = [
    ['Charges (' . $currency . ')', money_fmt_pdf($totalCharges), false],
    ['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) {
    $pdf->SetXY($tx, $ty);
    $pdf->SetFont('Arial', $tr[2] ? 'B' : '', 8.2);
    $pdf->Cell(45, 6, pdf_txt($tr[0]), 0, 0, 'L');
    $pdf->Cell(35, 6, pdf_txt($tr[1]), 0, 1, 'R');
    $ty += 6.5;
}
$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 removed per requirement.
----------------------------- */

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