<?php
/**
 * (c) ININOUT & PYXIS. All Rights Reserved.
 * Wszelkie modyfikacje tego pliku wymagaja zgloszenia oraz zgody autorow.
 * Any modifications to this file require notification and consent of the authors.
 */
/**
 * i-BOK 5 - MySQL Data Provider (PYXIS database)
 *
 * Funkcje db_read_*() — czytają dane z widoków PYXIS (vUserdb, vFvLista, vOplatyLista, vFvkLista, vMacLista, itd.)
 * Zwracają identyczne struktury jak vbk_read_*() — transparentna zamiana backendu.
 *
 * UWAGA: Dane w bazie PYXIS są zarządzane przez system PYXIS.
 *        ibok5 tylko CZYTA dane — nie zapisuje do tabel PYXIS.
 */

require_once __DIR__ . '/db_config.php';

// ============================================
// HELPERY
// ============================================

/**
 * Bezpieczny string (NULL → '')
 */
function db_str($val) {
    return $val === null ? '' : (string)$val;
}

/**
 * Format daty (NULL/0000-00-00 → '')
 */
function db_format_date($val) {
    if ($val === null || $val === '' || $val === '0000-00-00') return '';
    return (string)$val;
}

/**
 * Format kwoty DECIMAL → "65,00 zł" (format kompatybilny z parse_kwota())
 */
function db_format_kwota($val) {
    if ($val === null) return '0,00 zł';
    $num = (float)$val;
    return number_format(abs($num), 2, ',', '') . ' zł';
}

/**
 * Format kwoty z zachowaniem znaku (dla korekt, które mogą być ujemne)
 */
function db_format_kwota_signed($val) {
    if ($val === null) return '0,00 zł';
    $num = (float)$val;
    $sign = $num < 0 ? '-' : '';
    return $sign . number_format(abs($num), 2, ',', '') . ' zł';
}

/**
 * Oblicz status faktury/korekty z kwot PYXIS
 *
 * @param float $kwota_rozliczona Kwota rozliczona
 * @param float $kwota_do_rozliczenia Kwota do rozliczenia
 * @return string Status: ROZLICZONA, CZĘŚCIOWO ROZLICZONA, NIEROZLICZONA
 */
function db_compute_invoice_status($kwota_rozliczona, $kwota_do_rozliczenia) {
    $rozl = (float)$kwota_rozliczona;
    $do_rozl = (float)$kwota_do_rozliczenia;

    if ($do_rozl <= 0.005) {
        return 'ROZLICZONA';
    }
    if ($rozl > 0.005) {
        return 'CZĘŚCIOWO ROZLICZONA';
    }
    return 'NIEROZLICZONA';
}

// ============================================
// READ FUNCTIONS (odpowiedniki vbk_read_*)
// ============================================

/**
 * Czytaj klientów z PYXIS vUserdb — zwraca mapę [id => dane]
 * Odpowiednik vbk_read_klienci()
 */
function db_read_klienci() {
    $pdo = db_get_pdo();
    if (!$pdo) return [];

    try {
        $stmt = $pdo->query(
            'SELECT U_NRKLIENTA, U_LOGIN, U_HASLO, U_NAZWISKO, U_IMIE,
                    U_NRRACHUNKU1, U_NRRACHUNKU2, U_SALDO, U_SALDOZALEGLE,
                    U_DATAAKTYWACJI, U_DATAKONCAUMOWY, U_DATADEAKTYWACJI,
                    U_DATAMODYFIKACJI, U_ZAZNACZ,
                    U_PASMODOWNLOADDZIENTXT, U_PASMOUPLOADDZIENTXT,
                    U_PASMODOWNLOADNOCTXT, U_PASMOUPLOADNOCTXT,
                    U_POWIADAMIAJ, U_BLOKUJ, vU_DataOstatniejWplaty,
                    U_ADR_MIEJSCOWOSC, U_PODMIOT
             FROM vUserdb
             ORDER BY U_NRKLIENTA'
        );
        $rows = $stmt->fetchAll();
        $clients = [];

        foreach ($rows as $row) {
            $id = (string)$row['U_NRKLIENTA'];
            $saldo = (float)$row['U_SALDO'];
            $saldo_zalegle = (float)$row['U_SALDOZALEGLE'];
            $nadplata = $saldo > 0 ? number_format($saldo, 2, ',', '') : '0';
            $deaktywacja = db_str($row['U_DATADEAKTYWACJI']);
            $active = ($deaktywacja === '' || $deaktywacja === '0000-00-00') ? '1' : '0';

            $clients[$id] = [
                'data_aktualizacji' => db_format_date($row['U_DATAMODYFIKACJI']),
                'id'                => $id,
                'login'             => db_str($row['U_LOGIN']),
                'haslo_encoded'     => db_str($row['U_HASLO']),
                'nazwisko'          => trim(db_str($row['U_NAZWISKO']) . ' ' . db_str($row['U_IMIE'])),
                'pole5'             => '',
                'nr_rachunku'       => db_str($row['U_NRRACHUNKU1']),
                'nadplata'          => $nadplata,
                'umowa_od'          => db_format_date($row['U_DATAAKTYWACJI']),
                'umowa_do'          => db_format_date($row['U_DATAKONCAUMOWY']),
                'zalega'            => ($saldo_zalegle > 0) ? '1' : '0',
                'zaznaczony'        => db_str($row['U_ZAZNACZ']),
                'dl_dzien'          => db_str($row['U_PASMODOWNLOADDZIENTXT']),
                'ul_dzien'          => db_str($row['U_PASMOUPLOADDZIENTXT']),
                'dl_noc'            => db_str($row['U_PASMODOWNLOADNOCTXT']),
                'ul_noc'            => db_str($row['U_PASMOUPLOADNOCTXT']),
                'bank_sub'          => db_str($row['U_NRRACHUNKU2']),
                'wg_salda'          => (string)(int)$saldo_zalegle,
                'wg_salda2'         => '0',
                'active_flag'       => $active,
                'saldo_raw'         => number_format(abs($saldo), 2, ',', ''),
                'saldo_zalegle_raw' => number_format(abs($saldo_zalegle), 2, ',', ''),
                'powiadamiaj'       => db_str($row['U_POWIADAMIAJ'] ?? ''),
                'blokuj'            => db_str($row['U_BLOKUJ'] ?? ''),
                'data_ostatniej_wplaty' => db_format_date($row['vU_DataOstatniejWplaty'] ?? null),
                'miejscowosc'       => db_str($row['U_ADR_MIEJSCOWOSC'] ?? ''),
                'podmiot'           => (int)($row['U_PODMIOT'] ?? 0),
            ];
        }

        return $clients;
    } catch (PDOException $e) {
        ibok_log("db_read_klienci error: " . $e->getMessage(), 'error');
        return [];
    }
}

/**
 * Znajdź klienta po loginie — odpowiednik vbk_find_client_by_login()
 */
function db_find_client_by_login($login) {
    $pdo = db_get_pdo();
    if (!$pdo) return null;

    try {
        $stmt = $pdo->prepare(
            'SELECT U_NRKLIENTA, U_LOGIN, U_HASLO, U_NAZWISKO, U_IMIE,
                    U_NRRACHUNKU1, U_NRRACHUNKU2, U_SALDO, U_SALDOZALEGLE,
                    U_DATAAKTYWACJI, U_DATAKONCAUMOWY, U_DATADEAKTYWACJI,
                    U_DATAMODYFIKACJI, U_ZAZNACZ,
                    U_PASMODOWNLOADDZIENTXT, U_PASMOUPLOADDZIENTXT,
                    U_PASMODOWNLOADNOCTXT, U_PASMOUPLOADNOCTXT,
                    U_POWIADAMIAJ, U_BLOKUJ, vU_DataOstatniejWplaty,
                    U_PODMIOT
             FROM vUserdb
             WHERE LOWER(U_LOGIN) = LOWER(?)
             LIMIT 1'
        );
        $stmt->execute([trim($login)]);
        $row = $stmt->fetch();

        if (!$row) return null;

        $id = (string)$row['U_NRKLIENTA'];
        $saldo = (float)$row['U_SALDO'];
        $saldo_zalegle = (float)$row['U_SALDOZALEGLE'];
        $nadplata = $saldo > 0 ? number_format($saldo, 2, ',', '') : '0';
        $deaktywacja = db_str($row['U_DATADEAKTYWACJI']);
        $active = ($deaktywacja === '' || $deaktywacja === '0000-00-00') ? '1' : '0';

        return [
            'data_aktualizacji' => db_format_date($row['U_DATAMODYFIKACJI']),
            'id'                => $id,
            'login'             => db_str($row['U_LOGIN']),
            'haslo_encoded'     => db_str($row['U_HASLO']),
            'nazwisko'          => trim(db_str($row['U_NAZWISKO']) . ' ' . db_str($row['U_IMIE'])),
            'pole5'             => '',
            'nr_rachunku'       => db_str($row['U_NRRACHUNKU1']),
            'nadplata'          => $nadplata,
            'umowa_od'          => db_format_date($row['U_DATAAKTYWACJI']),
            'umowa_do'          => db_format_date($row['U_DATAKONCAUMOWY']),
            'zalega'            => ($saldo_zalegle > 0) ? '1' : '0',
            'zaznaczony'        => db_str($row['U_ZAZNACZ']),
            'dl_dzien'          => db_str($row['U_PASMODOWNLOADDZIENTXT']),
            'ul_dzien'          => db_str($row['U_PASMOUPLOADDZIENTXT']),
            'dl_noc'            => db_str($row['U_PASMODOWNLOADNOCTXT']),
            'ul_noc'            => db_str($row['U_PASMOUPLOADNOCTXT']),
            'bank_sub'          => db_str($row['U_NRRACHUNKU2']),
            'wg_salda'          => (string)(int)$saldo_zalegle,
            'wg_salda2'         => '0',
            'active_flag'       => $active,
            'saldo_raw'         => number_format(abs($saldo), 2, ',', ''),
            'saldo_zalegle_raw' => number_format(abs($saldo_zalegle), 2, ',', ''),
            'powiadamiaj'       => db_str($row['U_POWIADAMIAJ'] ?? ''),
            'blokuj'            => db_str($row['U_BLOKUJ'] ?? ''),
            'data_ostatniej_wplaty' => db_format_date($row['vU_DataOstatniejWplaty'] ?? null),
            'podmiot'           => (int)($row['U_PODMIOT'] ?? 0),
        ];
    } catch (PDOException $e) {
        ibok_log("db_find_client_by_login error: " . $e->getMessage(), 'error');
        return null;
    }
}

