<?php
/**
 * fiscal_fdms.php - ZIMRA FDMS fiscalization adapter for RoomZ (FiscalPro API)
 *
 * Exposes fdms_fiscalize_order(PDO $pdo, int $orderId): array which
 * accounting.php's acc_fiscalize_order_hook() picks up automatically -
 * deploy this file alongside accounting.php and "Fiscalize Selected"
 * submits for real instead of queueing.
 *
 * Mapping from RoomZ to the FiscalPro InvoiceRQ (same payload shape as the
 * Dabula Safaris integration, adapted to RoomZ's data model):
 *   number                -> orders.order_no (falls back to RZ-{id})
 *   currencyId            -> orders.currency when it already says USD/ZWG,
 *                            otherwise USD
 *   creationDate          -> booking check_out if in the past, else today
 *   lines[]               -> the master folio's CHARGE transactions
 *                            (is_voided=0, non-payment tx types, debit>0),
 *                            qty 1 each, unitPriceIncludingTax = debit,
 *                            taxPercent 15.5 (RoomZ prices are VAT-inclusive).
 *                            Bookings with no folio charges fall back to one
 *                            "Accommodation" line at orders.totalamount.
 *   buyer{}               -> the booking's agent (agents table, guarded
 *                            column detection for VAT/TIN/address/contacts),
 *                            falling back to the guest.
 *
 * Results (qrCodeUrl, verificationCode, fiscal id, fiscalDayNumber, ZIMRA
 * globalNumber) are stored in fdms_fiscal_invoices together with the full
 * request/response JSON for audit. IDEMPOTENT: a booking that already has a
 * successful fiscal invoice is NOT resubmitted - reselecting it in bulk
 * fiscalization safely returns the existing reference.
 *
 * =====================================================================
 * CONFIGURE BEFORE USE:
 *   - FDMS_API_TOKEN: paste the FiscalPro Bearer token for THIS property's
 *     fiscal device. Do NOT reuse another company's token (the Dabula
 *     Safaris token belongs to a different taxpayer/device).
 *   - FDMS_ENABLED: set true once the token is in.
 * =====================================================================
 */

