<?php
/**
 * fiscal_credit_note.php - ZIMRA FDMS credit notes for RoomZ (FiscalPro API)
 *
 * ZIMRA recognises two operations, and FiscalPro's /api/creditNotes endpoint
 * only ever credits the FULL invoice (its payload is just the invoice number
 * plus comments - no lines, no amounts). So:
 *
 *   1. FULL CREDIT      - credit the whole invoice. Done.
 *                         fdms_issue_credit_note($pdo, $invoiceId, $reason)
 *
 *   2. PARTIAL CORRECTION - ZIMRA's rule: you may NOT issue a $25 credit note
 *                         against a $100 invoice. You credit the full $100,
 *                         then re-invoice for $75. This is done as ONE chained
 *                         operation so the two documents can never drift apart:
 *                         fdms_partial_correction($pdo, $invoiceId, 25.00, $reason)
 *
 * THE INVARIANT that keeps this compliant:
 *   A credit note's amount is ALWAYS the original invoice's full value.
 *   The credit amount the user types never becomes a credit note - it only
 *   determines the replacement invoice's value (original - credit). It is
 *   therefore impossible to file a partial credit note through this module.
 *
 * The original invoice row in fdms_fiscal_invoices is NEVER mutated - it must
 * stay immutable for ZIMRA. Credit notes are separate documents that reference
 * it, and a replacement invoice is a new invoice row.
 *
 * Requires fiscal_fdms.php (shares its config constants, schema helpers and
 * line/buyer builders).
 */

require_once __DIR__ . '/fiscal_fdms.php';

if (!defined('FDMS_CREDIT_NOTE_URL')) define('FDMS_CREDIT_NOTE_URL', 'https://api.fiscalpro.tech/api/creditNotes');

/* ZIMRA credit-note reason codes. Kept here so the UI and the stored record
   always speak the same language. */
if (!function_exists('fdms_credit_reasons')) {
    function fdms_credit_reasons(): array {
        return [
            1 => 'Cancellation of the transaction',
            2 => 'Return of goods / services',
            3 => 'Discount or price adjustment',
            4 => 'Other correction',
        ];
    }
}