/**
 * Czytaj dane firmy z PYXIS Podmioty — odpowiednik vbk_read_firma()
 * @param int $podmiot_id P_INDEX podmiotu (0 = pierwszy aktywny)
 */
function db_read_firma(int $podmiot_id = 0) {
    $pdo = db_get_pdo();
    if (!$pdo) return [];

    try {
        if ($podmiot_id > 0) {
            $stmt = $pdo->prepare(
                'SELECT P_NAZWA1, P_NAZWA2, P_ULICA, P_NRDOMU, P_NRLOKALU,
                        P_KODPOCZTOWY, P_MIEJSCOWOSC, P_NIP, P_REGON,
                        P_BANK, P_NRKONTA, P_TELEFONEMAIL, P_WWW
                 FROM Podmioty
                 WHERE P_INDEX = ? AND P_CZYAKTYWNY = 1
                 LIMIT 1'
            );
            $stmt->execute([$podmiot_id]);
        } else {
            $stmt = $pdo->query(
                'SELECT P_NAZWA1, P_NAZWA2, P_ULICA, P_NRDOMU, P_NRLOKALU,
                        P_KODPOCZTOWY, P_MIEJSCOWOSC, P_NIP, P_REGON,
                        P_BANK, P_NRKONTA, P_TELEFONEMAIL, P_WWW
                 FROM Podmioty
                 WHERE P_CZYAKTYWNY = 1
                 ORDER BY P_INDEX
                 LIMIT 1'
            );
        }
        $row = $stmt->fetch();

        if (!$row) return [];

        $adres1 = trim(db_str($row['P_ULICA']) . ' ' . db_str($row['P_NRDOMU']));
        if (!empty($row['P_NRLOKALU'])) {
            $adres1 .= '/' . db_str($row['P_NRLOKALU']);
        }
        $adres2 = trim(db_str($row['P_KODPOCZTOWY']) . ' ' . db_str($row['P_MIEJSCOWOSC']));

        // P_TELEFONEMAIL zawiera telefon i email razem — rozdziel
        $tel_email_raw = db_str($row['P_TELEFONEMAIL']);
        $firma_telefon = '';
        $firma_email = '';

        if (!empty($tel_email_raw)) {
            // Wyłuskaj wszystkie adresy email z całego tekstu
            $emails = [];
            if (preg_match_all('/\S+@\S+\.\S+/', $tel_email_raw, $m)) {
                $emails = $m[0];
            }
            // Reszta (po usunięciu emaili i oczyszczeniu) = telefon
            $remainder = $tel_email_raw;
            foreach ($emails as $em) {
                $remainder = str_replace($em, '', $remainder);
            }
            // Wyczyść separatory i nadmiarowe spacje
            $remainder = preg_replace('/[,;\r\n]+/', ' ', $remainder);
            $remainder = trim(preg_replace('/\s{2,}/', ' ', $remainder));

            $firma_telefon = $remainder;
            $firma_email = implode(', ', $emails);
        }

        return [
            'nazwa'      => db_str($row['P_NAZWA1']),
            'wlasciciel' => db_str($row['P_NAZWA2']),
            'adres1'     => $adres1,
            'adres2'     => $adres2,
            'nip'        => db_str($row['P_NIP']),
            'regon'      => db_str($row['P_REGON']),
            'bank'       => db_str($row['P_BANK']),
            'nr_konta'   => db_str($row['P_NRKONTA']),
            'telefon'    => $firma_telefon,
            'email'      => $firma_email,
            'pole10'     => '',
            'pole11'     => '',
            'www'        => db_str($row['P_WWW']),
        ];
    } catch (PDOException $e) {
        ibok_log("db_read_firma error: " . $e->getMessage(), 'error');
        return [];
    }
}

/**
 * Czytaj faktury klienta z PYXIS vFvLista — odpowiednik vbk_read_faktury()
 *
 * @param string|int $client_id Numer klienta (U_NRKLIENTA)
 */
function db_read_faktury($client_id) {
    $pdo = db_get_pdo();
    if (!$pdo) return [];

    try {
        $stmt = $pdo->prepare(
            'SELECT FV_NUMERTXT, FV_SUMABRUTTO,
                    FV_KWOTAROZLICZONA, FV_KWOTADOROZLICZENIA,
                    FV_DATAWYSTAWIENIA, FV_DATAPLATNOSCI,
                    FV_NumerPdf, FV_PoTerminie, FV_UWAGI,
                    U_NRKLIENTA
             FROM vFvLista
             WHERE U_NRKLIENTA = ?
             ORDER BY FV_DATAWYSTAWIENIA DESC'
        );
        $stmt->execute([(int)$client_id]);
        $rows = $stmt->fetchAll();
        $faktury = [];

        foreach ($rows as $row) {
            $status = db_compute_invoice_status(
                $row['FV_KWOTAROZLICZONA'],
                $row['FV_KWOTADOROZLICZENIA']
            );
            $dni = $row['FV_PoTerminie'];
            $dni_str = ($dni !== null && $dni > 0) ? (string)(int)$dni : '0';

            $faktury[] = [
                'id_klienta'       => (string)$row['U_NRKLIENTA'],
                'nr_wewnetrzny'    => db_str($row['FV_NUMERTXT']),
                'nr_faktury'       => db_str($row['FV_NUMERTXT']),
                'opis'             => db_str($row['FV_UWAGI']),
                'pdf_filename'     => db_str($row['FV_NumerPdf']),
                'kwota_brutto'     => db_format_kwota($row['FV_SUMABRUTTO']),
                'status'           => $status,
                'dni_po_terminie'  => $dni_str,
                'kwota_do_rozl'    => db_format_kwota($row['FV_KWOTADOROZLICZENIA']),
                'data_wystawienia' => db_format_date($row['FV_DATAWYSTAWIENIA']),
                'termin_platnosci' => db_format_date($row['FV_DATAPLATNOSCI']),
            ];
        }

        return $faktury;
    } catch (PDOException $e) {
        ibok_log("db_read_faktury error: " . $e->getMessage(), 'error');
        return [];
    }
}

/**
 * Czytaj dokumenty klienta (FV + PZ) z PYXIS vListaDokumentow
 *
 * Widok vListaDokumentow łączy faktury (FV) i polecenia zapłaty (PZ)
 * w jedną listę. Używa tych samych kolumn FV_* co vFvLista.
 *
 * @param string|int $client_id Numer klienta (U_NRKLIENTA)
 */
function db_read_dokumenty($client_id) {
    $pdo = db_get_pdo();
    if (!$pdo) return [];

    $all_rows = [];

    // 1. Faktury z vFvLista
    try {
        $stmt = $pdo->prepare(
            'SELECT FV_NUMERTXT, FV_SUMABRUTTO,
                    FV_KWOTAROZLICZONA, FV_KWOTADOROZLICZENIA,
                    FV_DATAWYSTAWIENIA, FV_DATAPLATNOSCI,
                    FV_NumerPdf, FV_PoTerminie, FV_UWAGI,
                    U_NRKLIENTA
             FROM vFvLista
             WHERE U_NRKLIENTA = ?
             ORDER BY FV_DATAWYSTAWIENIA DESC'
        );
        $stmt->execute([(int)$client_id]);
        $all_rows = $stmt->fetchAll();
    } catch (PDOException $e) {
        ibok_log("db_read_dokumenty vFvLista: " . $e->getMessage(), 'warning');
    }

    // 2. Polecenia Zapłaty z vPzLista (prefiks PZ_*)
    $pz_rows = [];
    try {
        $stmt2 = $pdo->prepare(
            'SELECT PZ_NUMERTXT, PZ_SUMABRUTTO,
                    PZ_KWOTAROZLICZONA, PZ_KWOTADOROZLICZENIA,
                    PZ_DATAWYSTAWIENIA, PZ_DATAPLATNOSCI,
                    PZ_NumerPdf, PZ_PoTerminie, PZ_UWAGI,
                    U_NRKLIENTA
             FROM vPzLista
             WHERE U_NRKLIENTA = ?
             ORDER BY PZ_DATAWYSTAWIENIA DESC'
        );
        $stmt2->execute([(int)$client_id]);
        $pz_rows = $stmt2->fetchAll();
    } catch (PDOException $e) {
        ibok_log("db_read_dokumenty vPzLista: " . $e->getMessage(), 'warning');
    }

    // 3. Korekty z vFvkLista (prefiks FVK_*)
    $fvk_rows = [];
    try {
        $stmt3 = $pdo->prepare(
            'SELECT FVK_NUMERTXT, FVK_SUMABRUTTO,
                    FVK_KWOTAROZLICZONA, FVK_KWOTADOROZLICZENIA,
                    FVK_DATAWYSTAWIENIA, FVK_DATAPLATNOSCI,
                    FVK_NumerPdf, FVK_PoTerminie, FVK_OPIS,
                    U_NRKLIENTA
             FROM vFvkLista
             WHERE U_NRKLIENTA = ?
             ORDER BY FVK_DATAWYSTAWIENIA DESC'
        );
        $stmt3->execute([(int)$client_id]);
        $fvk_rows = $stmt3->fetchAll();
    } catch (PDOException $e) {
        ibok_log("db_read_dokumenty vFvkLista: " . $e->getMessage(), 'warning');
    }

    // Buduj listę dokumentów
    $dokumenty = [];

    // FV
    foreach ($all_rows as $row) {
        $status = db_compute_invoice_status(
            $row['FV_KWOTAROZLICZONA'],
            $row['FV_KWOTADOROZLICZENIA']
        );
        $dni = $row['FV_PoTerminie'];
        $dni_str = ($dni !== null && $dni > 0) ? (string)(int)$dni : '0';

        $dokumenty[] = [
            'id_klienta'       => (string)$row['U_NRKLIENTA'],
            'nr_wewnetrzny'    => db_str($row['FV_NUMERTXT']),
            'nr_faktury'       => db_str($row['FV_NUMERTXT']),
            'typ_dokumentu'    => 'FV',
            'opis'             => db_str($row['FV_UWAGI']),
            'pdf_filename'     => db_str($row['FV_NumerPdf']),
            'kwota_brutto'     => db_format_kwota($row['FV_SUMABRUTTO']),
            'status'           => $status,
            'dni_po_terminie'  => $dni_str,
            'kwota_do_rozl'    => db_format_kwota($row['FV_KWOTADOROZLICZENIA']),
            'data_wystawienia' => db_format_date($row['FV_DATAWYSTAWIENIA']),
            'termin_platnosci' => db_format_date($row['FV_DATAPLATNOSCI']),
        ];
    }

    // FVK (korekty)
    foreach ($fvk_rows as $row) {
        $status = db_compute_invoice_status(
            $row['FVK_KWOTAROZLICZONA'],
            $row['FVK_KWOTADOROZLICZENIA']
        );
        $dni = $row['FVK_PoTerminie'];
        $dni_str = ($dni !== null && $dni > 0) ? (string)(int)$dni : '0';

        $dokumenty[] = [
            'id_klienta'       => (string)$row['U_NRKLIENTA'],
            'nr_wewnetrzny'    => db_str($row['FVK_NUMERTXT']),
            'nr_faktury'       => db_str($row['FVK_NUMERTXT']),
            'typ_dokumentu'    => 'FVK',
            'opis'             => db_str($row['FVK_OPIS']),
            'pdf_filename'     => db_str($row['FVK_NumerPdf']),
            'kwota_brutto'     => db_format_kwota_signed($row['FVK_SUMABRUTTO']),
            'status'           => $status,
            'dni_po_terminie'  => $dni_str,
            'kwota_do_rozl'    => db_format_kwota($row['FVK_KWOTADOROZLICZENIA']),
            'data_wystawienia' => db_format_date($row['FVK_DATAWYSTAWIENIA']),
            'termin_platnosci' => db_format_date($row['FVK_DATAPLATNOSCI']),
        ];
    }

    // PZ (polecenia zapłaty)
    foreach ($pz_rows as $row) {
        $status = db_compute_invoice_status(
            $row['PZ_KWOTAROZLICZONA'],
            $row['PZ_KWOTADOROZLICZENIA']
        );
        $dni = $row['PZ_PoTerminie'];
        $dni_str = ($dni !== null && $dni > 0) ? (string)(int)$dni : '0';

        $dokumenty[] = [
            'id_klienta'       => (string)$row['U_NRKLIENTA'],
            'nr_wewnetrzny'    => db_str($row['PZ_NUMERTXT']),
            'nr_faktury'       => db_str($row['PZ_NUMERTXT']),
            'typ_dokumentu'    => 'PZ',
            'opis'             => db_str($row['PZ_UWAGI']),
            'pdf_filename'     => db_str($row['PZ_NumerPdf']),
            'kwota_brutto'     => db_format_kwota($row['PZ_SUMABRUTTO']),
            'status'           => $status,
            'dni_po_terminie'  => $dni_str,
            'kwota_do_rozl'    => db_format_kwota($row['PZ_KWOTADOROZLICZENIA']),
            'data_wystawienia' => db_format_date($row['PZ_DATAWYSTAWIENIA']),
            'termin_platnosci' => db_format_date($row['PZ_DATAPLATNOSCI']),
        ];
    }

    // Sortuj po dacie wystawienia malejąco
    usort($dokumenty, function($a, $b) {
        return strcmp($b['data_wystawienia'], $a['data_wystawienia']);
    });

    return $dokumenty;
}

