    <?php
require_once __DIR__ . '/SystemSettings.php';
require_once __DIR__ . '/Suppliers.php';
require_once __DIR__ . '/MraEisOnboarding.php';
require_once __DIR__ . '/MraEisOfflineThreshold.php';
require_once __DIR__ . '/../includes/payment_options.php';

class MraEisCompliance {
    private $conn;
    private $settings;
    private $warehouseBranchId;
    private $vatPercentage;
    private $offlineThreshold;

    public function __construct(PDO $conn) {
        $this->conn = $conn;
        $this->settings = new SystemSettings(null, $conn);
        $this->offlineThreshold = new MraEisOfflineThreshold($conn);
        $this->ensureTables();
        $this->vatPercentage = $this->settings->vatPercentage();
        $this->warehouseBranchId = $this->settings->eisWarehouseBranchId();
    }

    public function ensureTables() {
        $this->conn->exec("
            CREATE TABLE IF NOT EXISTS mra_eis_outbox (
                id INT AUTO_INCREMENT PRIMARY KEY,
                entity_type VARCHAR(40) NOT NULL,
                source_table VARCHAR(80) NOT NULL,
                source_id VARCHAR(120) DEFAULT NULL,
                source_key VARCHAR(160) NOT NULL,
                branch_id INT DEFAULT NULL,
                terminal_local_id INT DEFAULT NULL,
                endpoint VARCHAR(255) NOT NULL,
                payload_json MEDIUMTEXT NOT NULL,
                status ENUM('pending','ready','blocked','processing','submitted','failed') NOT NULL DEFAULT 'pending',
                attempts INT NOT NULL DEFAULT 0,
                http_status INT DEFAULT NULL,
                response_body MEDIUMTEXT NULL,
                mra_reference VARCHAR(255) DEFAULT NULL,
                validation_url TEXT NULL,
                local_audit_json MEDIUMTEXT NULL,
                last_error TEXT NULL,
                created_by INT DEFAULT NULL,
                submitted_at DATETIME DEFAULT NULL,
                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                UNIQUE KEY uniq_mra_eis_outbox_source (entity_type, source_key),
                KEY idx_mra_eis_outbox_status (status),
                KEY idx_mra_eis_outbox_branch (branch_id),
                KEY idx_mra_eis_outbox_terminal (terminal_local_id)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
        ");

        $this->conn->exec("
            CREATE TABLE IF NOT EXISTS mra_eis_compliance_events (
                id INT AUTO_INCREMENT PRIMARY KEY,
                outbox_id INT DEFAULT NULL,
                entity_type VARCHAR(40) NOT NULL,
                source_key VARCHAR(160) NOT NULL,
                event_type VARCHAR(80) NOT NULL,
                message TEXT NULL,
                payload_snapshot MEDIUMTEXT NULL,
                created_by INT DEFAULT NULL,
                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                KEY idx_mra_eis_events_outbox (outbox_id),
                KEY idx_mra_eis_events_source (entity_type, source_key)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
        ");

        $this->ensureTerminalMetadataColumns();
        $this->ensureConfigurationTable();
        $this->ensureOutboxColumns();
        $this->ensureItemProductCodesForEis();
    }

    private function ensureItemProductCodesForEis() {
        $tableStmt = $this->conn->prepare("SHOW TABLES LIKE :table_name");
        $tableStmt->execute([':table_name' => 'items']);
        if (!$tableStmt->fetchColumn() || !$this->columnExists('items', 'product_code')) {
            return;
        }

        $this->conn->exec("
            UPDATE items
            SET product_code = TRIM(SUBSTRING_INDEX(technical_specs, ' barcode: ', -1))
            WHERE technical_specs LIKE '% barcode: %'
              AND (product_code IS NULL OR product_code = '' OR product_code LIKE 'IMS-%')
        ");

        $tableStmt->execute([':table_name' => 'mra_eis_product_code_map']);
        if ($tableStmt->fetchColumn()) {
            $this->conn->exec("
                UPDATE items i
                INNER JOIN mra_eis_product_code_map pcm
                    ON pcm.ims_product_code = i.product_code
                SET i.product_code = pcm.eis_product_code
                WHERE pcm.eis_product_code IS NOT NULL
                  AND pcm.eis_product_code != ''
            ");
        }

        $tableStmt->execute([':table_name' => 'product_stock']);
        if ($tableStmt->fetchColumn()) {
            $this->conn->exec("
                UPDATE items i
                SET i.product_code = (
                    SELECT MIN(ps.barcode)
                    FROM product_stock ps
                    WHERE ps.product_name = i.item_name
                      AND ps.barcode IS NOT NULL
                      AND ps.barcode != ''
                      AND (ps.branch_id = i.current_location_id OR i.current_location_id IS NULL)
                )
                WHERE (i.product_code IS NULL OR i.product_code = '' OR i.product_code LIKE 'IMS-%')
                  AND EXISTS (
                      SELECT 1
                      FROM product_stock ps
                      WHERE ps.product_name = i.item_name
                        AND ps.barcode IS NOT NULL
                        AND ps.barcode != ''
                        AND (ps.branch_id = i.current_location_id OR i.current_location_id IS NULL)
                  )
            ");
        }
    }

    private function ensureOutboxColumns() {
        $columns = [
            'validation_url' => 'TEXT NULL',
            'local_audit_json' => 'MEDIUMTEXT NULL',
            'lock_token' => 'CHAR(36) DEFAULT NULL',
            'locked_at' => 'DATETIME DEFAULT NULL',
        ];

        foreach ($columns as $column => $definition) {
            if ($this->columnExists('mra_eis_outbox', $column)) {
                continue;
            }
            $this->conn->exec("ALTER TABLE mra_eis_outbox ADD COLUMN `$column` $definition");
        }
        $statusColumn = $this->conn->query("SHOW COLUMNS FROM mra_eis_outbox LIKE 'status'")->fetch(PDO::FETCH_ASSOC);
        if (!$statusColumn || stripos((string)($statusColumn['Type'] ?? ''), 'processing') === false) {
            $this->conn->exec(
                "ALTER TABLE mra_eis_outbox MODIFY COLUMN status "
                . "ENUM('pending','ready','blocked','processing','submitted','failed') NOT NULL DEFAULT 'pending'"
            );
        }

        // A sale is only verifiably submitted when MRA returned the validation
        // URL used on the fiscal receipt. Reopen legacy HTTP-only successes.
        $this->conn->exec("
            UPDATE mra_eis_outbox
            SET status = 'ready',
                submitted_at = NULL,
                last_error = 'Previous submission had no verifiable MRA EIS validation URL and was reopened.'
            WHERE entity_type = 'sale'
              AND status = 'submitted'
              AND (
                  validation_url IS NULL
                  OR validation_url = ''
                  OR (validation_url NOT LIKE 'http://%' AND validation_url NOT LIKE 'https://%')
              )
        ");

        // Payment method stored per sale line for receipts/EIS payloads.
        $siPaymentTables = $this->conn->prepare("SHOW TABLES LIKE 'sold_items'");
        $siPaymentTables->execute();
        if ($siPaymentTables->fetchColumn() && !$this->columnExists('sold_items', 'payment_method')) {
            $this->conn->exec("ALTER TABLE sold_items ADD COLUMN payment_method VARCHAR(40) NOT NULL DEFAULT 'cash'");
        }
        // VAT-5 certificate number stored per sale line for relief/exemption invoices.
        $siTables = $this->conn->prepare("SHOW TABLES LIKE 'sold_items'");
        $siTables->execute();
        if ($siTables->fetchColumn()) {
            if (!$this->columnExists('sold_items', 'vat5_certificate_number')) {
                $this->conn->exec("ALTER TABLE sold_items ADD COLUMN vat5_certificate_number VARCHAR(120) DEFAULT NULL");
            }
        }

        // Tourism / other levy rates per product in both mapping tables.
        $siteProductsCheck = $this->conn->prepare("SHOW TABLES LIKE 'mra_eis_site_products'");
        $siteProductsCheck->execute();
        if ($siteProductsCheck->fetchColumn()) {
            if (!$this->columnExists('mra_eis_site_products', 'levy_rate_id')) {
                $this->conn->exec("ALTER TABLE mra_eis_site_products ADD COLUMN levy_rate_id VARCHAR(50) DEFAULT NULL");
            }
        }

        $itemMapCheck = $this->conn->prepare("SHOW TABLES LIKE 'mra_eis_item_product_map'");
        $itemMapCheck->execute();
        if ($itemMapCheck->fetchColumn()) {
            if (!$this->columnExists('mra_eis_item_product_map', 'levy_rate_id')) {
                $this->conn->exec("ALTER TABLE mra_eis_item_product_map ADD COLUMN levy_rate_id VARCHAR(50) DEFAULT NULL");
            }
        }
    }

    private function ensureTerminalMetadataColumns() {
        $stmt = $this->conn->prepare("SHOW TABLES LIKE 'mra_eis_terminals'");
        $stmt->execute();
        if (!$stmt->fetchColumn()) {
            return;
        }

        $columns = [
            'site_id' => 'VARCHAR(80) DEFAULT NULL',
            'global_config_version' => 'INT NOT NULL DEFAULT 1',
            'taxpayer_config_version' => 'INT NOT NULL DEFAULT 1',
            'terminal_config_version' => 'INT NOT NULL DEFAULT 1',
            'is_blocked' => 'TINYINT(1) NOT NULL DEFAULT 0',
            'blocked_at' => 'DATETIME DEFAULT NULL',
            'block_reason' => 'TEXT NULL',
            'latest_config_response' => 'MEDIUMTEXT NULL',
            'last_server_datetime' => 'DATETIME DEFAULT NULL',
            'last_server_sync_at' => 'DATETIME DEFAULT NULL',
        ];

        foreach ($columns as $column => $definition) {
            if ($this->columnExists('mra_eis_terminals', $column)) {
                continue;
            }
            $this->conn->exec("ALTER TABLE mra_eis_terminals ADD COLUMN `$column` $definition");
        }
    }

    private function ensureConfigurationTable() {
        $sql = 'CREATE TABLE IF NOT EXISTS mra_eis_configurations ('
            . 'id BIGINT AUTO_INCREMENT PRIMARY KEY,'
            . 'terminal_local_id INT NOT NULL,'
            . 'global_config_version INT NOT NULL DEFAULT 1,'
            . 'taxpayer_config_version INT NOT NULL DEFAULT 1,'
            . 'terminal_config_version INT NOT NULL DEFAULT 1,'
            . 'standard_vat_percentage DECIMAL(7,4) DEFAULT NULL,'
            . 'offline_time_threshold_hours DECIMAL(10,4) DEFAULT NULL,'
            . 'offline_started_at DATETIME DEFAULT NULL,'
            . 'last_successful_eis_connection_at DATETIME DEFAULT NULL,'
            . 'last_eis_sync_at DATETIME DEFAULT NULL,'
            . "eis_connection_status ENUM('unknown','online','offline') NOT NULL DEFAULT 'unknown',"
            . 'global_configuration_json LONGTEXT NOT NULL,'
            . 'taxpayer_configuration_json LONGTEXT NOT NULL,'
            . 'terminal_configuration_json LONGTEXT NOT NULL,'
            . 'tax_rates_json LONGTEXT NOT NULL,'
            . 'levy_rates_json LONGTEXT NOT NULL,'
            . 'source_response_json LONGTEXT NOT NULL,'
            . 'source_hash CHAR(64) NOT NULL,'
            . 'mra_synced_at DATETIME NOT NULL,'
            . 'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,'
            . 'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,'
            . 'UNIQUE KEY uniq_mra_eis_configuration_terminal (terminal_local_id),'
            . 'KEY idx_mra_eis_configuration_vat (standard_vat_percentage),'
            . 'KEY idx_mra_eis_configuration_synced (mra_synced_at)'
            . ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci';
        $this->conn->exec($sql);
        $configurationColumns = [
            'offline_time_threshold_hours' => 'DECIMAL(10,4) DEFAULT NULL',
            'offline_started_at' => 'DATETIME DEFAULT NULL',
            'last_successful_eis_connection_at' => 'DATETIME DEFAULT NULL',
            'last_eis_sync_at' => 'DATETIME DEFAULT NULL',
            'eis_connection_status' => "ENUM('unknown','online','offline') NOT NULL DEFAULT 'unknown'",
        ];
        foreach ($configurationColumns as $column => $definition) {
            if (!$this->columnExists('mra_eis_configurations', $column)) {
                $this->conn->exec("ALTER TABLE mra_eis_configurations ADD COLUMN `$column` $definition");
            }
        }
        $this->backfillStoredConfigurations();
    }

    private function backfillStoredConfigurations() {
        $table = $this->conn->prepare('SHOW TABLES LIKE :table_name');
        $table->execute([':table_name' => 'mra_eis_terminals']);
        if (!$table->fetchColumn()) {
            return;
        }
        $stmt = $this->conn->query(
            'SELECT t.id, t.latest_config_response, t.confirmation_response, t.activation_response '
            . 'FROM mra_eis_terminals t LEFT JOIN mra_eis_configurations c ON c.terminal_local_id = t.id '
            . 'WHERE c.id IS NULL'
        );
        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $terminal) {
            foreach (['latest_config_response', 'confirmation_response', 'activation_response'] as $field) {
                $decoded = json_decode((string)($terminal[$field] ?? ''), true);
                if (!is_array($decoded)) {
                    continue;
                }
                $configuration = $decoded['data']['configuration'] ?? $decoded['configuration'] ?? null;
                if (is_array($configuration)) {
                    $this->persistMraConfiguration((int)$terminal['id'], $configuration, $decoded);
                    break;
                }
            }
        }
    }

    private function persistMraConfiguration($terminalId, array $configuration, array $sourceResponse = []) {
        $global = is_array($configuration['globalConfiguration'] ?? null) ? $configuration['globalConfiguration'] : [];
        $taxpayer = is_array($configuration['taxpayerConfiguration'] ?? null) ? $configuration['taxpayerConfiguration'] : [];
        $terminal = is_array($configuration['terminalConfiguration'] ?? null) ? $configuration['terminalConfiguration'] : [];
        $taxRates = $global['taxRates'] ?? $global['taxrates'] ?? [];
        $levyRates = $global['levyRates'] ?? $global['levyrates'] ?? [];
        $taxRates = is_array($taxRates) ? $taxRates : [];
        $levyRates = is_array($levyRates) ? $levyRates : [];
        $standardVat = $this->standardVatFromRates($taxRates);
        $sourceJson = json_encode($sourceResponse ?: ['data' => ['configuration' => $configuration]], JSON_UNESCAPED_SLASHES);
        $hash = hash('sha256', (string)$sourceJson);

        $existing = $this->conn->prepare('SELECT source_hash FROM mra_eis_configurations WHERE terminal_local_id = :terminal_id');
        $existing->execute([':terminal_id' => (int)$terminalId]);
        if ($existing->fetchColumn() === $hash) {
            $this->conn->prepare(
                'UPDATE mra_eis_configurations SET offline_time_threshold_hours = :hours WHERE terminal_local_id = :terminal_id'
            )->execute([
                ':hours' => MraEisOfflineThreshold::thresholdFromTerminalConfiguration($terminal),
                ':terminal_id' => (int)$terminalId,
            ]);
            if ($standardVat !== null) {
                $this->updateSystemVatFromMra($standardVat);
            }
            return false;
        }
        $this->saveMraConfigurationRow($terminalId, $global, $taxpayer, $terminal, $taxRates, $levyRates, $standardVat, $sourceJson, $hash);
        if ($standardVat !== null) {
            $this->updateSystemVatFromMra($standardVat);
        }
        return true;
    }

    private function updateSystemVatFromMra($standardVat) {
        if (method_exists($this->settings, 'updateVatFromMra')) {
            $this->settings->updateVatFromMra($standardVat);
            return;
        }

        // Backward-compatible deployment path: a long-running PHP worker or
        // OPcache may still have the pre-database SystemSettings class loaded.
        // Persist the MRA rate directly instead of failing the configuration sync.
        $this->conn->exec(
            "CREATE TABLE IF NOT EXISTS system_configurations (
                setting_key VARCHAR(100) NOT NULL PRIMARY KEY,
                setting_value LONGTEXT NULL,
                value_type VARCHAR(20) NOT NULL DEFAULT 'string',
                source VARCHAR(30) NOT NULL DEFAULT 'system',
                description VARCHAR(255) DEFAULT NULL,
                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                KEY idx_system_configurations_source (source)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"
        );
        $stmt = $this->conn->prepare(
            'INSERT INTO system_configurations (setting_key, setting_value, value_type, source, description) '
            . 'VALUES (\'vat_percentage\', :vat, \'float\', \'mra\', :description) '
            . 'ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value), value_type = VALUES(value_type), '
            . 'source = VALUES(source), description = VALUES(description)'
        );
        $stmt->execute([
            ':vat' => (string)round((float)$standardVat, 4),
            ':description' => 'Standard VAT percentage supplied by the latest MRA EIS global configuration.',
        ]);
    }

    private function saveMraConfigurationRow($terminalId, array $global, array $taxpayer, array $terminal, array $taxRates, array $levyRates, $standardVat, $sourceJson, $hash) {
        $offlineThresholdHours = MraEisOfflineThreshold::thresholdFromTerminalConfiguration($terminal);
        $sql = 'INSERT INTO mra_eis_configurations ('
            . 'terminal_local_id, global_config_version, taxpayer_config_version, terminal_config_version, '
            . 'standard_vat_percentage, offline_time_threshold_hours, global_configuration_json, taxpayer_configuration_json, '
            . 'terminal_configuration_json, tax_rates_json, levy_rates_json, source_response_json, source_hash, mra_synced_at'
            . ') VALUES ('
            . ':terminal_id, :global_version, :taxpayer_version, :terminal_version, :vat, :offline_threshold, :global_json, '
            . ':taxpayer_json, :terminal_json, :tax_rates_json, :levy_rates_json, :source_json, :source_hash, NOW()'
            . ') ON DUPLICATE KEY UPDATE global_config_version = VALUES(global_config_version), '
            . 'taxpayer_config_version = VALUES(taxpayer_config_version), terminal_config_version = VALUES(terminal_config_version), '
            . 'offline_time_threshold_hours = VALUES(offline_time_threshold_hours), '
            . 'standard_vat_percentage = VALUES(standard_vat_percentage), global_configuration_json = VALUES(global_configuration_json), '
            . 'taxpayer_configuration_json = VALUES(taxpayer_configuration_json), terminal_configuration_json = VALUES(terminal_configuration_json), '
            . 'tax_rates_json = VALUES(tax_rates_json), levy_rates_json = VALUES(levy_rates_json), '
            . 'source_response_json = VALUES(source_response_json), source_hash = VALUES(source_hash), mra_synced_at = NOW()';
        $stmt = $this->conn->prepare($sql);
        $params = $this->mraConfigurationParams($terminalId, $global, $taxpayer, $terminal, $taxRates, $levyRates, $standardVat, $sourceJson, $hash);
        $params[':offline_threshold'] = $offlineThresholdHours;
        $stmt->execute($params);
    }

    private function mraConfigurationParams($terminalId, array $global, array $taxpayer, array $terminal, array $taxRates, array $levyRates, $standardVat, $sourceJson, $hash) {
        return [
            ':terminal_id' => (int)$terminalId,
            ':global_version' => max(1, (int)($global['versionNo'] ?? $global['globalConfigVersion'] ?? 1)),
            ':taxpayer_version' => max(1, (int)($taxpayer['versionNo'] ?? $taxpayer['taxpayerConfigVersion'] ?? 1)),
            ':terminal_version' => max(1, (int)($terminal['versionNo'] ?? $terminal['terminalConfigVersion'] ?? 1)),
            ':vat' => $standardVat,
            ':global_json' => json_encode($global, JSON_UNESCAPED_SLASHES),
            ':taxpayer_json' => json_encode($taxpayer, JSON_UNESCAPED_SLASHES),
            ':terminal_json' => json_encode($terminal, JSON_UNESCAPED_SLASHES),
            ':tax_rates_json' => json_encode($taxRates, JSON_UNESCAPED_SLASHES),
            ':levy_rates_json' => json_encode($levyRates, JSON_UNESCAPED_SLASHES),
            ':source_json' => (string)$sourceJson,
            ':source_hash' => (string)$hash,
        ];
    }

    private function standardVatFromRates(array $taxRates) {
        $defaultRateId = $this->settings->eisDefaultTaxRateId();
        foreach ($taxRates as $rate) {
            if (!is_array($rate)) {
                continue;
            }
            $rateId = strtoupper(trim((string)($rate['id'] ?? $rate['rateId'] ?? '')));
            $percentage = $rate['rate'] ?? $rate['percentage'] ?? null;
            if ($rateId === $defaultRateId && is_numeric($percentage)) {
                return (float)$percentage;
            }
        }
        return null;
    }

    private function columnExists($table, $column) {
        $stmt = $this->conn->prepare("SHOW COLUMNS FROM `$table` LIKE :column_name");
        $stmt->execute([':column_name' => $column]);
        return (bool)$stmt->fetch(PDO::FETCH_ASSOC);
    }

    public function prepareRecentSales($branchId = null, $startDate = null, $endDate = null, $limit = 100, $userId = null) {
        $sql = "
            SELECT si.transaction_id, si.branch_id
            FROM sold_items si
            LEFT JOIN mra_eis_outbox outbox
              ON outbox.entity_type = 'sale'
             AND outbox.source_key = si.transaction_id
             AND outbox.status = 'submitted'
            WHERE si.transaction_id IS NOT NULL
              AND si.transaction_id != ''
              AND outbox.id IS NULL
        ";
        $params = [];

        if ($branchId !== null && $branchId !== '') {
            $sql .= " AND si.branch_id = :branch_id";
            $params[':branch_id'] = (int)$branchId;
        }
        if ($startDate !== null && $startDate !== '') {
            $sql .= " AND DATE(si.sold_at) >= :start_date";
            $params[':start_date'] = $startDate;
        }
        if ($endDate !== null && $endDate !== '') {
            $sql .= " AND DATE(si.sold_at) <= :end_date";
            $params[':end_date'] = $endDate;
        }

        $sql .= " GROUP BY si.transaction_id, si.branch_id ORDER BY MAX(si.sold_at) DESC LIMIT " . max(1, min(500, (int)$limit));
        $stmt = $this->conn->prepare($sql);
        foreach ($params as $key => $value) {
            $stmt->bindValue($key, $value, is_int($value) ? PDO::PARAM_INT : PDO::PARAM_STR);
        }
        $stmt->execute();

        $summary = ['prepared' => 0, 'ready' => 0, 'blocked' => 0];
        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
            $result = $this->prepareSale((string)$row['transaction_id'], (int)$row['branch_id'], $userId);
            $summary['prepared']++;
            $summary[$result['status']] = ($summary[$result['status']] ?? 0) + 1;
        }

        return $summary;
    }

    public function prepareSale($transactionId, $branchId, $userId = null, array $extraContext = []) {
        $sale = $this->saleSnapshot($transactionId, $branchId);
        if (!$sale) {
            throw new RuntimeException('Sale transaction was not found.');
        }

        $terminal = $this->terminalForSale($sale);
        $issues = [];
        if (!$terminal) {
            $issues[] = 'No activated EIS terminal is assigned to this branch or sale user.';
        }
        $terminalTaxRates = $this->terminalTaxRateMap($terminal);

        $groupedLines = [];
        $localAuditItems = [];
        $taxBreakdown = [];
        $levyBreakdown = [];
        $invoiceTotal = 0.0;
        $totalVat = 0.0;
        $isReliefSupply = !empty($sale['vat_relief']);
        $offlineMode = $terminal
            ? $this->isOfflineMode((int)$terminal['id'])
            : $this->isOfflineMode();
        $terminalLevyRates = $this->terminalLevyRateMap($terminal);
        $isVatRegistered = $this->terminalVatRegistrationStatus($terminal);
        foreach ($sale['items'] as $item) {
            if (!$this->isUsableEisProductCode($item['product_code'] ?? null)) {
                $issues[] = 'Missing items.product_code for local unique_code ' . $item['unique_code'] . '.';
                continue;
            }

            $lineTotal = (float)$item['total_price'];
            $taxRateId = (string)($item['tax_rate_id'] ?? 'VAT_ZERO');
            $vat = ($isReliefSupply || $isVatRegistered === false)
                ? 0.0
                : $this->vatComponent($lineTotal, $taxRateId, $terminalTaxRates);
            $levyRateId = $isReliefSupply ? '' : trim((string)($item['levy_rate_id'] ?? ''));
            $levy = $levyRateId !== '' ? $this->levyComponent($lineTotal, $levyRateId, $terminalLevyRates) : 0.0;
            $unitPrice = (float)($item['original_unit_price'] ?? $item['unit_price']);
            $discountAmount = max(0, (float)($item['discount_amount'] ?? ($unitPrice - $lineTotal)));
            $groupKey = implode('|', [
                (string)$item['product_code'],
                number_format($unitPrice, 4, '.', ''),
                $taxRateId,
                $levyRateId,
            ]);

            if (!isset($groupedLines[$groupKey])) {
                $productName = (string)($item['product_name'] ?: $item['item_name']);
                $unitOfMeasure = trim((string)($item['unit_of_measure'] ?? ''));
                $groupedLines[$groupKey] = [
                    'productCode' => (string)$item['product_code'],
                    'productName' => $productName,
                    'description' => (string)($item['eis_description'] ?: $productName),
                    'unitOfMeasure' => $unitOfMeasure !== '' ? $unitOfMeasure : 'EA',
                    'unitPrice' => $unitPrice,
                    'quantity' => 0,
                    'discount' => 0,
                    'total' => 0.0,
                    'totalVAT' => 0.0,
                    'taxRateId' => $taxRateId,
                    'isProduct' => (bool)($item['is_product'] ?? true),
                    '_unique_codes' => [],
                    '_item_ids' => [],
                    '_sale_ids' => [],
                ];
            }

            $groupedLines[$groupKey]['quantity']++;
            $groupedLines[$groupKey]['discount'] += $discountAmount;
            $groupedLines[$groupKey]['total'] += $lineTotal;
            $groupedLines[$groupKey]['totalVAT'] += $vat;
            $groupedLines[$groupKey]['_unique_codes'][] = (string)$item['unique_code'];
            $groupedLines[$groupKey]['_item_ids'][] = (int)$item['item_id'];
            $groupedLines[$groupKey]['_sale_ids'][] = (int)$item['sale_id'];
            $invoiceTotal += $lineTotal;
            $totalVat += $vat;
            if (!isset($taxBreakdown[$taxRateId])) {
                $taxBreakdown[$taxRateId] = ['rateId' => $taxRateId, 'taxableAmount' => 0, 'taxAmount' => 0];
            }
            $taxBreakdown[$taxRateId]['taxableAmount'] += $lineTotal - $vat;
            $taxBreakdown[$taxRateId]['taxAmount'] += $vat;

            if ($levy > 0.0 && $levyRateId !== '') {
                if (!isset($levyBreakdown[$levyRateId])) {
                    $lookupId = strtoupper(trim($levyRateId));
                    $levyRate = $terminalLevyRates[$lookupId] ?? null;
                    if ($levyRate === null && preg_match('/(\d+(?:\.\d+)?)/', $lookupId, $m)) {
                        $levyRate = (float)$m[1];
                    }
                    $levyBreakdown[$levyRateId] = [
                        'levyTypeId' => $levyRateId,
                        'levyRate'   => (float)($levyRate ?? 0),
                        'levyAmount' => 0.0,
                    ];
                }
                $levyBreakdown[$levyRateId]['levyAmount'] += $levy;
            }

            $localAuditItems[] = [
                'sale_id' => (int)$item['sale_id'],
                'item_id' => (int)$item['item_id'],
                'unique_code' => (string)$item['unique_code'],
                'ims_item_name' => (string)$item['item_name'],
                'eis_product_code' => (string)$item['product_code'],
                'eis_product_name' => (string)($item['product_name'] ?: $item['item_name']),
                'unit_price' => $unitPrice,
                'line_total' => $lineTotal,
            ];
        }

        $lines = [];
        $lineAudit = [];
        $lineNumber = 1;
        foreach ($groupedLines as $line) {
            $uniqueCodes = $line['_unique_codes'];
            $itemIds = $line['_item_ids'];
            $saleIds = $line['_sale_ids'];
            unset($line['_unique_codes'], $line['_item_ids'], $line['_sale_ids']);

            $line['id'] = $lineNumber;
            $line['total'] = round((float)$line['total'], 2);
            $line['totalVAT'] = round((float)$line['totalVAT'], 2);
            $lines[] = $line;

            $lineAudit[] = [
                'eis_line_id' => $lineNumber,
                'eis_product_code' => $line['productCode'],
                'eis_product_name' => $line['description'],
                'quantity_sent_to_eis' => (int)$line['quantity'],
                'ims_item_ids' => $itemIds,
                'ims_sale_ids' => $saleIds,
                'ims_unique_codes' => $uniqueCodes,
            ];
            $lineNumber++;
        }

        $siteId = $this->siteIdForTerminal($terminal, $sale);
        if ($siteId === '') {
            $issues[] = 'No EIS site ID was found for the terminal or mapped products.';
        }

        $transactionType = strtoupper(trim((string)($sale['eis_transaction_type'] ?? '')));
        if (trim((string)($sale['buyer_tin'] ?? '')) !== '') {
            $transactionType = 'B2B';
        } elseif ($transactionType !== 'B2B' && $transactionType !== 'B2C') {
            $transactionType = 'B2C';
        }
        $isB2B = $transactionType === 'B2B';
        if ($offlineMode && $isB2B) {
            $issues[] = 'B2B transactions are online-only and cannot be prepared while EIS Offline is enabled.';
        }

        // The API-derived project number/quantity/dates are only handed in via
        // $extraContext at the moment of the original sale. Any later re-preparation
        // (retry button, offline outbox sync, "recall") calls this with an empty
        // context, so fall back to what was persisted on the sale itself or the
        // relief-supply invoice would silently lose its project number.
        $vat5CertDetails = $extraContext['vat5_certificate_details'] ?? $sale['vat5_certificate_details'] ?? null;
        $vat5Certificate = $isReliefSupply && !empty($sale['vat5_certificate_number'])
            ? array_filter([
                'certificateNumber'     => (string)$sale['vat5_certificate_number'],
                'projectNumber'         => $vat5CertDetails['projectNumber'] ?? null,
                'quantity'              => isset($vat5CertDetails['quantity']) ? (float)$vat5CertDetails['quantity'] : null,
                'certificateIssueDate'  => $vat5CertDetails['dateOfIssue'] ?? null,
                'certificateExpiryDate' => $vat5CertDetails['dateOfExpiry'] ?? null,
            ], static fn($v) => $v !== null)
            : null;
        $payload = [
            'invoiceHeader' => [
                'invoiceNumber' => (string)$sale['transaction_id'],
                'invoiceDateTime' => $this->isoDate($sale['sold_at']),
                'sellerTIN' => (string)($terminal['taxpayer_tin'] ?? ''),
                'buyerTIN' => $isB2B ? $this->nullIfBlank($sale['buyer_tin'] ?? null) : null,
                'buyerName' => $this->nullIfBlank($sale['customer_name'] ?? null),
                'buyerAuthorizationCode' => $isB2B ? $this->nullIfBlank($sale['buyer_authorization_code'] ?? null) : null,
                'siteId' => $siteId,
                'globalConfigVersion' => (int)($terminal['global_config_version'] ?? 1),
                'taxpayerConfigVersion' => (int)($terminal['taxpayer_config_version'] ?? 1),
                'terminalConfigVersion' => (int)($terminal['terminal_config_version'] ?? 1),
                'isExport' => false,
                'isReliefSupply' => $isReliefSupply,
                'paymentMethod' => paymentMethodToEis($sale['payment_method'] ?? 'cash', $this->settings->eisDefaultPaymentMethod()),
                'vat5CertificateDetails' => $vat5Certificate,
            ],
            'invoiceLineItems' => $lines,
            'invoiceSummary' => [
                'taxBreakDown' => array_values($taxBreakdown),
                'levyBreakDown' => array_values(array_map(static function ($b) {
                    return ['levyTypeId' => $b['levyTypeId'], 'levyRate' => $b['levyRate'], 'levyAmount' => round($b['levyAmount'], 2)];
                }, $levyBreakdown)),
                'totalVAT' => round($totalVat, 2),
                'offlineSignature' => null,
                'invoiceTotal' => round($invoiceTotal, 2),
                'amountTendered' => round($invoiceTotal, 2),
            ],
        ];
        $payload = $this->normalizeSaleSubmissionPayload($payload);
        $offlineReceipt = null;
        if ($offlineMode && !$isB2B && $terminal) {
            $this->assertOfflineSaleAllowed(
                (int)$sale['branch_id'],
                $sale['sold_by'] ?? null,
                $invoiceTotal,
                $sale['sold_at'],
                (string)$sale['transaction_id']
            );
            $offlineReceipt = $this->generateOfflineSignature($payload, $terminal);
            $payload['invoiceSummary']['offlineSignature'] = $offlineReceipt['signature'];
        }

        if (($payload['invoiceHeader']['sellerTIN'] ?? '') === '') {
            $issues[] = 'The selected terminal has no taxpayer TIN.';
        }
        if ($isB2B && ($payload['invoiceHeader']['buyerTIN'] ?? '') === '') {
            $issues[] = 'Buyer TIN is required for a B2B transaction.';
        }
        if ($isReliefSupply && ($payload['invoiceHeader']['buyerTIN'] ?? '') === '') {
            $issues[] = 'Buyer TIN is required for a VAT relief supply.';
        }
        $payloadIssues = $this->validateSaleSubmissionPayload($payload);
        if (!empty($payloadIssues)) {
            $issues = array_merge($issues, $payloadIssues);
        }
        if (empty($lines)) {
            $issues[] = 'No EIS-ready invoice lines could be prepared.';
        }

        $localAudit = [
            'bridge' => 'items.product_code submitted to EIS; items.unique_code retained as the local serialized-item reference',
            'entity_type' => 'sale',
            'transaction_id' => (string)$sale['transaction_id'],
            'eis_transaction_type' => $transactionType,
            'is_vat_registered' => $isVatRegistered,
            'eis_offline_transaction' => $offlineMode,
            'offline_receipt_data' => $offlineReceipt['receipt_data'] ?? null,
            'branch_id' => (int)$sale['branch_id'],
            'ims_item_count' => count($sale['items']),
            'eis_line_count' => count($lines),
            'items' => $localAuditItems,
            'eis_lines' => $lineAudit,
        ];

        return $this->upsertOutbox([
            'entity_type' => 'sale',
            'source_table' => 'sold_items',
            'source_id' => null,
            'source_key' => (string)$sale['transaction_id'],
            'branch_id' => (int)$sale['branch_id'],
            'terminal_local_id' => $terminal ? (int)$terminal['id'] : null,
            'endpoint' => '/sales/submit-sales-transaction',
            'payload_json' => json_encode($payload, JSON_UNESCAPED_SLASHES),
            'status' => empty($issues) ? 'ready' : 'blocked',
            'last_error' => empty($issues) ? null : implode(' ', array_unique($issues)),
            'created_by' => $userId,
            'local_audit_json' => json_encode($localAudit, JSON_UNESCAPED_SLASHES),
        ]);
    }

    public function prepareTransfer($shipmentId, $userId = null, $terminalId = null) {
        $shipment = $this->shipmentSnapshot($shipmentId);
        if (!$shipment) {
            throw new RuntimeException('Shipment was not found.');
        }

        $issues = [];
        if ($terminalId) {
            // Caller explicitly picked a terminal (e.g. to work around a branch_id mismatch between
            // the shipment's branches and the terminal's assigned branch) - use exactly that terminal
            // rather than silently falling back to auto-detection, which would defeat the point of
            // an explicit override.
            $terminal = $this->terminalById($terminalId);
            if (!$terminal) {
                $issues[] = 'The selected terminal (ID ' . (int)$terminalId . ') is not an activated EIS terminal.';
            }
        } else {
            // The warehouse branch typically has no EIS terminal of its own (no POS device sits at a
            // warehouse - see the "no EIS site ID" exemption for it below), so a warehouse-to-site
            // transfer must fall back to the destination branch's terminal. Falling back only when
            // from_branch_id is falsy (the old behavior) never triggered for a real warehouse transfer,
            // since branch IDs are never 0 - it only ever looked up the source branch's terminal.
            // If neither branch has a matching terminal (e.g. a branch_id mismatch), fall back to the
            // configured default transfer terminal as a last resort.
            $terminal = $this->terminalForBranch($shipment['from_branch_id'])
                ?: $this->terminalForBranch($shipment['to_branch_id'])
                ?: $this->terminalById($this->settings->eisDefaultTransferTerminalId());
            if (!$terminal) {
                $issues[] = sprintf(
                    'No activated EIS terminal was found for this transfer (checked from_branch_id=%d, to_branch_id=%d, and no default transfer terminal is configured).',
                    (int)$shipment['from_branch_id'],
                    (int)$shipment['to_branch_id']
                );
            }
        }

        $fromSite = $this->siteIdForBranch((int)$shipment['from_branch_id']);
        $toSite = $this->siteIdForBranch((int)$shipment['to_branch_id']);
        $fromWarehouse = (int)$shipment['from_branch_id'] === $this->warehouseBranchId;
        if (!$fromWarehouse && $fromSite === '') {
            $issues[] = 'Source branch has no EIS site ID.';
        }
        if ($toSite === '') {
            $issues[] = 'Destination branch has no EIS site ID.';
        }
        if ((int)$shipment['to_branch_id'] === $this->warehouseBranchId) {
            $issues[] = 'EIS API supports Warehouse-to-Site and Site-to-Site transfers. Site-to-Warehouse transfer is not enabled here.';
        }

        // The destination site's own terminal - not necessarily $terminal above, which may be the
        // source branch's terminal or an explicit override - owns the product catalog we need to
        // snapshot "before" quantities from, so a later receipt confirmation can compute the delta.
        $destinationTerminal = $this->terminalForBranch($shipment['to_branch_id']);

        $items = [];
        $lineAudit = [];
        foreach ($shipment['items'] as $item) {
            $barcode = (string)($item['product_code'] ?? '');
            if (!$this->isUsableEisProductCode($barcode)) {
                $issues[] = 'Missing items.product_code for local unique_code ' . $item['unique_code'] . '.';
                continue;
            }
            if (!isset($items[$barcode])) {
                $items[$barcode] = [
                    'barcode' => $barcode,
                    'quantity' => 0,
                    'price' => (float)$item['unit_price'],
                    '_product_name' => (string)($item['product_name'] ?: $item['item_name']),
                    '_unique_codes' => [],
                    '_item_ids' => [],
                ];
            }
            $items[$barcode]['quantity']++;
            $items[$barcode]['_unique_codes'][] = (string)$item['unique_code'];
            $items[$barcode]['_item_ids'][] = (int)$item['item_id'];
        }

        $payloadItems = [];
        foreach ($items as $item) {
            $uniqueCodes = $item['_unique_codes'];
            $itemIds = $item['_item_ids'];
            $productName = $item['_product_name'];
            unset($item['_unique_codes'], $item['_item_ids'], $item['_product_name']);
            $payloadItems[] = $item;
            $lineAudit[] = [
                'eis_product_code' => $item['barcode'],
                'eis_product_name' => $productName,
                'quantity_sent_to_eis' => (int)$item['quantity'],
                'ims_item_ids' => $itemIds,
                'ims_unique_codes' => $uniqueCodes,
                'eis_destination_terminal_id' => $destinationTerminal ? (int)$destinationTerminal['id'] : null,
                'eis_site_quantity_before' => $destinationTerminal
                    ? $this->siteProductQuantity((int)$destinationTerminal['id'], $item['barcode'])
                    : null,
            ];
        }

        $terminalMraId = $terminal ? trim((string)($terminal['mra_terminal_id'] ?? '')) : '';
        if ($terminal && $terminalMraId === '') {
            $issues[] = 'The selected terminal has no MRA terminal ID recorded - re-confirm terminal activation.';
        }

        $payload = [
            'terminalId' => $terminalMraId,
            'fromWarehouseToSite' => $fromWarehouse,
            'siteToWarehouse' => false,
            'fromSiteId' => $fromWarehouse ? null : $fromSite,
            'toSiteId' => $toSite,
            'items' => $payloadItems,
        ];

        if (empty($items)) {
            $issues[] = 'No EIS-ready transfer items could be prepared.';
        }

        $localAudit = [
            'bridge' => 'items.product_code submitted to EIS transfers; items.unique_code retained as the local serialized-item reference',
            'entity_type' => 'transfer',
            'shipment_id' => (int)$shipment['shipment_id'],
            'from_branch_id' => (int)$shipment['from_branch_id'],
            'to_branch_id' => (int)$shipment['to_branch_id'],
            'ims_item_count' => count($shipment['items']),
            'eis_line_count' => count($payloadItems),
            'eis_lines' => $lineAudit,
        ];

        return $this->upsertOutbox([
            'entity_type' => 'transfer',
            'source_table' => 'shipments',
            'source_id' => (string)$shipment['shipment_id'],
            'source_key' => 'shipment:' . $shipment['shipment_id'],
            'branch_id' => (int)$shipment['from_branch_id'],
            'terminal_local_id' => $terminal ? (int)$terminal['id'] : null,
            'endpoint' => '/stock/transfer-inventory',
            'payload_json' => json_encode($payload, JSON_UNESCAPED_SLASHES),
            'status' => empty($issues) ? 'ready' : 'blocked',
            'last_error' => empty($issues) ? null : implode(' ', array_unique($issues)),
            'created_by' => $userId,
            'local_audit_json' => json_encode($localAudit, JSON_UNESCAPED_SLASHES),
        ]);
    }

    /**
     * True if this shipment's EIS stock transfer has already been submitted to MRA. Once MRA has
     * acknowledged a transfer there is no "cancel transfer" endpoint (unlike sales, which support
     * /sales/cancel-receipt) - so undoing the shipment locally at that point would leave MRA's
     * records out of sync with IMS and must be blocked.
     */
    public function transferAlreadySubmitted($shipmentId) {
        $stmt = $this->conn->prepare("
            SELECT status FROM mra_eis_outbox
            WHERE entity_type = 'transfer' AND source_key = :source_key
            ORDER BY id DESC LIMIT 1
        ");
        $stmt->execute([':source_key' => 'shipment:' . (int)$shipmentId]);
        return $stmt->fetchColumn() === 'submitted';
    }

    /**
     * Removes a not-yet-submitted transfer outbox entry when its shipment is undone locally, so a
     * later retry sweep (retryBlockedTransfers/prepareRecentTransfers) doesn't submit a transfer for
     * a shipment that no longer exists. No-op if the entry was already submitted.
     */
    public function cancelUnsubmittedTransfer($shipmentId) {
        $stmt = $this->conn->prepare("
            DELETE FROM mra_eis_outbox
            WHERE entity_type = 'transfer' AND source_key = :source_key AND status != 'submitted'
        ");
        $stmt->execute([':source_key' => 'shipment:' . (int)$shipmentId]);
    }

    public function prepareRecentTransfers($branchId = null, $startDate = null, $endDate = null, $limit = 100, $userId = null) {
        $sql = "
            SELECT s.shipment_id
            FROM shipments s
            LEFT JOIN mra_eis_outbox outbox
              ON outbox.entity_type = 'transfer'
             AND outbox.source_key = CONCAT('shipment:', s.shipment_id)
             AND outbox.status = 'submitted'
            WHERE outbox.id IS NULL
              AND s.status IN ('pending','shipped','verified','completed')
        ";
        $params = [];

        if ($branchId !== null && $branchId !== '') {
            $sql .= " AND (s.from_branch_id = :branch_id OR s.to_branch_id = :branch_id)";
            $params[':branch_id'] = (int)$branchId;
        }
        if ($startDate !== null && $startDate !== '') {
            $sql .= " AND DATE(s.shipment_date) >= :start_date";
            $params[':start_date'] = $startDate;
        }
        if ($endDate !== null && $endDate !== '') {
            $sql .= " AND DATE(s.shipment_date) <= :end_date";
            $params[':end_date'] = $endDate;
        }

        $sql .= " ORDER BY s.shipment_date DESC LIMIT " . max(1, min(500, (int)$limit));
        $stmt = $this->conn->prepare($sql);
        foreach ($params as $key => $value) {
            $stmt->bindValue($key, $value, is_int($value) ? PDO::PARAM_INT : PDO::PARAM_STR);
        }
        $stmt->execute();

        $summary = ['prepared' => 0, 'ready' => 0, 'blocked' => 0];
        foreach ($stmt->fetchAll(PDO::FETCH_COLUMN) as $shipmentId) {
            $result = $this->prepareTransfer($shipmentId, $userId);
            $summary['prepared']++;
            $summary[$result['status']] = ($summary[$result['status']] ?? 0) + 1;
        }

        return $summary;
    }

    private function siteProductQuantity($terminalId, $productCode) {
        $stmt = $this->conn->prepare("
            SELECT quantity FROM mra_eis_site_products
            WHERE terminal_local_id = :terminal_id AND product_code = :product_code
            LIMIT 1
        ");
        $stmt->execute([':terminal_id' => (int)$terminalId, ':product_code' => (string)$productCode]);
        $value = $stmt->fetchColumn();
        return $value !== false ? (float)$value : null;
    }

    /**
     * Reads back the per-barcode breakdown of a submitted transfer (product, quantity sent, and the
     * exact IMS unique_codes that account for it) from the outbox row's local_audit_json, so the
     * receiving side can be prompted to reconcile the newly-arrived unique_codes against the
     * destination site's EIS-reported stock increase. Returns null if the transfer hasn't actually
     * been submitted yet (nothing to reconcile) or has no per-line audit data.
     */
    public function transferReceiptSummary($shipmentId) {
        $stmt = $this->conn->prepare("
            SELECT * FROM mra_eis_outbox
            WHERE entity_type = 'transfer' AND source_key = :source_key
            LIMIT 1
        ");
        $stmt->execute([':source_key' => 'shipment:' . (int)$shipmentId]);
        $outbox = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$outbox || $outbox['status'] !== 'submitted') {
            return null;
        }

        $branchStmt = $this->conn->prepare("
            SELECT s.to_branch_id, b.branch_name
            FROM shipments s
            LEFT JOIN branches b ON b.branch_id = s.to_branch_id
            WHERE s.shipment_id = :shipment_id
        ");
        $branchStmt->execute([':shipment_id' => (int)$shipmentId]);
        $branchRow = $branchStmt->fetch(PDO::FETCH_ASSOC);
        if (!$branchRow) {
            return null;
        }

        $audit = json_decode((string)($outbox['local_audit_json'] ?? ''), true);
        $lines = is_array($audit['eis_lines'] ?? null) ? $audit['eis_lines'] : [];
        if (empty($lines)) {
            return null;
        }

        return [
            'shipment_id' => (int)$shipmentId,
            'outbox_id' => (int)$outbox['id'],
            'to_branch_id' => (int)$branchRow['to_branch_id'],
            'to_branch_name' => $branchRow['branch_name'],
            'lines' => $lines,
        ];
    }

    /**
     * Refreshes the destination site's product catalog from MRA and compares the resulting quantity
     * delta per barcode against what this transfer actually sent, tying the comparison back to the
     * specific IMS unique_codes shipped. Persists the outcome as a compliance event either way, so
     * there's a permanent audit trail of "these unique_codes = this EIS stock increase" regardless of
     * whether it matched.
     */
    public function confirmTransferReceipt($shipmentId, $userId = null) {
        $summary = $this->transferReceiptSummary($shipmentId);
        if (!$summary) {
            throw new RuntimeException('No submitted EIS transfer was found for this shipment.');
        }

        $destinationTerminal = $this->terminalForBranch($summary['to_branch_id']);
        if (!$destinationTerminal) {
            throw new RuntimeException('The destination branch has no activated EIS terminal to verify stock against.');
        }

        $onboarding = new MraEisOnboarding($this->conn);
        $onboarding->syncSiteProducts(
            (int)$destinationTerminal['id'],
            (string)($destinationTerminal['taxpayer_tin'] ?? ''),
            (string)($destinationTerminal['site_id'] ?? '')
        );

        $results = [];
        $allMatched = true;
        foreach ($summary['lines'] as $line) {
            $productCode = (string)($line['eis_product_code'] ?? '');
            $quantitySent = (int)($line['quantity_sent_to_eis'] ?? 0);
            $quantityBefore = isset($line['eis_site_quantity_before']) && $line['eis_site_quantity_before'] !== null
                ? (float)$line['eis_site_quantity_before']
                : null;
            $quantityAfter = $this->siteProductQuantity((int)$destinationTerminal['id'], $productCode);
            $delta = ($quantityAfter !== null && $quantityBefore !== null) ? ($quantityAfter - $quantityBefore) : null;
            $matched = $delta !== null && abs($delta - $quantitySent) < 0.001;
            if (!$matched) {
                $allMatched = false;
            }
            $results[] = [
                'product_code' => $productCode,
                'product_name' => $line['eis_product_name'] ?? '',
                'unique_codes' => $line['ims_unique_codes'] ?? [],
                'quantity_sent' => $quantitySent,
                'quantity_before' => $quantityBefore,
                'quantity_after' => $quantityAfter,
                'matched' => $matched,
            ];
        }

        $this->event(
            $summary['outbox_id'],
            'transfer',
            'shipment:' . (int)$shipmentId,
            $allMatched ? 'transfer_receipt_matched' : 'transfer_receipt_mismatch',
            $allMatched
                ? 'Destination site EIS stock confirmed to match the transferred unique codes.'
                : 'Destination site EIS stock does not match the transferred quantity for one or more products - review manually.',
            json_encode(['to_branch_id' => $summary['to_branch_id'], 'lines' => $results], JSON_UNESCAPED_SLASHES),
            $userId
        );

        return ['matched' => $allMatched, 'to_branch_name' => $summary['to_branch_name'], 'lines' => $results];
    }

    /** Reconciles only the mismatched transfer barcodes in the destination IMS branch to live EIS stock. */
    public function calibrateTransferReceipt($shipmentId, $userId = null) {
        $summary = $this->transferReceiptSummary($shipmentId);
        if (!$summary) {
            throw new RuntimeException('No submitted EIS transfer was found for this shipment.');
        }

        $verification = $this->confirmTransferReceipt($shipmentId, $userId);
        $mismatchedCodes = [];
        foreach ($verification['lines'] as $line) {
            if (empty($line['matched']) && trim((string)($line['product_code'] ?? '')) !== '') {
                $mismatchedCodes[] = (string)$line['product_code'];
            }
        }
        $mismatchedCodes = array_values(array_unique($mismatchedCodes));
        if (!$mismatchedCodes) {
            return ['already_matched' => true, 'created' => 0, 'removed' => 0,
                'unchanged' => count($verification['lines']), 'flagged_variance' => []];
        }

        $destinationTerminal = $this->terminalForBranch($summary['to_branch_id']);
        if (!$destinationTerminal) {
            throw new RuntimeException('The destination branch has no activated EIS terminal to calibrate against.');
        }

        $onboarding = new MraEisOnboarding($this->conn);
        $result = $onboarding->syncSiteTerminalProductsToBranch(
            (int)$destinationTerminal['id'], (int)$summary['to_branch_id'], $mismatchedCodes, false
        );

        $this->event(
            $summary['outbox_id'], 'transfer', 'shipment:' . (int)$shipmentId,
            'transfer_receipt_calibrated',
            'Destination IMS inventory was calibrated to live EIS stock for mismatched transfer products.',
            json_encode(['product_codes' => $mismatchedCodes, 'result' => $result], JSON_UNESCAPED_SLASHES),
            $userId
        );

        return ['already_matched' => false, 'product_codes' => $mismatchedCodes] + $result;
    }

    /**
     * Re-prepares and immediately submits transfers currently stuck in 'blocked' status (e.g. ones
     * that failed before a bug fix such as the warehouse-terminal fallback). Unlike
     * prepareRecentTransfers() (queue-only), this both re-runs prepareTransfer() and calls
     * submitOutbox() right away, since the point is clearing a backlog in one click rather than
     * leaving newly-ready items to be submitted one at a time later. Admin-triggered only - never
     * call this from the shipment-creation request path, since each submission is a live MRA call.
     */
    public function retryBlockedTransfers($limit = 20, $userId = null) {
        $stmt = $this->conn->prepare("
            SELECT id, source_id
            FROM mra_eis_outbox
            WHERE entity_type = 'transfer' AND status = 'blocked'
            ORDER BY id ASC
            LIMIT " . max(1, min(200, (int)$limit)) . "
        ");
        $stmt->execute();
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

        $summary = ['attempted' => 0, 'submitted' => 0, 'still_blocked' => 0, 'errors' => []];
        foreach ($rows as $row) {
            $shipmentId = (int)$row['source_id'];
            if ($shipmentId <= 0) {
                continue;
            }
            $summary['attempted']++;
            try {
                $result = $this->prepareTransfer($shipmentId, $userId);
                if (($result['status'] ?? '') === 'ready' && !empty($result['id'])) {
                    $submitResult = $this->submitOutbox((int)$result['id'], $userId);
                    if (!empty($submitResult['success'])) {
                        $summary['submitted']++;
                    } else {
                        $summary['still_blocked']++;
                    }
                } else {
                    $summary['still_blocked']++;
                }
            } catch (Throwable $e) {
                $summary['still_blocked']++;
                $summary['errors'][] = 'Shipment #' . $shipmentId . ': ' . $e->getMessage();
                error_log('MRA EIS blocked-transfer retry failed for shipment #' . $shipmentId . ': ' . $e->getMessage());
            }
        }

        return $summary;
    }

    public function prepareVoidReceipt($receiptNumber, $reason, $branchId = null, $userId = null) {
        $receiptNumber = trim((string)$receiptNumber);
        $reason = trim((string)$reason);
        if ($receiptNumber === '' || $reason === '') {
            throw new InvalidArgumentException('Receipt number and reason are required.');
        }

        $terminal = $this->terminalForBranch($branchId);
        $payload = ['receiptNumber' => $receiptNumber, 'reason' => $reason, 'supportingDocuments' => null];

        return $this->upsertOutbox([
            'entity_type' => 'void_receipt',
            'source_table' => 'mra_eis_outbox',
            'source_id' => null,
            'source_key' => 'void:' . $receiptNumber,
            'branch_id' => $branchId ? (int)$branchId : null,
            'terminal_local_id' => $terminal ? (int)$terminal['id'] : null,
            'endpoint' => '/sales/cancel-receipt',
            'payload_json' => json_encode($payload, JSON_UNESCAPED_SLASHES),
            'status' => $terminal ? 'ready' : 'blocked',
            'last_error' => $terminal ? null : 'No activated EIS terminal was found for this branch.',
            'created_by' => $userId,
        ]);
    }

    /**
     * Resolve a single item's MRA tax rate group (A/B/E/etc.) from its EIS
     * product mapping, before any sale exists — mirrors the same COALESCE
     * chain saleSnapshot() uses against sold_items, but keyed on items.item_id
     * directly so the cart can know an item's tax group at lookup time,
     * not only after the sale is already recorded.
     */
    public function resolveTaxRateId($itemId): string {
        $stmt = $this->conn->prepare(
            "SELECT COALESCE(NULLIF(i.tax_rate_id, ''), itemsp.tax_rate_id, msp.tax_rate_id, pcmsp.tax_rate_id, map.tax_rate_id, pcm.tax_rate_id, 'A') AS tax_rate_id
             FROM items i
             LEFT JOIN mra_eis_site_products itemsp ON itemsp.id = i.eis_site_product_id
             LEFT JOIN mra_eis_item_product_map map ON map.item_id = i.item_id
             LEFT JOIN mra_eis_site_products msp ON msp.id = map.site_product_id
             LEFT JOIN mra_eis_product_code_map pcm ON pcm.eis_product_code = i.product_code
             LEFT JOIN mra_eis_site_products pcmsp ON pcmsp.id = pcm.site_product_id
             WHERE i.item_id = :item_id
             LIMIT 1"
        );
        $stmt->execute([':item_id' => (int)$itemId]);
        return strtoupper((string)($stmt->fetchColumn() ?: 'A'));
    }

    /**
     * Return the original EIS line items for a submitted invoice so the UI
     * can pre-populate the adjustment form.
     */
    public function getOutboxLineItems($transactionId) {
        $stmt = $this->conn->prepare(
            "SELECT payload_json FROM mra_eis_outbox
             WHERE entity_type = 'sale' AND source_key = :key LIMIT 1"
        );
        $stmt->execute([':key' => trim((string)$transactionId)]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$row) {
            throw new RuntimeException('No EIS outbox entry found for this transaction.');
        }
        $payload = json_decode((string)$row['payload_json'], true);
        return is_array($payload['invoiceLineItems'] ?? null) ? $payload['invoiceLineItems'] : [];
    }

    /**
     * Process a credit or debit note (invoice adjustment) via /sales/process-credit-debit-note.
     * MRA determines the note type (Credit/Debit) based on the adjusted totals vs. the original.
     */
    public function processInvoiceAdjustment($originalInvoiceNumber, array $adjustedLines, $reason, $branchId, $userId = null) {
        $originalInvoiceNumber = trim((string)$originalInvoiceNumber);
        $reason = trim((string)$reason);
        if ($originalInvoiceNumber === '' || $reason === '') {
            throw new InvalidArgumentException('Original invoice number and reason are required.');
        }
        if (empty($adjustedLines)) {
            throw new InvalidArgumentException('At least one adjusted line item is required.');
        }

        $terminal = $this->terminalForBranch($branchId);
        if (!$terminal || trim((string)($terminal['jwt_token'] ?? '')) === '') {
            throw new RuntimeException('No activated EIS terminal is available for this branch.');
        }

        // Pull original header fields from the stored outbox payload
        $outboxStmt = $this->conn->prepare(
            "SELECT payload_json FROM mra_eis_outbox
             WHERE entity_type = 'sale' AND source_key = :key LIMIT 1"
        );
        $outboxStmt->execute([':key' => $originalInvoiceNumber]);
        $outboxRow = $outboxStmt->fetch(PDO::FETCH_ASSOC);
        $originalPayload = $outboxRow ? (json_decode((string)$outboxRow['payload_json'], true) ?? []) : [];
        $origHeader = is_array($originalPayload['invoiceHeader'] ?? null) ? $originalPayload['invoiceHeader'] : [];

        $mraTime = $this->getMraServerDateTime($terminal);

        // Build adjusted line items and recalculate totals
        $invoiceTotal = 0.0;
        $totalVat = 0.0;
        $taxBreakdown = [];
        $lineItems = [];
        $lineNumber = 1;
        foreach ($adjustedLines as $line) {
            $lineTotal   = round((float)($line['total']    ?? 0), 2);
            $lineVat     = round((float)($line['totalVAT'] ?? 0), 2);
            $taxRateId   = (string)($line['taxRateId'] ?? 'VAT_ZERO');

            $lineItems[] = [
                'id'          => $lineNumber++,
                'productCode' => (string)($line['productCode'] ?? ''),
                'description' => (string)($line['description'] ?? ''),
                'unitPrice'   => round((float)($line['unitPrice'] ?? 0), 2),
                'quantity'    => (float)($line['quantity'] ?? 0),
                'discount'    => round((float)($line['discount'] ?? 0), 2),
                'total'       => $lineTotal,
                'totalVAT'    => $lineVat,
                'taxRateId'   => $taxRateId,
                'isProduct'   => (bool)($line['isProduct'] ?? true),
            ];

            $invoiceTotal += $lineTotal;
            $totalVat     += $lineVat;

            if (!isset($taxBreakdown[$taxRateId])) {
                $taxBreakdown[$taxRateId] = ['rateId' => $taxRateId, 'taxableAmount' => 0.0, 'taxAmount' => 0.0];
            }
            $taxBreakdown[$taxRateId]['taxableAmount'] += $lineTotal - $lineVat;
            $taxBreakdown[$taxRateId]['taxAmount']     += $lineVat;
        }

        $payload = [
            'invoiceHeader' => [
                'invoiceNumber'        => $originalInvoiceNumber,
                'invoiceDateTime'      => $this->isoDate($mraTime ?? new DateTimeImmutable()),
                'sellerTIN'            => $origHeader['sellerTIN'] ?? (string)($terminal['taxpayer_tin'] ?? ''),
                'buyerTIN'             => $origHeader['buyerTIN'] ?? null,
                'buyerName'            => $origHeader['buyerName'] ?? null,
                'buyerAuthorizationCode' => $origHeader['buyerAuthorizationCode'] ?? null,
                'siteId'               => $origHeader['siteId'] ?? '',
                'globalConfigVersion'  => (int)($terminal['global_config_version'] ?? 1),
                'taxpayerConfigVersion'=> (int)($terminal['taxpayer_config_version'] ?? 1),
                'terminalConfigVersion'=> (int)($terminal['terminal_config_version'] ?? 1),
                'paymentMethod'        => $origHeader['paymentMethod'] ?? 'CASH',
                'isReliefSupply'       => $origHeader['isReliefSupply'] ?? false,
            ],
            'invoiceLineItems'    => $lineItems,
            'invoiceSummary'      => [
                'taxBreakDown'    => array_values($taxBreakdown),
                'levyBreakDown'   => [],
                'totalVAT'        => round($totalVat, 2),
                'invoiceTotal'    => round($invoiceTotal, 2),
                'amountTendered'  => round($invoiceTotal, 2),
                'offlineSignature'=> null,
            ],
            'reasonForAdjustment' => $reason,
        ];

        $url = rtrim((string)$terminal['api_base_url'], '/') . '/sales/process-credit-debit-note';
        $response = $this->postJsonWithAuthorization($url, $payload, $terminal);
        $decoded  = json_decode((string)$response['body'], true);
        $statusCode = (int)($decoded['statusCode'] ?? 0);

        if ((int)$response['status'] !== 200 || $statusCode <= 0) {
            $remark   = trim((string)($decoded['remark'] ?? ''));
            $errors   = is_array($decoded['errors'] ?? null) ? $decoded['errors'] : [];
            $errorMsg = !empty($errors) ? implode('; ', array_column($errors, 'errorMessage')) : '';
            throw new RuntimeException(
                'MRA EIS adjustment failed: ' . ($remark ?: $errorMsg ?: 'HTTP ' . $response['status'])
            );
        }

        $data = is_array($decoded['data'] ?? null) ? $decoded['data'] : [];
        $this->applyConfigVersionsFromResponse($decoded, (int)$terminal['id']);

        return [
            'note_type'               => $data['noteType'] ?? null,
            'new_invoice_number'      => $data['invoiceNumber'] ?? null,
            'original_invoice_number' => $data['originalInvoiceNumber'] ?? $originalInvoiceNumber,
            'invoice_total'           => $data['invoiceTotal'] ?? null,
            'total_vat'               => $data['totalVat'] ?? null,
            'validation_url'          => $data['validationUrl'] ?? null,
        ];
    }

    public function prepareStockAdjustment($barcode, $quantity, $adjustmentType, $reason, $siteId = '', $remarks = '', $branchId = null, $userId = null) {
        $adjustmentType = ucfirst(strtolower(trim((string)$adjustmentType)));
        if (!in_array($adjustmentType, ['Increase', 'Decrease'], true)) {
            throw new InvalidArgumentException('Adjustment type must be Increase or Decrease.');
        }

        $terminal = $this->terminalForBranch($branchId);
        $payload = [
            'barcode' => trim((string)$barcode),
            'quantity' => (float)$quantity,
            'adjustmentReason' => trim((string)$reason),
            'adjustmentType' => $adjustmentType,
            'siteId' => trim((string)$siteId) ?: null,
            'taxpayerRemarks' => trim((string)$remarks) ?: null,
        ];
        $issues = [];
        foreach (['barcode', 'quantity', 'adjustmentReason'] as $field) {
            if ($payload[$field] === '' || $payload[$field] === 0.0) {
                $issues[] = $field . ' is required.';
            }
        }
        if (!$terminal) {
            $issues[] = 'No activated EIS terminal was found for this branch.';
        }

        return $this->upsertOutbox([
            'entity_type' => 'stock_adjustment',
            'source_table' => 'mra_eis_outbox',
            'source_id' => null,
            'source_key' => 'adjustment:' . sha1(json_encode($payload)),
            'branch_id' => $branchId ? (int)$branchId : null,
            'terminal_local_id' => $terminal ? (int)$terminal['id'] : null,
            'endpoint' => '/stock/submit-adjustment',
            'payload_json' => json_encode($payload, JSON_UNESCAPED_SLASHES),
            'status' => empty($issues) ? 'ready' : 'blocked',
            'last_error' => empty($issues) ? null : implode(' ', $issues),
            'created_by' => $userId,
        ]);
    }

    public function prepareInitialInventoryUpload($branchId = null, $isLastBatch = true, $limit = 50, $userId = null) {
        $terminal = $this->terminalForBranch($branchId);
        $issues = [];
        if (!$terminal) {
            $issues[] = 'No activated EIS terminal was found for this inventory upload.';
        }

        $sql = "
            SELECT i.item_id, i.unique_code, i.item_name, i.item_details, i.description, i.unit_price, i.current_location_id,
                   NULLIF(TRIM(i.product_code), '') AS product_code,
                   COALESCE(map.product_name, i.item_name) AS product_name,
                   COALESCE(map.tax_rate_id, 'A') AS tax_rate_id
            FROM items i
            LEFT JOIN mra_eis_item_product_map map ON map.item_id = i.item_id
            LEFT JOIN sold_items si ON si.item_id = i.item_id AND si.transaction_id NOT LIKE '%_VERIFIED_%'
            WHERE si.sale_id IS NULL
              AND i.unique_code IS NOT NULL
              AND i.unique_code != ''
        ";
        $params = [];
        if ($branchId !== null && $branchId !== '') {
            $sql .= " AND i.current_location_id = :branch_id";
            $params[':branch_id'] = (int)$branchId;
        }
        $sql .= " ORDER BY i.item_name, i.item_id LIMIT " . max(1, min(50, (int)$limit));

        $stmt = $this->conn->prepare($sql);
        foreach ($params as $key => $value) {
            $stmt->bindValue($key, $value, PDO::PARAM_INT);
        }
        $stmt->execute();
        $items = $stmt->fetchAll(PDO::FETCH_ASSOC);

        $groupedProducts = [];
        $localAuditItems = [];
        $itemIds = [];
        foreach ($items as $item) {
            $itemIds[] = (int)$item['item_id'];

            if (!$this->isUsableEisProductCode($item['product_code'] ?? null)) {
                $issues[] = 'Missing items.product_code for local unique_code ' . $item['unique_code'] . '.';
                $localAuditItems[] = [
                    'item_id' => (int)$item['item_id'],
                    'unique_code' => (string)$item['unique_code'],
                    'ims_item_name' => (string)$item['item_name'],
                    'eis_product_code' => null,
                    'eis_product_name' => null,
                    'mapping_status' => 'missing',
                ];
                continue;
            }

            $price = max(0, (float)$item['unit_price']);
            $groupKey = implode('|', [
                (string)$item['product_code'],
                number_format($price, 4, '.', ''),
            ]);

            if (!isset($groupedProducts[$groupKey])) {
                $groupedProducts[$groupKey] = [
                    'barCode' => (string)$item['product_code'],
                    'productName' => (string)($item['product_name'] ?: $item['item_name']),
                    'productDescription' => trim((string)($item['item_details'] ?? '')) ?: (trim((string)($item['description'] ?? '')) ?: (string)($item['product_name'] ?: $item['item_name'])),
                    'unitPrice' => $price,
                    'quantityInStock' => 0,
                    'costPrice' => $price,
                    'sellingPrice' => $price,
                    'reorderLevel' => null,
                    'overQuantityStockLevel' => null,
                    '_unique_codes' => [],
                    '_item_ids' => [],
                ];
            }

            $groupedProducts[$groupKey]['quantityInStock']++;
            $groupedProducts[$groupKey]['_unique_codes'][] = (string)$item['unique_code'];
            $groupedProducts[$groupKey]['_item_ids'][] = (int)$item['item_id'];

            $localAuditItems[] = [
                'item_id' => (int)$item['item_id'],
                'unique_code' => (string)$item['unique_code'],
                'ims_item_name' => (string)$item['item_name'],
                'eis_product_code' => (string)$item['product_code'],
                'eis_product_name' => (string)($item['product_name'] ?: $item['item_name']),
                'mapping_status' => 'mapped',
                'unit_price' => $price,
            ];
        }

        $products = [];
        $lineAudit = [];
        foreach ($groupedProducts as $product) {
            $uniqueCodes = $product['_unique_codes'];
            $groupItemIds = $product['_item_ids'];
            unset($product['_unique_codes'], $product['_item_ids']);
            $products[] = $product;
            $lineAudit[] = [
                'eis_product_code' => $product['barCode'],
                'eis_product_name' => $product['productName'],
                'quantity_sent_to_eis' => (int)$product['quantityInStock'],
                'ims_item_ids' => $groupItemIds,
                'ims_unique_codes' => $uniqueCodes,
            ];
        }

        if (empty($products)) {
            $issues[] = 'No available inventory items were found for upload.';
        }

        $payload = [
            'tin' => $terminal ? ($terminal['taxpayer_tin'] ?? null) : null,
            'isLastBatch' => (bool)$isLastBatch,
            'products' => $products,
        ];

        if (($payload['tin'] ?? '') === '') {
            $issues[] = 'The selected terminal has no taxpayer TIN.';
        }

        $localAudit = [
            'bridge' => 'items.product_code submitted to EIS inventory; items.unique_code retained as the local serialized-item reference',
            'entity_type' => 'initial_inventory',
            'branch_id' => $branchId ? (int)$branchId : null,
            'ims_item_count' => count($items),
            'eis_product_count' => count($products),
            'items' => $localAuditItems,
            'eis_lines' => $lineAudit,
        ];

        return $this->upsertOutbox([
            'entity_type' => 'initial_inventory',
            'source_table' => 'items',
            'source_id' => null,
            'source_key' => 'initial_inventory:' . ($branchId ?: 'all') . ':' . sha1(implode(',', $itemIds)),
            'branch_id' => $branchId ? (int)$branchId : null,
            'terminal_local_id' => $terminal ? (int)$terminal['id'] : null,
            'endpoint' => '/utilities/taxpayer-initial-inventory-upload',
            'payload_json' => json_encode($payload, JSON_UNESCAPED_SLASHES),
            'status' => empty($issues) ? 'ready' : 'blocked',
            'last_error' => empty($issues) ? null : implode(' ', array_unique($issues)),
            'created_by' => $userId,
            'local_audit_json' => json_encode($localAudit, JSON_UNESCAPED_SLASHES),
        ]);
    }

    public function prepareInformalPurchase(array $input, $userId = null) {
        $branchId = $input['branch_id'] ?? null;
        // An informal purchase is a taxpayer-wide goods-receiving record, not something
        // scoped to whichever branch happens to hold the receiving terminal - same reasoning
        // as syncSuppliersFromEis(). Fall back to any other activated terminal rather than
        // hard-failing just because the receiving branch's own terminal isn't activated.
        $terminal = $this->terminalForBranch($branchId) ?: $this->terminalForBranch(null);
        $issues = [];
        if (!$terminal) {
            $issues[] = 'No activated EIS terminal was found for this informal purchase.';
        }

        $quantity = max(0, (float)($input['quantity'] ?? 0));
        $unitPrice = max(0, (float)($input['unit_price'] ?? 0));
        $itemCode = trim((string)($input['item_code'] ?? ''));
        $description = trim((string)($input['description'] ?? ''));
        $unitOfMeasure = trim((string)($input['unit_of_measure'] ?? ''));
        $supplierId = (int)($input['supplier_id'] ?? 0);
        $receivedBy = trim((string)($input['received_by'] ?? ''));

        if ($supplierId <= 0) {
            $issues[] = 'Supplier ID is required.';
        }
        if ($itemCode === '') {
            $issues[] = 'Item code is required.';
        }
        if ($description === '') {
            $issues[] = 'Description is required.';
        }
        if ($quantity <= 0) {
            $issues[] = 'Quantity must be greater than zero.';
        }
        if ($unitPrice <= 0) {
            $issues[] = 'Unit price must be greater than zero.';
        }
        if ($unitOfMeasure === '') {
            $issues[] = 'Unit of measure is required.';
        }
        if ($receivedBy === '') {
            $issues[] = 'Received by is required.';
        }

        $payload = [
            'supplierId' => $supplierId,
            'deliveryNoteNumber' => trim((string)($input['delivery_note_number'] ?? '')) ?: null,
            'receivingDate' => $this->isoDate($input['receiving_date'] ?? date('Y-m-d H:i:s')),
            'purchaseOrderNumber' => trim((string)($input['purchase_order_number'] ?? '')) ?: null,
            'receivedBy' => $receivedBy,
            'totalItems' => 1,
            'totalQuantity' => $quantity,
            'totalValue' => round($quantity * $unitPrice, 2),
            'notes' => trim((string)($input['notes'] ?? '')) ?: null,
            'items' => [[
                'itemCode' => $itemCode,
                'description' => $description,
                'quantityOrdered' => $quantity,
                'quantityReceived' => $quantity,
                'unitOfMeasure' => $unitOfMeasure,
                'unitPrice' => $unitPrice,
                'totalPrice' => round($quantity * $unitPrice, 2),
                'isFinishedProduct' => !empty($input['is_finished_product']),
            ]],
        ];

        return $this->upsertOutbox([
            'entity_type' => 'informal_purchase',
            'source_table' => 'mra_eis_outbox',
            'source_id' => null,
            'source_key' => 'informal_purchase:' . sha1(json_encode($payload)),
            'branch_id' => $branchId ? (int)$branchId : null,
            'terminal_local_id' => $terminal ? (int)$terminal['id'] : null,
            'endpoint' => '/stock/submit-informal-purchase',
            'payload_json' => json_encode($payload, JSON_UNESCAPED_SLASHES),
            'status' => empty($issues) ? 'ready' : 'blocked',
            'last_error' => empty($issues) ? null : implode(' ', array_unique($issues)),
            'created_by' => $userId,
            'local_audit_json' => null,
        ]);
    }

    /**
     * Batch version of prepareInformalPurchase(): one EIS submission covering every distinct
     * item in a single stock-receiving batch (e.g. one Excel import or one code-generation run),
     * instead of one submission per item. Mirrors prepareTransfer()'s aggregation-by-product-code
     * and never-throw-for-validation-issues conventions - bad/missing data yields a 'blocked'
     * outbox row rather than an exception, so it's always safe to call after a stock add succeeds.
     *
     * @param int|null $branchId Branch the stock was added to (used to resolve the EIS terminal).
     * @param array $items [['item_code','description','quantity','unit_price','is_finished_product'?,'item_id'?,'unique_code'?], ...]
     * @param array $receivingMeta ['supplier_id' (local suppliers.supplier_id), 'delivery_note_number', 'purchase_order_number',
     *                              'received_by', 'receiving_date', 'notes', 'unit_of_measure', 'is_finished_product']
     */
    public function prepareInformalPurchaseBatch($branchId, array $items, array $receivingMeta, $userId = null) {
        // An informal purchase is a taxpayer-wide goods-receiving record, not something
        // scoped to whichever branch happens to hold the receiving terminal - same reasoning
        // as syncSuppliersFromEis(). Fall back to any other activated terminal rather than
        // hard-failing just because the receiving branch's own terminal isn't activated.
        $terminal = $this->terminalForBranch($branchId) ?: $this->terminalForBranch(null);
        $issues = [];
        if (!$terminal) {
            $issues[] = 'No activated EIS terminal was found for this informal purchase.';
        }

        $localSupplierId = (int)($receivingMeta['supplier_id'] ?? 0);
        $eisSupplierId = 0;
        if ($localSupplierId > 0) {
            $supplier = (new Suppliers($this->conn))->getSupplierById($localSupplierId);
            $eisSupplierId = (int)($supplier['eis_supplier_id'] ?? 0);
            if ($eisSupplierId <= 0) {
                $issues[] = 'Selected supplier has no EIS Supplier ID configured. Set one on the Suppliers page.';
            }
        } else {
            $issues[] = 'Supplier is required.';
        }

        $receivedBy = trim((string)($receivingMeta['received_by'] ?? ''));
        if ($receivedBy === '') {
            $issues[] = 'Received by is required.';
        }
        $unitOfMeasure = trim((string)($receivingMeta['unit_of_measure'] ?? ''));
        if ($unitOfMeasure === '') {
            $issues[] = 'Unit of measure is required.';
        }
        $isFinishedProductDefault = !empty($receivingMeta['is_finished_product']);

        $aggregated = [];
        $lineAudit = [];
        foreach ($items as $item) {
            $itemCode = trim((string)($item['item_code'] ?? ''));
            $quantity = max(0, (float)($item['quantity'] ?? 0));
            $unitPrice = max(0, (float)($item['unit_price'] ?? 0));
            $description = trim((string)($item['description'] ?? '')) ?: $itemCode;
            $lineUnitOfMeasure = trim((string)($item['unit_of_measure'] ?? '')) ?: $unitOfMeasure;

            if (!$this->isUsableEisProductCode($itemCode)) {
                $issues[] = 'Missing a usable EIS product code for item "' . ($description ?: '(unnamed)') . '".';
                continue;
            }
            if ($quantity <= 0 || $unitPrice <= 0) {
                $issues[] = 'Item "' . $itemCode . '" needs a quantity and unit price greater than zero.';
                continue;
            }

            if (!isset($aggregated[$itemCode])) {
                $aggregated[$itemCode] = [
                    'itemCode' => $itemCode,
                    'description' => $description,
                    'quantityOrdered' => 0.0,
                    'quantityReceived' => 0.0,
                    'unitOfMeasure' => $lineUnitOfMeasure,
                    'unitPrice' => $unitPrice,
                    'isFinishedProduct' => array_key_exists('is_finished_product', $item) ? !empty($item['is_finished_product']) : $isFinishedProductDefault,
                    '_item_ids' => [],
                    '_unique_codes' => [],
                ];
            }
            $aggregated[$itemCode]['quantityOrdered'] += $quantity;
            $aggregated[$itemCode]['quantityReceived'] += $quantity;
            $aggregated[$itemCode]['unitPrice'] = $unitPrice;
            if (!empty($item['item_id'])) {
                $aggregated[$itemCode]['_item_ids'][] = (int)$item['item_id'];
            }
            if (!empty($item['unique_code'])) {
                $aggregated[$itemCode]['_unique_codes'][] = (string)$item['unique_code'];
            }
        }

        $payloadItems = [];
        $totalQuantity = 0;
        $totalValue = 0;
        foreach ($aggregated as $line) {
            $itemIds = $line['_item_ids'];
            $uniqueCodes = $line['_unique_codes'];
            unset($line['_item_ids'], $line['_unique_codes']);
            $line['totalPrice'] = round($line['quantityReceived'] * $line['unitPrice'], 2);
            $payloadItems[] = $line;
            $totalQuantity += $line['quantityReceived'];
            $totalValue += $line['totalPrice'];
            $lineAudit[] = [
                'eis_product_code' => $line['itemCode'],
                'quantity_sent_to_eis' => $line['quantityReceived'],
                'ims_item_ids' => $itemIds,
                'ims_unique_codes' => $uniqueCodes,
            ];
        }

        if (empty($payloadItems)) {
            $issues[] = 'No EIS-ready items could be prepared for this informal purchase.';
        }

        $payload = [
            'supplierId' => $eisSupplierId,
            'deliveryNoteNumber' => trim((string)($receivingMeta['delivery_note_number'] ?? '')) ?: null,
            'receivingDate' => $this->isoDate($receivingMeta['receiving_date'] ?? date('Y-m-d H:i:s')),
            'purchaseOrderNumber' => trim((string)($receivingMeta['purchase_order_number'] ?? '')) ?: null,
            'receivedBy' => $receivedBy,
            'totalItems' => count($payloadItems),
            'totalQuantity' => $totalQuantity,
            'totalValue' => round($totalValue, 2),
            'notes' => trim((string)($receivingMeta['notes'] ?? '')) ?: null,
            'items' => $payloadItems,
        ];

        $localAudit = [
            'bridge' => 'items.product_code submitted to EIS informal purchase; items.unique_code retained as the local serialized-item reference',
            'entity_type' => 'informal_purchase_batch',
            'local_supplier_id' => $localSupplierId ?: null,
            'ims_item_count' => count($items),
            'eis_line_count' => count($payloadItems),
            'eis_lines' => $lineAudit,
        ];

        return $this->upsertOutbox([
            'entity_type' => 'informal_purchase_batch',
            'source_table' => 'items',
            'source_id' => null,
            'source_key' => 'informal_purchase_batch:' . sha1(json_encode($payload)),
            'branch_id' => $branchId ? (int)$branchId : null,
            'terminal_local_id' => $terminal ? (int)$terminal['id'] : null,
            'endpoint' => '/stock/submit-informal-purchase',
            'payload_json' => json_encode($payload, JSON_UNESCAPED_SLASHES),
            'status' => empty($issues) ? 'ready' : 'blocked',
            'last_error' => empty($issues) ? null : implode(' ', array_unique($issues)),
            'created_by' => $userId,
            'local_audit_json' => json_encode($localAudit, JSON_UNESCAPED_SLASHES),
        ]);
    }

    public function outbox($status = '', $entityType = '', $limit = 100) {
        $sql = "
            SELECT outbox.*, b.branch_name, met.terminal_label
            FROM mra_eis_outbox outbox
            LEFT JOIN branches b ON b.branch_id = outbox.branch_id
            LEFT JOIN mra_eis_terminals met ON met.id = outbox.terminal_local_id
            WHERE 1 = 1
        ";
        $params = [];
        if ($status !== '') {
            $sql .= " AND outbox.status = :status";
            $params[':status'] = $status;
        }
        if ($entityType !== '') {
            $sql .= " AND outbox.entity_type = :entity_type";
            $params[':entity_type'] = $entityType;
        }
        $sql .= " ORDER BY outbox.updated_at DESC, outbox.id DESC LIMIT " . max(1, min(500, (int)$limit));

        $stmt = $this->conn->prepare($sql);
        foreach ($params as $key => $value) {
            $stmt->bindValue($key, $value);
        }
        $stmt->execute();
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    /**
     * B2B sales are online-only. Validate the buyer with MRA EIS before
     * changing local inventory or recording the sale.
     */
    public function validateOnlineB2BBuyer($branchId, $userId, $buyerTin, $authorizationCode = null) {
        $buyerTin = trim((string)$buyerTin);
        $authorizationCode = trim((string)$authorizationCode);
        if ($buyerTin === '') {
            throw new InvalidArgumentException('A buyer TIN is required for a B2B transaction.');
        }

        $terminal = $this->terminalForUserAndBranch($userId, $branchId);
        if (!$terminal) {
            $terminal = $this->terminalForBranch($branchId);
        }
        if (!$terminal) {
            throw new RuntimeException('B2B transactions require an activated online MRA EIS terminal assigned to this user or branch.');
        }

        $payload = ['invoiceHeader' => [
            'buyerTIN' => $buyerTin,
            'buyerAuthorizationCode' => $authorizationCode !== '' ? $authorizationCode : null,
        ]];

        try {
            $result = $this->validateB2BBuyerForSubmission($payload, $terminal);
        } catch (Throwable $e) {
            throw new RuntimeException('B2B transactions are online-only. MRA EIS buyer validation could not be completed: ' . $e->getMessage(), 0, $e);
        }
        if (empty($result['success'])) {
            throw new RuntimeException(trim((string)($result['message'] ?? 'MRA EIS did not approve the B2B buyer.')));
        }

        return [
            'terminal_id' => (int)$terminal['id'],
            'buyer_tin' => $buyerTin,
            'buyer_authorization_code' => $payload['invoiceHeader']['buyerAuthorizationCode'] ?? null,
        ];
    }

    public function prepareOnlineSaleContext($branchId, $userId) {
        $terminal = $this->terminalForUserAndBranch($userId, $branchId) ?: $this->terminalForBranch($branchId);
        if (!$terminal) {
            throw new RuntimeException('No activated MRA EIS terminal is assigned to this user or branch.');
        }
        if ($this->isOfflineMode((int)$terminal['id'])) {
            throw new RuntimeException('MRA online sale context is unavailable while this terminal is offline.');
        }
        $this->assertTerminalNotBlocked($terminal);
        $serverDate = $this->getMraServerDateTime($terminal);
        if ($serverDate === null) {
            throw new RuntimeException('MRA EIS server time could not be obtained. The transaction was not started.');
        }
        return [
            'terminal_id' => (int)$terminal['id'],
            'server_datetime' => $serverDate,
            'is_vat_registered' => $this->terminalVatRegistrationStatus($terminal),
        ];
    }

    public function validateVat5Certificate($branchId, $userId, $certificateNumber, $projectNumber) {
        $certificateNumber = trim((string)$certificateNumber);
        $projectNumber = trim((string)$projectNumber);
        if ($certificateNumber === '') {
            throw new InvalidArgumentException('A VAT-5 certificate number is required for a relief supply.');
        }
        if ($projectNumber === '') {
            throw new InvalidArgumentException('The VAT-5 project number is required for a relief supply.');
        }
        if ($this->isOfflineMode()) {
            throw new RuntimeException('VAT-5 relief supplies require an online MRA EIS certificate validation.');
        }
        $terminal = $this->terminalForUserAndBranch($userId, $branchId) ?: $this->terminalForBranch($branchId);
        if (!$terminal) {
            throw new RuntimeException('No activated MRA EIS terminal is available for VAT-5 validation.');
        }
        $response = $this->postJsonWithAuthorization(
            rtrim((string)$terminal['api_base_url'], '/') . '/utilities/validate-vat5-certificate',
            [
                'CertificateNumber' => $certificateNumber,
                'ProjectNumber' => $projectNumber,
            ],
            $terminal
        );
        $decoded = json_decode((string)$response['body'], true);
        $data = is_array($decoded['data'] ?? null) ? $decoded['data'] : [];
        $isValid = $data['isValid'] ?? $data['IsValid'] ?? null;
        if (
            $response['status'] < 200
            || $response['status'] >= 300
            || $isValid === null
            || !$this->eisBoolean($isValid)
        ) {
            throw new RuntimeException('MRA EIS rejected VAT-5 certificate ' . $certificateNumber . ': ' . $this->friendlySubmissionError($response));
        }
        $expiry = trim((string)($data['dateOfExpiry'] ?? $data['DateOfExpiry'] ?? ''));
        if ($expiry !== '' && strtotime($expiry) !== false && strtotime($expiry) < time()) {
            throw new RuntimeException('VAT-5 certificate ' . $certificateNumber . ' has expired.');
        }
        return [
            'certificateNumber' => trim((string)($data['certificateNumber'] ?? $data['CertificateNumber'] ?? $data['vat5CertificateNumber'] ?? $certificateNumber)),
            'projectNumber' => $this->nullIfBlank($data['projectNumber'] ?? $data['ProjectNumber'] ?? $projectNumber),
            'quantity' => isset($data['quantity']) || isset($data['Quantity']) ? (float)($data['quantity'] ?? $data['Quantity']) : null,
            'dateOfIssue' => $this->nullIfBlank($data['dateOfIssue'] ?? $data['DateOfIssue'] ?? null),
            'dateOfExpiry' => $this->nullIfBlank($data['dateOfExpiry'] ?? $data['DateOfExpiry'] ?? null),
        ];
    }

    /**
     * Pulls the taxpayer's registered supplier list from MRA EIS (/stock/get-suppliers)
     * and upserts it into the local `suppliers` table, so the supplier picker used for
     * informal-purchase (stock receiving) submissions always has a current, MRA-matched
     * eis_supplier_id instead of relying on someone typing that ID in by hand.
     */
    public function syncSuppliersFromEis($branchId) {
        if ($this->isOfflineMode()) {
            throw new RuntimeException('EIS Offline is enabled. Supplier sync requires an online MRA EIS connection.');
        }
        // Suppliers aren't branch-specific data (MRA EIS's /stock/get-suppliers is a single
        // taxpayer-wide list), so if the configured Warehouse Branch ID has no activated
        // terminal of its own, any other activated terminal can fetch the same list - falling
        // back here avoids a hard failure caused purely by which branch happens to hold the
        // token, not by suppliers actually being unavailable.
        $terminal = $this->terminalForBranch($branchId) ?: $this->terminalForBranch(null);
        if (!$terminal) {
            throw new RuntimeException('No activated MRA EIS terminal was found.');
        }
        $response = $this->postJsonWithAuthorization(
            rtrim((string)$terminal['api_base_url'], '/') . '/stock/get-suppliers',
            [],
            $terminal
        );
        $decoded = json_decode((string)$response['body'], true);
        // HTTP 200 is not success when the EIS wrapper statusCode is negative - same check
        // validateB2BBuyerForSubmission() and others in this class use. Without it, an EIS-side
        // business error (still HTTP 200) fell through to the "$decoded is an array" fallback
        // below and silently treated the whole {statusCode,remark,data,errors} envelope as the
        // supplier list, upserting nothing and reporting "0 added, 0 updated" with no error.
        $apiStatusCode = is_array($decoded) && is_numeric($decoded['statusCode'] ?? null)
            ? (int)$decoded['statusCode']
            : null;
        $httpSuccess = (int)$response['status'] >= 200 && (int)$response['status'] < 300;
        if (!$httpSuccess || ($apiStatusCode !== null && $apiStatusCode <= 0)) {
            throw new RuntimeException('MRA EIS supplier sync failed: ' . $this->friendlySubmissionError($response));
        }
        $list = is_array($decoded['data'] ?? null) ? $decoded['data'] : [];

        return (new Suppliers($this->conn))->upsertFromEis($list);
    }

    public function reliefAdjustedPrices($branchId, $userId, array $uniqueCodes, array $grossPrices) {
        $terminal = $this->terminalForUserAndBranch($userId, $branchId) ?: $this->terminalForBranch($branchId);
        if (!$terminal) {
            throw new RuntimeException('An activated terminal is required to calculate relief-supply prices.');
        }
        $rates = $this->terminalTaxRateMap($terminal);
        $itemIdStmt = $this->conn->prepare('SELECT item_id FROM items WHERE unique_code = :unique_code LIMIT 1');
        $adjusted = [];
        foreach ($grossPrices as $index => $grossPrice) {
            $itemIdStmt->execute([':unique_code' => trim((string)($uniqueCodes[$index] ?? ''))]);
            $itemId = $itemIdStmt->fetchColumn();
            // Use the same tax-rate resolver as everywhere else (receipt, invoice
            // submission) so relief-supply stripping never diverges from what the
            // item is actually taxed at.
            $taxRateId = $itemId !== false ? $this->resolveTaxRateId((int)$itemId) : 'A';
            $rate = $rates[$taxRateId] ?? ($taxRateId === 'A' ? ($rates['A'] ?? $this->vatPercentage) : 0.0);
            $isStandardRated = $taxRateId === 'A' || strpos($taxRateId, 'VAT') !== false;
            $adjusted[] = $isStandardRated && $rate > 0
                ? round(((float)$grossPrice * 100) / (100 + $rate), 2)
                : round((float)$grossPrice, 2);
        }
        return $adjusted;
    }

    public function isOfflineMode($terminalId = null) {
        if ($this->settings->eisOfflineMode()) {
            return true;
        }
        if ($terminalId) {
            $status = $this->offlineThreshold->getStatus((int)$terminalId);
            return $status && $status['connection_status'] === 'offline';
        }
        return false;
    }

    public function reconnectOfflineTerminals($userId = null) {
        $sql = 'SELECT terminal_local_id FROM mra_eis_configurations ';
        $sql .= 'WHERE eis_connection_status = \'offline\'';
        $rows = $this->conn->query($sql)->fetchAll(PDO::FETCH_COLUMN);
        $result = ['checked' => count($rows), 'online' => 0, 'failed' => 0];
        foreach ($rows as $row) {
            $id = (int)$row;
            try {
                $terminal = $this->terminalById($id);
                $url = $terminal ? rtrim((string)$terminal['api_base_url'], '/') . '/utilities/ping' : null;
                $response = $url ? $this->postJsonWithAuthorization($url, [], $terminal) : null;
                if ($response && !MraEisOfflineThreshold::isConnectivityFailure($response)) {
                    $this->offlineThreshold->markTerminalOnline($id, $userId);
                    $result['online']++;
                } else {
                    $result['failed']++;
                }
            } catch (Throwable $e) {
                $result['failed']++;
                error_log('MRA EIS reconnect failed: ' . $e->getMessage());
            }
        }
        return $result;
    }

    public function enableManualOfflineTerminals($userId = null) {
        $sql = 'SELECT c.terminal_local_id FROM mra_eis_configurations c ';
        $sql .= 'INNER JOIN mra_eis_terminals t ON t.id = c.terminal_local_id ';
        $sql .= 'WHERE t.status = \'activated\'';
        $ids = $this->conn->query($sql)->fetchAll(PDO::FETCH_COLUMN);
        foreach ($ids as $id) {
            $this->offlineThreshold->startOfflinePeriod(
                (int)$id, $userId, 'EIS Offline enabled by an administrator.'
            );
        }
        return ['offline' => count($ids)];
    }

    public function offlineStatusForSale($branchId, $userId = null) {
        $terminal = $this->terminalForUserAndBranch($userId, $branchId) ?: $this->terminalForBranch($branchId);
        if (!$terminal) {
            return [
                'connection_status' => 'missing',
                'terminal_id' => null,
                'pending_count' => 0,
                'expired' => true,
            ];
        }
        $status = $this->offlineThreshold->getStatus((int)$terminal['id']);
        if (!$status) {
            return [
                'connection_status' => 'missing',
                'terminal_id' => (int)$terminal['id'],
                'pending_count' => 0,
                'expired' => true,
            ];
        }
        $status['pending_count'] = $this->offlineThreshold->pendingCount((int)$terminal['id']);
        return $status;
    }

    public function probeConnection($branchId, $userId = null) {
        $terminal = $this->terminalForUserAndBranch($userId, $branchId) ?: $this->terminalForBranch($branchId);
        if (!$terminal) {
            throw new RuntimeException('No activated MRA EIS terminal is assigned to this user or branch.');
        }
        $this->getMraServerDateTime($terminal);
        $status = $this->offlineThreshold->getStatus((int)$terminal['id']);
        return $status && $status['connection_status'] === 'online';
    }

    public function assertOfflineSaleAllowed($branchId, $userId, $invoiceTotal, $transactionDate, $sourceKey = null) {
        $terminal = $this->terminalForUserAndBranch($userId, $branchId) ?: $this->terminalForBranch($branchId);
        if (!$terminal) {
            throw new RuntimeException('Offline EIS sales require an activated terminal assigned to this user or branch.');
        }
        if (!$this->isOfflineMode((int)$terminal['id'])) {
            if ($this->isOfflineMode()) {
                throw new RuntimeException('Offline sales require a confirmed MRA EIS connectivity failure for this terminal.');
            }
            return null;
        }

        // Reference the sale's own recorded date/time (not the wall clock) so a
        // backdated/forward-dated entered sale date is what's checked against the
        // offline threshold expiry — this is also what lets the expiry be exercised
        // in testing without waiting out the real threshold window.
        $referenceTime = (is_string($transactionDate) && trim($transactionDate) !== '') ? $transactionDate : 'now';
        $thresholdStatus = $this->offlineThreshold->assertOfflineSaleAllowed((int)$terminal['id'], $userId, $sourceKey, $referenceTime);
        $limits = $this->offlineLimits($terminal);
        if ($limits['max_age_hours'] <= 0 || $limits['max_amount'] <= 0) {
            throw new RuntimeException('The activated terminal has no valid MRA offline time and amount limits. Offline sales are blocked.');
        }

        $currentAmount = $this->pendingOfflineAmount((int)$terminal['id'], $sourceKey);
        $newAmount = round((float)$invoiceTotal, 2);
        if ($newAmount <= 0 || ($currentAmount + $newAmount) > $limits['max_amount']) {
            throw new RuntimeException(
                'MRA EIS offline cumulative amount limit reached. Current queued amount: MWK '
                . number_format($currentAmount, 2) . '; limit: MWK ' . number_format($limits['max_amount'], 2) . '.'
            );
        }

        return [
            'terminal' => $terminal,
            'max_age_hours' => $thresholdStatus['offline_time_threshold_hours'],
            'offline_status' => $thresholdStatus,
            'max_amount' => $limits['max_amount'],
            'current_amount' => $currentAmount,
            'remaining_amount' => round($limits['max_amount'] - $currentAmount - $newAmount, 2),
        ];
    }

    public function syncOfflineSales($limit = 100, $userId = null) {
        $this->conn->exec(
            "UPDATE mra_eis_outbox SET status = 'failed', lock_token = NULL, locked_at = NULL "
            . "WHERE status = 'processing' AND locked_at < (UTC_TIMESTAMP() - INTERVAL 5 MINUTE)"
        );
        $stmt = $this->conn->query(
            'SELECT id, payload_json FROM mra_eis_outbox '
            . "WHERE entity_type = 'sale' AND status IN ('ready','failed') ORDER BY created_at, id LIMIT "
            . max(1, min(500, (int)$limit))
        );
        $submitted = 0;
        $failed = 0;
        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
            $payload = json_decode((string)$row['payload_json'], true);
            if (trim((string)($payload['invoiceSummary']['offlineSignature'] ?? '')) === '') {
                continue;
            }
            try {
                $result = $this->submitOutbox((int)$row['id'], $userId);
                !empty($result['success']) ? $submitted++ : $failed++;
            } catch (Throwable $e) {
                $failed++;
            }
        }

        return [
            'submitted' => $submitted,
            'failed' => $failed,
            'remaining' => $this->countPendingOfflineSales(),
        ];
    }

    public function submitOutbox($outboxId, $userId = null) {
        $claimToken = null;
        try {
            return $this->submitOutboxClaimed($outboxId, $userId, $claimToken);
        } catch (Throwable $e) {
            if ($claimToken !== null) {
                $failedOutbox = $this->findOutbox($outboxId);
                if ($failedOutbox) {
                    $this->event(
                        (int)$outboxId,
                        $failedOutbox['entity_type'],
                        $failedOutbox['source_key'],
                        'sync_failed',
                        'MRA EIS synchronization failed: ' . $e->getMessage(),
                        null,
                        $userId
                    );
                }
            }
            throw $e;
        } finally {
            if ($claimToken !== null) {
                $release = $this->conn->prepare(
                    "UPDATE mra_eis_outbox SET status = CASE WHEN status = 'processing' THEN 'failed' ELSE status END, "
                    . 'lock_token = NULL, locked_at = NULL WHERE id = :id AND lock_token = :lock_token'
                );
                $release->execute([
                    ':id' => (int)$outboxId,
                    ':lock_token' => $claimToken,
                ]);
            }
        }
    }

    private function submitOutboxClaimed($outboxId, $userId, &$claimToken) {
        $outbox = $this->findOutbox($outboxId);
        if (!$outbox) {
            throw new RuntimeException('EIS outbox item was not found.');
        }
        if ($outbox['status'] === 'submitted' && !$this->hasValidAcknowledgement($outbox)) {
            $reopen = $this->conn->prepare("
                UPDATE mra_eis_outbox
                SET status = 'ready',
                    submitted_at = NULL,
                    last_error = 'Previous submission had no verifiable MRA EIS validation URL and was reopened.'
                WHERE id = :id
            ");
            $reopen->execute([':id' => (int)$outbox['id']]);
            $outbox = $this->findOutbox($outboxId);
        }
        if ($outbox['status'] === 'submitted') {
            return $this->submittedOutboxResult($outbox);
        }

        $storedPayload = json_decode((string)($outbox['payload_json'] ?? ''), true);
        $isPreparedOfflineSale = is_array($storedPayload)
            && trim((string)($storedPayload['invoiceSummary']['offlineSignature'] ?? '')) !== '';
        if (
            ($outbox['entity_type'] ?? '') === 'sale'
            && !$isPreparedOfflineSale
            && !empty($outbox['source_key'])
            && !empty($outbox['branch_id'])
        ) {
            $this->prepareSale((string)$outbox['source_key'], (int)$outbox['branch_id'], $userId);
            $outbox = $this->findOutbox($outboxId);
            if (!$outbox) {
                throw new RuntimeException('The sale could not be prepared for EIS submission.');
            }
            if ($outbox['status'] === 'submitted' && $this->hasValidAcknowledgement($outbox)) {
                return $this->submittedOutboxResult($outbox);
            }
            if (!empty($outbox['last_error']) && $outbox['status'] === 'blocked') {
                throw new RuntimeException($outbox['last_error']);
            }
            if ($outbox['status'] !== 'ready' && $outbox['status'] !== 'failed') {
                throw new RuntimeException('The sale could not be prepared for EIS submission.');
            }
        }

        if ($outbox['status'] !== 'ready' && $outbox['status'] !== 'failed') {
            $reason = trim((string)($outbox['last_error'] ?? ''));
            throw new RuntimeException(
                'EIS item is ' . $outbox['status'] . ' and cannot be submitted.'
                . ($reason !== '' ? ' ' . $reason : ' Prepare it successfully before retrying.')
            );
        }
        $claimToken = bin2hex(random_bytes(16));
        $claim = $this->conn->prepare(
            "UPDATE mra_eis_outbox SET status = 'processing', lock_token = :lock_token, locked_at = UTC_TIMESTAMP() "
            . "WHERE id = :id AND status IN ('ready','failed') "
            . 'AND (lock_token IS NULL OR locked_at < (UTC_TIMESTAMP() - INTERVAL 5 MINUTE))'
        );
        $claim->execute([':lock_token' => $claimToken, ':id' => (int)$outboxId]);
        if ($claim->rowCount() !== 1) {
            throw new RuntimeException('This MRA EIS transaction is already being synchronized by another process.');
        }
        $outbox = $this->findOutbox($outboxId);
        $this->event(
            (int)$outboxId,
            $outbox['entity_type'],
            $outbox['source_key'],
            'sync_started',
            'MRA EIS synchronization started for terminal ' . (int)($outbox['terminal_local_id'] ?? 0)
                . ' and branch ' . (int)($outbox['branch_id'] ?? 0) . '.',
            null,
            $userId
        );
        $terminal = $this->terminalById($outbox['terminal_local_id']);
        if (!$terminal && !empty($outbox['branch_id'])) {
            $terminal = $this->terminalForBranch($outbox['branch_id']);
        }
        if (!$terminal || trim((string)$terminal['jwt_token']) === '') {
            throw new RuntimeException('The queued item has no activated EIS terminal token.');
        }
        if (($outbox['entity_type'] ?? '') === 'sale') {
            $this->assertTerminalNotBlocked($terminal);
        }

        $url = rtrim((string)$terminal['api_base_url'], '/') . $outbox['endpoint'];
        $payload = json_decode($outbox['payload_json'], true);
        if (!is_array($payload)) {
            throw new RuntimeException('Queued EIS payload is invalid JSON.');
        }
        if (($outbox['entity_type'] ?? '') === 'sale') {
            $payload = $this->normalizeSaleSubmissionPayload($payload);
        }

        // Refresh invoiceDateTime to MRA server time immediately before submission.
        if (($outbox['entity_type'] ?? '') === 'sale'
            && isset($payload['invoiceHeader'])
            && trim((string)($payload['invoiceSummary']['offlineSignature'] ?? '')) === '') {
            $mraServerDateTime = $this->getMraServerDateTime($terminal);
            if ($mraServerDateTime === null) {
                throw new RuntimeException('MRA EIS server time could not be refreshed. The sale remains queued and was not submitted.');
            }
            $payload['invoiceHeader']['invoiceDateTime'] = $this->isoDate($mraServerDateTime);
        }

        $preflightError = null;
        $preflightTransient = null;
        $preflight = (($outbox['entity_type'] ?? '') === 'sale')
            ? $this->validateB2BBuyerForSubmission($payload, $terminal)
            : ['success' => true];
        if (empty($preflight['success'])) {
            $response = $preflight['response'];
            $preflightError = $preflight['message'];
            $preflightTransient = !empty($preflight['transient']);
        } else {
            $response = $this->postJsonWithAuthorization($url, $payload, $terminal);
        }
        if ((int)$response['status'] === 401 && !empty($outbox['branch_id'])) {
            $branchTerminal = $this->terminalForBranch($outbox['branch_id']);
            if ($branchTerminal && (int)$branchTerminal['id'] !== (int)$terminal['id']) {
                $terminal = $branchTerminal;
                $url = rtrim((string)$terminal['api_base_url'], '/') . $outbox['endpoint'];
                if ($preflightError !== null) {
                    $preflight = $this->validateB2BBuyerForSubmission($payload, $terminal);
                    if (empty($preflight['success'])) {
                        $response = $preflight['response'];
                        $preflightError = $preflight['message'];
                        $preflightTransient = !empty($preflight['transient']);
                    } else {
                        $preflightError = null;
                        $preflightTransient = null;
                        $response = $this->postJsonWithAuthorization($url, $payload, $terminal);
                    }
                } else {
                    $response = $this->postJsonWithAuthorization($url, $payload, $terminal);
                }
            }
        }
        $decoded = json_decode($response['body'], true);
        // Persist any updated configuration versions MRA returns in the response.
        $this->applyConfigVersionsFromResponse($decoded, (int)$terminal['id']);
        // Detect and enforce MRA-signalled terminal blocking.
        if ($this->applyTerminalBlockFromResponse($decoded, (int)$terminal['id'])) {
            throw new RuntimeException('MRA EIS has blocked this terminal. Contact MRA to resolve the issue before submitting further transactions.');
        }
        $httpSuccess = $response['status'] >= 200 && $response['status'] < 300;
        $apiStatusCode = is_array($decoded) && isset($decoded['statusCode']) && is_numeric($decoded['statusCode'])
            ? (int)$decoded['statusCode']
            : null;
        $validationMessages = $this->responseValidationMessages($decoded);
        $validationUrl = is_array($decoded) ? $this->firstNestedValue($decoded, ['validationURL', 'validationUrl', 'validation_url']) : null;
        $validationUrl = is_string($validationUrl) ? trim($validationUrl) : null;
        $hasValidationUrl = filter_var($validationUrl, FILTER_VALIDATE_URL) !== false;
        if (($outbox['entity_type'] ?? '') === 'sale' && $preflightError === null && $httpSuccess && !$hasValidationUrl) {
            $validationMessages[] = 'MRA EIS did not return a validation URL, so the sale is not treated as submitted.';
        }
        $success = $httpSuccess
            && ($apiStatusCode === null || $apiStatusCode > 0)
            && empty($validationMessages)
            && (($outbox['entity_type'] ?? '') !== 'sale' || $hasValidationUrl);
        $queuedForLater = !$success && ($preflightTransient !== null
            ? $preflightTransient
            : $this->isTransientSubmissionFailure($response));
        $outboxStatus = $success ? 'submitted' : ($queuedForLater ? 'ready' : 'failed');
        $lastError = null;
        if (!$success) {
            $lastError = $preflightError !== null
                ? $preflightError . ($queuedForLater ? ' The sale remains in the EIS outbox for retry.' : '')
                : ($queuedForLater
                    ? $this->queuedSubmissionMessage($response)
                    : (!empty($validationMessages)
                        ? 'MRA EIS validation failed: ' . implode(' ', array_unique($validationMessages))
                        : $this->friendlySubmissionError($response)));
        }
        $reference = is_array($decoded) ? $this->firstNestedValue($decoded, ['invoiceNumber', 'receiptNumber', 'invoiceNo', 'receiptNo', 'reference']) : null;
        // The validation URL is the EIS acknowledgement artifact used by the QR
        // algorithm. Never manufacture a validation QR for an unacknowledged sale.
        $acknowledged = $success && $hasValidationUrl;

        $stmt = $this->conn->prepare("
            UPDATE mra_eis_outbox SET
                status = :status,
                terminal_local_id = :terminal_local_id,
                payload_json = :payload_json,
                attempts = attempts + 1,
                http_status = :http_status,
                response_body = :response_body,
                mra_reference = :mra_reference,
                validation_url = :validation_url,
                last_error = :last_error,
                submitted_at = CASE WHEN :submitted = 1 THEN NOW() ELSE submitted_at END
            WHERE id = :id
        ");
        $stmt->execute([
            ':status' => $outboxStatus,
            ':terminal_local_id' => (int)$terminal['id'],
            ':payload_json' => json_encode($payload, JSON_UNESCAPED_SLASHES),
            ':http_status' => (int)$response['status'],
            ':response_body' => $response['body'],
            ':mra_reference' => $reference,
            ':validation_url' => $validationUrl,
            ':last_error' => $lastError,
            ':submitted' => $success ? 1 : 0,
            ':id' => (int)$outboxId,
        ]);

        $eventType = $success ? 'submitted' : ($queuedForLater ? 'queued_for_retry' : 'failed');
        $this->event((int)$outboxId, $outbox['entity_type'], $outbox['source_key'], $eventType, $lastError ?: $response['body'], $this->auditSnapshot(json_encode($payload, JSON_UNESCAPED_SLASHES), $outbox['local_audit_json'] ?? null), $userId);
        if ($success) {
            $this->offlineThreshold->markSyncSuccessful((int)$terminal['id'], $userId, (string)$outbox['source_key']);
        }

        return [
            'success' => $success,
            'queued' => $queuedForLater,
            'outbox_status' => $outboxStatus,
            'status' => $response['status'],
            'body' => $response['body'],
            'mra_reference' => $reference,
            'validation_url' => $validationUrl,
            'acknowledged' => $acknowledged,
            'message' => $lastError,
        ];
    }

    public function recordOfflineSaleAccepted($branchId, $userId, $sourceKey) {
        $terminal = $this->terminalForUserAndBranch($userId, $branchId) ?: $this->terminalForBranch($branchId);
        if ($terminal && $this->isOfflineMode((int)$terminal['id'])) {
            $this->offlineThreshold->recordOfflineSaleAccepted(
                (int)$terminal['id'],
                (int)$branchId,
                $userId,
                (string)$sourceKey
            );
        }
    }

    public function preserveOfflinePeriodAfterRollback($branchId, $userId = null) {
        $terminal = $this->terminalForUserAndBranch($userId, $branchId) ?: $this->terminalForBranch($branchId);
        if ($terminal) {
            return $this->offlineThreshold->startOfflinePeriod(
                (int)$terminal['id'],
                $userId,
                'MRA EIS connectivity failed while a local sale transaction was active.'
            );
        }
        return null;
    }

    private function hasValidAcknowledgement(array $outbox) {
        $validationUrl = trim((string)($outbox['validation_url'] ?? ''));
        return ($outbox['status'] ?? '') === 'submitted'
            && filter_var($validationUrl, FILTER_VALIDATE_URL) !== false;
    }
    private function submittedOutboxResult(array $outbox) {
        $validationUrl = trim((string)($outbox['validation_url'] ?? ''));
        return [
            'success' => true,
            'queued' => false,
            'already_submitted' => true,
            'outbox_status' => 'submitted',
            'status' => (int)($outbox['http_status'] ?: 200),
            'body' => (string)($outbox['response_body'] ?? ''),
            'mra_reference' => $outbox['mra_reference'] ?? null,
            'validation_url' => $validationUrl !== '' ? $validationUrl : null,
            'acknowledged' => filter_var($validationUrl, FILTER_VALIDATE_URL) !== false,
            'message' => 'This transaction was already submitted to MRA EIS.',
        ];
    }
    private function normalizeSaleSubmissionPayload(array $payload) {
        if (isset($payload['invoiceHeader']) && is_array($payload['invoiceHeader'])) {
            $paymentMethod = trim((string)($payload['invoiceHeader']['paymentMethod'] ?? ''));
            if ($paymentMethod !== '') {
                $payload['invoiceHeader']['paymentMethod'] = strtoupper($paymentMethod);
            }
            foreach (['buyerTIN', 'buyerName', 'buyerAuthorizationCode'] as $field) {
                if (array_key_exists($field, $payload['invoiceHeader'])) {
                    $payload['invoiceHeader'][$field] = $this->nullIfBlank($payload['invoiceHeader'][$field]);
                }
            }
            if (array_key_exists('vat5CertificateDetails', $payload['invoiceHeader'])) {
                $details = $payload['invoiceHeader']['vat5CertificateDetails'];
                if (is_string($details) && trim($details) !== '') {
                    $details = ['certificateNumber' => trim($details)];
                }
                if (is_array($details)) {
                    $details = array_filter([
                        'certificateNumber' => $this->nullIfBlank($details['certificateNumber'] ?? $details['CertificateNumber'] ?? null),
                        'projectNumber' => $this->nullIfBlank($details['projectNumber'] ?? $details['ProjectNumber'] ?? null),
                        'quantity' => isset($details['quantity']) || isset($details['Quantity'])
                            ? (float)($details['quantity'] ?? $details['Quantity'])
                            : null,
                        'certificateIssueDate' => $this->nullIfBlank($details['certificateIssueDate'] ?? $details['CertificateIssueDate'] ?? $details['dateOfIssue'] ?? null),
                        'certificateExpiryDate' => $this->nullIfBlank($details['certificateExpiryDate'] ?? $details['CertificateExpiryDate'] ?? $details['dateOfExpiry'] ?? null),
                    ], static fn($value) => $value !== null);
                    $details = !empty($details['certificateNumber']) ? $details : null;
                } else {
                    $details = null;
                }
                $payload['invoiceHeader']['vat5CertificateDetails'] = $details;
            }
            if (isset($payload['invoiceHeader']['invoiceDateTime'])) {
                $payload['invoiceHeader']['invoiceDateTime'] = $this->isoDate($payload['invoiceHeader']['invoiceDateTime']);
            }
            foreach (['globalConfigVersion', 'taxpayerConfigVersion', 'terminalConfigVersion'] as $field) {
                if (array_key_exists($field, $payload['invoiceHeader'])) {
                    $payload['invoiceHeader'][$field] = (int)$payload['invoiceHeader'][$field];
                }
            }
            foreach (['isExport', 'isReliefSupply'] as $field) {
                if (array_key_exists($field, $payload['invoiceHeader'])) {
                    $payload['invoiceHeader'][$field] = $this->eisBoolean($payload['invoiceHeader'][$field]);
                }
            }
            $payload['invoiceHeader'] = $this->filterKeys($payload['invoiceHeader'], [
                'invoiceNumber',
                'invoiceDateTime',
                'sellerTIN',
                'buyerTIN',
                'buyerName',
                'buyerAuthorizationCode',
                'siteId',
                'globalConfigVersion',
                'taxpayerConfigVersion',
                'terminalConfigVersion',
                'isExport',
                'isReliefSupply',
                'vat5CertificateDetails',
                'paymentMethod',
            ]);
        }

        if (isset($payload['invoiceSummary']) && is_array($payload['invoiceSummary'])) {
            if (array_key_exists('offlineSignature', $payload['invoiceSummary'])) {
                $payload['invoiceSummary']['offlineSignature'] = $this->nullIfBlank($payload['invoiceSummary']['offlineSignature']);
            }
            foreach (['totalVAT', 'invoiceTotal', 'amountTendered'] as $field) {
                if (array_key_exists($field, $payload['invoiceSummary'])) {
                    $payload['invoiceSummary'][$field] = round((float)$payload['invoiceSummary'][$field], 2);
                }
            }
            foreach (['taxBreakDown', 'levyBreakDown'] as $breakdownKey) {
                if (isset($payload['invoiceSummary'][$breakdownKey]) && is_array($payload['invoiceSummary'][$breakdownKey])) {
                    foreach ($payload['invoiceSummary'][$breakdownKey] as &$bucket) {
                        if (!is_array($bucket)) {
                            continue;
                        }
                        foreach (['taxableAmount', 'taxAmount'] as $field) {
                            if (array_key_exists($field, $bucket)) {
                                $bucket[$field] = round((float)$bucket[$field], 2);
                            }
                        }
                        $bucket = $this->filterKeys($bucket, ['rateId', 'taxableAmount', 'taxAmount']);
                    }
                    unset($bucket);
                }
            }
            $payload['invoiceSummary'] = $this->filterKeys($payload['invoiceSummary'], [
                'taxBreakDown',
                'levyBreakDown',
                'totalVAT',
                'offlineSignature',
                'invoiceTotal',
                'amountTendered',
            ]);
        }

        if (empty($payload['invoiceLineItems']) || !is_array($payload['invoiceLineItems'])) {
            return $payload;
        }

        foreach ($payload['invoiceLineItems'] as &$line) {
            if (!is_array($line)) {
                continue;
            }
            $name = trim((string)($line['productName'] ?? ''));
            $description = trim((string)($line['description'] ?? ''));
            if ($description === '' && $name !== '') {
                $line['description'] = $name;
            }
            if ($name === '' && $description !== '') {
                $line['productName'] = $description;
            }
            if (trim((string)($line['unitOfMeasure'] ?? '')) === '') {
                $line['unitOfMeasure'] = 'EA';
            }
            if (!array_key_exists('isProduct', $line)) {
                $line['isProduct'] = true;
            }
            foreach (['id', 'quantity'] as $field) {
                if (array_key_exists($field, $line)) {
                    $line[$field] = (int)$line[$field];
                }
            }
            foreach (['unitPrice', 'discount', 'total', 'totalVAT'] as $field) {
                if (array_key_exists($field, $line)) {
                    $line[$field] = round((float)$line[$field], 2);
                }
            }
            $line['isProduct'] = (bool)$line['isProduct'];
            $line = $this->filterKeys($line, [
                'id',
                'productCode',
                'description',
                'unitPrice',
                'quantity',
                'discount',
                'total',
                'totalVAT',
                'taxRateId',
                'isProduct',
            ]);
        }
        unset($line);

        return $payload;
    }

    private function filterKeys(array $data, array $allowedKeys) {
        $filtered = [];
        foreach ($allowedKeys as $key) {
            if (array_key_exists($key, $data)) {
                $filtered[$key] = $data[$key];
            }
        }
        return $filtered;
    }

    private function validateSaleSubmissionPayload(array $payload) {
        $issues = [];
        $header = $payload['invoiceHeader'] ?? [];
        foreach (['invoiceNumber', 'invoiceDateTime', 'sellerTIN', 'siteId', 'paymentMethod'] as $field) {
            if (trim((string)($header[$field] ?? '')) === '') {
                $issues[] = 'Missing EIS invoice header field: ' . $field . '.';
            }
        }
        if (!empty($header['isReliefSupply'])) {
            $vat5Details = is_array($header['vat5CertificateDetails'] ?? null)
                ? $header['vat5CertificateDetails']
                : [];
            $certificateNumber = trim((string)($vat5Details['certificateNumber'] ?? ''));
            $projectNumber = trim((string)($vat5Details['projectNumber'] ?? ''));
            if ($certificateNumber === '') {
                $issues[] = 'A validated VAT-5 certificate is required for a relief supply.';
            }
            if ($projectNumber === '') {
                $issues[] = 'A validated VAT-5 project number is required for a relief supply.';
            }
        }

        $lines = $payload['invoiceLineItems'] ?? [];
        if (!is_array($lines) || empty($lines)) {
            $issues[] = 'EIS invoice must include at least one line item.';
            return $issues;
        }

        foreach ($lines as $index => $line) {
            if (!is_array($line)) {
                $issues[] = 'EIS invoice line ' . ($index + 1) . ' is invalid.';
                continue;
            }

            foreach (['productCode', 'description', 'taxRateId'] as $field) {
                if (trim((string)($line[$field] ?? '')) === '') {
                    $issues[] = 'Missing EIS invoice line ' . ($index + 1) . ' field: ' . $field . '.';
                }
            }

            foreach (['unitPrice', 'quantity', 'total'] as $field) {
                if (!isset($line[$field]) || !is_numeric($line[$field]) || (float)$line[$field] <= 0) {
                    $issues[] = 'Invalid EIS invoice line ' . ($index + 1) . ' value for: ' . $field . '.';
                }
            }
        }

        return array_unique($issues);
    }

    public function findOutbox($id) {
        $stmt = $this->conn->prepare("SELECT * FROM mra_eis_outbox WHERE id = :id LIMIT 1");
        $stmt->execute([':id' => (int)$id]);
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }

    private function saleSnapshot($transactionId, $branchId) {
        $stmt = $this->conn->prepare("
            SELECT si.*, i.item_name, i.item_details, i.description,
                   NULLIF(TRIM(i.product_code), '') AS product_code,
                   COALESCE(itemsp.product_name, msp.product_name, pcmsp.product_name, map.product_name, pcm.eis_product_name, i.item_name) AS product_name,
                   COALESCE(NULLIF(i.tax_rate_id, ''), itemsp.tax_rate_id, msp.tax_rate_id, pcmsp.tax_rate_id, map.tax_rate_id, pcm.tax_rate_id, 'A') AS tax_rate_id,
                   COALESCE(itemsp.levy_rate_id, msp.levy_rate_id, pcmsp.levy_rate_id, map.levy_rate_id, pcm.levy_rate_id) AS levy_rate_id,
                   COALESCE(itemsp.terminal_local_id, map.terminal_local_id, msp.terminal_local_id, pcm.terminal_local_id, pcmsp.terminal_local_id) AS terminal_local_id,
                   COALESCE(itemsp.site_id, msp.site_id, pcmsp.site_id) AS site_id,
                   COALESCE(itemsp.description, msp.description, pcmsp.description, i.item_details, i.description, i.item_name) AS eis_description,
                   COALESCE(NULLIF(itemsp.unit_of_measure, ''), NULLIF(msp.unit_of_measure, ''), NULLIF(pcmsp.unit_of_measure, ''), 'EA') AS unit_of_measure,
                   COALESCE(itemsp.is_product, msp.is_product, pcmsp.is_product, 1) AS is_product,
                   u.terminal_id AS user_terminal_id,
                   si.payment_method
            FROM sold_items si
            JOIN items i ON i.item_id = si.item_id
            LEFT JOIN mra_eis_site_products itemsp ON itemsp.id = i.eis_site_product_id
            LEFT JOIN mra_eis_item_product_map map ON map.item_id = si.item_id
            LEFT JOIN mra_eis_site_products msp ON msp.id = map.site_product_id
            LEFT JOIN mra_eis_product_code_map pcm ON pcm.eis_product_code = i.product_code
            LEFT JOIN mra_eis_site_products pcmsp ON pcmsp.id = pcm.site_product_id
            LEFT JOIN users u ON u.user_id = si.sold_by
            WHERE si.transaction_id = :transaction_id
              AND si.branch_id = :branch_id
            ORDER BY si.sale_id
        ");
        $stmt->execute([':transaction_id' => $transactionId, ':branch_id' => (int)$branchId]);
        $items = $stmt->fetchAll(PDO::FETCH_ASSOC);
        if (!$items) {
            return null;
        }

        return [
            'transaction_id' => $transactionId,
            'branch_id' => (int)$branchId,
            'sold_by' => $items[0]['sold_by'] ?? null,
            'sold_at' => $items[0]['sold_at'] ?? date('Y-m-d H:i:s'),
            'customer_name' => $items[0]['customer_name'] ?? null,
            'customer_phone' => $items[0]['customer_phone'] ?? null,
            'vat_relief' => !empty($items[0]['vat_relief']),
            'buyer_tin' => $items[0]['buyer_tin'] ?? null,
            'eis_transaction_type' => $items[0]['eis_transaction_type'] ?? null,
            'buyer_authorization_code' => $items[0]['buyer_authorization_code'] ?? null,
            'vat5_certificate_number' => $items[0]['vat5_certificate_number'] ?? null,
            'vat5_certificate_details' => !empty($items[0]['vat5_certificate_details'])
                ? (json_decode((string)$items[0]['vat5_certificate_details'], true) ?: null)
                : null,
            'payment_method' => $items[0]['payment_method'] ?? 'cash',
            'items' => $items,
        ];
    }

    private function shipmentSnapshot($shipmentId) {
        $stmt = $this->conn->prepare("
            SELECT s.*, si.shipment_item_id, i.item_id, i.unique_code, i.item_name, i.item_details, i.unit_price,
                   NULLIF(TRIM(i.product_code), '') AS product_code,
                   COALESCE(map.product_name, i.item_name) AS product_name,
                   COALESCE(map.tax_rate_id, 'A') AS tax_rate_id
            FROM shipments s
            JOIN shipment_items si ON si.shipment_id = s.shipment_id
            JOIN items i ON i.item_id = si.item_id
            LEFT JOIN mra_eis_item_product_map map ON map.item_id = i.item_id
            WHERE s.shipment_id = :shipment_id
            ORDER BY si.shipment_item_id
        ");
        $stmt->execute([':shipment_id' => (int)$shipmentId]);
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
        if (!$rows) {
            return null;
        }
        $shipment = $rows[0];
        $shipment['items'] = $rows;
        return $shipment;
    }

    private function terminalForSale(array $sale) {
        foreach ($sale['items'] as $item) {
            if (!empty($item['terminal_local_id'])) {
                $terminal = $this->terminalById($item['terminal_local_id']);
                if ($terminal) {
                    return $terminal;
                }
            }
        }

        $terminal = $this->terminalForBranch($sale['branch_id']);
        if ($terminal) {
            return $terminal;
        }

        $terminalId = $sale['items'][0]['user_terminal_id'] ?? null;
        if ($terminalId) {
            return $this->terminalById($terminalId);
        }

        return null;
    }

    private function terminalForUserAndBranch($userId, $branchId) {
        if (empty($userId) || empty($branchId)) {
            return null;
        }

        $sql = 'SELECT met.* FROM users u '
            . 'INNER JOIN mra_eis_terminals met ON met.id = u.terminal_id '
            . 'WHERE u.user_id = :user_id AND u.branch_id = :branch_id '
            . 'AND met.branch_id = :branch_id AND met.status = \'activated\' '
            . 'AND met.jwt_token IS NOT NULL AND met.jwt_token != \'\' LIMIT 1';
        $stmt = $this->conn->prepare($sql);
        $stmt->execute([
            ':user_id' => (int)$userId,
            ':branch_id' => (int)$branchId,
        ]);
        return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
    }

    private function terminalForBranch($branchId) {
        if ($branchId === null || $branchId === '') {
            $sql = "
                SELECT *
                FROM mra_eis_terminals
                WHERE status = 'activated'
                  AND COALESCE(is_blocked, 0) = 0
                  AND jwt_token IS NOT NULL
                  AND jwt_token != ''
                ORDER BY (environment = 'production') DESC, updated_at DESC, id DESC
                LIMIT 1
            ";
            $stmt = $this->conn->prepare($sql);
            $stmt->execute();
            return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
        }

        $stmt = $this->conn->prepare("
            SELECT *
            FROM mra_eis_terminals
            WHERE branch_id = :branch_id
              AND status = 'activated'
              AND COALESCE(is_blocked, 0) = 0
              AND jwt_token IS NOT NULL
              AND jwt_token != ''
            ORDER BY (environment = 'production') DESC, updated_at DESC, id DESC
            LIMIT 1
        ");
        $stmt->execute([':branch_id' => (int)$branchId]);
        return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
    }

    private function terminalById($terminalId) {
        if (!$terminalId) {
            return null;
        }
        $stmt = $this->conn->prepare("
            SELECT *
            FROM mra_eis_terminals
            WHERE id = :id
              AND status = 'activated'
              AND COALESCE(is_blocked, 0) = 0
              AND jwt_token IS NOT NULL
              AND jwt_token != ''
            LIMIT 1
        ");
        $stmt->execute([':id' => (int)$terminalId]);
        return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
    }

    private function siteIdForTerminal($terminal, array $sale) {
        if ($terminal) {
            foreach ($sale['items'] as $item) {
                if (!empty($item['site_id']) && (int)($item['terminal_local_id'] ?? 0) === (int)$terminal['id']) {
                    return (string)$item['site_id'];
                }
            }
        }
        if ($terminal && !empty($terminal['site_id'])) {
            return (string)$terminal['site_id'];
        }
        foreach ($sale['items'] as $item) {
            if (!empty($item['site_id'])) {
                return (string)$item['site_id'];
            }
        }
        return '';
    }

    private function siteIdForBranch($branchId) {
        $stmt = $this->conn->prepare("
            SELECT COALESCE(site_id, '') AS site_id
            FROM mra_eis_terminals
            WHERE branch_id = :branch_id
              AND status = 'activated'
            ORDER BY (environment = 'production') DESC, updated_at DESC, id DESC
            LIMIT 1
        ");
        $stmt->execute([':branch_id' => (int)$branchId]);
        return (string)$stmt->fetchColumn();
    }

    private function upsertOutbox(array $data) {
        $stmt = $this->conn->prepare("
            INSERT INTO mra_eis_outbox (
                entity_type, source_table, source_id, source_key, branch_id, terminal_local_id,
                endpoint, payload_json, status, local_audit_json, last_error, created_by
            ) VALUES (
                :entity_type, :source_table, :source_id, :source_key, :branch_id, :terminal_local_id,
                :endpoint, :payload_json, :status, :local_audit_json, :last_error, :created_by
            )
            ON DUPLICATE KEY UPDATE
                branch_id = VALUES(branch_id),
                terminal_local_id = VALUES(terminal_local_id),
                endpoint = IF(status = 'submitted' AND validation_url IS NOT NULL AND (validation_url LIKE 'http://%' OR validation_url LIKE 'https://%'), endpoint, VALUES(endpoint)),
                payload_json = IF(status = 'submitted' AND validation_url IS NOT NULL AND (validation_url LIKE 'http://%' OR validation_url LIKE 'https://%'), payload_json, VALUES(payload_json)),
                local_audit_json = IF(status = 'submitted' AND validation_url IS NOT NULL AND (validation_url LIKE 'http://%' OR validation_url LIKE 'https://%'), local_audit_json, VALUES(local_audit_json)),
                status = IF(status = 'submitted' AND validation_url IS NOT NULL AND (validation_url LIKE 'http://%' OR validation_url LIKE 'https://%'), status, VALUES(status)),
                last_error = IF(status = 'submitted' AND validation_url IS NOT NULL AND (validation_url LIKE 'http://%' OR validation_url LIKE 'https://%'), last_error, VALUES(last_error)),
                created_by = VALUES(created_by)
        ");
        $stmt->execute([
            ':entity_type' => $data['entity_type'],
            ':source_table' => $data['source_table'],
            ':source_id' => $data['source_id'],
            ':source_key' => $data['source_key'],
            ':branch_id' => $data['branch_id'],
            ':terminal_local_id' => $data['terminal_local_id'],
            ':endpoint' => $data['endpoint'],
            ':payload_json' => $data['payload_json'],
            ':status' => $data['status'],
            ':local_audit_json' => $data['local_audit_json'] ?? null,
            ':last_error' => $data['last_error'],
            ':created_by' => $data['created_by'],
        ]);

        $id = (int)$this->conn->lastInsertId();
        if ($id === 0) {
            $lookup = $this->conn->prepare("SELECT id FROM mra_eis_outbox WHERE entity_type = :entity_type AND source_key = :source_key LIMIT 1");
            $lookup->execute([':entity_type' => $data['entity_type'], ':source_key' => $data['source_key']]);
            $id = (int)$lookup->fetchColumn();
        }

        $actual = $this->findOutbox($id);
        $actualStatus = $actual['status'] ?? $data['status'];
        $actualError = is_array($actual) && array_key_exists('last_error', $actual)
            ? $actual['last_error']
            : $data['last_error'];
        $eventType = $actualStatus === 'submitted' ? 'already_submitted' : 'prepared';
        $this->event($id, $data['entity_type'], $data['source_key'], $eventType, $actualError, $this->auditSnapshot($actual['payload_json'] ?? $data['payload_json'], $actual['local_audit_json'] ?? ($data['local_audit_json'] ?? null)), $data['created_by']);

        return [
            'id' => $id,
            'status' => $actualStatus,
            'last_error' => $actualError,
            'mra_reference' => $actual['mra_reference'] ?? null,
            'validation_url' => $actual['validation_url'] ?? null,
            'payload_json' => $actual['payload_json'] ?? $data['payload_json'],
            'local_audit_json' => $actual['local_audit_json'] ?? ($data['local_audit_json'] ?? null),
        ];
    }

    private function event($outboxId, $entityType, $sourceKey, $eventType, $message, $payloadSnapshot, $userId = null) {
        $stmt = $this->conn->prepare("
            INSERT INTO mra_eis_compliance_events (
                outbox_id, entity_type, source_key, event_type, message, payload_snapshot, created_by
            ) VALUES (
                :outbox_id, :entity_type, :source_key, :event_type, :message, :payload_snapshot, :created_by
            )
        ");
        $stmt->execute([
            ':outbox_id' => $outboxId ?: null,
            ':entity_type' => $entityType,
            ':source_key' => $sourceKey,
            ':event_type' => $eventType,
            ':message' => $message,
            ':payload_snapshot' => $payloadSnapshot,
            ':created_by' => $userId,
        ]);
    }

    private function auditSnapshot($eisPayloadJson, $localAuditJson = null) {
        if ($localAuditJson === null || $localAuditJson === '') {
            return $eisPayloadJson;
        }

        $snapshot = [
            'eis_payload' => json_decode((string)$eisPayloadJson, true),
            'local_ims_unique_code_audit' => json_decode((string)$localAuditJson, true),
        ];

        return json_encode($snapshot, JSON_UNESCAPED_SLASHES);
    }

    private function offlineLimits(array $terminal) {
        $stored = $this->conn->prepare(
            'SELECT terminal_configuration_json FROM mra_eis_configurations WHERE terminal_local_id = :terminal_id LIMIT 1'
        );
        $stored->execute([':terminal_id' => (int)$terminal['id']]);
        $terminalConfiguration = json_decode((string)$stored->fetchColumn(), true);
        $offline = is_array($terminalConfiguration)
            ? ($terminalConfiguration['offlineLimit'] ?? [])
            : [];
        if (!is_array($offline) || !$offline) {
            $decoded = json_decode((string)($terminal['activation_response'] ?? ''), true);
            $offline = is_array($decoded)
                ? ($decoded['data']['configuration']['terminalConfiguration']['offlineLimit']
                    ?? $decoded['configuration']['terminalConfiguration']['offlineLimit']
                    ?? [])
                : [];
        }
        return [
            'max_age_hours' => max(0, (float)($offline['maxTransactionAgeInHours'] ?? 0)),
            'max_amount' => max(0, (float)($offline['maxCummulativeAmount'] ?? $offline['maxCumulativeAmount'] ?? 0)),
        ];
    }

    private function pendingOfflineAmount($terminalId, $excludeSourceKey = null) {
        $stmt = $this->conn->prepare(
            'SELECT source_key, payload_json FROM mra_eis_outbox '
            . "WHERE entity_type = 'sale' AND terminal_local_id = :terminal_id AND status != 'submitted'"
        );
        $stmt->execute([':terminal_id' => (int)$terminalId]);
        $total = 0.0;
        foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
            if ($excludeSourceKey !== null && (string)$row['source_key'] === (string)$excludeSourceKey) {
                continue;
            }
            $payload = json_decode((string)$row['payload_json'], true);
            if (trim((string)($payload['invoiceSummary']['offlineSignature'] ?? '')) === '') {
                continue;
            }
            $total += (float)($payload['invoiceSummary']['invoiceTotal'] ?? 0);
        }
        return round($total, 2);
    }

    private function countPendingOfflineSales() {
        $stmt = $this->conn->query(
            'SELECT payload_json FROM mra_eis_outbox '
            . "WHERE entity_type = 'sale' AND status != 'submitted'"
        );
        $count = 0;
        foreach ($stmt->fetchAll(PDO::FETCH_COLUMN) as $payloadJson) {
            $payload = json_decode((string)$payloadJson, true);
            if (trim((string)($payload['invoiceSummary']['offlineSignature'] ?? '')) !== '') {
                $count++;
            }
        }
        return $count;
    }

    private function generateOfflineSignature(array $payload, array $terminal) {
        $invoiceNumber = trim((string)($payload['invoiceHeader']['invoiceNumber'] ?? ''));
        $secretKey = trim((string)($terminal['secret_key'] ?? ''));
        if ($invoiceNumber === '' || $secretKey === '') {
            throw new RuntimeException('The activated terminal is missing invoice-signing credentials.');
        }

        $segments = explode('-', $invoiceNumber);
        $encodedTransactionDate = $segments[2] ?? '';
        $numberOfItems = 0;
        foreach ((array)($payload['invoiceLineItems'] ?? []) as $line) {
            $numberOfItems += max(0, (int)($line['quantity'] ?? 0));
        }
        // MRA's reference implementation interpolates the raw decimal
        // (`$"I={request.InvoiceTotal}"`) with no format specifier. A .NET
        // decimal keeps the scale it was computed with, and currency totals
        // built from 2-decimal-place line items always carry that scale
        // through arithmetic — so MRA's server reconstructs a fixed 2-decimal
        // string (e.g. "1500.00"), never a trimmed one ("1500"). Stripping
        // trailing zeros here would sign a different string than the server
        // recomputes and break every whole-amount receipt's signature.
        $formatNumber = static function ($value) {
            return number_format((float)$value, 2, '.', '');
        };
        // Per MRA's reference implementation, the HMAC is computed over the bare
        // "TI=...&N=...&I=...&V=...&T=..." param string with no leading "?" — the
        // "?" is only prepended when the param string is appended to the validation
        // URL. Signing the URL-prefixed string here would not match MRA's
        // server-side HMAC recomputation and would fail receipt validation.
        $param = 'TI=' . $invoiceNumber
            . '&N=' . $numberOfItems
            . '&I=' . $formatNumber($payload['invoiceSummary']['invoiceTotal'] ?? 0)
            . '&V=' . $formatNumber($payload['invoiceSummary']['totalVAT'] ?? 0)
            . '&T=' . $encodedTransactionDate;
        $signature = rtrim(strtr(base64_encode(hash_hmac('sha256', $param, $secretKey, true)), '+/', '-_'), '=');
        $isProduction = strtolower((string)($terminal['environment'] ?? '')) === 'production';
        $envKey = $isProduction ? 'production' : 'sandbox';
        $validationBaseUrl = rtrim((string)$this->settings->get(
            'eis_validation_url_' . $envKey,
            $isProduction
                ? 'https://eis-portal.mra.mw/ReceiptValidation/Validate/'
                : 'https://dev-eis-portal.mra.mw/ReceiptValidation/Validate/'
        ), '/') . '/';
        // MRA's custom Base64 alphabet (ABC...xyz0-9+/) means the TI and T
        // segments can legitimately contain literal "+" or "/" characters. The
        // HMAC above is signed over those raw characters per spec, but a raw "+"
        // left unescaped in a URL query string decodes as a space on any
        // standard HTTP query parser, corrupting the value before MRA's server
        // can recompute the same signature. Percent-encode only the values that
        // carry MRA's Base64 alphabet when building the scannable URL; the
        // signature itself must stay computed over the un-encoded param string.
        $urlParam = 'TI=' . rawurlencode($invoiceNumber)
            . '&N=' . $numberOfItems
            . '&I=' . $formatNumber($payload['invoiceSummary']['invoiceTotal'] ?? 0)
            . '&V=' . $formatNumber($payload['invoiceSummary']['totalVAT'] ?? 0)
            . '&T=' . rawurlencode($encodedTransactionDate);
        return [
            'signature' => $signature,
            'receipt_data' => $validationBaseUrl . '?' . $urlParam . '&S=' . $signature,
        ];
    }

    private function postJson($url, array $payload, array $extraHeaders = []) {
        $body = json_encode($payload);
        if ($body === false) {
            throw new RuntimeException('Unable to encode MRA EIS payload.');
        }
        $headers = array_merge(['accept: text/plain', 'Content-Type: application/json'], $extraHeaders);

        if (function_exists('curl_init')) {
            $ch = curl_init($url);
            curl_setopt_array($ch, [
                CURLOPT_POST => true,
                CURLOPT_HTTPHEADER => $headers,
                CURLOPT_POSTFIELDS => $body,
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT => 30,
            ]);
            $responseBody = curl_exec($ch);
            if ($responseBody === false) {
                $error = curl_error($ch);
                curl_close($ch);
                throw new RuntimeException('MRA EIS request failed: ' . $error);
            }
            $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);
            return ['status' => $status, 'body' => (string)$responseBody];
        }

        $context = stream_context_create([
            'http' => [
                'method' => 'POST',
                'header' => implode("\r\n", $headers) . "\r\n",
                'content' => $body,
                'timeout' => 30,
                'ignore_errors' => true,
            ],
        ]);
        $responseBody = file_get_contents($url, false, $context);
        if ($responseBody === false) {
            throw new RuntimeException('MRA EIS request failed.');
        }
        $status = 0;
        if (isset($http_response_header[0]) && preg_match('/\s(\d{3})\s/', $http_response_header[0], $matches)) {
            $status = (int)$matches[1];
        }
        return ['status' => $status, 'body' => (string)$responseBody];
    }


    /**
     * EIS requires B2B buyer eligibility to be checked before invoice submission.
     * HTTP 200 is not success when the EIS wrapper statusCode is negative.
     */
    private function validateB2BBuyerForSubmission(array &$payload, array $terminal) {
        $header = $payload['invoiceHeader'] ?? [];
        $buyerTin = trim((string)($header['buyerTIN'] ?? ''));
        if ($buyerTin === '') {
            return ['success' => true];
        }

        $baseUrl = rtrim((string)$terminal['api_base_url'], '/');
        $response = $this->postJsonWithAuthorization(
            $baseUrl . '/utilities/check-tin-authorization-requirement',
            ['tin' => $buyerTin],
            $terminal
        );
        $decoded = json_decode((string)$response['body'], true);
        $apiStatusCode = is_array($decoded) && is_numeric($decoded['statusCode'] ?? null)
            ? (int)$decoded['statusCode']
            : null;
        $httpSuccess = (int)$response['status'] >= 200 && (int)$response['status'] < 300;
        if (!$httpSuccess || ($apiStatusCode !== null && $apiStatusCode <= 0)
            || !empty($this->responseValidationMessages($decoded))) {
            return [
                'success' => false,
                'transient' => $this->isTransientSubmissionFailure($response),
                'response' => $response,
                'message' => 'MRA EIS could not verify B2B buyer TIN ' . $buyerTin . ': '
                    . $this->friendlySubmissionError($response),
            ];
        }

        $data = is_array($decoded['data'] ?? null) ? $decoded['data'] : [];
        if (array_key_exists('tinExists', $data) && !$this->eisBoolean($data['tinExists'])) {
            return [
                'success' => false,
                'transient' => false,
                'response' => $response,
                'message' => 'MRA EIS rejected the B2B buyer because TIN ' . $buyerTin . ' was not found.',
            ];
        }

        $requiresAuthorizationCode = array_key_exists('requiresAuthorizationCode', $data)
            && $this->eisBoolean($data['requiresAuthorizationCode']);
        $authorizationCode = trim((string)($header['buyerAuthorizationCode'] ?? ''));
        if (!$requiresAuthorizationCode) {
            $payload['invoiceHeader']['buyerAuthorizationCode'] = null;
            return ['success' => true];
        }
        if ($authorizationCode === '') {
            return [
                'success' => false,
                'transient' => false,
                'response' => $response,
                'message' => 'Buyer TIN ' . $buyerTin . ' requires an MRA EIS purchase authorization code. Enter the code and retry the sale.',
            ];
        }
        $authorizationResponse = $this->postJsonWithAuthorization(
            $baseUrl . '/utilities/validate-authorization-code',
            ['authorizationCode' => $authorizationCode],
            $terminal
        );
        $authorizationDecoded = json_decode((string)$authorizationResponse['body'], true);
        $authorizationStatusCode = is_array($authorizationDecoded)
            && is_numeric($authorizationDecoded['statusCode'] ?? null)
            ? (int)$authorizationDecoded['statusCode']
            : null;
        $authorizationHttpSuccess = (int)$authorizationResponse['status'] >= 200
            && (int)$authorizationResponse['status'] < 300;
        if (!$authorizationHttpSuccess
            || ($authorizationStatusCode !== null && $authorizationStatusCode <= 0)
            || !empty($this->responseValidationMessages($authorizationDecoded))) {
            return [
                'success' => false,
                'transient' => $this->isTransientSubmissionFailure($authorizationResponse),
                'response' => $authorizationResponse,
                'message' => 'MRA EIS could not validate the B2B purchase authorization code: '
                    . $this->friendlySubmissionError($authorizationResponse),
            ];
        }

        $authorizationData = is_array($authorizationDecoded['data'] ?? null)
            ? $authorizationDecoded['data']
            : [];
        if (array_key_exists('isValidAuthorizationCode', $authorizationData)
            && !$this->eisBoolean($authorizationData['isValidAuthorizationCode'])) {
            $reason = trim((string)($authorizationData['authorizationReason'] ?? ''));
            return [
                'success' => false,
                'transient' => false,
                'response' => $authorizationResponse,
                'message' => 'MRA EIS rejected the B2B purchase authorization code.'
                    . ($reason !== '' ? ' ' . $reason : ''),
            ];
        }

        return ['success' => true];
    }

    private function eisBoolean($value) {
        if (is_bool($value)) {
            return $value;
        }
        if (is_numeric($value)) {
            return (int)$value === 1;
        }
        return in_array(strtolower(trim((string)$value)), ['true', 'yes', 'y'], true);
    }

    private function postJsonWithAuthorization($url, array $payload, array $terminal) {
        $token = trim((string)($terminal['jwt_token'] ?? ''));

        $response = null;
        $attempts = [];

        if ($token !== '') {
            $attempts[] = ['Authorization: Bearer ' . $token];
            $attempts[] = ['Authorization: ' . $token];
        }

        $failedAttempts = [];
        foreach ($attempts as $headers) {
            for ($try = 1; $try <= 2; $try++) {
                try {
                    $response = $this->postJson($url, $payload, $headers);
                } catch (Throwable $e) {
                    if (strpos($e->getMessage(), 'MRA EIS request failed') !== 0) {
                        throw $e;
                    }
                    $response = ['status' => 0, 'body' => 'MRA EIS connection failed.'];
                }
                if (!$this->settings->eisOfflineMode()
                    && !MraEisOfflineThreshold::isConnectivityFailure($response)) {
                    $this->offlineThreshold->markTerminalOnline((int)$terminal['id']);
                }
                if ($response['status'] >= 200 && $response['status'] < 300) {
                    return $response;
                }

                $failedAttempts[] = [
                    'status' => (int)$response['status'],
                    'try' => $try,
                    'headers' => array_map(function ($header) {
                        return preg_replace('/(Authorization:\\s*)(.+)$/i', '$1[redacted]', $header);
                    }, $headers),
                    'body' => substr((string)$response['body'], 0, 500),
                ];

                if ((int)$response['status'] === 401) {
                    break;
                }

                if (!$this->isTransientSubmissionFailure($response) || $try === 2) {
                    $this->recordTerminalConnectivity($response, $terminal, $headers);
                    $response['authorization_attempts'] = $failedAttempts;
                    return $response;
                }

                usleep(250000);
            }
        }

        if ($response) {
            $this->recordTerminalConnectivity($response, $terminal, $headers ?? []);
            $response['authorization_attempts'] = $failedAttempts;
            return $response;
        }

        return ['status' => 0, 'body' => 'No EIS authorization attempt was made.'];
    }

    private function recordTerminalConnectivity(array $response, array $terminal, array $headers = []) {
        if ($this->settings->eisOfflineMode()) {
            $this->offlineThreshold->startOfflinePeriod(
                (int)$terminal['id'], null, 'EIS Offline is enabled by an administrator.'
            );
            return;
        }
        if (!MraEisOfflineThreshold::isConnectivityFailure($response)) {
            $this->offlineThreshold->markTerminalOnline((int)$terminal['id']);
            return;
        }
        $baseUrl = rtrim((string)($terminal['api_base_url'] ?? ''), '/');
        if ($baseUrl !== '') {
            try {
                $ping = $this->postJson($baseUrl . '/utilities/ping', [], $headers);
                if (!MraEisOfflineThreshold::isConnectivityFailure($ping)) {
                    $this->offlineThreshold->markTerminalOnline((int)$terminal['id']);
                    return;
                }
            } catch (Throwable $e) {
                // Keep the original failure when the independent ping also fails.
            }
        }
        $this->offlineThreshold->startOfflinePeriod(
            (int)$terminal['id'],
            null,
            'MRA EIS request and connectivity ping failed with status ' . (int)$response['status'] . '.'
        );
    }

    private function firstNestedValue($value, array $keys) {
        if (!is_array($value)) {
            return null;
        }
        foreach ($keys as $key) {
            if (isset($value[$key]) && !is_array($value[$key])) {
                return (string)$value[$key];
            }
        }
        foreach ($value as $child) {
            if (is_array($child)) {
                $found = $this->firstNestedValue($child, $keys);
                if ($found !== null) {
                    return $found;
                }
            }
        }
        return null;
    }

    private function responseValidationMessages($decoded) {
        if (!is_array($decoded)) {
            return [];
        }

        $messages = [];
        $collect = function ($value) use (&$collect, &$messages) {
            if (is_string($value) && trim($value) !== '') {
                $messages[] = trim($value);
                return;
            }
            if (!is_array($value)) {
                return;
            }

            $message = $value['message'] ?? $value['errorMessage'] ?? $value['description'] ?? null;
            if (is_string($message) && trim($message) !== '') {
                $messages[] = trim($message);
                return;
            }
            foreach ($value as $child) {
                $collect($child);
            }
        };

        $collect($decoded['errors'] ?? null);
        $collect($decoded['data']['validationErrors'] ?? null);
        $collect($decoded['validationErrors'] ?? null);

        return array_values(array_unique($messages));
    }
    private function friendlySubmissionError(array $response) {
        $status = (int)($response['status'] ?? 0);
        $body = trim((string)($response['body'] ?? ''));

        if ($status === 401) {
            return 'MRA EIS authorization failed. The selected terminal token was rejected. Activate and confirm the terminal again to obtain a current JWT.';
        }

        $decoded = json_decode($body, true);
        $messages = $this->responseValidationMessages($decoded);
        if (!empty($messages)) {
            return 'MRA EIS validation failed: ' . implode(' ', $messages);
        }
        if (is_array($decoded) && trim((string)($decoded['remark'] ?? '')) !== '') {
            return trim((string)$decoded['remark']);
        }

        if ($body !== '') {
            return $body;
        }

        return 'MRA EIS submission failed with HTTP ' . $status . '.';
    }

    private function isTransientSubmissionFailure(array $response) {
        $status = (int)($response['status'] ?? 0);
        if (in_array($status, [0, 408, 429, 500, 502, 503, 504], true)) {
            return true;
        }

        $decoded = json_decode((string)($response['body'] ?? ''), true);
        $apiStatusCode = is_array($decoded) && is_numeric($decoded['statusCode'] ?? null)
            ? (int)$decoded['statusCode']
            : null;
        $remark = strtolower(trim((string)($decoded['remark'] ?? '')));
        foreach (['invalid', 'does not exist', 'not exist', 'expired', 'already used', 'not found'] as $permanentError) {
            if (strpos($remark, $permanentError) !== false) {
                return false;
            }
        }
        return $apiStatusCode === -2
            || strpos($remark, 'try again') !== false
            || strpos($remark, 'temporarily') !== false;
    }

    private function queuedSubmissionMessage(array $response) {
        $status = (int)($response['status'] ?? 0);
        if ($status === 0) {
            return 'MRA EIS is unreachable right now. The sale has been saved in the EIS outbox and can be submitted later.';
        }

        $decoded = json_decode((string)($response['body'] ?? ''), true);
        $remark = is_array($decoded) ? trim((string)($decoded['remark'] ?? '')) : '';
        if ($remark !== '') {
            return 'MRA EIS did not accept the transaction: ' . $remark
                . ' The sale has been saved in the EIS outbox and can be submitted later.';
        }

        return 'MRA EIS is temporarily unavailable or returned HTTP ' . $status
            . '. The sale has been saved in the EIS outbox and can be submitted later.';
    }

    private function terminalTaxRateMap(?array $terminal) {
        if (!$terminal) {
            return [];
        }

        $stmt = $this->conn->prepare('SELECT tax_rates_json FROM mra_eis_configurations WHERE terminal_local_id = :terminal_id LIMIT 1');
        $stmt->execute([':terminal_id' => (int)$terminal['id']]);
        $storedRates = json_decode((string)$stmt->fetchColumn(), true);
        $storedMap = $this->rateMap(is_array($storedRates) ? $storedRates : []);
        if (!empty($storedMap)) {
            return $storedMap;
        }

        $payloads = [
            $terminal['latest_config_response'] ?? '',
            $terminal['confirmation_response'] ?? '',
            $terminal['activation_response'] ?? '',
        ];

        foreach ($payloads as $payload) {
            $decoded = json_decode((string)$payload, true);
            if (!is_array($decoded)) {
                continue;
            }

            $taxRates = $decoded['data']['configuration']['globalConfiguration']['taxrates']
                ?? $decoded['configuration']['globalConfiguration']['taxrates']
                ?? [];
            if (!is_array($taxRates)) {
                continue;
            }

            $map = $this->rateMap($taxRates);

            if (!empty($map)) {
                return $map;
            }
        }

        return [];
    }

    private function rateMap(array $rates) {
        $map = [];
        foreach ($rates as $rate) {
            if (!is_array($rate) || !isset($rate['id'])) {
                continue;
            }
            $map[strtoupper((string)$rate['id'])] = is_numeric($rate['rate'] ?? null) ? (float)$rate['rate'] : 0.0;
        }
        return $map;
    }

    private function vatComponent($grossAmount, $taxRateId, array $taxRateMap = []) {
        $taxRateId = strtoupper((string)$taxRateId);
        $zeroRatedIds = $this->settings->eisZeroRatedTaxIds();
        if (in_array($taxRateId, $zeroRatedIds, true)) {
            return 0.0;
        }

        // EIS validates VAT against the downloaded terminal configuration.
        // Use that configured rate first and keep the local setting only as a
        // fallback when the terminal response did not contain a usable rate.
        if ($taxRateId === 'A' || strpos($taxRateId, 'VAT') !== false) {
            $rate = $taxRateMap[$taxRateId] ?? $taxRateMap['A'] ?? $this->vatPercentage;
        } else {
            $rate = $taxRateMap[$taxRateId] ?? null;
        }

        if ($rate === null && preg_match('/(\d+(?:\.\d+)?)/', $taxRateId, $matches)) {
            $rate = (float)$matches[1];
        }

        if ($rate !== null && $rate > 0) {
            return round(((float)$grossAmount * $rate) / (100 + $rate), 2);
        }

        return 0.0;
    }

    private function isoDate($value) {
        $raw = trim((string)$value);
        if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $raw)) {
            $raw .= ' ' . date('H:i:s');
        }

        $timestamp = strtotime($raw);
        if (!$timestamp || substr($raw, 0, 10) === '0000-00-00') {
            $timestamp = time();
        }
        return date('c', $timestamp);
    }

    private function isUsableEisProductCode($value) {
        $value = trim((string)$value);
        return $value !== '' && stripos($value, 'IMS-') !== 0;
    }
    private function nullIfBlank($value) {
        if ($value === null) {
            return null;
        }

        $value = trim((string)$value);
        return $value === '' ? null : $value;
    }

    private function getMraServerDateTime($terminal) {
        if (!$terminal) {
            return null;
        }
        try {
            $baseUrl = rtrim((string)($terminal['api_base_url'] ?? ''), '/');
            $response = $this->postJsonWithAuthorization(
                $baseUrl . '/utilities/ping',
                [],
                $terminal
            );
            if ($response['status'] >= 200 && $response['status'] < 300) {
                $decoded = json_decode($response['body'], true);
                $dt = is_array($decoded)
                    ? ($decoded['data']['serverDate']
                        ?? $decoded['data']['dateTime']
                        ?? $decoded['data']['serverDateTime']
                        ?? $decoded['data']['currentDateTime']
                        ?? $decoded['serverDate']
                        ?? $decoded['serverDateTime']
                        ?? $decoded['dateTime']
                        ?? $decoded['currentDateTime']
                        ?? null)
                    : null;
                if (is_string($dt) && trim($dt) !== '') {
                    try {
                        $normalized = (new DateTimeImmutable($dt))->format('Y-m-d H:i:s');
                        $this->conn->prepare(
                            'UPDATE mra_eis_terminals SET last_server_datetime = :server_datetime, last_server_sync_at = NOW() WHERE id = :id'
                        )->execute([':server_datetime' => $normalized, ':id' => (int)$terminal['id']]);
                    } catch (Throwable $e) {
                        error_log('MRA EIS server datetime persist failed: ' . $e->getMessage());
                    }
                    return trim($dt);
                }
            }
        } catch (Throwable $e) {
            error_log('MRA EIS server datetime fetch failed: ' . $e->getMessage());
        }
        return null;
    }

    private function terminalVatRegistrationStatus(?array $terminal) {
        if (!$terminal) {
            return null;
        }
        $stmt = $this->conn->prepare('SELECT taxpayer_configuration_json FROM mra_eis_configurations WHERE terminal_local_id = :terminal_id LIMIT 1');
        $stmt->execute([':terminal_id' => (int)$terminal['id']]);
        $stored = json_decode((string)$stmt->fetchColumn(), true);
        if (is_array($stored) && array_key_exists('isVATRegistered', $stored)) {
            return $this->eisBoolean($stored['isVATRegistered']);
        }
        $payloads = [
            $terminal['latest_config_response'] ?? '',
            $terminal['confirmation_response'] ?? '',
            $terminal['activation_response'] ?? '',
        ];
        foreach ($payloads as $raw) {
            $decoded = json_decode((string)$raw, true);
            if (!is_array($decoded)) {
                continue;
            }
            $isRegistered = $decoded['data']['configuration']['taxpayerConfiguration']['isVATRegistered']
                ?? $decoded['configuration']['taxpayerConfiguration']['isVATRegistered']
                ?? $decoded['data']['taxpayerConfiguration']['isVATRegistered']
                ?? $decoded['taxpayerConfiguration']['isVATRegistered']
                ?? null;
            if ($isRegistered !== null) {
                return $this->eisBoolean($isRegistered);
            }
        }
        return null;
    }

    private function terminalLevyRateMap(?array $terminal) {
        if (!$terminal) {
            return [];
        }
        $stmt = $this->conn->prepare('SELECT levy_rates_json FROM mra_eis_configurations WHERE terminal_local_id = :terminal_id LIMIT 1');
        $stmt->execute([':terminal_id' => (int)$terminal['id']]);
        $storedRates = json_decode((string)$stmt->fetchColumn(), true);
        $storedMap = $this->rateMap(is_array($storedRates) ? $storedRates : []);
        if (!empty($storedMap)) {
            return $storedMap;
        }
        $payloads = [
            $terminal['latest_config_response'] ?? '',
            $terminal['confirmation_response'] ?? '',
            $terminal['activation_response'] ?? '',
        ];
        foreach ($payloads as $raw) {
            $decoded = json_decode((string)$raw, true);
            if (!is_array($decoded)) {
                continue;
            }
            $globalCfg = $decoded['data']['configuration']['globalConfiguration']
                ?? $decoded['configuration']['globalConfiguration']
                ?? $decoded['data']['globalConfiguration']
                ?? [];
            $levyRates = $globalCfg['levyRates']
                ?? $globalCfg['levyrates']
                ?? $decoded['data']['levyRates']
                ?? $decoded['data']['levyrates']
                ?? [];
            if (!is_array($levyRates) || empty($levyRates)) {
                continue;
            }
            $map = [];
            foreach ($levyRates as $rate) {
                if (!is_array($rate) || !isset($rate['id'])) {
                    continue;
                }
                $map[strtoupper((string)$rate['id'])] = is_numeric($rate['rate'] ?? null) ? (float)$rate['rate'] : 0.0;
            }
            if (!empty($map)) {
                return $map;
            }
        }
        return [];
    }

    private function levyComponent($grossAmount, $levyRateId, array $levyRateMap = []) {
        $levyRateId = strtoupper(trim((string)$levyRateId));
        if ($levyRateId === '' || $levyRateId === 'NONE' || $levyRateId === 'LEVY_ZERO') {
            return 0.0;
        }
        $rate = $levyRateMap[$levyRateId] ?? null;
        if ($rate === null && preg_match('/(\d+(?:\.\d+)?)/', $levyRateId, $matches)) {
            $rate = (float)$matches[1];
        }
        if ($rate !== null && $rate > 0) {
            return round(((float)$grossAmount * $rate) / (100 + $rate), 2);
        }
        return 0.0;
    }

    private function applyConfigVersionsFromResponse($decoded, $terminalId) {
        if (!is_array($decoded) || !$terminalId) {
            return;
        }
        $config = $decoded['data']['configuration'] ?? $decoded['configuration'] ?? [];
        if (!is_array($config)) {
            $config = [];
        }
        $globalVersion = $config['globalConfiguration']['versionNo']
            ?? $config['globalConfiguration']['version']
            ?? $decoded['data']['globalConfigVersion']
            ?? $decoded['globalConfigVersion']
            ?? null;
        $taxpayerVersion = $config['taxpayerConfiguration']['versionNo']
            ?? $config['taxpayerConfiguration']['version']
            ?? $decoded['data']['taxpayerConfigVersion']
            ?? $decoded['taxpayerConfigVersion']
            ?? null;
        $terminalVersion = $config['terminalConfiguration']['versionNo']
            ?? $config['terminalConfiguration']['version']
            ?? $decoded['data']['terminalConfigVersion']
            ?? $decoded['terminalConfigVersion']
            ?? null;

        $updates = [];
        $params = [':id' => (int)$terminalId];
        if ($globalVersion !== null && is_numeric($globalVersion)) {
            $updates[] = 'global_config_version = :global_config_version';
            $params[':global_config_version'] = (int)$globalVersion;
        }
        if ($taxpayerVersion !== null && is_numeric($taxpayerVersion)) {
            $updates[] = 'taxpayer_config_version = :taxpayer_config_version';
            $params[':taxpayer_config_version'] = (int)$taxpayerVersion;
        }
        if ($terminalVersion !== null && is_numeric($terminalVersion)) {
            $updates[] = 'terminal_config_version = :terminal_config_version';
            $params[':terminal_config_version'] = (int)$terminalVersion;
        }
        if (!empty($updates)) {
            try {
                $this->conn->prepare(
                    'UPDATE mra_eis_terminals SET ' . implode(', ', $updates) . ' WHERE id = :id'
                )->execute($params);
            } catch (Throwable $e) {
                error_log('MRA EIS config version update failed: ' . $e->getMessage());
            }
        }
        $shouldDownload = $this->eisBoolean($decoded['data']['shouldDownloadLatestConfig'] ?? false)
            || $this->eisBoolean($decoded['shouldDownloadLatestConfig'] ?? false);
        if ($shouldDownload) {
            $terminal = $this->terminalById($terminalId);
            if ($terminal) {
                try {
                    $this->downloadLatestConfiguration($terminal);
                } catch (Throwable $e) {
                    error_log('MRA EIS latest configuration refresh failed: ' . $e->getMessage());
                }
            }
        }
    }

    private function applyTerminalBlockFromResponse($decoded, $terminalId) {
        if (!is_array($decoded) || !$terminalId) {
            return false;
        }
        $data = is_array($decoded['data'] ?? null) ? $decoded['data'] : [];
        $shouldBlock = $this->eisBoolean($data['shouldBlockTerminal'] ?? false)
            || $this->eisBoolean($decoded['shouldBlockTerminal'] ?? false);
        $isBlocked = $shouldBlock
            || $this->eisBoolean($data['terminalBlocked'] ?? false)
            || $this->eisBoolean($data['isTerminalBlocked'] ?? false)
            || $this->eisBoolean($decoded['terminalBlocked'] ?? false)
            || (is_string($data['terminalStatus'] ?? null) && strtoupper((string)$data['terminalStatus']) === 'BLOCKED');
        if ($isBlocked) {
            $terminal = $this->terminalById($terminalId);
            $reason = 'Terminal blocked by MRA EIS.';
            if ($terminal) {
                try {
                    $block = $this->fetchTerminalBlock($terminal);
                    $reason = $block['reason'] ?: $reason;
                    // An explicit transaction-response block is authoritative.
                    // A failed or stale detail lookup must never clear it.
                    $isBlocked = $isBlocked || $block['blocked'];
                } catch (Throwable $e) {
                    error_log('MRA EIS terminal block detail fetch failed: ' . $e->getMessage());
                }
            }
            try {
                $this->conn->prepare(
                    'UPDATE mra_eis_terminals SET is_blocked = :is_blocked, block_reason = :reason, blocked_at = CASE WHEN :is_blocked = 1 THEN NOW() ELSE NULL END WHERE id = :id'
                )->execute([':is_blocked' => $isBlocked ? 1 : 0, ':reason' => $reason, ':id' => (int)$terminalId]);
            } catch (Throwable $e) {
                error_log('MRA EIS terminal block persist failed: ' . $e->getMessage());
            }
        }
        return $isBlocked;
    }

    private function downloadLatestConfiguration(array $terminal) {
        $response = $this->postJsonWithAuthorization(
            rtrim((string)$terminal['api_base_url'], '/') . '/configuration/get-latest-configs',
            [],
            $terminal
        );
        if ($response['status'] < 200 || $response['status'] >= 300) {
            throw new RuntimeException('MRA latest configuration download failed: ' . $this->friendlySubmissionError($response));
        }
        $decoded = json_decode((string)$response['body'], true);
        if (!is_array($decoded)) {
            throw new RuntimeException('MRA latest configuration response was invalid JSON.');
        }
        $configuration = $decoded['data']['configuration'] ?? $decoded['data'] ?? $decoded['configuration'] ?? null;
        if (!is_array($configuration)) {
            throw new RuntimeException('MRA latest configuration response did not contain configuration data.');
        }
        $globalVersion = (int)($configuration['globalConfiguration']['versionNo'] ?? 1);
        $taxpayerVersion = (int)($configuration['taxpayerConfiguration']['versionNo'] ?? 1);
        $terminalVersion = (int)($configuration['terminalConfiguration']['versionNo'] ?? 1);
        $siteId = trim((string)($configuration['terminalConfiguration']['terminalSite']['siteId'] ?? $terminal['site_id'] ?? ''));
        $stored = ['data' => ['configuration' => $configuration]];
        $this->conn->prepare(
            'UPDATE mra_eis_terminals SET latest_config_response = :response, global_config_version = :global_version, '
            . 'taxpayer_config_version = :taxpayer_version, terminal_config_version = :terminal_version, site_id = :site_id WHERE id = :id'
        )->execute([
            ':response' => json_encode($stored, JSON_UNESCAPED_SLASHES),
            ':global_version' => max(1, $globalVersion),
            ':taxpayer_version' => max(1, $taxpayerVersion),
            ':terminal_version' => max(1, $terminalVersion),
            ':site_id' => $siteId !== '' ? $siteId : null,
            ':id' => (int)$terminal['id'],
        ]);
        $changed = $this->persistMraConfiguration((int)$terminal['id'], $configuration, $stored);
        $this->vatPercentage = $this->settings->vatPercentage();
        return ['configuration' => $configuration, 'changed' => $changed];
    }

    private function fetchTerminalBlock(array $terminal) {
        $response = $this->postJsonWithAuthorization(
            rtrim((string)$terminal['api_base_url'], '/') . '/utilities/get-terminal-blocking-message',
            ['terminalId' => (string)$terminal['mra_terminal_id']],
            $terminal
        );
        $decoded = json_decode((string)$response['body'], true);
        $data = is_array($decoded['data'] ?? null) ? $decoded['data'] : [];
        return [
            'blocked' => $response['status'] >= 200 && $response['status'] < 300 && $this->eisBoolean($data['isBlocked'] ?? false),
            'reason' => trim((string)($data['blockingReason'] ?? '')),
        ];
    }

    private function assertTerminalNotBlocked(array $terminal) {
        $terminal = $this->resolveTerminalBlock($terminal);
        if (!empty($terminal['is_blocked'])) {
            throw new RuntimeException('MRA EIS terminal is blocked. ' . trim((string)($terminal['block_reason'] ?? 'Contact MRA for assistance.')));
        }
    }

    /**
     * Re-checks a terminal's block status with MRA and persists the result, without throwing.
     * terminalForBranch()/terminalById() exclude is_blocked=1 rows entirely, so once a terminal is
     * blocked it becomes invisible to every submission path (transfers, stock adjustments, offline
     * sale resubmission) and assertTerminalNotBlocked() - the only self-heal check - never gets a
     * terminal row to run against. This is the admin-facing escape hatch: it fetches the terminal
     * unfiltered (terminalByIdRaw) so a blocked row can still be found and checked.
     */
    public function refreshTerminalBlockStatus($terminalId) {
        $terminal = $this->terminalByIdRaw($terminalId);
        if (!$terminal) {
            throw new RuntimeException('Terminal not found.');
        }
        if (trim((string)$terminal['jwt_token']) === '' || trim((string)($terminal['mra_terminal_id'] ?? '')) === '') {
            throw new RuntimeException('Terminal has no MRA credentials yet - complete activation first.');
        }
        return $this->resolveTerminalBlock($terminal);
    }

    /** Re-downloads /configuration/get-latest-configs for one terminal and returns the parsed summary (used by the EIS Configuration admin page's manual "Refresh" action). */
    public function refreshLatestConfiguration($terminalId) {
        $terminal = $this->terminalByIdRaw($terminalId);
        if (!$terminal) {
            throw new RuntimeException('Terminal not found.');
        }
        if (trim((string)$terminal['jwt_token']) === '' || trim((string)($terminal['mra_terminal_id'] ?? '')) === '') {
            throw new RuntimeException('Terminal has no MRA credentials yet - complete activation and confirmation first.');
        }
        $this->downloadLatestConfiguration($terminal);
        return $this->configurationSummary($terminalId);
    }

    /** Default staleness window for the automatic (no-button) configuration sync below. */
    const AUTO_CONFIG_SYNC_INTERVAL_HOURS = 6;

    /**
     * Opportunistic, no-button config refresh: re-downloads /configuration/get-latest-configs
     * for every activated terminal whose last sync is older than $intervalHours (or has never
     * synced), applies the result to that terminal, and raises one admin dashboard notification
     * summarizing whichever terminals actually changed. Cheap to call on every superadmin
     * dashboard load - terminals that are still fresh are skipped without an HTTP call, so the
     * only cost on a "nothing due" request is a single lightweight query.
     */
    public function syncAllTerminalConfigurations($intervalHours = null) {
        $intervalHours = $intervalHours !== null ? (float)$intervalHours : self::AUTO_CONFIG_SYNC_INTERVAL_HOURS;
        $cutoff = time() - (int)round($intervalHours * 3600);

        $stmt = $this->conn->prepare("
            SELECT met.*, b.branch_name, c.mra_synced_at
            FROM mra_eis_terminals met
            LEFT JOIN branches b ON b.branch_id = met.branch_id
            LEFT JOIN mra_eis_configurations c ON c.terminal_local_id = met.id
            WHERE met.jwt_token IS NOT NULL AND met.jwt_token != ''
              AND met.mra_terminal_id IS NOT NULL AND met.mra_terminal_id != ''
        ");
        $stmt->execute();
        $terminals = $stmt->fetchAll(PDO::FETCH_ASSOC);

        $checked = 0;
        $changedTerminals = [];
        $errors = [];

        foreach ($terminals as $terminal) {
            $lastSync = $terminal['mra_synced_at'] ?? null;
            if ($lastSync && strtotime($lastSync) > $cutoff) {
                continue;
            }
            $checked++;
            try {
                $result = $this->downloadLatestConfiguration($terminal);
                if (!empty($result['changed'])) {
                    $changedTerminals[] = $terminal;
                    $this->event(null, 'configuration', 'terminal:' . $terminal['id'], 'auto_config_sync_changed', 'Automatic sync downloaded a new MRA EIS configuration.', null, null);
                }
            } catch (Throwable $e) {
                $errors[] = trim(($terminal['terminal_label'] ?? '') . ' #' . $terminal['id']) . ': ' . $e->getMessage();
                error_log('MRA EIS automatic configuration sync failed for terminal ' . $terminal['id'] . ': ' . $e->getMessage());
            }
        }

        if (!empty($changedTerminals)) {
            $this->notifyAdminsOfConfigChange($changedTerminals);
        }

        return [
            'total' => count($terminals),
            'checked' => $checked,
            'changed' => count($changedTerminals),
            'errors' => $errors,
        ];
    }

    /** Raises one deduped Admin dashboard notification listing every terminal an automatic sync just updated. */
    private function notifyAdminsOfConfigChange(array $changedTerminals) {
        require_once __DIR__ . '/Notifications.php';
        $notifications = new Notifications($this->conn);

        $labels = array_map(function ($terminal) {
            $label = trim((string)($terminal['terminal_label'] ?? '')) ?: ('Terminal #' . $terminal['id']);
            $branchName = trim((string)($terminal['branch_name'] ?? ''));
            return $branchName !== '' ? "{$label} ({$branchName})" : $label;
        }, $changedTerminals);

        $vat = $this->settings->vatPercentage();
        $message = count($labels) === 1
            ? "MRA EIS configuration updated automatically for {$labels[0]}. Standard VAT is now " . number_format($vat, 2) . '%.'
            : 'MRA EIS configuration updated automatically for ' . count($labels) . ' terminal(s): ' . implode(', ', $labels) . '. Standard VAT is now ' . number_format($vat, 2) . '%.';

        $branchId = count($changedTerminals) === 1 ? (int)($changedTerminals[0]['branch_id'] ?: 7) : 7;
        $notifications->createNotification($branchId, $message, 'Admin', 0, true);
    }

    /**
     * Parses a terminal's stored latest_config_response into the taxpayer/global/terminal
     * configuration groupings the EIS Configuration admin page displays. Field casing between
     * globalConfiguration.taxRates/taxrates has been inconsistent across responses (see
     * terminalTaxRateMap()/terminalLevyRateMap() above), so both are checked here too.
     */
    public function configurationSummary($terminalId) {
        $terminal = $this->terminalByIdRaw($terminalId);
        if (!$terminal) {
            throw new RuntimeException('Terminal not found.');
        }

        $configuration = [];
        $storedStmt = $this->conn->prepare(
            'SELECT global_configuration_json, taxpayer_configuration_json, terminal_configuration_json '
            . 'FROM mra_eis_configurations WHERE terminal_local_id = :terminal_id LIMIT 1'
        );
        $storedStmt->execute([':terminal_id' => (int)$terminalId]);
        $stored = $storedStmt->fetch(PDO::FETCH_ASSOC);
        if ($stored) {
            $configuration = [
                'globalConfiguration' => json_decode((string)$stored['global_configuration_json'], true) ?: [],
                'taxpayerConfiguration' => json_decode((string)$stored['taxpayer_configuration_json'], true) ?: [],
                'terminalConfiguration' => json_decode((string)$stored['terminal_configuration_json'], true) ?: [],
            ];
        }
        $raw = trim((string)($terminal['latest_config_response'] ?? ''));
        if (empty($configuration) && $raw !== '') {
            $decoded = json_decode($raw, true);
            if (is_array($decoded)) {
                $configuration = $decoded['data']['configuration'] ?? $decoded['configuration'] ?? [];
                if (!is_array($configuration)) {
                    $configuration = [];
                }
            }
        }

        $globalCfg = is_array($configuration['globalConfiguration'] ?? null) ? $configuration['globalConfiguration'] : [];
        $taxpayerCfg = is_array($configuration['taxpayerConfiguration'] ?? null) ? $configuration['taxpayerConfiguration'] : [];
        $terminalCfg = is_array($configuration['terminalConfiguration'] ?? null) ? $configuration['terminalConfiguration'] : [];

        $taxRates = $globalCfg['taxRates'] ?? $globalCfg['taxrates'] ?? [];
        $levyRates = $globalCfg['levyRates'] ?? $globalCfg['levyrates'] ?? [];

        return [
            'terminal' => $terminal,
            'has_downloaded_config' => !empty($configuration),
            'taxpayer' => [
                'taxpayer_id' => $taxpayerCfg['taxpayerId'] ?? null,
                'taxpayer_name' => $taxpayerCfg['taxpayerName'] ?? null,
                'taxpayer_tin' => $taxpayerCfg['taxpayerTIN'] ?? $terminal['taxpayer_tin'] ?? null,
                'site_id' => $terminal['site_id'] ?? null,
                'is_vat_registered' => array_key_exists('isVATRegistered', $taxpayerCfg)
                    ? $this->eisBoolean($taxpayerCfg['isVATRegistered'])
                    : null,
                'activated_tax_rates' => is_array($taxpayerCfg['activatedTaxRates'] ?? null) ? $taxpayerCfg['activatedTaxRates'] : [],
                'activated_levies' => is_array($taxpayerCfg['activatedLevies'] ?? null) ? $taxpayerCfg['activatedLevies'] : [],
                'config_version' => (int)($taxpayerCfg['taxpayerConfigVersion'] ?? $terminal['taxpayer_config_version'] ?? 1),
            ],
            'global' => [
                'config_version' => (int)($globalCfg['globalConfigVersion'] ?? $terminal['global_config_version'] ?? 1),
                'tax_rates' => is_array($taxRates) ? $taxRates : [],
                'levy_rates' => is_array($levyRates) ? $levyRates : [],
            ],
            'terminal_config' => [
                'config_version' => (int)($terminalCfg['terminalConfigVersion'] ?? $terminal['terminal_config_version'] ?? 1),
                'terminal_id' => $terminalCfg['terminalId'] ?? $terminal['mra_terminal_id'] ?? null,
                'offline_limit' => is_array($terminalCfg['offlineLimit'] ?? null) ? $terminalCfg['offlineLimit'] : null,
            ],
        ];
    }

    private function terminalByIdRaw($terminalId) {
        if (!$terminalId) {
            return null;
        }
        $stmt = $this->conn->prepare('SELECT * FROM mra_eis_terminals WHERE id = :id LIMIT 1');
        $stmt->execute([':id' => (int)$terminalId]);
        return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
    }

    private function resolveTerminalBlock(array $terminal) {
        if (!empty($terminal['is_blocked'])) {
            $response = $this->postJsonWithAuthorization(
                rtrim((string)$terminal['api_base_url'], '/') . '/utilities/check-terminal-unblock-status',
                ['terminalId' => (string)$terminal['mra_terminal_id']],
                $terminal
            );
            $decoded = json_decode((string)$response['body'], true);
            $isUnblocked = $this->eisBoolean($decoded['data']['isUnblocked'] ?? false);
            if ($isUnblocked) {
                $this->conn->prepare(
                    'UPDATE mra_eis_terminals SET is_blocked = 0, block_reason = NULL, blocked_at = NULL WHERE id = :id'
                )->execute([':id' => (int)$terminal['id']]);
                $terminal['is_blocked'] = 0;
                $terminal['block_reason'] = null;
            }
        }
        if (empty($terminal['is_blocked'])) {
            $block = $this->fetchTerminalBlock($terminal);
            if ($block['blocked']) {
                $this->conn->prepare(
                    'UPDATE mra_eis_terminals SET is_blocked = 1, block_reason = :reason, blocked_at = NOW() WHERE id = :id'
                )->execute([':reason' => $block['reason'], ':id' => (int)$terminal['id']]);
                $terminal['is_blocked'] = 1;
                $terminal['block_reason'] = $block['reason'];
            }
        }
        return $terminal;
    }

    /**
     * DB-only production-readiness checks for the admin Settings page. No outbound HTTP calls —
     * safe to run on every page load. Each check is 'pass' (green), 'warn' (yellow, review before
     * going live), or 'fail' (red, blocks correct production operation).
     */
    public function productionReadinessChecklist() {
        $checks = [];

        $environment = strtolower(trim((string)$this->settings->get('eis_environment', 'sandbox')));
        $checks[] = [
            'key' => 'environment',
            'label' => 'EIS environment set to Production',
            'status' => $environment === 'production' ? 'pass' : 'warn',
            'detail' => $environment === 'production'
                ? 'Environment is set to Production — all EIS calls go to the live MRA API.'
                : 'Currently set to Sandbox. Flip to Production only after every other check below passes.',
        ];

        $eisEnabled = (bool)$this->settings->eisEnabled();
        $checks[] = [
            'key' => 'eis_enabled',
            'label' => 'MRA EIS enabled',
            'status' => $eisEnabled ? 'pass' : 'fail',
            'detail' => $eisEnabled
                ? 'EIS submission is enabled.'
                : 'EIS is disabled — sales are not being submitted to MRA at all.',
        ];

        $offlineMode = (bool)$this->settings->eisOfflineMode();
        $checks[] = [
            'key' => 'offline_mode',
            'label' => 'Offline mode is off',
            'status' => $offlineMode ? 'warn' : 'pass',
            'detail' => $offlineMode
                ? 'EIS Offline is switched on (since ' . ($this->settings->eisOfflineSince() ?? 'unknown') . '). Connectivity to MRA is deliberately cut — fine for planned downtime, not for normal production operation.'
                : 'Offline mode is off; sales submit live to MRA.',
        ];

        $vatMeta = $this->settings->settingMetadata('vat_percentage');
        $vatSource = strtolower(trim((string)($vatMeta['source'] ?? '')));
        $checks[] = [
            'key' => 'vat_source',
            'label' => 'VAT rate confirmed by MRA',
            'status' => $vatSource === 'mra' ? 'pass' : 'warn',
            'detail' => $vatSource === 'mra'
                ? 'Standard VAT (' . rtrim(rtrim(number_format($this->settings->vatPercentage(), 2), '0'), '.') . '%) was supplied by MRA configuration and is locked from manual edits.'
                : 'VAT percentage has not been confirmed by a downloaded MRA configuration yet — it is still a manually-set value.',
        ];

        $terminals = [];
        try {
            $terminals = $this->conn->query(
                "SELECT * FROM mra_eis_terminals ORDER BY (environment = 'production') DESC, updated_at DESC"
            )->fetchAll(PDO::FETCH_ASSOC);
        } catch (Throwable $e) {
            $terminals = [];
        }

        $prodActivated = array_values(array_filter($terminals, function ($t) {
            return strtolower((string)$t['status']) === 'activated'
                && strtolower((string)$t['environment']) === 'production'
                && trim((string)$t['jwt_token']) !== ''
                && empty($t['is_blocked']);
        }));
        $checks[] = [
            'key' => 'production_terminal',
            'label' => 'At least one Production terminal activated',
            'status' => !empty($prodActivated) ? 'pass' : 'fail',
            'detail' => !empty($prodActivated)
                ? count($prodActivated) . ' production terminal(s) activated and unblocked: '
                    . implode(', ', array_map(function ($t) { return $t['terminal_label']; }, $prodActivated)) . '.'
                : 'No activated, unblocked terminal is configured for the Production environment yet. Activate one on the Terminal Activation page.',
        ];

        $blocked = array_values(array_filter($terminals, function ($t) { return !empty($t['is_blocked']); }));
        $checks[] = [
            'key' => 'no_blocked_terminals',
            'label' => 'No blocked terminals',
            'status' => empty($blocked) ? 'pass' : 'fail',
            'detail' => empty($blocked)
                ? 'No terminal is currently blocked by MRA.'
                : count($blocked) . ' terminal(s) blocked: ' . implode(', ', array_map(function ($t) {
                    return $t['terminal_label'] . ' (' . ($t['block_reason'] ?: 'no reason given') . ')';
                }, $blocked)) . '.',
        ];

        $terminalIds = array_map(function ($t) { return (int)$t['id']; }, $terminals);
        $staleConfigs = [];
        $configuredCount = 0;
        if (!empty($terminalIds)) {
            $placeholders = implode(',', array_fill(0, count($terminalIds), '?'));
            try {
                $configStmt = $this->conn->prepare(
                    "SELECT c.terminal_local_id, c.mra_synced_at, c.eis_connection_status, t.terminal_label, t.environment
                     FROM mra_eis_configurations c
                     INNER JOIN mra_eis_terminals t ON t.id = c.terminal_local_id
                     WHERE c.terminal_local_id IN ($placeholders)"
                );
                $configStmt->execute($terminalIds);
                $configs = $configStmt->fetchAll(PDO::FETCH_ASSOC);
                $configuredCount = count($configs);
                foreach ($configs as $cfg) {
                    $syncedAt = $cfg['mra_synced_at'] ? strtotime($cfg['mra_synced_at']) : false;
                    $ageDays = $syncedAt ? (time() - $syncedAt) / 86400 : null;
                    if ($ageDays === null || $ageDays > 7) {
                        $staleConfigs[] = $cfg['terminal_label'] . ' (' . ($cfg['mra_synced_at'] ?? 'never') . ')';
                    }
                }
            } catch (Throwable $e) {
                // leave as unknown below
            }
        }
        $checks[] = [
            'key' => 'config_sync',
            'label' => 'Terminal configuration synced recently',
            'status' => empty($terminalIds) ? 'fail' : (empty($staleConfigs) && $configuredCount > 0 ? 'pass' : 'warn'),
            'detail' => empty($terminalIds)
                ? 'No terminal exists to check configuration sync for.'
                : (empty($staleConfigs) && $configuredCount > 0
                    ? 'All ' . $configuredCount . ' terminal(s) have a configuration downloaded from MRA within the last 7 days.'
                    : (empty($staleConfigs)
                        ? 'No terminal has ever downloaded a configuration from MRA yet.'
                        : count($staleConfigs) . ' terminal(s) have a stale or missing configuration sync: ' . implode(', ', $staleConfigs) . '.')),
        ];

        $failedOutbox = 0;
        try {
            $failedOutbox = (int)$this->conn->query(
                "SELECT COUNT(*) FROM mra_eis_outbox WHERE status = 'failed'"
            )->fetchColumn();
        } catch (Throwable $e) {
            $failedOutbox = 0;
        }
        $checks[] = [
            'key' => 'outbox_failures',
            'label' => 'No permanently failed outbox submissions',
            'status' => $failedOutbox === 0 ? 'pass' : 'warn',
            'detail' => $failedOutbox === 0
                ? 'No outbox entries are stuck in a failed state.'
                : $failedOutbox . ' outbox entr' . ($failedOutbox === 1 ? 'y is' : 'ies are') . ' marked failed — review them on the MRA EIS Compliance page.',
        ];

        $backlog = 0;
        try {
            $backlog = (int)$this->conn->query(
                "SELECT COUNT(*) FROM mra_eis_outbox WHERE status IN ('pending','ready') AND created_at < (NOW() - INTERVAL 24 HOUR)"
            )->fetchColumn();
        } catch (Throwable $e) {
            $backlog = 0;
        }
        $checks[] = [
            'key' => 'outbox_backlog',
            'label' => 'No stale unsent submissions',
            'status' => $backlog === 0 ? 'pass' : 'warn',
            'detail' => $backlog === 0
                ? 'No submission has been queued for more than 24 hours without reaching MRA.'
                : $backlog . ' submission(s) have been queued for over 24 hours without reaching MRA — check connectivity or run the outbox sync.',
        ];

        $siteProductCount = 0;
        try {
            $siteProductCount = (int)$this->conn->query('SELECT COUNT(*) FROM mra_eis_site_products')->fetchColumn();
        } catch (Throwable $e) {
            $siteProductCount = 0;
        }
        $checks[] = [
            'key' => 'site_products',
            'label' => 'Site products downloaded',
            'status' => $siteProductCount > 0 ? 'pass' : 'warn',
            'detail' => $siteProductCount > 0
                ? $siteProductCount . ' site product(s) synced from MRA.'
                : 'No site products have been downloaded from MRA yet — sync them on the Onboarding page.',
        ];

        $summary = ['pass' => 0, 'warn' => 0, 'fail' => 0];
        foreach ($checks as $check) {
            $summary[$check['status']]++;
        }

        return [
            'checks' => $checks,
            'summary' => $summary,
            'ready' => $summary['fail'] === 0,
        ];
    }
}