if (!defined('FDMS_ENABLED'))        define('FDMS_ENABLED', true); // set true once the token below is configured
if (!defined('FDMS_API_URL'))        define('FDMS_API_URL', 'https://api.fiscalpro.tech/api/invoices');
if (!defined('FDMS_API_TOKEN'))      define('FDMS_API_TOKEN', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjU5OTZlMWNkLTliZTMtNDNlZS1hZmJjLWU0M2Q3YWJiZTUxYiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL2VtYWlsYWRkcmVzcyI6ImFkbWludGVzdC5hY2NvdW50c0BiYXRvbmthZ3Vlc3Rsb2RnZS5jb20iLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiIyIiwiSWQiOiI1OTk2ZTFjZC05YmUzLTQzZWUtYWZiYy1lNDNkN2FiYmU1MWIiLCJSb2xlSWQiOiIyIiwiUmlnaHRJZCI6IjMyNzY3IiwiQ29tcGFueUlkIjoiNDYwNGRkZDEtZDc2OC00NDYyLTljMTMtNWUzZDFhYzNhNDdhIiwiSGFzaCI6Ik4vQSIsIk5hbWUiOiJMb3ZlbmVzcyBTaWJhbmRhIiwiVXNlcm5hbWUiOiJhZG1pbnRlc3QuYWNjb3VudHNAYmF0b25rYWd1ZXN0bG9kZ2UuY29tIiwiV2FyZWhvdXNlSWQiOiIxOTIxYTFjMy00MjQ5LTQ2YTUtYTY2YS03MjdhMTRkYTNkNDIiLCJGaXNjYWxEZXZpY2VJZCI6ImQ3NjI1YTQ2LWQwMGItNDUyYy05YmFiLTA5YTkzMzZhM2E5MCIsIkZpc2NhbERldmljZU51bWJlciI6IjM1ODkwIiwiV2FyZWhvdXNlTmFtZSI6IlBpb25lZXJzIExvZGdlIFB2dCBMdGQiLCJUYXhBdXRob3JpdHlJZCI6IjQ4ZmE1NjUyLTdjM2QtNDYyNS04MTJjLWUwYmFkMTg2YmE1YSIsIldhcmVob3VzZSI6IntcclxuICBcImlkXCI6IFwiMTkyMWExYzMtNDI0OS00NmE1LWE2NmEtNzI3YTE0ZGEzZDQyXCIsXHJcbiAgXCJuYW1lXCI6IFwiUGlvbmVlcnMgTG9kZ2UgUHZ0IEx0ZFwiLFxyXG4gIFwic2l0ZUlkXCI6IFwiNDFlZWYwNWItNDdhYy00MTJlLWE3NjYtNzNlMzZkZjcwMjc3XCIsXHJcbiAgXCJjcmVhdG9ySWRcIjogXCI1OTk2ZTFjZC05YmUzLTQzZWUtYWZiYy1lNDNkN2FiYmU1MWJcIixcclxuICBcImNyZWF0aW9uRGF0ZVwiOiBcIjIwMjYtMDYtMTJUMTU6NDI6MDIuODc4Njk1XCIsXHJcbiAgXCJhZGRyZXNzXCI6IFwiNjU3LCBNYWhvZ2FueSBSb2FkLCBWaWN0b3JpYSBGYWxscywgTWF0ZWJlbGVsYW5kXCIsXHJcbiAgXCJ0eXBlSWRcIjogMSxcclxuICBcImZpc2NhbGl6YXRpb25Nb2RlSWRcIjogMFxyXG59IiwiQ29tcGFueSI6IntcclxuICBcImlkXCI6IFwiNDYwNGRkZDEtZDc2OC00NDYyLTljMTMtNWUzZDFhYzNhNDdhXCIsXHJcbiAgXCJuYW1lXCI6IFwiUGlvbmVlcnMgTG9kZ2UgUHZ0IEx0ZFwiLFxyXG4gIFwiZGVzY3JpcHRpb25cIjogXCJSZXRhaWxcIixcclxuICBcInNlY3RvcklkXCI6IDEsXHJcbiAgXCJjcmVhdGlvbkRhdGVcIjogXCIyMDI2LTA2LTEyVDE1OjQxOjQzLjk2MDExNlwiLFxyXG4gIFwic3RhdHVzSWRcIjogMCxcclxuICBcImFkZHJlc3NcIjogXCI2NTcgTWFob2dhbnkgUm9hZCBNYXRlYmVsZWxhbmQgTWF0ZWJlbGVsYW5kXCIsXHJcbiAgXCJ2YXRcIjogMC4wMCxcclxuICBcInRpbk51bWJlclwiOiBcIjIwMDExMzQ4NzBcIixcclxuICBcInZhdE51bWJlclwiOiBcIjIyMDA2ODM1MVwiLFxyXG4gIFwibW9iaWxlXCI6IFwiMDc3NTE2MjM1OFwiLFxyXG4gIFwiZW1haWxcIjogXCJhY2NvdW50c0BiYXRvbmthZ3Vlc3Rsb2RnZS5jb21cIixcclxuICBcImlzU3luY2VkXCI6IGZhbHNlLFxyXG4gIFwiY291bnRyeUlkXCI6IFwiWldcIlxyXG59IiwiT3JnSWQiOiJQTDA2MTIwMDIyN0RDIiwiUmVjZWlwdFByaW50Rm9ybSI6IjAiLCJDb3VudHJ5SWQiOiJaVyIsImV4cCI6MTgxNjQ0NTA4MywiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NzI0MCIsImF1ZCI6Imh0dHBzOi8vbG9jYWxob3N0OjcyNDAifQ.plsf2LRlSt9nWTeCHuAfUeYqHt0U4uG5PAl_6oROwks');
if (!defined('FDMS_VAT_PERCENT'))    define('FDMS_VAT_PERCENT', 15.5);
/* ZTA levy percentage. RoomZ charge debits are inclusive of BOTH VAT (15.5%)
   and the ZTA levy (2%) on the same net base. ZIMRA FDMS has no ZTA tax code,
   so we submit each accommodation line at its VAT-taxable portion (gross minus
   ZTA) at 15.5%, and add the ZTA as a single separate zero-rated (0%) line so
   the fiscal total still equals the gross. Mirrors the printed tax invoice. */
if (!defined('FDMS_ZTA_PERCENT'))    define('FDMS_ZTA_PERCENT', 2.0);
if (!defined('FDMS_DEFAULT_CURRENCY')) define('FDMS_DEFAULT_CURRENCY', 'USD');

if (!function_exists('fdms_column_exists')) {
    function fdms_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 = :t AND COLUMN_NAME = :c");
            $stmt->execute([':t' => $table, ':c' => $column]);
            $cache[$key] = ((int)$stmt->fetchColumn() > 0);
        } catch (Throwable $e) { $cache[$key] = false; }
        return $cache[$key];
    }
}