/**
 * Czytaj płatności klienta z PYXIS vOplatyLista — odpowiednik vbk_read_oplaty()
 */
function db_read_oplaty($client_id) {
    $pdo = db_get_pdo();
    if (!$pdo) return [];

    try {
        $stmt = $pdo->prepare(
            'SELECT O_INDEX, O_NUMERTXT, O_KWOTA, O_DATAWPLATY,
                    O_FORMAPLATNOSCI
             FROM vOplatyLista
             WHERE U_NRKLIENTA = ?
             ORDER BY O_INDEX'
        );
        $stmt->execute([(int)$client_id]);
        $rows = $stmt->fetchAll();
        $oplaty = [];

        foreach ($rows as $row) {
            $oplaty[] = [
                'pole0'       => (string)$row['O_INDEX'],
                'nr_wplaty'   => db_str($row['O_NUMERTXT']),
                'data_ksieg'  => db_format_date($row['O_DATAWPLATY']),
                'forma_platn' => db_str($row['O_FORMAPLATNOSCI']),
                'kwota'       => db_format_kwota($row['O_KWOTA']),
            ];
        }

        return $oplaty;
    } catch (PDOException $e) {
        ibok_log("db_read_oplaty error: " . $e->getMessage(), 'error');
        return [];
    }
}

/**
 * Czytaj korekty klienta z PYXIS vFvkLista — odpowiednik vbk_read_korekty()
 */
function db_read_korekty($client_id) {
    $pdo = db_get_pdo();
    if (!$pdo) return [];

    try {
        $stmt = $pdo->prepare(
            'SELECT FVK_NUMERTXT, FVK_SUMABRUTTO,
                    FVK_KWOTAROZLICZONA, FVK_KWOTADOROZLICZENIA,
                    FVK_DATAWYSTAWIENIA, FVK_DATAPLATNOSCI,
                    FVK_NumerPdf, FVK_PoTerminie, FVK_OPIS,
                    U_NRKLIENTA
             FROM vFvkLista
             WHERE U_NRKLIENTA = ?
             ORDER BY FVK_DATAWYSTAWIENIA DESC'
        );
        $stmt->execute([(int)$client_id]);
        $rows = $stmt->fetchAll();
        $korekty = [];

        foreach ($rows as $row) {
            $status = db_compute_invoice_status(
                $row['FVK_KWOTAROZLICZONA'],
                $row['FVK_KWOTADOROZLICZENIA']
            );
            $dni = $row['FVK_PoTerminie'];
            $dni_str = ($dni !== null && $dni > 0) ? (string)(int)$dni : '0';

            $korekty[] = [
                'id_klienta'       => (string)$row['U_NRKLIENTA'],
                'nr_wewnetrzny'    => db_str($row['FVK_NUMERTXT']),
                'nr_korekty'       => db_str($row['FVK_NUMERTXT']),
                'opis'             => db_str($row['FVK_OPIS']),
                'pdf_filename'     => db_str($row['FVK_NumerPdf']),
                'kwota_brutto'     => db_format_kwota_signed($row['FVK_SUMABRUTTO']),
                'status'           => $status,
                'dni_po_terminie'  => $dni_str,
                'kwota_do_rozl'    => db_format_kwota($row['FVK_KWOTADOROZLICZENIA']),
                'data_wystawienia' => db_format_date($row['FVK_DATAWYSTAWIENIA']),
                'termin_platnosci' => db_format_date($row['FVK_DATAPLATNOSCI']),
            ];
        }

        return $korekty;
    } catch (PDOException $e) {
        ibok_log("db_read_korekty error: " . $e->getMessage(), 'error');
        return [];
    }
}

/**
 * Czy widok vMac zawiera kolumny prędkości per-urządzenie (M_PASMO*)?
 * Standardowy PYXIS ich nie ma — prędkości żyją na Userdb (U_PASMO*).
 * Wynik cache'owany na czas żądania (jedno zapytanie do information_schema).
 */
function db_mac_has_speed_columns($pdo) {
    static $has = null;
    if ($has !== null) return $has;
    try {
        $stmt = $pdo->query(
            "SELECT COUNT(*) FROM information_schema.COLUMNS
             WHERE TABLE_SCHEMA = DATABASE()
               AND TABLE_NAME = 'vMac'
               AND COLUMN_NAME = 'M_PASMODOWNLOADDZIENTXT'"
        );
        $has = ((int)$stmt->fetchColumn()) > 0;
    } catch (PDOException $e) {
        $has = false;
    }
    return $has;
}

/**
 * Czytaj urządzenia klienta z PYXIS vMac — odpowiednik vbk_read_mac()
 *
 * vMac łączy się z vUserdb przez M_INDEXUSER = U_INDEX.
 * Filtrujemy po U_NRKLIENTA przez JOIN z vUserdb.
 */
function db_read_mac($client_id) {
    $pdo = db_get_pdo();
    if (!$pdo) return [];

    // Standardowy widok PYXIS vMac nie zawiera prędkości per-urządzenie
    // (M_PASMO*) — żyją one na Userdb (U_PASMO*). Dołączamy te kolumny tylko
    // gdy faktycznie istnieją w widoku, inaczej całe zapytanie pada na bazach
    // bez nich (SQLSTATE 42S22 Unknown column). Feature 'speed_from_devices'
    // konsumuje je, gdy są dostępne; gdy ich nie ma — zwracamy puste prędkości.
    $speed_select = db_mac_has_speed_columns($pdo)
        ? ', vm.M_PASMODOWNLOADDZIENTXT, vm.M_PASMOUPLOADDZIENTXT,
             vm.M_PASMODOWNLOADNOCTXT, vm.M_PASMOUPLOADNOCTXT'
        : '';

    try {
        $stmt = $pdo->prepare(
            'SELECT vm.M_INDEX, vm.M_IP, vm.M_PORT, vm.M_MAC,
                    vm.M_DATAPODLACZENIA, vm.M_DATAPRZYDZIELENIAPUBIP,
                    vm.M_PUBIP, vm.M_UWAGI,
                    vm.M_CZYZAZNACZONE, vm.M_CZYZAWIESZONE,
                    vm.TL_NAZWA, vm.RU_NAZWA, vm.TU_NAZWA, vm.INF_NAZWA'
                    . $speed_select . '
             FROM vMac vm
             JOIN vUserdb u ON vm.M_INDEXUSER = u.U_INDEX
             WHERE u.U_NRKLIENTA = ?
             ORDER BY vm.M_INDEX'
        );
        $stmt->execute([(int)$client_id]);
        $rows = $stmt->fetchAll();
        $devices = [];

        foreach ($rows as $row) {
            $devices[] = [
                'nr'         => (string)$row['M_INDEX'],
                'ip'         => db_str($row['M_IP']),
                'port'       => (string)($row['M_PORT'] ?? 0),
                'mac'        => db_str($row['M_MAC']),
                'punkt_podl' => db_str($row['INF_NAZWA']),
                'data_podl'  => db_format_date($row['M_DATAPODLACZENIA']),
                'typ'        => db_str($row['TU_NAZWA']),
                'rodzaj'     => db_str($row['RU_NAZWA']),
                'ip_pub'     => db_str($row['M_PUBIP']),
                'data_ip'    => db_format_date($row['M_DATAPRZYDZIELENIAPUBIP']),
                'uwagi'      => db_str($row['M_UWAGI']),
                'zaznaczony' => (string)($row['M_CZYZAZNACZONE'] ?? 0),
                'wylaczony'  => (string)($row['M_CZYZAWIESZONE'] ?? 0),
                'taryfa'     => db_str($row['TL_NAZWA']),
                'dl_dzien'   => db_str($row['M_PASMODOWNLOADDZIENTXT'] ?? ''),
                'ul_dzien'   => db_str($row['M_PASMOUPLOADDZIENTXT'] ?? ''),
                'dl_noc'     => db_str($row['M_PASMODOWNLOADNOCTXT'] ?? ''),
                'ul_noc'     => db_str($row['M_PASMOUPLOADNOCTXT'] ?? ''),
            ];
        }

        return $devices;
    } catch (PDOException $e) {
        ibok_log("db_read_mac error: " . $e->getMessage(), 'error');
        return [];
    }
}