if (!function_exists('fdms_ensure_credit_note_schema')) {
    function fdms_ensure_credit_note_schema(PDO $pdo): void {
        $pdo->exec("CREATE TABLE IF NOT EXISTS fdms_credit_notes (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
            order_id BIGINT UNSIGNED NOT NULL,
            original_invoice_id BIGINT UNSIGNED NOT NULL,
            original_invoice_no VARCHAR(120) NULL,
            credit_type ENUM('full','correction') NOT NULL DEFAULT 'full',
            reason_code TINYINT UNSIGNED NULL,
            reason_text VARCHAR(255) NULL,
            currency VARCHAR(10) NULL,
            amount_including_tax DECIMAL(14,2) NOT NULL DEFAULT 0,
            replacement_invoice_id BIGINT UNSIGNED NULL,
            replacement_amount DECIMAL(14,2) NULL,
            fiscal_id VARCHAR(120) NULL,
            zimra_credit_no VARCHAR(120) NULL,
            fiscal_device_id 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_by_name VARCHAR(180) NULL,
            created_at DATETIME NULL,
            INDEX idx_fcn_order (order_id),
            INDEX idx_fcn_invoice (original_invoice_id)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");

        /* A replacement invoice must know which credit note spawned it, so the
           chain is walkable from either end. */
        if (!fdms_column_exists($pdo, 'fdms_fiscal_invoices', 'replaces_credit_note_id')) {
            try { $pdo->exec("ALTER TABLE fdms_fiscal_invoices ADD COLUMN replaces_credit_note_id BIGINT UNSIGNED NULL"); } catch (Throwable $e) {}
        }
        if (!fdms_column_exists($pdo, 'fdms_fiscal_invoices', 'credited')) {
            try { $pdo->exec("ALTER TABLE fdms_fiscal_invoices ADD COLUMN credited TINYINT(1) NOT NULL DEFAULT 0"); } catch (Throwable $e) {}
        }
    }
}

if (!function_exists('fdms_current_user_name_cn')) {
    function fdms_current_user_name_cn(): string {
        foreach (['user_name', 'username', 'full_name', 'name'] as $k) {
            if (!empty($_SESSION[$k])) return (string)$_SESSION[$k];
        }
        return 'User #' . (int)($_SESSION['user_id'] ?? 0);
    }
}

/* Load a successful fiscal invoice row. */
if (!function_exists('fdms_load_invoice')) {
    function fdms_load_invoice(PDO $pdo, int $invoiceId): ?array {
        $stmt = $pdo->prepare("SELECT * FROM fdms_fiscal_invoices WHERE id = :id AND success = 1 LIMIT 1");
        $stmt->execute([':id' => $invoiceId]);
        return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
    }
}

/* Has this invoice already been credited (successfully)? */
if (!function_exists('fdms_invoice_already_credited')) {
    function fdms_invoice_already_credited(PDO $pdo, int $invoiceId): ?array {
        try {
            $stmt = $pdo->prepare("SELECT * FROM fdms_credit_notes WHERE original_invoice_id = :id AND success = 1 ORDER BY id DESC LIMIT 1");
            $stmt->execute([':id' => $invoiceId]);
            return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
        } catch (Throwable $e) { return null; }
    }
}

/* ---------------------------------------------------------------------
   The FiscalPro credit-note API call. Payload is exactly what their
   endpoint accepts: the invoice number to credit, plus free-text comments.
   The credit is ALWAYS for the invoice's full value - FiscalPro derives the
   amount from the referenced invoice, we never send one.
   --------------------------------------------------------------------- */
if (!function_exists('fdms_post_credit_note')) {
    function fdms_post_credit_note(string $invoiceNumber, string $comments): array {
        $payload = [
            'invoiceNumber' => $invoiceNumber,
            'comments' => $comments,
        ];
        $postData = json_encode($payload, JSON_UNESCAPED_UNICODE);

        $ch = curl_init(FDMS_CREDIT_NOTE_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']));

        return [
            'ok' => $ok,
            'data' => is_array($data) ? $data : [],
            'request_json' => $postData,
            'response_json' => (string)$response,
            'message' => $ok ? 'Credit note fiscalized.'
                : ($curlError !== '' ? ('Connection error: ' . $curlError)
                : (is_array($data) && isset($data['message']) ? (string)$data['message'] : ('FiscalPro HTTP ' . $httpCode))),
        ];
    }
}

/* =====================================================================
   1. FULL CREDIT NOTE
   Credits the entire invoice. Nothing is reinvoiced.
   Returns array('ok'=>bool,'credit_note_id'=>int,'ref'=>string,'message'=>string)
   ===================================================================== */
if (!function_exists('fdms_issue_credit_note')) {
    function fdms_issue_credit_note(PDO $pdo, int $invoiceId, int $reasonCode = 4, string $reasonText = '', string $creditType = 'full', ?float $replacementAmount = null): array {
        fdms_ensure_schema($pdo);
        fdms_ensure_credit_note_schema($pdo);

        $invoice = fdms_load_invoice($pdo, $invoiceId);
        if (!$invoice) return ['ok' => false, 'credit_note_id' => 0, 'ref' => '', 'message' => 'Fiscal invoice not found (or it was never successfully fiscalized).'];

        // Idempotency: never credit the same invoice twice - mirrors the
        // strlen($qrCodeUrl)<5 guard in the Dabula implementation.
        $existing = fdms_invoice_already_credited($pdo, $invoiceId);
        if ($existing) {
            return [
                'ok' => true,
                'credit_note_id' => (int)$existing['id'],
                'ref' => (string)($existing['zimra_credit_no'] ?? $existing['fiscal_id'] ?? ''),
                'message' => 'This invoice was already credited (credit note #' . (int)$existing['id'] . ') - not resubmitted.',
            ];
        }

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

        $reasons = fdms_credit_reasons();
        if (!isset($reasons[$reasonCode])) $reasonCode = 4;
        $comments = $reasons[$reasonCode] . ($reasonText !== '' ? ' - ' . $reasonText : '');

        $invoiceNo = (string)$invoice['invoice_no'];
        $res = fdms_post_credit_note($invoiceNo, $comments);
        $d = $res['data'];

        $stmt = $pdo->prepare("INSERT INTO fdms_credit_notes
            (order_id, original_invoice_id, original_invoice_no, credit_type, reason_code, reason_text, currency,
             amount_including_tax, replacement_amount, fiscal_id, zimra_credit_no, fiscal_device_id, fiscal_day_number,
             verification_code, qr_code_url, success, request_json, response_json, created_by_name, created_at)
            VALUES (:oid, :iid, :ino, :ctype, :rcode, :rtext, :cur, :amt, :ramt, :fid, :zno, :dev, :fday,
                    :vcode, :qr, :ok, :req, :resp, :by, NOW())");
        $stmt->execute([
            ':oid'  => (int)$invoice['order_id'],
            ':iid'  => $invoiceId,
            ':ino'  => $invoiceNo,
            ':ctype'=> $creditType === 'correction' ? 'correction' : 'full',
            ':rcode'=> $reasonCode,
            ':rtext'=> $reasonText !== '' ? $reasonText : null,
            ':cur'  => (string)($invoice['currency'] ?? ''),
            // INVARIANT: always the full original invoice value.
            ':amt'  => round((float)$invoice['amount_including_tax'], 2),
            ':ramt' => $replacementAmount !== null ? round($replacementAmount, 2) : null,
            ':fid'  => $res['ok'] ? (string)($d['id'] ?? '') : null,
            ':zno'  => $res['ok'] ? (string)($d['globalNumber'] ?? '') : null,
            ':dev'  => $res['ok'] ? (string)($d['fiscalDeviceId'] ?? '') : null,
            ':fday' => $res['ok'] ? (int)($d['fiscalDayNumber'] ?? 0) : null,
            ':vcode'=> $res['ok'] ? (string)($d['verificationCode'] ?? '') : null,
            ':qr'   => $res['ok'] ? (string)($d['qrCodeUrl'] ?? '') : null,
            ':ok'   => $res['ok'] ? 1 : 0,
            ':req'  => $res['request_json'],
            ':resp' => $res['response_json'],
            ':by'   => fdms_current_user_name_cn(),
        ]);
        $creditNoteId = (int)$pdo->lastInsertId();

        if (!$res['ok']) {
            return ['ok' => false, 'credit_note_id' => $creditNoteId, 'ref' => '', 'message' => 'Credit note failed: ' . $res['message']];
        }

        // Mark the original invoice as credited (a flag, not a mutation of its
        // fiscal data - the invoice document itself stays immutable).
        try { $pdo->prepare("UPDATE fdms_fiscal_invoices SET credited = 1 WHERE id = :id")->execute([':id' => $invoiceId]); } catch (Throwable $e) {}

        $ref = (string)($d['globalNumber'] ?? '') !== '' ? (string)$d['globalNumber'] : (string)($d['id'] ?? '');
        return [
            'ok' => true,
            'credit_note_id' => $creditNoteId,
            'ref' => $ref,
            'message' => 'Credit note fiscalized: ZIMRA ' . $ref . ' crediting invoice ' . $invoiceNo . ' in full (' . number_format((float)$invoice['amount_including_tax'], 2) . ').',
        ];
    }
}

/* =====================================================================
   2. PARTIAL CORRECTION - credit in FULL, then reinvoice the remainder.
   $creditAmount is what the customer is being credited (e.g. 25.00 off a
   100.00 invoice). The credit note is still for the full 100.00; a new
   invoice is raised for 75.00.

   Both documents are fiscalized. If the replacement invoice fails to
   fiscalize, the credit note has ALREADY been filed with ZIMRA and cannot
   be unfiled - so that state is reported loudly and recorded, and the
   replacement can be retried without re-crediting.
   ===================================================================== */
if (!function_exists('fdms_partial_correction')) {
    function fdms_partial_correction(PDO $pdo, int $invoiceId, float $creditAmount, int $reasonCode = 3, string $reasonText = ''): array {
        fdms_ensure_schema($pdo);
        fdms_ensure_credit_note_schema($pdo);

        $invoice = fdms_load_invoice($pdo, $invoiceId);
        if (!$invoice) return ['ok' => false, 'message' => 'Fiscal invoice not found (or never successfully fiscalized).'];

        $originalTotal = round((float)$invoice['amount_including_tax'], 2);
        $creditAmount = round($creditAmount, 2);

        if ($creditAmount <= 0) {
            return ['ok' => false, 'message' => 'The credit amount must be greater than zero.'];
        }
        if ($creditAmount > $originalTotal + 0.005) {
            return ['ok' => false, 'message' => 'The credit amount (' . number_format($creditAmount, 2) . ') cannot exceed the invoice total (' . number_format($originalTotal, 2) . ').'];
        }
        if (abs($creditAmount - $originalTotal) < 0.005) {
            // Crediting the whole invoice is simply a full credit note.
            return fdms_issue_credit_note($pdo, $invoiceId, $reasonCode, $reasonText, 'full', null);
        }

        $replacementTotal = round($originalTotal - $creditAmount, 2);
        $orderId = (int)$invoice['order_id'];

        // STEP 1: credit the ORIGINAL invoice in full.
        $note = 'Partial correction: crediting invoice in full, replacement invoice to be raised for '
              . number_format($replacementTotal, 2) . ' (credit of ' . number_format($creditAmount, 2) . ').'
              . ($reasonText !== '' ? ' ' . $reasonText : '');
        $credit = fdms_issue_credit_note($pdo, $invoiceId, $reasonCode, $note, 'correction', $replacementTotal);
        if (!$credit['ok']) {
            return ['ok' => false, 'message' => 'Correction aborted - the credit note was not fiscalized, so nothing was changed. ' . $credit['message']];
        }
        $creditNoteId = (int)$credit['credit_note_id'];

        // STEP 2: raise and fiscalize the REPLACEMENT invoice for the remainder.
        $replacement = fdms_issue_replacement_invoice($pdo, $orderId, $invoice, $replacementTotal, $creditNoteId, $reasonText);

        if (!$replacement['ok']) {
            return [
                'ok' => false,
                'credit_note_id' => $creditNoteId,
                'message' => 'IMPORTANT: the credit note WAS fiscalized with ZIMRA (' . $credit['ref'] . ') and cannot be reversed, '
                           . 'but the replacement invoice for ' . number_format($replacementTotal, 2) . ' FAILED to fiscalize: '
                           . $replacement['message'] . ' Retry the replacement invoice - do NOT issue another credit note.',
            ];
        }

        try {
            $pdo->prepare("UPDATE fdms_credit_notes SET replacement_invoice_id = :rid WHERE id = :cid")
                ->execute([':rid' => (int)$replacement['invoice_id'], ':cid' => $creditNoteId]);
        } catch (Throwable $e) {}

        return [
            'ok' => true,
            'credit_note_id' => $creditNoteId,
            'replacement_invoice_id' => (int)$replacement['invoice_id'],
            'ref' => $credit['ref'],
            'message' => 'Correction complete. Credit note ' . $credit['ref'] . ' credited invoice '
                       . (string)$invoice['invoice_no'] . ' in full (' . number_format($originalTotal, 2) . '), '
                       . 'and replacement invoice ' . $replacement['ref'] . ' was raised for '
                       . number_format($replacementTotal, 2) . ' (customer credited ' . number_format($creditAmount, 2) . ').',
        ];
    }
}

/* =====================================================================
   Replacement invoice: a NEW fiscal invoice for the reduced amount, posted
   through the same /api/invoices endpoint as an ordinary invoice.

   The lines are the ORIGINAL invoice's lines scaled proportionally to the
   new total, so VAT stays correct and each line still describes what was
   sold. If the original request JSON is unavailable, a single summary line
   is used instead.
   ===================================================================== */
if (!function_exists('fdms_issue_replacement_invoice')) {
    function fdms_issue_replacement_invoice(PDO $pdo, int $orderId, array $originalInvoice, float $newTotal, int $creditNoteId, string $reasonText = ''): array {
        $newTotal = round($newTotal, 2);
        if ($newTotal <= 0) return ['ok' => false, 'invoice_id' => 0, 'ref' => '', 'message' => 'Replacement total must be greater than zero.'];

        $currency = (string)($originalInvoice['currency'] ?? FDMS_DEFAULT_CURRENCY);
        $originalTotal = round((float)$originalInvoice['amount_including_tax'], 2);

        /* Rebuild lines from the original request, scaled to the new total.
           Rounding drift is absorbed into the LAST line so the line sum
           always equals the invoice total to the cent. */
        $lines = [];
        $origReq = json_decode((string)($originalInvoice['request_json'] ?? ''), true);
        if (is_array($origReq) && !empty($origReq['lines']) && $originalTotal > 0) {
            $scale = $newTotal / $originalTotal;
            $running = 0.0;
            $count = count($origReq['lines']);
            $i = 0;
            foreach ($origReq['lines'] as $ol) {
                $i++;
                $qty = (int)($ol['quantity'] ?? 1);
                if ($qty < 1) $qty = 1;
                $origLineTotal = round(((float)($ol['unitPriceIncludingTax'] ?? 0)) * $qty, 2);
                if ($i < $count) {
                    $newLineTotal = round($origLineTotal * $scale, 2);
                    $running += $newLineTotal;
                } else {
                    $newLineTotal = round($newTotal - $running, 2); // absorb drift
                }
                if ($newLineTotal <= 0) continue;
                $unit = round($newLineTotal / $qty, 2);
                $lines[] = [
                    'quantity' => $qty,
                    'unitPriceIncludingTax' => $unit,
                    'taxPercent' => (float)($ol['taxPercent'] ?? FDMS_VAT_PERCENT),
                    'description' => (string)($ol['description'] ?? 'Charge'),
                    'productName' => (string)($ol['productName'] ?? 'Charge'),
                ];
            }
        }
        if (!$lines) {
            $label = 'Accommodation (corrected) - replaces invoice ' . (string)$originalInvoice['invoice_no'];
            $lines[] = [
                'quantity' => 1,
                'unitPriceIncludingTax' => $newTotal,
                'taxPercent' => FDMS_VAT_PERCENT,
                'description' => $label,
                'productName' => $label,
            ];
        }

        /* Buyer: reuse the original invoice's buyer exactly, so the
           replacement is issued to the same party ZIMRA saw originally. */
        $buyer = (is_array($origReq) && !empty($origReq['buyer'])) ? $origReq['buyer'] : null;
        if (!$buyer) {
            $o = $pdo->prepare("SELECT * FROM orders WHERE id = :oid LIMIT 1");
            $o->execute([':oid' => $orderId]);
            $order = $o->fetch(PDO::FETCH_ASSOC) ?: [];
            $buyer = fdms_resolve_buyer($pdo, $order);
        }

        /* New invoice number - the original number is spent (it is the one the
           credit note referenced), so the replacement needs its own. */
        $baseNo = (string)$originalInvoice['invoice_no'];
        $suffix = 1;
        $newInvoiceNo = $baseNo . '-R' . $suffix;
        try {
            $chk = $pdo->prepare("SELECT COUNT(*) FROM fdms_fiscal_invoices WHERE invoice_no = :ino");
            while ($suffix < 20) {
                $chk->execute([':ino' => $newInvoiceNo]);
                if ((int)$chk->fetchColumn() === 0) break;
                $suffix++;
                $newInvoiceNo = $baseNo . '-R' . $suffix;
            }
        } catch (Throwable $e) {}

        $payload = [
            'number' => $newInvoiceNo,
            'currencyId' => in_array(strtoupper($currency), ['USD', 'ZWG'], true) ? strtoupper($currency) : FDMS_DEFAULT_CURRENCY,
            'creationDate' => date('Y-m-d'),
            'amountIncludingTax' => $newTotal,
            'amountTendered' => $newTotal,
            'qrCodeUrl' => '',
            'verificationCode' => '',
            'fiscalDeviceId' => 0,
            'globalNumber' => 0,
            'fiscalDayNumber' => 0,
            'lines' => $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);

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

        $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,
                 replaces_credit_note_id, created_at)
                VALUES (:oid, :inv, :cur, :amt, :fid, :zno, :fday, :vcode, :qr, :ok, :req, :resp, :cnid, NOW())")
            ->execute([
                ':oid' => $orderId,
                ':inv' => $newInvoiceNo,
                ':cur' => $payload['currencyId'],
                ':amt' => $newTotal,
                ':fid' => $ok ? (string)($d['id'] ?? '') : null,
                ':zno' => $ok ? (string)($d['globalNumber'] ?? '') : null,
                ':fday'=> $ok ? (int)($d['fiscalDayNumber'] ?? 0) : null,
                ':vcode'=>$ok ? (string)($d['verificationCode'] ?? '') : null,
                ':qr'  => $ok ? (string)($d['qrCodeUrl'] ?? '') : null,
                ':ok'  => $ok ? 1 : 0,
                ':req' => $postData,
                ':resp'=> (string)$response,
                ':cnid'=> $creditNoteId,
            ]);
        $newInvoiceId = (int)$pdo->lastInsertId();

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

        $ref = (string)($d['globalNumber'] ?? '') !== '' ? (string)$d['globalNumber'] : (string)($d['id'] ?? '');
        return ['ok' => true, 'invoice_id' => $newInvoiceId, 'ref' => $ref, 'message' => 'Replacement invoice fiscalized.'];
    }
}

/* Retry a failed replacement invoice WITHOUT re-crediting - for the state
   where the credit note filed but the replacement did not. */
if (!function_exists('fdms_retry_replacement_invoice')) {
    function fdms_retry_replacement_invoice(PDO $pdo, int $creditNoteId): array {
        fdms_ensure_credit_note_schema($pdo);
        $cn = $pdo->prepare("SELECT * FROM fdms_credit_notes WHERE id = :id AND success = 1 LIMIT 1");
        $cn->execute([':id' => $creditNoteId]);
        $note = $cn->fetch(PDO::FETCH_ASSOC);
        if (!$note) return ['ok' => false, 'message' => 'Credit note not found (or it never fiscalized).'];
        if ((string)$note['credit_type'] !== 'correction') return ['ok' => false, 'message' => 'This is a full credit note - there is no replacement invoice to raise.'];
        if ((int)($note['replacement_invoice_id'] ?? 0) > 0) return ['ok' => false, 'message' => 'The replacement invoice has already been raised successfully.'];

        $invoice = fdms_load_invoice($pdo, (int)$note['original_invoice_id']);
        if (!$invoice) return ['ok' => false, 'message' => 'Original invoice not found.'];

        $newTotal = round((float)($note['replacement_amount'] ?? 0), 2);
        if ($newTotal <= 0) return ['ok' => false, 'message' => 'Stored replacement amount is invalid.'];

        $res = fdms_issue_replacement_invoice($pdo, (int)$note['order_id'], $invoice, $newTotal, $creditNoteId, (string)($note['reason_text'] ?? ''));
        if ($res['ok']) {
            $pdo->prepare("UPDATE fdms_credit_notes SET replacement_invoice_id = :rid WHERE id = :cid")
                ->execute([':rid' => (int)$res['invoice_id'], ':cid' => $creditNoteId]);
            return ['ok' => true, 'message' => 'Replacement invoice ' . $res['ref'] . ' fiscalized for ' . number_format($newTotal, 2) . '.'];
        }
        return ['ok' => false, 'message' => 'Replacement invoice still failing: ' . $res['message']];
    }
}