if (!function_exists('fdms_ensure_schema')) {
    function fdms_ensure_schema(PDO $pdo): void {
        $pdo->exec("CREATE TABLE IF NOT EXISTS fdms_fiscal_invoices (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
            order_id BIGINT UNSIGNED NOT NULL,
            invoice_no VARCHAR(120) NULL,
            currency VARCHAR(10) NULL,
            amount_including_tax DECIMAL(14,2) NOT NULL DEFAULT 0,
            fiscal_id VARCHAR(120) NULL,
            zimra_invoice_no VARCHAR(120) NULL,
            fiscal_day_number INT NULL,
            verification_code VARCHAR(190) NULL,
            qr_code_url TEXT NULL,
            success TINYINT(1) NOT NULL DEFAULT 0,
            request_json MEDIUMTEXT NULL,
            response_json MEDIUMTEXT NULL,
            created_at DATETIME NULL,
            INDEX idx_fdms_order (order_id)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
    }
}

if (!function_exists('fdms_first_nonempty')) {
    function fdms_first_nonempty(array $row, array $candidates): string {
        foreach ($candidates as $c) {
            if (isset($row[$c]) && trim((string)$row[$c]) !== '') return trim((string)$row[$c]);
        }
        return '';
    }
}

/*
   A booking becomes invoiced only after FiscalPro/ZIMRA has accepted it.
   These helpers safely create the tracking columns when required and then
   stamp the booking. They are also used by the idempotency path so an older
   successful fiscal record can repair a missing orders.invoiced flag.
*/
if (!function_exists('fdms_ensure_order_invoice_columns')) {
    function fdms_ensure_order_invoice_columns(PDO $pdo): void {
        if (!fdms_column_exists($pdo, 'orders', 'invoiced')) {
            try {
                $pdo->exec("ALTER TABLE orders ADD COLUMN invoiced TINYINT(1) NOT NULL DEFAULT 0");
            } catch (Throwable $e) {}
        }
        if (!fdms_column_exists($pdo, 'orders', 'invoiced_at')) {
            try {
                $pdo->exec("ALTER TABLE orders ADD COLUMN invoiced_at DATETIME NULL");
            } catch (Throwable $e) {}
        }
    }
}

if (!function_exists('fdms_mark_order_invoiced')) {
    function fdms_mark_order_invoiced(PDO $pdo, int $orderId): array {
        fdms_ensure_order_invoice_columns($pdo);

        if (!fdms_column_exists($pdo, 'orders', 'invoiced')) {
            return ['ok' => false, 'message' => 'The orders.invoiced column could not be created or found.'];
        }

        try {
            $sets = ['invoiced = 1'];
            if (fdms_column_exists($pdo, 'orders', 'invoiced_at')) {
                // Preserve the original invoice timestamp when repairing an
                // already-fiscalized booking.
                $sets[] = 'invoiced_at = COALESCE(invoiced_at, NOW())';
            }
            $stmt = $pdo->prepare('UPDATE orders SET ' . implode(', ', $sets) . ' WHERE id = :oid');
            $stmt->execute([':oid' => $orderId]);
            return ['ok' => true, 'message' => ''];
        } catch (Throwable $e) {
            return ['ok' => false, 'message' => $e->getMessage()];
        }
    }
}

/*
   Buyer resolution: agent first (guarded columns), guest as fallback.
   Returns the FiscalPro buyer array.
*/
if (!function_exists('fdms_resolve_buyer')) {
    function fdms_resolve_buyer(PDO $pdo, array $order): array {
        $buyer = ['name' => '', 'tradeName' => '', 'mobile' => '', 'email' => '', 'address' => '', 'vatNumber' => '', 'tinNumber' => ''];

        $agentRow = null;
        $agentId = (int)($order['agent_id'] ?? 0);
        if ($agentId > 0 && fdms_column_exists($pdo, 'agents', 'id')) {
            try {
                $stmt = $pdo->prepare("SELECT * FROM agents WHERE id = :id LIMIT 1");
                $stmt->execute([':id' => $agentId]);
                $agentRow = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
            } catch (Throwable $e) { $agentRow = null; }
        }

        if ($agentRow) {
            $agentName = trim(fdms_first_nonempty($agentRow, ['firstname']) . ' ' . fdms_first_nonempty($agentRow, ['lastname']));
            if ($agentName === '') $agentName = fdms_first_nonempty($agentRow, ['agent', 'name', 'company', 'company_name']);
            $buyer['name'] = $agentName;
            $buyer['tradeName'] = fdms_first_nonempty($agentRow, ['company', 'company_name', 'registered_name', 'trade_name']) ?: $agentName;
            $buyer['mobile'] = fdms_first_nonempty($agentRow, ['phone', 'tel', 'mobile', 'contact_no']);
            $buyer['email'] = fdms_first_nonempty($agentRow, ['email']);
            $buyer['address'] = fdms_first_nonempty($agentRow, ['address', 'address1', 'physical_address']);
            $buyer['vatNumber'] = fdms_first_nonempty($agentRow, ['vatno', 'vat_number', 'vatref', 'vat_ref']);
            $buyer['tinNumber'] = fdms_first_nonempty($agentRow, ['tin', 'tinno', 'bp_no', 'bpn', 'bpno']);
        }

        // Originating agent text as a trade-name fallback
        if ($buyer['name'] === '' && trim((string)($order['originating_agent'] ?? '')) !== '') {
            $buyer['name'] = trim((string)$order['originating_agent']);
            $buyer['tradeName'] = $buyer['name'];
        }

        // Guest fallback
        if ($buyer['name'] === '') {
            $guestId = (int)($order['guest_id'] ?? 0);
            if ($guestId > 0) {
                try {
                    $g = $pdo->prepare("SELECT * FROM guests WHERE id = :id LIMIT 1");
                    $g->execute([':id' => $guestId]);
                    $guest = $g->fetch(PDO::FETCH_ASSOC);
                    if ($guest) {
                        $buyer['name'] = trim(fdms_first_nonempty($guest, ['firstname']) . ' ' . fdms_first_nonempty($guest, ['lastname']));
                        $buyer['tradeName'] = $buyer['name'];
                        $buyer['mobile'] = fdms_first_nonempty($guest, ['phone', 'mobile', 'contact_no']);
                        $buyer['email'] = fdms_first_nonempty($guest, ['email']);
                        $buyer['address'] = fdms_first_nonempty($guest, ['address', 'address1']);
                    }
                } catch (Throwable $e) {}
            }
        }

        if ($buyer['name'] === '') {
            $buyer['name'] = 'Walk-in Guest';
            $buyer['tradeName'] = 'Walk-in Guest';
        }
        return $buyer;
    }
}

/*
   Build the exact customer-facing line set used by proforma_invoice.php.

   Rules intentionally mirror the proforma:
   - master folio only;
   - non-voided debit lines only;
   - exclude reversal/refund/void types AND descriptions;
   - use extra_charge_items detail only when its total reconciles exactly to
     the parent folio transaction;
   - cancelled bookings fiscalize explicit cancellation-fee lines only;
   - no orders.totalamount fallback, because the strict master-folio proforma
     does not invent an unposted line either.
*/
if (!function_exists('fdms_table_exists')) {
    function fdms_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 = :t");
            $stmt->execute([':t' => $table]);
            $cache[$table] = ((int)$stmt->fetchColumn() > 0);
        } catch (Throwable $e) { $cache[$table] = false; }
        return $cache[$table];
    }
}