/**
 * Czytaj notatki klienta z PYXIS vUserdb — odpowiednik vbk_read_info()
 */
function db_read_info($client_id) {
    $pdo = db_get_pdo();
    if (!$pdo) return ['notatka1' => '', 'notatka2' => ''];

    try {
        $stmt = $pdo->prepare(
            'SELECT U_NOTATKA, U_NOTATKA2
             FROM vUserdb
             WHERE U_NRKLIENTA = ?
             LIMIT 1'
        );
        $stmt->execute([(int)$client_id]);
        $row = $stmt->fetch();

        if (!$row) return ['notatka1' => '', 'notatka2' => ''];

        return [
            'notatka1' => db_str($row['U_NOTATKA']),
            'notatka2' => db_str($row['U_NOTATKA2']),
        ];
    } catch (PDOException $e) {
        ibok_log("db_read_info error: " . $e->getMessage(), 'error');
        return ['notatka1' => '', 'notatka2' => ''];
    }
}

/**
 * Czytaj dane kontaktowe klienta z PYXIS vUserdb — odpowiednik vbk_read_infoadmin()
 *
 * VBK infoadmin zwraca: raw (sformatowany string), nazwisko, ulica, miejscowosc, all_fields
 * Z bazy budujemy te same pola z kolumn adresowych Userdb.
 */
function db_read_infoadmin($client_id) {
    $pdo = db_get_pdo();
    if (!$pdo) return [];

    try {
        $stmt = $pdo->prepare(
            'SELECT U_NAZWISKO, U_IMIE, vU_NazwiskoImie,
                    U_ADR_ULICA, U_ADR_NRDOMU, U_ADR_NRLOKALU,
                    U_ADR_KOD, U_ADR_MIEJSCOWOSC, vU_ADR_Adres,
                    vU_LOK_Adres, vU_KOR_Adres,
                    U_TEL1, U_TEL2, U_EMAIL,
                    U_NIP, U_PESEL
             FROM vUserdb
             WHERE U_NRKLIENTA = ?
             LIMIT 1'
        );
        $stmt->execute([(int)$client_id]);
        $row = $stmt->fetch();

        if (!$row) return [];

        // Preferuj vU_NazwiskoImie (pełna nazwa firmy/osoby)
        $nazwisko = db_str($row['vU_NazwiskoImie'] ?? '');
        if (empty($nazwisko)) {
            $nazwisko = trim(db_str($row['U_NAZWISKO']) . ' ' . db_str($row['U_IMIE']));
        }

        // Preferuj vU_ADR_Adres (pełny sformatowany adres)
        $adres_pelny = db_str($row['vU_ADR_Adres'] ?? '');

        $ulica = db_str($row['U_ADR_ULICA']);
        if (!empty($row['U_ADR_NRDOMU'])) {
            $ulica .= ' ' . db_str($row['U_ADR_NRDOMU']);
        }
        if (!empty($row['U_ADR_NRLOKALU'])) {
            $ulica .= '/' . db_str($row['U_ADR_NRLOKALU']);
        }
        $ulica = trim($ulica);

        $miejscowosc = trim(db_str($row['U_ADR_KOD']) . ' ' . db_str($row['U_ADR_MIEJSCOWOSC']));

        $raw = $nazwisko . '  ' . $ulica . '  ' . $miejscowosc;

        // all_fields: dodatkowe pola kontaktowe oddzielone |
        $extra = [];
        $extra[] = $raw;
        if (!empty($row['U_TEL1'])) $extra[] = db_str($row['U_TEL1']);
        if (!empty($row['U_TEL2'])) $extra[] = db_str($row['U_TEL2']);
        if (!empty($row['U_EMAIL'])) $extra[] = db_str($row['U_EMAIL']);
        if (!empty($row['U_NIP'])) $extra[] = 'NIP: ' . db_str($row['U_NIP']);
        if (!empty($row['U_PESEL'])) $extra[] = 'PESEL: ' . db_str($row['U_PESEL']);

        return [
            'raw'            => $raw,
            'nazwisko'       => $nazwisko,
            'nazwa_pelna'    => $nazwisko,
            'adres_pelny'    => $adres_pelny,
            'ulica'          => $ulica,
            'miejscowosc'    => $miejscowosc,
            'adres_instalacji'      => db_str($row['vU_LOK_Adres'] ?? ''),
            'adres_korespondencyjny'=> db_str($row['vU_KOR_Adres'] ?? ''),
            'all_fields'     => $extra,
            'tel1'           => db_str($row['U_TEL1']),
            'tel2'           => db_str($row['U_TEL2']),
            'email'          => db_str($row['U_EMAIL']),
            'nip'            => db_str($row['U_NIP']),
            'pesel'          => db_str($row['U_PESEL']),
        ];
    } catch (PDOException $e) {
        ibok_log("db_read_infoadmin error: " . $e->getMessage(), 'error');
        return [];
    }
}

/**
 * Czytaj infrastrukturę z PYXIS vInfrastruktura — odpowiednik vbk_read_infra()
 */
function db_read_infra() {
    $pdo = db_get_pdo();
    if (!$pdo) return [];

    try {
        $stmt = $pdo->query(
            'SELECT INF_INDEX, INF_IP, INF_MAC, INF_NAZWA, INF_FUNKCJA,
                    INF_TypUrzadzenia, INF_GrNazwaAll,
                    INF_CZYZAZNACZONE, INF_CZYZAWIESZONE
             FROM vInfrastruktura
             ORDER BY INF_INDEX'
        );
        $rows = $stmt->fetchAll();
        $devices = [];

        foreach ($rows as $row) {
            $devices[] = [
                'nr'         => (string)$row['INF_INDEX'],
                'ip'         => db_str($row['INF_IP']),
                'mac'        => db_str($row['INF_MAC']),
                'nazwa'      => db_str($row['INF_NAZWA']),
                'typ_model'  => db_str($row['INF_TypUrzadzenia']),
                'funkcja'    => db_str($row['INF_FUNKCJA']),
                'grupa'      => db_str($row['INF_GrNazwaAll']),
                'zaznaczony' => (string)($row['INF_CZYZAZNACZONE'] ?? 0),
                'wylaczony'  => (string)($row['INF_CZYZAWIESZONE'] ?? 0),
            ];
        }

        return $devices;
    } catch (PDOException $e) {
        ibok_log("db_read_infra error: " . $e->getMessage(), 'error');
        return [];
    }
}

/**
 * Czytaj listę MAC z PYXIS vMac — odpowiednik vbk_read_maclista()
 */
function db_read_maclista() {
    $pdo = db_get_pdo();
    if (!$pdo) return [];

    try {
        $stmt = $pdo->query(
            'SELECT vm.M_INDEX, vm.M_IP, vm.M_MAC, vm.RU_NAZWA, vm.TU_NAZWA,
                    vm.INF_NAZWA, vm.M_PUBIP,
                    vm.M_CZYZAZNACZONE, vm.M_CZYZAWIESZONE,
                    u.vU_NazwiskoImie
             FROM vMac vm
             LEFT JOIN vUserdb u ON vm.M_INDEXUSER = u.U_INDEX
             ORDER BY vm.M_INDEX'
        );
        $rows = $stmt->fetchAll();
        $devices = [];

        foreach ($rows as $row) {
            $devices[] = [
                'nr'         => (string)$row['M_INDEX'],
                'ip'         => db_str($row['M_IP']),
                'mac'        => db_str($row['M_MAC']),
                'rodzaj'     => db_str($row['RU_NAZWA']),
                'typ'        => db_str($row['TU_NAZWA']),
                'podl_do'    => db_str($row['INF_NAZWA']),
                'ip_pub'     => db_str($row['M_PUBIP']),
                'zaznaczony' => (string)($row['M_CZYZAZNACZONE'] ?? 0),
                'wylaczony'  => (string)($row['M_CZYZAWIESZONE'] ?? 0),
                'klient'     => db_str($row['vU_NazwiskoImie'] ?? ''),
            ];
        }

        return $devices;
    } catch (PDOException $e) {
        ibok_log("db_read_maclista error: " . $e->getMessage(), 'error');
        return [];
    }
}

/**
 * Czytaj zlecenia serwisowe z PYXIS vZleceniaLista — odpowiednik vbk_read_zlecenia()
 */
function db_read_zlecenia() {
    $pdo = db_get_pdo();
    if (!$pdo) return [];

    try {
        $stmt = $pdo->query(
            'SELECT z.ZS_INDEX, z.ZS_NUMERTXT, z.ZS_ZAKRES, z.ZS_UWAGI,
                    z.ZS_DATAREJESTRACJI, z.ZS_DATAZAKONCZENIA, z.ZS_CzasTrwania,
                    z.U_NRKLIENTA, z.U_NAZWISKO, z.U_ADR_MIEJSCOWOSC,
                    u.vU_NazwiskoImie, u.vU_ADR_Adres,
                    u.U_TEL1, u.U_TEL2, u.U_EMAIL
             FROM vZleceniaLista z
             LEFT JOIN vUserdb u ON z.U_NRKLIENTA = u.U_NRKLIENTA
             ORDER BY z.ZS_INDEX'
        );
        $rows = $stmt->fetchAll();
        $zlecenia = [];

        foreach ($rows as $row) {
            $data_zakonczenia = db_format_date($row['ZS_DATAZAKONCZENIA'] ?? null);

            // Preferuj pełną nazwę z vU_NazwiskoImie
            $nazwisko = db_str($row['vU_NazwiskoImie'] ?? '');
            if (empty($nazwisko)) {
                $nazwisko = db_str($row['U_NAZWISKO']);
            }

            $zlecenia[] = [
                'nr'               => (string)$row['ZS_INDEX'],
                'id_klienta'       => (string)($row['U_NRKLIENTA'] ?? ''),
                'nazwisko'         => $nazwisko,
                'miejscowosc'      => db_str($row['U_ADR_MIEJSCOWOSC']),
                'adres'            => db_str($row['vU_ADR_Adres'] ?? ''),
                'data'             => db_format_date($row['ZS_DATAREJESTRACJI']),
                'data_zakonczenia' => $data_zakonczenia,
                'pole5'            => '',
                'tresc'            => db_str($row['ZS_ZAKRES']),
                'opis'             => db_str($row['ZS_UWAGI']),
                'uwagi'            => db_str($row['ZS_UWAGI']),
                'dni'              => (string)(int)($row['ZS_CzasTrwania'] ?? 0),
                'tel1'             => db_str($row['U_TEL1'] ?? ''),
                'tel2'             => db_str($row['U_TEL2'] ?? ''),
                'email'            => db_str($row['U_EMAIL'] ?? ''),
            ];
        }

        return $zlecenia;
    } catch (PDOException $e) {
        ibok_log("db_read_zlecenia error: " . $e->getMessage(), 'error');
        return [];
    }
}

/**
 * Czytaj zlecenia serwisowe dla konkretnego klienta z PYXIS vZleceniaLista
 */
function db_read_zlecenia_klienta($client_id) {
    $pdo = db_get_pdo();
    if (!$pdo) return [];

    try {
        $stmt = $pdo->prepare(
            'SELECT z.ZS_INDEX, z.ZS_NUMERTXT, z.ZS_ZAKRES, z.ZS_UWAGI,
                    z.ZS_DATAREJESTRACJI, z.ZS_DATAZAKONCZENIA, z.ZS_CzasTrwania,
                    z.U_NRKLIENTA
             FROM vZleceniaLista z
             WHERE z.U_NRKLIENTA = ?
             ORDER BY z.ZS_DATAREJESTRACJI DESC'
        );
        $stmt->execute([(int)$client_id]);
        $rows = $stmt->fetchAll();
        $zlecenia = [];

        foreach ($rows as $row) {
            $data_zakonczenia = db_format_date($row['ZS_DATAZAKONCZENIA'] ?? null);

            $zlecenia[] = [
                'nr'               => (string)$row['ZS_INDEX'],
                'nr_txt'           => db_str($row['ZS_NUMERTXT'] ?? ''),
                'id_klienta'       => (string)($row['U_NRKLIENTA'] ?? ''),
                'data'             => db_format_date($row['ZS_DATAREJESTRACJI']),
                'data_zakonczenia' => $data_zakonczenia,
                'tresc'            => db_str($row['ZS_ZAKRES']),
                'opis'             => db_str($row['ZS_UWAGI']),
                'uwagi'            => db_str($row['ZS_UWAGI']),
                'dni'              => (string)(int)($row['ZS_CzasTrwania'] ?? 0),
                'otwarte'          => empty($data_zakonczenia),
            ];
        }

        return $zlecenia;
    } catch (PDOException $e) {
        ibok_log("db_read_zlecenia_klienta error: " . $e->getMessage(), 'error');
        return [];
    }
}

/**
 * Czytaj globalne aktualności — odpowiednik vbk_read_global_info()
 *
 * Baza PYXIS nie przechowuje globalnych aktualności iBOK.
 * Czytamy z plików VBK z katalogu data/.
 */
function db_read_global_info($num) {
    $file = IBOK_DATA_DIR . '/info' . (int)$num . '.vbk';
    if (!file_exists($file)) return '';

    $content = file_get_contents($file);
    if ($content === false) return '';

    if (function_exists('vbk_to_utf8')) {
        return vbk_to_utf8(trim($content));
    }
    return trim($content);
}

/**
 * Czytaj admina po loginie z tabeli INI (PYXIS)
 *
 * Hasło admina: SELECT I_VALUE FROM INI WHERE I_IDENT = 'iBOK_HasloAdmin'
 * Serwisanci:   iBOK_Serw1Login / iBOK_Serw1Haslo, iBOK_Serw2Login / iBOK_Serw2Haslo, iBOK_Serw3Login / iBOK_Serw3Haslo
 *
 * @return array|null ['login' => string, 'password' => string, 'source' => 'ini_admin'|'ini_serw'] lub null
 */
function db_read_admin($login) {
    $pdo = db_get_pdo();
    if (!$pdo) return null;

    $login_trimmed = trim($login);
    if ($login_trimmed === '') return null;

    try {
        $stmt = $pdo->prepare(
            "SELECT I_IDENT, I_VALUE FROM INI WHERE I_IDENT LIKE 'iBOK_%'"
        );
        $stmt->execute();
        $rows = $stmt->fetchAll();

        $ini = [];
        foreach ($rows as $row) {
            $ini[db_str($row['I_IDENT'])] = db_str($row['I_VALUE']);
        }


        // Konto "admin" — hasło z iBOK_HasloAdmin
        if (strcasecmp($login_trimmed, 'admin') === 0) {
            $haslo = trim($ini['iBOK_HasloAdmin'] ?? '');
            if ($haslo !== '') {
                return [
                    'login'    => 'admin',
                    'password' => $haslo,
                    'source'   => 'ini_admin',
                ];
            }
        }

        // Konta serwisowe (Serw1..Serw3)
        for ($i = 1; $i <= 3; $i++) {
            $serw_login = trim($ini["iBOK_Serw{$i}Login"] ?? '');
            $serw_haslo = trim($ini["iBOK_Serw{$i}Haslo"] ?? '');

            if ($serw_login !== '' && strcasecmp($login_trimmed, $serw_login) === 0) {
                return [
                    'login'    => $serw_login,
                    'password' => $serw_haslo,
                    'source'   => 'ini_serw',
                ];
            }
        }
    } catch (PDOException $e) {
        ibok_log("db_read_admin INI error: " . $e->getMessage(), 'warning');
    }

    // Fallback: tabela Operator (stara metoda, MD5)
    try {
        $stmt2 = $pdo->prepare(
            'SELECT OP_LOGIN, OP_HASLO, OP_AKTYWNY
             FROM Operator
             WHERE LOWER(OP_LOGIN) = LOWER(?)
             LIMIT 1'
        );
        $stmt2->execute([$login_trimmed]);
        $row2 = $stmt2->fetch();

        if ($row2) {
            if (db_str($row2['OP_AKTYWNY']) === 'A') {
                return [
                    'login'      => db_str($row2['OP_LOGIN']),
                    'haslo_hash' => db_str($row2['OP_HASLO']),
                    'source'     => 'operator',
                ];
            }
        }
    } catch (PDOException $e) {
        ibok_log("db_read_admin: Operator query error: " . $e->getMessage(), 'warning');
    }

    return null;
}

// ============================================
// STATS — zoptymalizowane zapytania SQL
// ============================================

/**
 * Przychód miesięczny z PYXIS Fv — SQL SUM zamiast N+1
 * Odpowiednik stats_revenue_by_month() ale jednym zapytaniem SQL
 */
function db_stats_revenue_by_month($months = 12) {
    $pdo = db_get_pdo();
    if (!$pdo) return ['labels' => [], 'values' => []];

    $data = [];
    $now = time();
    for ($i = $months - 1; $i >= 0; $i--) {
        $key = date('Y-m', strtotime("-{$i} months", $now));
        $data[$key] = 0.0;
    }

    try {
        $first_month = array_key_first($data);
        $stmt = $pdo->prepare(
            "SELECT LEFT(FV_DATAWYSTAWIENIA, 7) AS month_key, SUM(FV_SUMABRUTTO) AS total
             FROM Fv
             WHERE FV_DATAWYSTAWIENIA >= ?
               AND FV_DATAWYSTAWIENIA <> '0000-00-00'
             GROUP BY month_key
             ORDER BY month_key"
        );
        $stmt->execute([$first_month . '-01']);
        $rows = $stmt->fetchAll();

        foreach ($rows as $row) {
            $key = $row['month_key'];
            if (isset($data[$key])) {
                $data[$key] = (float)$row['total'];
            }
        }
    } catch (PDOException $e) {
        ibok_log("db_stats_revenue_by_month error: " . $e->getMessage(), 'error');
    }

    return [
        'labels' => array_keys($data),
        'values' => array_values($data),
    ];
}

/**
 * Wykryj nazwę kolumny BLOB w tabeli (cache per-request)
 *
 * @param string $table Nazwa tabeli (MagazynBlob2, Fv, Fvk)
 * @return string|null Nazwa kolumny BLOB lub null
 */
function db_detect_blob_column($table) {
    static $cache = [];
    if (isset($cache[$table])) return $cache[$table];

    $pdo = db_get_pdo();
    if (!$pdo) return null;

    try {
        $config = db_load_config();
        $stmt = $pdo->prepare(
            "SELECT COLUMN_NAME
             FROM information_schema.COLUMNS
             WHERE TABLE_SCHEMA = ?
               AND TABLE_NAME = ?
               AND DATA_TYPE IN ('blob', 'mediumblob', 'longblob', 'tinyblob')
             LIMIT 1"
        );
        $stmt->execute([$config['database'], $table]);
        $row = $stmt->fetch();
        $cache[$table] = $row ? $row['COLUMN_NAME'] : null;
        return $cache[$table];
    } catch (PDOException $e) {
        ibok_log("db_detect_blob_column error ($table): " . $e->getMessage(), 'error');
        return null;
    }
}

/**
 * Wykryj kolumny tekstowe w tabeli do wyszukiwania po nazwie pliku (cache per-request)
 *
 * Zwraca listę nazw kolumn VARCHAR/TEXT, posortowaną wg prawdopodobieństwa
 * dopasowania (kolumny z "numer", "pdf", "nazwa" w nazwie — pierwsze).
 *
 * @param string $table Nazwa tabeli
 * @return array Lista nazw kolumn tekstowych
 */
function db_detect_text_columns($table) {
    static $cache = [];
    if (isset($cache[$table])) return $cache[$table];

    $pdo = db_get_pdo();
    if (!$pdo) return ($cache[$table] = []);

    try {
        $config = db_load_config();
        $stmt = $pdo->prepare(
            "SELECT COLUMN_NAME
             FROM information_schema.COLUMNS
             WHERE TABLE_SCHEMA = ?
               AND TABLE_NAME = ?
               AND DATA_TYPE IN ('varchar', 'char', 'text', 'mediumtext', 'longtext', 'tinytext')
             ORDER BY ORDINAL_POSITION"
        );
        $stmt->execute([$config['database'], $table]);
        $cols = $stmt->fetchAll(PDO::FETCH_COLUMN);

        // Priorytet: kolumny zawierające "numer", "pdf", "nazwa" w nazwie
        $priority = [];
        $rest = [];
        foreach ($cols as $col) {
            $lower = strtolower($col);
            if (strpos($lower, 'numer') !== false || strpos($lower, 'pdf') !== false || strpos($lower, 'nazwa') !== false) {
                $priority[] = $col;
            } else {
                $rest[] = $col;
            }
        }
        $cache[$table] = array_merge($priority, $rest);
        return $cache[$table];
    } catch (PDOException $e) {
        ibok_log("db_detect_text_columns error ($table): " . $e->getMessage(), 'error');
        return ($cache[$table] = []);
    }
}