if (!function_exists('fdms_order_is_cancelled')) {
    function fdms_order_is_cancelled(PDO $pdo, array $order): bool {
        foreach (['is_cancelled','is_canceled','cancelled','canceled'] as $c) {
            if (array_key_exists($c, $order) && (int)$order[$c] === 1) return true;
        }
        foreach (['cancelled_at','canceled_at','cancellation_date','cancelled_date','date_cancelled'] as $c) {
            $v = trim((string)($order[$c] ?? ''));
            if ($v !== '' && $v !== '0000-00-00' && $v !== '0000-00-00 00:00:00') return true;
        }
        foreach (['cancelled_reason','canceled_reason','cancellation_reason'] as $c) {
            if (trim((string)($order[$c] ?? '')) !== '') return true;
        }
        foreach (['booking_status','status','status_name','booking_status_name','reservation_status'] as $c) {
            if (!array_key_exists($c, $order)) continue;
            $v = strtolower(trim((string)$order[$c]));
            if ($v !== '' && (strpos($v, 'cancel') !== false || strpos($v, 'void') !== false)) return true;
        }

        $statusId = 0;
        foreach (['booking_status_id','status_id','booking_status','status'] as $c) {
            if (isset($order[$c]) && is_numeric($order[$c]) && (int)$order[$c] > 0) {
                $statusId = (int)$order[$c];
                break;
            }
        }
        if ($statusId > 0) {
            foreach (['booking_statuses','booking_status','order_statuses','reservation_statuses','statuses'] as $table) {
                if (!fdms_table_exists($pdo, $table)) continue;
                $idCol = '';
                foreach (['id','ID','status_id'] as $c) if (fdms_column_exists($pdo, $table, $c)) { $idCol = $c; break; }
                $nameCol = '';
                foreach (['name','title','status','status_name','description'] as $c) if (fdms_column_exists($pdo, $table, $c)) { $nameCol = $c; break; }
                if ($idCol === '' || $nameCol === '') continue;
                try {
                    $stmt = $pdo->prepare("SELECT `{$nameCol}` FROM `{$table}` WHERE `{$idCol}` = :id LIMIT 1");
                    $stmt->execute([':id' => $statusId]);
                    $name = strtolower(trim((string)($stmt->fetchColumn() ?: '')));
                    if ($name !== '' && (strpos($name, 'cancel') !== false || strpos($name, 'void') !== false)) return true;
                } catch (Throwable $e) {}
            }
        }
        return false;
    }
}