/**
 * Pobierz PDF (BLOB) dokumentu z bazy po nazwie pliku
 *
 * Źródło: MagazynBlob2 (MB_NAZWA = FV_NumerPdf, BLOB w MB_PLIK)
 * Autoryzacja: vFvLista / vFvkLista (mają U_NRKLIENTA)
 *
 * @param string $pdf_filename Wartość FV_NumerPdf / FVK_NumerPdf (np. "F_1_NET_1_2026.pdf")
 * @param int $client_id Numer klienta (do weryfikacji dostępu)
 * @param bool $is_admin Czy admin (pomija weryfikację klienta)
 * @return string|null Dane binarne PDF lub null
 */
function db_get_document_pdf($pdf_filename, $client_id = 0, $is_admin = false) {
    $pdo = db_get_pdo();
    if (!$pdo) return null;

    $pdf_filename = trim($pdf_filename);
    if ($pdf_filename === '') return null;

    // Weryfikacja dostępu klienta przez widoki PYXIS
    if (!$is_admin) {
        $authorized = false;

        // Sprawdź vFvLista (faktury FV + PZ)
        try {
            $stmt = $pdo->prepare(
                'SELECT 1 FROM vFvLista WHERE FV_NumerPdf = ? AND U_NRKLIENTA = ? LIMIT 1'
            );
            $stmt->execute([$pdf_filename, (int)$client_id]);
            if ($stmt->fetch()) $authorized = true;
        } catch (PDOException $e) {
            ibok_log("db_get_document_pdf vFvLista check: " . $e->getMessage(), 'error');
        }

        // Sprawdź vPzLista (polecenia zapłaty PZ)
        if (!$authorized) {
            try {
                $stmt = $pdo->prepare(
                    'SELECT 1 FROM vPzLista WHERE PZ_NumerPdf = ? AND U_NRKLIENTA = ? LIMIT 1'
                );
                $stmt->execute([$pdf_filename, (int)$client_id]);
                if ($stmt->fetch()) $authorized = true;
            } catch (PDOException $e) {
                ibok_log("db_get_document_pdf vPzLista check: " . $e->getMessage(), 'error');
            }
        }

        // Sprawdź vFvkLista (korekty FVK)
        if (!$authorized) {
            try {
                $stmt = $pdo->prepare(
                    'SELECT 1 FROM vFvkLista WHERE FVK_NumerPdf = ? AND U_NRKLIENTA = ? LIMIT 1'
                );
                $stmt->execute([$pdf_filename, (int)$client_id]);
                if ($stmt->fetch()) $authorized = true;
            } catch (PDOException $e) {
                ibok_log("db_get_document_pdf vFvkLista check: " . $e->getMessage(), 'error');
            }
        }

        // Sprawdź vUmowyLista (umowy — PDF = prefiks + UM_NUMERTXT + .pdf)
        // PYXIS zamienia '/' na '_' w nazwie pliku, więc porównujemy REPLACE(UM_NUMERTXT,'/',_')
        if (!$authorized) {
            try {
                $um_numer = '';
                if (preg_match('/^(?:Umowa|Aneks|Pzo)_(.+)\.pdf$/i', $pdf_filename, $m)) {
                    $um_numer = $m[1];
                }
                if ($um_numer !== '') {
                    $stmt = $pdo->prepare(
                        "SELECT 1 FROM vUmowyLista um
                         JOIN Userdb u ON um.UM_INDEXUSER = u.U_INDEX
                         WHERE REPLACE(um.UM_NUMERTXT, '/', '_') = ?
                           AND u.U_NRKLIENTA = ? LIMIT 1"
                    );
                    $stmt->execute([$um_numer, (int)$client_id]);
                    if ($stmt->fetch()) $authorized = true;
                }
            } catch (PDOException $e) {
                ibok_log("db_get_document_pdf vUmowyLista check: " . $e->getMessage(), 'error');
            }
        }

        if (!$authorized) return null;
    }

    // Pobierz BLOB z MagazynBlob2 (MB_NAZWA = nazwa pliku PDF)
    try {
        $stmt = $pdo->prepare(
            'SELECT MB_PLIK FROM MagazynBlob2 WHERE MB_NAZWA = ? LIMIT 1'
        );
        $stmt->execute([$pdf_filename]);
        $row = $stmt->fetch(PDO::FETCH_NUM);
        if ($row && !empty($row[0])) {
            return db_unwrap_blob($row[0]);
        }
    } catch (PDOException $e) {
        ibok_log("db_get_document_pdf MagazynBlob2 error: " . $e->getMessage(), 'error');
    }

    return null;
}

/**
 * Rozpakuj BLOB jeśli jest w formacie ZIP (PYXIS pakuje PDFy w ZIP)
 *
 * @param string $data Surowe dane z bazy
 * @return string Dane PDF (rozpakowane lub oryginalne)
 */
function db_unwrap_blob($data) {
    if (empty($data)) return $data;

    // Już jest PDF — zwróć bez zmian
    if (substr($data, 0, 4) === '%PDF') {
        return $data;
    }

    // ZIP (PK magic) — rozpakuj pierwszy plik
    if (substr($data, 0, 2) === "PK" && class_exists('ZipArchive')) {
        $tmp = tempnam(sys_get_temp_dir(), 'ibok_blob_');
        if ($tmp !== false) {
            file_put_contents($tmp, $data);
            $zip = new ZipArchive();
            if ($zip->open($tmp) === true && $zip->numFiles > 0) {
                $extracted = $zip->getFromIndex(0);
                $zip->close();
                @unlink($tmp);
                if ($extracted !== false) {
                    return $extracted;
                }
            } else {
                @unlink($tmp);
            }
        }
    }

    // Gzip (1F 8B)
    if (substr($data, 0, 2) === "\x1f\x8b") {
        $unpacked = @gzdecode($data);
        if ($unpacked !== false) return $unpacked;
    }

    // Zlib (78 9C / 78 01 / 78 DA)
    $h = substr($data, 0, 2);
    if ($h === "\x78\x9c" || $h === "\x78\x01" || $h === "\x78\xda") {
        $unpacked = @gzuncompress($data);
        if ($unpacked !== false) return $unpacked;
    }

    // Nieznany format — zwróć jak jest
    return $data;
}

/**
 * Batch-check which PDF filenames exist in MagazynBlob2 or on disk
 *
 * @param array $filenames Lista nazw plików PDF
 * @param int $client_id ID klienta (dla fallbacku dyskowego)
 * @return array Asocjacyjna: filename => true/false
 */
function db_check_pdfs_exist(array $filenames, $client_id = 0) {
    $result = array_fill_keys($filenames, false);
    if (empty($filenames)) return $result;

    // Sprawdź BLOB w bazie
    $pdo = db_get_pdo();
    if ($pdo) {
        try {
            $placeholders = implode(',', array_fill(0, count($filenames), '?'));
            $stmt = $pdo->prepare(
                "SELECT MB_NAZWA FROM MagazynBlob2 WHERE MB_NAZWA IN ($placeholders) AND MB_PLIK IS NOT NULL AND LENGTH(MB_PLIK) > 0"
            );
            $stmt->execute(array_values($filenames));
            $found = $stmt->fetchAll(PDO::FETCH_COLUMN);
            foreach ($found as $name) {
                $result[$name] = true;
            }
        } catch (PDOException $e) {
            // Kontynuuj do fallbacku dyskowego
        }
    }

    // Fallback: sprawdź pliki na dysku
    if ($client_id > 0) {
        $client_dir = IBOK_DATA_DIR . '/' . $client_id;
        foreach ($filenames as $fn) {
            if (!$result[$fn] && file_exists($client_dir . '/' . $fn)) {
                $result[$fn] = true;
            }
        }
    }

    return $result;
}

/**
 * Szukaj klientów po danych kontaktowych (tel, email, NIP, PESEL, adres)
 * Jedno zapytanie SQL zamiast pętli po wszystkich klientach
 *
 * @param string $query Szukany fragment
 * @param int $limit Maksymalna liczba wyników
 * @param array $exclude_ids ID klientów do pominięcia
 * @return array Pasujący klienci
 */
function db_search_clients_by_contact($query, $limit = 5, $exclude_ids = []) {
    $pdo = db_get_pdo();
    if (!$pdo) return [];

    $query = trim($query);
    if ($query === '') return [];

    $like = '%' . $query . '%';

    try {
        $sql = 'SELECT U_NRKLIENTA, U_LOGIN, U_NAZWISKO, U_IMIE, vU_NazwiskoImie,
                       U_TEL1, U_TEL2, U_EMAIL, U_NIP, U_PESEL, vU_ADR_Adres
                FROM vUserdb
                WHERE (U_TEL1 LIKE ? OR U_TEL2 LIKE ? OR U_EMAIL LIKE ?
                       OR U_NIP LIKE ? OR U_PESEL LIKE ? OR vU_ADR_Adres LIKE ?)';

        $params = [$like, $like, $like, $like, $like, $like];

        if (!empty($exclude_ids)) {
            $placeholders = implode(',', array_fill(0, count($exclude_ids), '?'));
            $sql .= ' AND U_NRKLIENTA NOT IN (' . $placeholders . ')';
            foreach ($exclude_ids as $eid) {
                $params[] = (int)$eid;
            }
        }

        $sql .= ' LIMIT ' . (int)$limit;

        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
        $rows = $stmt->fetchAll();

        $results = [];
        foreach ($rows as $row) {
            $id = (string)$row['U_NRKLIENTA'];
            $nazwisko = db_str($row['vU_NazwiskoImie'] ?? '');
            if (empty($nazwisko)) {
                $nazwisko = trim(db_str($row['U_NAZWISKO']) . ' ' . db_str($row['U_IMIE']));
            }
            $login = db_str($row['U_LOGIN']);

            // Determine which field matched
            $q_lower = mb_strtolower($query);
            $match_detail = '';
            if (!empty($row['U_TEL1']) && mb_strpos(mb_strtolower($row['U_TEL1']), $q_lower) !== false) {
                $match_detail = db_str($row['U_TEL1']);
            } elseif (!empty($row['U_TEL2']) && mb_strpos(mb_strtolower($row['U_TEL2']), $q_lower) !== false) {
                $match_detail = db_str($row['U_TEL2']);
            } elseif (!empty($row['U_EMAIL']) && mb_strpos(mb_strtolower($row['U_EMAIL']), $q_lower) !== false) {
                $match_detail = db_str($row['U_EMAIL']);
            } elseif (!empty($row['U_NIP']) && mb_strpos(mb_strtolower($row['U_NIP']), $q_lower) !== false) {
                $match_detail = 'NIP: ' . db_str($row['U_NIP']);
            } elseif (!empty($row['U_PESEL']) && mb_strpos(mb_strtolower($row['U_PESEL']), $q_lower) !== false) {
                $match_detail = 'PESEL: ' . db_str($row['U_PESEL']);
            }

            $results[] = [
                'id'       => $id,
                'nazwisko' => $nazwisko,
                'login'    => $login,
                'detail'   => $match_detail,
            ];
        }
        return $results;
    } catch (PDOException $e) {
        ibok_log("db_search_clients_by_contact error: " . $e->getMessage(), 'warning');
        return [];
    }
}

/**
 * Szukaj dokumentów (FV, PZ, FVK) po numerze — SQL LIKE
 *
 * @param string $query Szukany fragment numeru dokumentu
 * @param int $limit Maksymalna liczba wyników
 * @return array Pasujące dokumenty z id klienta
 */
function db_search_dokumenty($query, $limit = 5) {
    $pdo = db_get_pdo();
    if (!$pdo) return [];

    $query = trim($query);
    if ($query === '') return [];

    $rows = [];

    // Próbuj vListaDokumentow, fallback na vFvLista
    foreach (['vListaDokumentow', 'vFvLista'] as $view) {
        try {
            $stmt = $pdo->prepare(
                'SELECT FV_NUMERTXT, FV_SUMABRUTTO,
                        FV_DATAWYSTAWIENIA, U_NRKLIENTA
                 FROM ' . $view . '
                 WHERE FV_NUMERTXT LIKE ?
                 ORDER BY FV_DATAWYSTAWIENIA DESC
                 LIMIT ' . (int)$limit
            );
            $stmt->execute(['%' . $query . '%']);
            $rows = $stmt->fetchAll();
            if (!empty($rows)) break;
        } catch (PDOException $e) {
            continue;
        }
    }

    $results = [];
    foreach ($rows as $row) {
        $results[] = [
            'nr_faktury'       => db_str($row['FV_NUMERTXT']),
            'kwota_brutto'     => db_format_kwota($row['FV_SUMABRUTTO']),
            'data_wystawienia' => db_format_date($row['FV_DATAWYSTAWIENIA']),
            'id_klienta'       => (string)$row['U_NRKLIENTA'],
        ];
    }

    return $results;
}

// ============================================
// UMOWY (vUmowyLista)
// ============================================

/**
 * Kolumny w vUmowyLista (PYXIS):
 *   UM_INDEX         — PK
 *   UM_INDEXUSER     — U_INDEX klienta (Userdb.U_INDEX, NIE U_NRKLIENTA!)
 *   UM_NUMERTXT      — numer/nazwa umowy
 *   UM_TYTUL         — tytuł umowy
 *   UM_OPIS          — opis
 *   UM_UWAGI         — uwagi
 *   UM_DATASTART     — data początku
 *   UM_DATAKONIEC    — data zakończenia
 *   UM_CZYPODPISANA  — 0/1
 *   UM_CZYNAIBOKU    — 0/1
 *   UM_CZYNIEAKTYWNA — 0/1
 *   UM_CZYANEKS      — 0=umowa, 1=aneks, 2=PZO
 *   UM_CzyAneksTyp   — 'UM', 'AN', 'PZO'
 *
 * PDF w MagazynBlob2 — nazwa konstruowana:
 *   UM  → Umowa_{UM_NUMERTXT}.pdf
 *   AN  → Aneks_{UM_NUMERTXT}.pdf
 *   PZO → Pzo_{UM_NUMERTXT}.pdf
 */

/** Kolumny SELECT dla vUmowyLista */
define('UMOWY_SELECT_COLS',
    'UM_INDEX, UM_INDEXUSER, UM_NUMERTXT, UM_TYTUL, UM_OPIS, UM_UWAGI,
     UM_DATASTART, UM_DATAKONIEC, UM_CZYPODPISANA, UM_CZYNAIBOKU,
     UM_CZYNIEAKTYWNA, UM_CZYANEKS, UM_CzyAneksTyp'
);

/**
 * Określ typ umowy z UM_CzyAneksTyp / UM_CZYANEKS
 */
function db_umowa_typ($row) {
    $typ = trim($row['UM_CzyAneksTyp'] ?? '');
    if ($typ === 'AN') return 'aneks';
    if ($typ === 'PZO') return 'pzo';
    if ($typ === 'UM') return 'umowa';
    // Fallback na UM_CZYANEKS
    $czyaneks = (int)($row['UM_CZYANEKS'] ?? 0);
    if ($czyaneks === 1) return 'aneks';
    if ($czyaneks === 2) return 'pzo';
    return 'umowa';
}

/**
 * Skonstruuj nazwę PDF w MagazynBlob2 z typu i numeru
 *
 * PYXIS zamienia '/' na '_' w nazwie pliku PDF
 * np. UM_NUMERTXT = "1/3/2026" → MB_NAZWA = "Umowa_1_3_2026.pdf"
 */
function db_umowa_pdf_filename($row) {
    $typ = db_umowa_typ($row);
    $numer = trim($row['UM_NUMERTXT'] ?? '');
    if ($numer === '') return '';
    $prefixes = [
        'umowa' => 'Umowa_',
        'aneks' => 'Aneks_',
        'pzo'   => 'Pzo_',
    ];
    $prefix = $prefixes[$typ] ?? 'Umowa_';
    // PYXIS zamienia '/' na '_' w nazwie pliku
    $numer_safe = str_replace('/', '_', $numer);
    return $prefix . $numer_safe . '.pdf';
}

/**
 * Mapuj wiersz vUmowyLista na znormalizowaną strukturę
 */
function db_map_umowa($row) {
    return [
        'um_index'     => (int)($row['UM_INDEX'] ?? 0),
        'numer'        => db_str($row['UM_NUMERTXT']),
        'tytul'        => db_str($row['UM_TYTUL']),
        'opis'         => db_str($row['UM_OPIS']),
        'pdf_filename' => db_umowa_pdf_filename($row),
        'data_od'      => db_format_date($row['UM_DATASTART']),
        'data_do'      => db_format_date($row['UM_DATAKONIEC']),
        'uwagi'        => db_str($row['UM_UWAGI']),
        'typ'          => db_umowa_typ($row),
        'podpisana'    => !empty($row['UM_CZYPODPISANA']),
        'na_iboku'     => !empty($row['UM_CZYNAIBOKU']),
        'nieaktywna'   => !empty($row['UM_CZYNIEAKTYWNA']),
    ];
}

/**
 * Pobierz umowy do podpisu (widoczne na iBOK, niepodpisane, aktywne)
 *
 * @param string $client_id Numer klienta (U_NRKLIENTA)
 * @return array Lista umów do podpisu
 */
function db_read_umowy_do_podpisu(string $client_id): array {
    $pdo = db_get_pdo();
    if (!$pdo) return [];

    try {
        $stmt = $pdo->prepare(
            'SELECT ' . UMOWY_SELECT_COLS . '
             FROM vUmowyLista um
             JOIN Userdb u ON um.UM_INDEXUSER = u.U_INDEX
             WHERE u.U_NRKLIENTA = ?
               AND um.UM_CZYNAIBOKU = 1
               AND um.UM_CZYPODPISANA = 0
               AND um.UM_CZYNIEAKTYWNA = 0
             ORDER BY um.UM_DATASTART DESC'
        );
        $stmt->execute([(int)$client_id]);
        $rows = $stmt->fetchAll();

        $result = [];
        foreach ($rows as $row) {
            $result[] = db_map_umowa($row);
        }
        return $result;
    } catch (PDOException $e) {
        ibok_log("db_read_umowy_do_podpisu error: " . $e->getMessage(), 'error');
        return [];
    }
}

/**
 * Pobierz wszystkie aktywne umowy klienta (podpisane + niepodpisane)
 *
 * @param string $client_id Numer klienta (U_NRKLIENTA)
 * @return array Lista umów
 */
function db_read_umowy_klienta(string $client_id): array {
    $pdo = db_get_pdo();
    if (!$pdo) return [];

    try {
        $stmt = $pdo->prepare(
            'SELECT ' . UMOWY_SELECT_COLS . '
             FROM vUmowyLista um
             JOIN Userdb u ON um.UM_INDEXUSER = u.U_INDEX
             WHERE u.U_NRKLIENTA = ?
               AND um.UM_CZYNIEAKTYWNA = 0
             ORDER BY um.UM_DATASTART DESC'
        );
        $stmt->execute([(int)$client_id]);
        $rows = $stmt->fetchAll();

        $result = [];
        foreach ($rows as $row) {
            $result[] = db_map_umowa($row);
        }
        return $result;
    } catch (PDOException $e) {
        ibok_log("db_read_umowy_klienta error: " . $e->getMessage(), 'error');
        return [];
    }
}

/**
 * Podpisz umowę — UPDATE UM_CZYPODPISANA + UM_UWAGI
 * Identyfikacja po UM_INDEX (unikalny klucz)
 *
 * @param int $um_index UM_INDEX umowy
 * @param string $client_id Numer klienta U_NRKLIENTA (weryfikacja właściciela)
 * @return bool Sukces
 */
function db_podpisz_umowe(int $um_index, string $client_id): bool {
    $pdo = db_get_pdo();
    if (!$pdo) return false;

    $user_index = db_get_user_index((int)$client_id);
    if ($user_index === null) {
        ibok_log("db_podpisz_umowe: nie znaleziono U_INDEX dla U_NRKLIENTA={$client_id}", 'error');
        return false;
    }

    try {
        $stmt = $pdo->prepare(
            "UPDATE Umowy
             SET UM_CZYPODPISANA = 1,
                 UM_UWAGI = CONCAT('Podpisana via iBOK', CHAR(13), CHAR(10), IFNULL(UM_UWAGI, ''))
             WHERE UM_INDEX = ?
               AND UM_INDEXUSER = ?
               AND UM_CZYPODPISANA = 0"
        );
        $stmt->execute([$um_index, $user_index]);
        return $stmt->rowCount() > 0;
    } catch (PDOException $e) {
        ibok_log("db_podpisz_umowe error: " . $e->getMessage(), 'error');
        return false;
    }
}

/**
 * Pobierz najnowszą podpisaną umowę (typ UM) — do dashboardu (daty od/do)
 *
 * @param string $client_id Numer klienta (U_NRKLIENTA)
 * @return array|null Dane umowy lub null
 */