if (!function_exists('fdms_room_pax_rows')) {
    function fdms_room_pax_rows(PDO $pdo, int $orderId): array {
        if (!fdms_table_exists($pdo, 'order_rooming')) return [];
        try {
            $stmt = $pdo->prepare("SELECT COALESCE(adults,0) AS adults, COALESCE(children,0) AS children FROM order_rooming WHERE order_id = :oid ORDER BY room_no ASC, id ASC");
            $stmt->execute([':oid' => $orderId]);
            return $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
        } catch (Throwable $e) { return []; }
    }
}

if (!function_exists('fdms_build_lines')) {
    function fdms_build_lines(PDO $pdo, int $orderId, array $order): array {
        $hasExtraItems = fdms_table_exists($pdo, 'extra_charge_items');
        $extraJoin = $hasExtraItems ? "
            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,
                       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
        " : '';
        $printable = $hasExtraItems ? "
            CASE
                WHEN COALESCE(eci.linked_item_description,'') <> ''
                 AND ABS(COALESCE(eci.linked_line_total,0) - COALESCE(ft.debit,0)) < 0.01
                    THEN eci.linked_item_description
                ELSE ft.description
            END AS printable_description
        " : "ft.description AS printable_description";

        try {
            $stmt = $pdo->prepare("
                SELECT ft.id, ft.tx_date, ft.tx_type, ft.source_table,
                       ft.description, ft.debit, ft.credit,
                       {$printable}
                FROM folio_transactions ft
                INNER JOIN folios f ON f.id = ft.folio_id
                {$extraJoin}
                WHERE ft.order_id = :oid
                  AND LOWER(COALESCE(f.folio_type,'')) = 'master'
                  AND 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%'
                ORDER BY ft.tx_date ASC, ft.id ASC
            ");
            $stmt->execute([':oid' => $orderId]);
            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
        } catch (Throwable $e) {
            return ['lines' => [], 'total' => 0.0];
        }

        $cancelled = fdms_order_is_cancelled($pdo, $order);
        $paxRows = fdms_room_pax_rows($pdo, $orderId);
        $paxCount = count($paxRows);
        $accomIndex = 0;
        $lines = [];
        $total = 0.0;
        $ztaTotal = 0.0;

        foreach ($rows as $row) {
            $txType = strtolower(trim((string)($row['tx_type'] ?? '')));
            $source = strtolower(trim((string)($row['source_table'] ?? '')));
            $rawDesc = trim((string)($row['description'] ?? ''));
            $descLower = strtolower($rawDesc);
            $printDesc = trim((string)($row['printable_description'] ?? $rawDesc));
            $printLower = strtolower($printDesc);

            if ($cancelled) {
                $isCancellationFee = in_array($txType, [
                    'cancellation_fee','cancellation','cancel_fee','cancellation_charge','cancel_charge','late_cancellation_fee'
                ], true)
                || in_array($source, ['cancellation_fees','cancellation_fee','booking_cancellation_fees','cancel_fees'], true)
                || strpos($descLower, 'cancellation fee') !== false
                || strpos($descLower, 'cancel fee') !== false
                || strpos($descLower, 'cancellation charge') !== false
                || strpos($descLower, 'cancel charge') !== false
                || strpos($printLower, 'cancellation fee') !== false
                || strpos($printLower, 'cancel fee') !== false
                || strpos($printLower, 'cancellation charge') !== false
                || strpos($printLower, 'cancel charge') !== false;
                if (!$isCancellationFee) continue;
            } else {
                $isExtra = in_array($txType, ['extra','extras','extra_charge','addon','add_on','transfer','restaurant','bar','spar','spa','curio','minibar','laundry','park_fee'], true)
                    || in_array($source, ['tblservices','restaurant_menu_items','inventory_items','extra_charge_items','extras','order_extras','booking_extras','reservation_extras','order_extra_items','manual_extra'], true);
                $isAccommodation = !$isExtra && (
                    in_array($txType, ['accommodation','room','room_charge','accommodation_charge','nightly','room_night'], true)
                    || $source === 'rel_orders_prices'
                    || strpos($descLower, 'accommodation') !== false
                    || strpos($descLower, 'room charge') !== false
                );
                if ($isAccommodation && $paxCount > 0 && stripos($printDesc, 'Pax:') === false) {
                    $pax = $paxRows[$accomIndex % $paxCount];
                    $a = (int)($pax['adults'] ?? 0);
                    $c = (int)($pax['children'] ?? 0);
                    $n = $a + $c;
                    if ($n > 0) {
                        $bits = [];
                        if ($a > 0) $bits[] = $a . 'A';
                        if ($c > 0) $bits[] = $c . 'C';
                        $printDesc .= ' | Pax: ' . $n . ' (' . implode(' ', $bits) . ')';
                    }
                    $accomIndex++;
                }
            }

            $amount = $cancelled
                ? round(max(0, (float)($row['debit'] ?? 0) - max(0, (float)($row['credit'] ?? 0))), 2)
                : round((float)($row['debit'] ?? 0), 2);
            if ($amount <= 0) continue;
            if ($printDesc === '') $printDesc = $cancelled ? 'Cancellation Fee' : 'Charge';
            if (function_exists('mb_substr')) $printDesc = mb_substr($printDesc, 0, 120);
            else $printDesc = substr($printDesc, 0, 120);

            /* Split the ZTA levy out of this line, exactly as the printed tax
               invoice does. ZTA is rounded FIRST, then the remainder is the
               VAT-taxable amount submitted at 15.5%. The stripped ZTA is
               accumulated and submitted once, after the loop, as a single
               zero-rated line so the fiscal total still equals the gross. */
            $lineZta = round((FDMS_ZTA_PERCENT / (100.0 + FDMS_VAT_PERCENT + FDMS_ZTA_PERCENT)) * $amount, 2);
            $vatable = round($amount - $lineZta, 2);
            $ztaTotal += $lineZta;

            $lines[] = [
                'quantity' => 1,
                'unitPriceIncludingTax' => $vatable,
                'taxPercent' => FDMS_VAT_PERCENT,
                'description' => $printDesc,
                'productName' => $printDesc,
            ];
            $total += $vatable;
        }

        /* Single consolidated ZTA levy line - zero-rated for VAT. Keeps the
           receipt total equal to the gross charges and mirrors the accepted
           ZIMRA format (a separate "ZTA" line at 0% VAT). */
        $ztaTotal = round($ztaTotal, 2);
        if ($ztaTotal > 0.005) {
            $lines[] = [
                'quantity' => 1,
                'unitPriceIncludingTax' => $ztaTotal,
                'taxPercent' => 0,
                'description' => 'ZTA Levy (' . rtrim(rtrim(number_format(FDMS_ZTA_PERCENT, 1), '0'), '.') . '%)',
                'productName' => 'ZTA Levy',
            ];
            $total += $ztaTotal;
        }

        return ['lines' => $lines, 'total' => round($total, 2)];
    }
}

/*
   MAIN ENTRY - called by accounting.php's fiscalization hook.
   Returns array('ok'=>bool, 'ref'=>string, 'message'=>string).
*/
if (!function_exists('fdms_fiscalize_order')) {
    function fdms_fiscalize_order(PDO $pdo, int $orderId): array {
        fdms_ensure_schema($pdo);

        // IDEMPOTENCY: never resubmit a booking that already fiscalized OK.
        $ex = $pdo->prepare("SELECT zimra_invoice_no, fiscal_id FROM fdms_fiscal_invoices WHERE order_id = :oid AND success = 1 ORDER BY id DESC LIMIT 1");
        $ex->execute([':oid' => $orderId]);
        $existing = $ex->fetch(PDO::FETCH_ASSOC);
        if ($existing) {
            $ref = trim((string)($existing['zimra_invoice_no'] ?? '')) ?: trim((string)($existing['fiscal_id'] ?? ''));
            $mark = fdms_mark_order_invoiced($pdo, $orderId);
            $message = 'Already fiscalized - existing ZIMRA invoice reused, not resubmitted.';
            if (!$mark['ok']) {
                $message .= ' Warning: fiscalization exists, but the booking could not be marked invoiced: ' . $mark['message'];
            }
            return ['ok' => true, 'ref' => $ref, 'message' => $message];
        }

        if (!FDMS_ENABLED || FDMS_API_TOKEN === '1eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjU5OTZlMWNkLTliZTMtNDNlZS1hZmJjLWU0M2Q3YWJiZTUxYiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL2VtYWlsYWRkcmVzcyI6ImFkbWludGVzdC5hY2NvdW50c0BiYXRvbmthZ3Vlc3Rsb2RnZS5jb20iLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiIyIiwiSWQiOiI1OTk2ZTFjZC05YmUzLTQzZWUtYWZiYy1lNDNkN2FiYmU1MWIiLCJSb2xlSWQiOiIyIiwiUmlnaHRJZCI6IjMyNzY3IiwiQ29tcGFueUlkIjoiNDYwNGRkZDEtZDc2OC00NDYyLTljMTMtNWUzZDFhYzNhNDdhIiwiSGFzaCI6Ik4vQSIsIk5hbWUiOiJMb3ZlbmVzcyBTaWJhbmRhIiwiVXNlcm5hbWUiOiJhZG1pbnRlc3QuYWNjb3VudHNAYmF0b25rYWd1ZXN0bG9kZ2UuY29tIiwiV2FyZWhvdXNlSWQiOiIxOTIxYTFjMy00MjQ5LTQ2YTUtYTY2YS03MjdhMTRkYTNkNDIiLCJGaXNjYWxEZXZpY2VJZCI6ImQ3NjI1YTQ2LWQwMGItNDUyYy05YmFiLTA5YTkzMzZhM2E5MCIsIkZpc2NhbERldmljZU51bWJlciI6IjM1ODkwIiwiV2FyZWhvdXNlTmFtZSI6IlBpb25lZXJzIExvZGdlIFB2dCBMdGQiLCJUYXhBdXRob3JpdHlJZCI6IjQ4ZmE1NjUyLTdjM2QtNDYyNS04MTJjLWUwYmFkMTg2YmE1YSIsIldhcmVob3VzZSI6IntcclxuICBcImlkXCI6IFwiMTkyMWExYzMtNDI0OS00NmE1LWE2NmEtNzI3YTE0ZGEzZDQyXCIsXHJcbiAgXCJuYW1lXCI6IFwiUGlvbmVlcnMgTG9kZ2UgUHZ0IEx0ZFwiLFxyXG4gIFwic2l0ZUlkXCI6IFwiNDFlZWYwNWItNDdhYy00MTJlLWE3NjYtNzNlMzZkZjcwMjc3XCIsXHJcbiAgXCJjcmVhdG9ySWRcIjogXCI1OTk2ZTFjZC05YmUzLTQzZWUtYWZiYy1lNDNkN2FiYmU1MWJcIixcclxuICBcImNyZWF0aW9uRGF0ZVwiOiBcIjIwMjYtMDYtMTJUMTU6NDI6MDIuODc4Njk1XCIsXHJcbiAgXCJhZGRyZXNzXCI6IFwiNjU3LCBNYWhvZ2FueSBSb2FkLCBWaWN0b3JpYSBGYWxscywgTWF0ZWJlbGVsYW5kXCIsXHJcbiAgXCJ0eXBlSWRcIjogMSxcclxuICBcImZpc2NhbGl6YXRpb25Nb2RlSWRcIjogMFxyXG59IiwiQ29tcGFueSI6IntcclxuICBcImlkXCI6IFwiNDYwNGRkZDEtZDc2OC00NDYyLTljMTMtNWUzZDFhYzNhNDdhXCIsXHJcbiAgXCJuYW1lXCI6IFwiUGlvbmVlcnMgTG9kZ2UgUHZ0IEx0ZFwiLFxyXG4gIFwiZGVzY3JpcHRpb25cIjogXCJSZXRhaWxcIixcclxuICBcInNlY3RvcklkXCI6IDEsXHJcbiAgXCJjcmVhdGlvbkRhdGVcIjogXCIyMDI2LTA2LTEyVDE1OjQxOjQzLjk2MDExNlwiLFxyXG4gIFwic3RhdHVzSWRcIjogMCxcclxuICBcImFkZHJlc3NcIjogXCI2NTcgTWFob2dhbnkgUm9hZCBNYXRlYmVsZWxhbmQgTWF0ZWJlbGVsYW5kXCIsXHJcbiAgXCJ2YXRcIjogMC4wMCxcclxuICBcInRpbk51bWJlclwiOiBcIjIwMDExMzQ4NzBcIixcclxuICBcInZhdE51bWJlclwiOiBcIjIyMDA2ODM1MVwiLFxyXG4gIFwibW9iaWxlXCI6IFwiMDc3NTE2MjM1OFwiLFxyXG4gIFwiZW1haWxcIjogXCJhY2NvdW50c0BiYXRvbmthZ3Vlc3Rsb2RnZS5jb21cIixcclxuICBcImlzU3luY2VkXCI6IGZhbHNlLFxyXG4gIFwiY291bnRyeUlkXCI6IFwiWldcIlxyXG59IiwiT3JnSWQiOiJQTDA2MTIwMDIyN0RDIiwiUmVjZWlwdFByaW50Rm9ybSI6IjAiLCJDb3VudHJ5SWQiOiJaVyIsImV4cCI6MTgxNjQ0NTA4MywiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NzI0MCIsImF1ZCI6Imh0dHBzOi8vbG9jYWxob3N0OjcyNDAifQ.plsf2LRlSt9nWTeCHuAfUeYqHt0U4uG5PAl_6oROwks') {
            return ['ok' => false, 'ref' => '', 'message' => 'FDMS adapter not configured yet - set FDMS_API_TOKEN and FDMS_ENABLED in fiscal_fdms.php.'];
        }

        $o = $pdo->prepare("SELECT * FROM orders WHERE id = :oid LIMIT 1");
        $o->execute([':oid' => $orderId]);
        $order = $o->fetch(PDO::FETCH_ASSOC);
        if (!$order) return ['ok' => false, 'ref' => '', 'message' => 'Booking not found.'];

        $built = fdms_build_lines($pdo, $orderId, $order);
        if (!$built['lines'] || $built['total'] <= 0) {
            return ['ok' => false, 'ref' => '', 'message' => 'Nothing to fiscalize - the proforma has no eligible master-folio charge lines.'];
        }

        $invoiceNo = trim((string)($order['order_no'] ?? ''));
        if ($invoiceNo === '') $invoiceNo = 'PGL-' . $orderId;

        $currencyRaw = strtoupper(trim((string)($order['currency'] ?? '')));
        $currencyId = in_array($currencyRaw, ['USD', 'ZWG'], true) ? $currencyRaw : FDMS_DEFAULT_CURRENCY;

        $checkOut = substr((string)($order['check_out'] ?? ''), 0, 10);
        $creationDate = ($checkOut !== '' && $checkOut <= date('Y-m-d')) ? $checkOut : date('Y-m-d');

        $buyer = fdms_resolve_buyer($pdo, $order);

        $payload = [
            'number' => $invoiceNo,
            'currencyId' => $currencyId,
            'creationDate' => $creationDate,
            'amountIncludingTax' => $built['total'],
            'amountTendered' => $built['total'],
            'qrCodeUrl' => '',
            'verificationCode' => '',
            'fiscalDeviceId' => 0,
            'globalNumber' => 0,
            'fiscalDayNumber' => 0,
            'lines' => $built['lines'],
            'buyer' => $buyer,
        ];
        $postData = json_encode($payload, JSON_UNESCAPED_UNICODE);

        $ch = curl_init(FDMS_API_URL);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $postData,
            CURLOPT_HTTPHEADER => [
                'Content-Type: application/json',
                'Authorization: Bearer ' . FDMS_API_TOKEN,
            ],
            CURLOPT_TIMEOUT => 45,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS => 5,
        ]);
        $response = curl_exec($ch);
        $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlError = curl_error($ch);
        curl_close($ch);

        $data = json_decode((string)$response, true);
        $ok = ($curlError === '' && $httpCode >= 200 && $httpCode < 300 && is_array($data) && !empty($data['id']));

        $zimraNo = $ok ? (string)($data['globalNumber'] ?? '') : '';
        $fiscalId = $ok ? (string)($data['id'] ?? '') : '';

        // Store the full record (prepared statements - never interpolate the
        // API response into SQL).
        $pdo->prepare("INSERT INTO fdms_fiscal_invoices
                (order_id, invoice_no, currency, amount_including_tax, fiscal_id, zimra_invoice_no,
                 fiscal_day_number, verification_code, qr_code_url, success, request_json, response_json, created_at)
                VALUES (:oid, :inv, :cur, :amt, :fid, :zno, :fday, :vcode, :qr, :ok, :req, :resp, NOW())")
            ->execute([
                ':oid' => $orderId,
                ':inv' => $invoiceNo,
                ':cur' => $currencyId,
                ':amt' => $built['total'],
                ':fid' => $fiscalId !== '' ? $fiscalId : null,
                ':zno' => $zimraNo !== '' ? $zimraNo : null,
                ':fday' => $ok ? (int)($data['fiscalDayNumber'] ?? 0) : null,
                ':vcode' => $ok ? (string)($data['verificationCode'] ?? '') : null,
                ':qr' => $ok ? (string)($data['qrCodeUrl'] ?? '') : null,
                ':ok' => $ok ? 1 : 0,
                ':req' => $postData,
                ':resp' => (string)$response,
            ]);

        if (!$ok) {
            $msg = $curlError !== '' ? ('Connection error: ' . $curlError)
                 : (is_array($data) && isset($data['message']) ? (string)$data['message'] : ('FiscalPro HTTP ' . $httpCode));
            return ['ok' => false, 'ref' => '', 'message' => 'Fiscalization failed: ' . $msg];
        }

        // FiscalPro/ZIMRA accepted the invoice and the successful audit row
        // has been saved. Only now may the booking be marked as invoiced.
        $mark = fdms_mark_order_invoiced($pdo, $orderId);
        $successMessage = 'Fiscalized: ZIMRA invoice ' . ($zimraNo !== '' ? $zimraNo : $fiscalId)
            . ' (' . $currencyId . ' ' . number_format($built['total'], 2)
            . ', ' . count($built['lines']) . ' line(s)).';

        if (!$mark['ok']) {
            $successMessage .= ' Warning: ZIMRA accepted the invoice, but the booking could not be marked invoiced: ' . $mark['message'];
        }

        return [
            'ok' => true,
            'ref' => $zimraNo !== '' ? $zimraNo : $fiscalId,
            'message' => $successMessage,
        ];
    }
}