function db_read_najnowsza_umowa(string $client_id): ?array {
    $pdo = db_get_pdo();
    if (!$pdo) return null;

    try {
        $stmt = $pdo->prepare(
            'SELECT ' . UMOWY_SELECT_COLS . '
             FROM vUmowyLista um
             JOIN Userdb u ON um.UM_INDEXUSER = u.U_INDEX
             WHERE u.U_NRKLIENTA = ?
               AND um.UM_CzyAneksTyp = \'UM\'
               AND um.UM_CZYPODPISANA = 1
               AND um.UM_CZYNIEAKTYWNA = 0
             ORDER BY um.UM_DATASTART DESC
             LIMIT 1'
        );
        $stmt->execute([(int)$client_id]);
        $row = $stmt->fetch();
        if (!$row) return null;
        return db_map_umowa($row);
    } catch (PDOException $e) {
        ibok_log("db_read_najnowsza_umowa error: " . $e->getMessage(), 'error');
        return null;
    }
}

/**
 * Policz umowy do podpisu — do badge w menu
 *
 * @param string $client_id Numer klienta (U_NRKLIENTA)
 * @return int Liczba umów oczekujących
 */
function db_count_umowy_do_podpisu(string $client_id): int {
    $pdo = db_get_pdo();
    if (!$pdo) return 0;

    try {
        $stmt = $pdo->prepare(
            'SELECT COUNT(*) FROM vUmowyLista um
             JOIN Userdb u ON um.UM_INDEXUSER = u.U_INDEX
             WHERE u.U_NRKLIENTA = ?
               AND um.UM_CZYNAIBOKU = 1
               AND um.UM_CZYPODPISANA = 0
               AND um.UM_CZYNIEAKTYWNA = 0'
        );
        $stmt->execute([(int)$client_id]);
        return (int)$stmt->fetchColumn();
    } catch (PDOException $e) {
        ibok_log("db_count_umowy_do_podpisu error: " . $e->getMessage(), 'error');
        return 0;
    }
}

/**
 * Pobierz wszystkie umowy klienta (admin view — w tym nieaktywne)
 *
 * @param string $client_id Numer klienta (U_NRKLIENTA)
 * @return array Lista umów
 */
function db_read_umowy_admin(string $client_id): array {
    $pdo = db_get_pdo();
    if (!$pdo) return [];

    try {
        $stmt = $pdo->prepare(
            'SELECT ' . UMOWY_SELECT_COLS . '
             FROM vUmowyLista um
             JOIN Userdb u ON um.UM_INDEXUSER = u.U_INDEX
             WHERE u.U_NRKLIENTA = ?
             ORDER BY um.UM_DATASTART DESC'
        );
        $stmt->execute([(int)$client_id]);
        $rows = $stmt->fetchAll();

        $result = [];
        foreach ($rows as $row) {
            $result[] = db_map_umowa($row);
        }
        return $result;
    } catch (PDOException $e) {
        ibok_log("db_read_umowy_admin error: " . $e->getMessage(), 'error');
        return [];
    }
}

/**
 * Pobierz wszystkie umowy ze wszystkich klientów (admin list view)
 *
 * @return array Lista umów z numerem klienta (U_NRKLIENTA)
 */
function db_read_umowy_all(): array {
    $pdo = db_get_pdo();
    if (!$pdo) return [];

    try {
        $stmt = $pdo->query(
            'SELECT ' . UMOWY_SELECT_COLS . ', u.U_NRKLIENTA
             FROM vUmowyLista um
             JOIN Userdb u ON um.UM_INDEXUSER = u.U_INDEX
             ORDER BY um.UM_DATASTART DESC'
        );
        $rows = $stmt->fetchAll();

        $result = [];
        foreach ($rows as $row) {
            $um = db_map_umowa($row);
            $um['client_id'] = (string)$row['U_NRKLIENTA'];
            $result[] = $um;
        }
        return $result;
    } catch (PDOException $e) {
        ibok_log("db_read_umowy_all error: " . $e->getMessage(), 'error');
        return [];
    }
}

// ============================================
// ZMIANA HASŁA KLIENTA
// ============================================

/**
 * Zmień hasło klienta w tabeli Userdb
 *
 * @param string $client_id U_NRKLIENTA klienta
 * @param string $new_password Nowe hasło (plain text)
 * @return bool Sukces
 */
function db_update_client_password(string $client_id, string $new_password): bool {
    $pdo = db_get_pdo();
    if (!$pdo) return false;

    try {
        $stmt = $pdo->prepare(
            "UPDATE Userdb SET U_HASLO = ? WHERE U_NRKLIENTA = ?"
        );
        $stmt->execute([$new_password, (int)$client_id]);
        return $stmt->rowCount() > 0;
    } catch (PDOException $e) {
        ibok_log("db_update_client_password error: " . $e->getMessage(), 'error');
        return false;
    }
}

// ============================================
// LICENCJONOWANIE i-BOK
// ============================================

/**
 * Sprawdź licencję i-BOK bezpośrednio w bazie PYXIS (tabela Licencja)
 *
 * Szuka wiersza L_ID = 'J', waliduje L_KLUCZ: litera J + min. 12 cyfr.
 *
 * @return bool true jeśli licencja jest ważna
 */
function db_check_license() {
    $pdo = db_get_pdo();
    if (!$pdo) return false;

    try {
        $stmt = $pdo->prepare("SELECT L_KLUCZ FROM Licencja WHERE L_ID = 'J' LIMIT 1");
        $stmt->execute();
        $row = $stmt->fetch();

        if (!$row || empty($row['L_KLUCZ'])) {
            return false;
        }

        return (bool)preg_match('/^J\d{12,}$/', $row['L_KLUCZ']);
    } catch (PDOException $e) {
        ibok_log("db_check_license error: " . $e->getMessage(), 'error');
        return false;
    }
}

/**
 * Sprawdź licencję i-BOK z cache sesyjnym (re-check co 5 minut)
 *
 * @return bool true jeśli licencja jest ważna
 */
function ibok_license_valid() {
    if (session_status() !== PHP_SESSION_ACTIVE) {
        return db_check_license();
    }

    $now = time();
    if (isset($_SESSION['license_valid']) && isset($_SESSION['license_checked_at'])) {
        if (($now - $_SESSION['license_checked_at']) < 300) {
            return (bool)$_SESSION['license_valid'];
        }
    }

    $valid = db_check_license();
    $_SESSION['license_valid'] = $valid;
    $_SESSION['license_checked_at'] = $now;

    return $valid;
}

/**
 * Pobierz dane licencji i-BOK z tabeli Licencja (wiersz L_ID = 'J')
 *
 * @return array|null Dane wiersza lub null gdy brak/błąd
 */
function db_read_license_info() {
    $pdo = db_get_pdo();
    if (!$pdo) return null;

    try {
        $stmt = $pdo->prepare("SELECT * FROM Licencja WHERE L_ID = 'J' LIMIT 1");
        $stmt->execute();
        $row = $stmt->fetch();
        return $row ?: null;
    } catch (PDOException $e) {
        ibok_log("db_read_license_info error: " . $e->getMessage(), 'error');
        return null;
    }
}

// ============================================
// OplatyIbok - wpłaty online z bramek płatności
// ============================================
// Struktura tabeli (PYXIS):
//   O_INDEX           INT AUTO_INCREMENT PRIMARY KEY
//   O_INDEXUSER       INT NOT NULL          -- U_INDEX klienta (Userdb.U_INDEX)
//   O_KWOTA           DECIMAL(16,2)         -- kwota wpłaty
//   O_DATAWPLATY      DATE                  -- data wpłaty
//   O_OPERATORID      INT NOT NULL          -- 0 = płatność online
//   O_DATAMODYFIKACJI TIMESTAMP (auto)      -- auto-aktualizacja
//   O_DATADODANIA     DATE                  -- data dodania rekordu
//   O_OPISPLATNOSCI   VARCHAR(255)          -- opis płatności (z szablonu bramki)
// ============================================

/**
 * Przetłumacz U_NRKLIENTA na U_INDEX (Userdb PK)
 *
 * @param int $nr_klienta Numer klienta (U_NRKLIENTA)
 * @return int|null U_INDEX lub null
 */
function db_get_user_index($nr_klienta) {
    $pdo = db_get_pdo();
    if (!$pdo) return null;

    try {
        $stmt = $pdo->prepare("SELECT U_INDEX FROM Userdb WHERE U_NRKLIENTA = ? LIMIT 1");
        $stmt->execute([(int)$nr_klienta]);
        $row = $stmt->fetch();
        return $row ? (int)$row['U_INDEX'] : null;
    } catch (PDOException $e) {
        return null;
    }
}

/**
 * Zapisz wpłatę online do tabeli OplatyIbok
 *
 * @param int         $nr_klienta   Numer klienta (U_NRKLIENTA)
 * @param float       $amount       Kwota wpłaty
 * @param string      $description  Opis płatności (z szablonu bramki)
 * @param string|null $payment_date Data wpłaty (Y-m-d), null = dziś
 * @return int|false ID wstawionego rekordu lub false
 */
function db_insert_oplata_ibok($nr_klienta, $amount, $description = '', $payment_date = null) {
    $pdo = db_get_pdo();
    if (!$pdo) return false;

    $user_index = db_get_user_index($nr_klienta);
    if ($user_index === null) {
        ibok_log("db_insert_oplata_ibok: nie znaleziono U_INDEX dla U_NRKLIENTA={$nr_klienta}", 'error');
        return false;
    }

    $today = date('Y-m-d');
    if ($payment_date === null) {
        $payment_date = $today;
    }

    try {
        $stmt = $pdo->prepare("
            INSERT INTO OplatyIbok (O_INDEXUSER, O_KWOTA, O_DATAWPLATY, O_OPERATORID, O_DATADODANIA, O_OPISPLATNOSCI)
            VALUES (:indexuser, :kwota, :datawplaty, 0, :datadodania, :opis)
        ");
        $stmt->execute([
            ':indexuser'   => $user_index,
            ':kwota'       => (float)$amount,
            ':datawplaty'  => $payment_date,
            ':datadodania' => $today,
            ':opis'        => mb_substr($description, 0, 255),
        ]);
        return (int)$pdo->lastInsertId();
    } catch (PDOException $e) {
        ibok_log("db_insert_oplata_ibok error: " . $e->getMessage(), 'error');
        return false;
    }
}

/**
 * Pobierz wpłaty online klienta z OplatyIbok
 *
 * @param int $nr_klienta Numer klienta (U_NRKLIENTA)
 * @return array Lista wpłat
 */
function db_read_oplaty_ibok($nr_klienta) {
    $pdo = db_get_pdo();
    if (!$pdo) return [];

    $user_index = db_get_user_index($nr_klienta);
    if ($user_index === null) return [];

    try {
        $stmt = $pdo->prepare("
            SELECT * FROM OplatyIbok
            WHERE O_INDEXUSER = :indexuser
            ORDER BY O_DATAWPLATY DESC
        ");
        $stmt->execute([':indexuser' => $user_index]);
        return $stmt->fetchAll();
    } catch (PDOException $e) {
        return [];
    }
}
