<?php
namespace Domain\Product;

/**
 * Repository odpowiedzialny za dostęp do danych produktów
 *
 * Zgodnie z wzorcem Repository Pattern, ta klasa enkapsuluje
 * logikę dostępu do bazy danych dla produktów.
 */
class ProductRepository
{
    private const MAX_PER_PAGE = 100;

    /**
     * @var \medoo Instancja Medoo ORM
     */
    private $db;

    /**
     * Konstruktor - przyjmuje instancję bazy danych
     *
     * @param \medoo $db Instancja Medoo ORM
     */
    public function __construct($db)
    {
        $this->db = $db;
    }

    /**
     * Pobiera stan magazynowy produktu
     *
     * @param int $productId ID produktu
     * @return int|null Ilość produktu lub null jeśli nie znaleziono
     */
    public function getQuantity(int $productId): ?int
    {
        $quantity = $this->db->get('pp_shop_products', 'quantity', ['id' => $productId]);

        // Medoo zwraca false jeśli nie znaleziono
        return $quantity !== false ? (int)$quantity : null;
    }

    /**
     * Pobiera produkt po ID
     *
     * @param int $productId ID produktu
     * @return array|null Dane produktu lub null
     */
    public function find(int $productId): ?array
    {
        $product = $this->db->get('pp_shop_products', '*', ['id' => $productId]);
        return $product ?: null;
    }

    /**
     * Zwraca liste produktow z archiwum do panelu admin.
     *
     * @return array{items: array<int, array<string, mixed>>, total: int}
     */
    public function listArchivedForAdmin(
        array $filters,
        string $sortColumn = 'id',
        string $sortDir = 'DESC',
        int $page = 1,
        int $perPage = 10
    ): array {
        $allowedSortColumns = [
            'id' => 'psp.id',
            'name' => 'name',
            'price_brutto' => 'psp.price_brutto',
            'price_brutto_promo' => 'psp.price_brutto_promo',
            'quantity' => 'psp.quantity',
            'combinations' => 'combinations',
        ];

        $sortSql = $allowedSortColumns[$sortColumn] ?? 'psp.id';
        $sortDir = strtoupper(trim($sortDir)) === 'ASC' ? 'ASC' : 'DESC';
        $page = max(1, $page);
        $perPage = min(self::MAX_PER_PAGE, max(1, $perPage));
        $offset = ($page - 1) * $perPage;

        $where = ['psp.archive = 1', 'psp.parent_id IS NULL'];
        $params = [];

        $phrase = trim((string)($filters['phrase'] ?? ''));
        if (strlen($phrase) > 255) {
            $phrase = substr($phrase, 0, 255);
        }

        if ($phrase !== '') {
            $where[] = '(
                psp.ean LIKE :phrase
                OR psp.sku LIKE :phrase
                OR EXISTS (
                    SELECT 1
                    FROM pp_shop_products_langs AS pspl2
                    WHERE pspl2.product_id = psp.id
                      AND pspl2.name LIKE :phrase
                )
            )';
            $params[':phrase'] = '%' . $phrase . '%';
        }

        $whereSql = implode(' AND ', $where);

        $sqlCount = "
            SELECT COUNT(0)
            FROM pp_shop_products AS psp
            WHERE {$whereSql}
        ";

        $stmtCount = $this->db->query($sqlCount, $params);
        $countRows = $stmtCount ? $stmtCount->fetchAll() : [];
        $total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;

        $sql = "
            SELECT
                psp.id,
                psp.price_brutto,
                psp.price_brutto_promo,
                psp.quantity,
                psp.sku,
                psp.ean,
                (
                    SELECT pspl.name
                    FROM pp_shop_products_langs AS pspl
                    INNER JOIN pp_langs AS pl ON pl.id = pspl.lang_id
                    WHERE pspl.product_id = psp.id
                      AND pspl.name <> ''
                    ORDER BY pl.o ASC
                    LIMIT 1
                ) AS name,
                (
                    SELECT pspi.src
                    FROM pp_shop_products_images AS pspi
                    WHERE pspi.product_id = psp.id
                    ORDER BY pspi.o ASC, pspi.id ASC
                    LIMIT 1
                ) AS image_src,
                (
                    SELECT pspi.alt
                    FROM pp_shop_products_images AS pspi
                    WHERE pspi.product_id = psp.id
                    ORDER BY pspi.o ASC, pspi.id ASC
                    LIMIT 1
                ) AS image_alt,
                (
                    SELECT COUNT(0)
                    FROM pp_shop_products AS pspc
                    WHERE pspc.parent_id = psp.id
                ) AS combinations
            FROM pp_shop_products AS psp
            WHERE {$whereSql}
            ORDER BY {$sortSql} {$sortDir}, psp.id {$sortDir}
            LIMIT {$perPage} OFFSET {$offset}
        ";

        $stmt = $this->db->query($sql, $params);
        $items = $stmt ? $stmt->fetchAll() : [];

        return [
            'items' => is_array($items) ? $items : [],
            'total' => $total,
        ];
    }

    /**
     * Pobiera cenę produktu (promocyjną jeśli jest niższa, w przeciwnym razie regularną)
     *
     * @param int $productId ID produktu
     * @return float|null Cena brutto lub null jeśli nie znaleziono
     */
    public function getPrice(int $productId): ?float
    {
        $prices = $this->db->get('pp_shop_products', ['price_brutto', 'price_brutto_promo'], ['id' => $productId]);

        if (!$prices) {
            return null;
        }

        if ($prices['price_brutto_promo'] != '' && $prices['price_brutto_promo'] < $prices['price_brutto']) {
            return (float)$prices['price_brutto_promo'];
        }

        return (float)$prices['price_brutto'];
    }

    /**
     * Pobiera nazwę produktu w danym języku
     *
     * @param int $productId ID produktu
     * @param string $langId ID języka
     * @return string|null Nazwa produktu lub null jeśli nie znaleziono
     */
    public function getName(int $productId, string $langId): ?string
    {
        $name = $this->db->get('pp_shop_products_langs', 'name', ['AND' => ['product_id' => $productId, 'lang_id' => $langId]]);

        return $name ?: null;
    }

    /**
     * Aktualizuje ilość produktu
     *
     * @param int $productId ID produktu
     * @param int $quantity Nowa ilość
     * @return bool Czy aktualizacja się powiodła
     */
    public function updateQuantity(int $productId, int $quantity): bool
    {
        $result = $this->db->update(
            'pp_shop_products',
            ['quantity' => $quantity],
            ['id' => $productId]
        );

        return $result !== false;
    }

    /**
     * Przywraca produkt z archiwum (wraz z kombinacjami)
     *
     * @param int $productId ID produktu
     * @return bool Czy operacja się powiodła
     */
    public function unarchive(int $productId): bool
    {
        $this->db->update( 'pp_shop_products', [ 'status' => 1, 'archive' => 0 ], [ 'id' => $productId ] );
        $this->db->update( 'pp_shop_products', [ 'status' => 1, 'archive' => 0 ], [ 'parent_id' => $productId ] );

        return true;
    }

    /**
     * Przenosi produkt do archiwum (wraz z kombinacjami)
     *
     * @param int $productId ID produktu
     * @return bool Czy operacja się powiodła
     */
    public function archive(int $productId): bool
    {
        $this->db->update( 'pp_shop_products', [ 'status' => 0, 'archive' => 1 ], [ 'id' => $productId ] );
        $this->db->update( 'pp_shop_products', [ 'status' => 0, 'archive' => 1 ], [ 'parent_id' => $productId ] );

        return true;
    }

    /**
     * Pobiera listę wszystkich produktów głównych (id => name) do masowej edycji.
     * Zwraca tylko produkty bez parent_id (bez kombinacji).
     *
     * @return array<int, string> Mapa id => nazwa produktu
     */
    public function allProductsForMassEdit(): array
    {
        $defaultLang = $this->db->get( 'pp_langs', 'id', [ 'start' => 1 ] );
        if ( !$defaultLang ) {
            $defaultLang = 'pl';
        }

        $results = $this->db->select( 'pp_shop_products', 'id', [ 'parent_id' => null ] );
        $products = [];

        if ( is_array( $results ) ) {
            foreach ( $results as $id ) {
                $name = $this->db->get( 'pp_shop_products_langs', 'name', [
                    'AND' => [ 'product_id' => $id, 'lang_id' => $defaultLang ]
                ] );
                $products[ (int) $id ] = $name ?: '';
            }
        }

        return $products;
    }

    /**
     * Pobiera listę ID produktów przypisanych do danej kategorii.
     *
     * @param int $categoryId ID kategorii
     * @return int[] Lista ID produktów
     */
    public function getProductsByCategory(int $categoryId): array
    {
        $results = $this->db->select(
            'pp_shop_products_categories',
            'product_id',
            [ 'category_id' => $categoryId ]
        );

        return is_array( $results ) ? $results : [];
    }

    /**
     * Aplikuje rabat procentowy na produkt (cena promocyjna = cena - X%).
     * Aktualizuje również ceny kombinacji produktu.
     *
     * @param int $productId ID produktu
     * @param float $discountPercent Procent rabatu
     * @return array|null Tablica z price_brutto i price_brutto_promo lub null przy błędzie
     */
    public function applyDiscountPercent(int $productId, float $discountPercent): ?array
    {
        $product = $this->db->get( 'pp_shop_products', [
            'vat', 'price_brutto', 'price_netto'
        ], [ 'id' => $productId ] );

        if ( !$product ) {
            return null;
        }

        $vat = $product['vat'];
        $priceBrutto = (float) $product['price_brutto'];
        $priceNetto = (float) $product['price_netto'];

        $priceBruttoPromo = $priceBrutto - ( $priceBrutto * ( $discountPercent / 100 ) );
        $priceNettoPromo = $priceNetto - ( $priceNetto * ( $discountPercent / 100 ) );

        if ( $priceBrutto == $priceBruttoPromo ) {
            $priceBruttoPromo = null;
        }
        if ( $priceNetto == $priceNettoPromo ) {
            $priceNettoPromo = null;
        }

        $this->db->update( 'pp_shop_products', [
            'price_brutto_promo' => $priceBruttoPromo,
            'price_netto_promo' => $priceNettoPromo
        ], [ 'id' => $productId ] );

        $this->updateCombinationPrices( $productId, $priceNetto, $vat, $priceNettoPromo );

        return [
            'price_brutto' => $priceBrutto,
            'price_brutto_promo' => $priceBruttoPromo
        ];
    }

    // ─── Krok 1: metody CRUD admin ───────────────────────────────────

    /**
     * Liczba produktów (nie-archiwum, parent_id IS NULL).
     */
    public function countProducts(?array $where = null): int
    {
        if ( $where ) {
            return (int) $this->db->count( 'pp_shop_products', $where );
        }

        return (int) $this->db->count( 'pp_shop_products', [ 'archive' => 0 ] );
    }

    /**
     * Lista produktów do panelu admin (paginacja, sortowanie, filtrowanie).
     *
     * @return array{items: array, total: int}
     */
    public function listForAdmin(
        array $filters = [],
        string $sortColumn = 'id',
        string $sortDir = 'DESC',
        int $page = 1,
        int $perPage = 15
    ): array {
        $page = max( 1, $page );
        $perPage = min( max( 1, $perPage ), self::MAX_PER_PAGE );
        $offset = ( $page - 1 ) * $perPage;

        $allowedSort = [ 'id', 'name', 'price_brutto', 'status', 'promoted', 'quantity' ];
        if ( !in_array( $sortColumn, $allowedSort, true ) ) {
            $sortColumn = 'id';
        }
        $sortDir = strtoupper( $sortDir ) === 'ASC' ? 'ASC' : 'DESC';

        $conditions = [ 'psp.archive = 0', 'psp.parent_id IS NULL' ];
        $params = [];

        $search = trim( (string) ( $filters['search'] ?? '' ) );
        if ( $search !== '' ) {
            $conditions[] = '( pspl.name LIKE :search OR psp.ean LIKE :search_ean OR psp.sku LIKE :search_sku )';
            $params[':search'] = '%' . $search . '%';
            $params[':search_ean'] = '%' . $search . '%';
            $params[':search_sku'] = '%' . $search . '%';
        }

        $statusFilter = (string) ( $filters['status'] ?? '' );
        if ( $statusFilter === '1' || $statusFilter === '0' ) {
            $conditions[] = 'psp.status = :status';
            $params[':status'] = (int) $statusFilter;
        }

        $promotedFilter = (string) ( $filters['promoted'] ?? '' );
        if ( $promotedFilter === '1' || $promotedFilter === '0' ) {
            $conditions[] = 'psp.promoted = :promoted';
            $params[':promoted'] = (int) $promotedFilter;
        }

        $whereSql = implode( ' AND ', $conditions );
        $needsJoin = $search !== '' || $sortColumn === 'name';

        $joinSql = $needsJoin
            ? 'INNER JOIN pp_shop_products_langs AS pspl ON pspl.product_id = psp.id AND pspl.lang_id = ' . $this->db->quote( $this->defaultLangId() )
            : '';

        $orderColumn = $sortColumn === 'name' ? 'pspl.name' : 'psp.' . $sortColumn;

        $countStmt = $this->db->query(
            'SELECT COUNT( DISTINCT psp.id ) AS total '
            . 'FROM pp_shop_products AS psp '
            . $joinSql . ' '
            . 'WHERE ' . $whereSql,
            $params
        );
        $countRow = $countStmt ? $countStmt->fetchAll( \PDO::FETCH_ASSOC ) : [];
        $total = isset( $countRow[0]['total'] ) ? (int) $countRow[0]['total'] : 0;

        $stmt = $this->db->query(
            'SELECT DISTINCT psp.id '
            . 'FROM pp_shop_products AS psp '
            . $joinSql . ' '
            . 'WHERE ' . $whereSql
            . ' ORDER BY ' . $orderColumn . ' ' . $sortDir . ', psp.id DESC'
            . ' LIMIT ' . $offset . ', ' . $perPage,
            $params
        );

        $rows = $stmt ? $stmt->fetchAll( \PDO::FETCH_ASSOC ) : [];
        $items = [];

        foreach ( $rows as $row ) {
            $id = (int) ( $row['id'] ?? 0 );
            if ( $id > 0 ) {
                $product = $this->findForAdmin( $id );
                if ( $product ) {
                    $items[] = $product;
                }
            }
        }

        return [
            'items' => $items,
            'total' => $total,
        ];
    }

    /**
     * Szczegóły produktu (admin) — zastępuje factory product_details().
     */
    public function findForAdmin(int $productId): ?array
    {
        $product = $this->db->get( 'pp_shop_products', '*', [ 'id' => $productId ] );
        if ( !$product ) {
            return null;
        }

        $results = $this->db->select( 'pp_shop_products_langs', '*', [ 'product_id' => $productId ] );
        if ( is_array( $results ) ) {
            foreach ( $results as $row ) {
                $product['languages'][ $row['lang_id'] ] = $row;
            }
        }

        $product['images'] = $this->db->select( 'pp_shop_products_images', '*', [ 'product_id' => $productId, 'ORDER' => [ 'o' => 'ASC', 'id' => 'ASC' ] ] );
        $product['files'] = $this->db->select( 'pp_shop_products_files', '*', [ 'product_id' => $productId ] );
        $product['categories'] = $this->db->select( 'pp_shop_products_categories', 'category_id', [ 'product_id' => $productId ] );
        $product['attributes'] = $this->db->select( 'pp_shop_products_attributes', [ 'attribute_id', 'value_id' ], [ 'product_id' => $productId ] );
        $product['products_related'] = $this->db->select( 'pp_shop_products_related', 'product_related_id', [ 'product_id' => $productId ] );
        $product['custom_fields'] = $this->db->select( 'pp_shop_products_custom_fields', '*', [ 'id_product' => $productId ] );

        return $product;
    }

    /**
     * Prosta lista produktów (id => name) — do selectów w formularzu.
     */
    public function allProductsList(): array
    {
        $results = $this->db->select( 'pp_shop_products', 'id', [ 'parent_id' => null ] );
        $products = [];

        if ( is_array( $results ) ) {
            foreach ( $results as $id ) {
                $products[ $id ] = $this->db->get( 'pp_shop_products_langs', 'name', [
                    'AND' => [ 'product_id' => $id, 'lang_id' => 'pl' ]
                ] );
            }
        }

        return $products;
    }

    /**
     * Tekst z nazwami kategorii produktu (oddzielone " / ").
     */
    public function productCategoriesText(int $productId): string
    {
        $results = $this->db->query(
            'SELECT category_id FROM pp_shop_products_categories WHERE product_id = :pid',
            [ ':pid' => $productId ]
        );
        $rows = $results ? $results->fetchAll( \PDO::FETCH_ASSOC ) : [];

        if ( !is_array( $rows ) || empty( $rows ) ) {
            return '';
        }

        $out = ' - ';
        foreach ( $rows as $i => $row ) {
            $title = $this->db->get( 'pp_shop_categories_langs', 'title', [
                'AND' => [
                    'category_id' => (int) $row['category_id'],
                    'lang_id' => $this->defaultLangId(),
                ]
            ] );
            $out .= $title ?: '';
            if ( $i < count( $rows ) - 1 ) {
                $out .= ' / ';
            }
        }

        return $out;
    }

    /**
     * ID produktu nadrzędnego (parent_id).
     */
    public function getParentId(int $productId): ?int
    {
        $parentId = $this->db->get( 'pp_shop_products', 'parent_id', [ 'id' => $productId ] );
        return $parentId ? (int) $parentId : null;
    }

    /**
     * Domyślna nazwa produktu (w domyślnym języku).
     */
    public function productDefaultName(int $productId): ?string
    {
        $name = $this->db->get( 'pp_shop_products_langs', 'name', [
            'AND' => [ 'product_id' => $productId, 'lang_id' => $this->defaultLangId() ]
        ] );
        return $name ?: null;
    }

    /**
     * ID domyślnego języka.
     */
    private function defaultLangId(): string
    {
        $langId = $this->db->get( 'pp_langs', 'id', [ 'start' => 1 ] );
        return $langId ?: 'pl';
    }

    // ─── Krok 2: zapis produktu ──────────────────────────────────────

    /**
     * Zapis produktu (insert lub update). Zastępuje factory save().
     *
     * @param array $d Dane produktu (z formularza)
     * @param int|null $userId ID aktualnego użytkownika admin
     * @return int|null ID produktu lub null przy błędzie
     */
    public function saveProduct(array $d, ?int $userId = null): ?int
    {
        $productId = (int) ( $d['id'] ?? 0 );
        $isNew = !$productId;

        $productData = [
            'date_modify' => date( 'Y-m-d H:i:s' ),
            'modify_by' => $userId,
            'status' => ( $d['status'] ?? '' ) === 'on' ? 1 : 0,
            'price_netto' => $this->nullIfEmpty( $d['price_netto'] ?? null ),
            'price_brutto' => $this->nullIfEmpty( $d['price_brutto'] ?? null ),
            'vat' => $d['vat'] ?? 0,
            'promoted' => ( $d['promoted'] ?? '' ) === 'on' ? 1 : 0,
            'layout_id' => $this->nullIfEmpty( $d['layout_id'] ?? null ),
            'price_netto_promo' => $this->nullIfEmpty( $d['price_netto_promo'] ?? null ),
            'price_brutto_promo' => $this->nullIfEmpty( $d['price_brutto_promo'] ?? null ),
            'new_to_date' => $this->nullIfEmpty( $d['new_to_date'] ?? null ),
            'stock_0_buy' => ( $d['stock_0_buy'] ?? '' ) === 'on' ? 1 : 0,
            'wp' => $this->nullIfEmpty( $d['wp'] ?? null ),
            'sku' => $this->nullIfEmpty( $d['sku'] ?? null ),
            'ean' => $this->nullIfEmpty( $d['ean'] ?? null ),
            'custom_label_0' => $this->nullIfEmpty( $d['custom_label_0'] ?? null ),
            'custom_label_1' => $this->nullIfEmpty( $d['custom_label_1'] ?? null ),
            'custom_label_2' => $this->nullIfEmpty( $d['custom_label_2'] ?? null ),
            'custom_label_3' => $this->nullIfEmpty( $d['custom_label_3'] ?? null ),
            'custom_label_4' => $this->nullIfEmpty( $d['custom_label_4'] ?? null ),
            'additional_message' => ( $d['additional_message'] ?? '' ) == 'on' ? 1 : 0,
            'set_id' => !empty( $d['set'] ) ? (int) $d['set'] : null,
            'quantity' => (int) ( $d['quantity'] ?? 0 ),
            'additional_message_text' => $this->nullIfEmpty( $d['additional_message_text'] ?? null ),
            'additional_message_required' => ( $d['additional_message_required'] ?? '' ) == 'on' ? 1 : 0,
            'producer_id' => !empty( $d['producer_id'] ) ? $d['producer_id'] : null,
            'product_unit_id' => !empty( $d['product_unit'] ) ? $d['product_unit'] : null,
            'weight' => !empty( $d['weight'] ) ? $d['weight'] : null,
        ];

        if ( $isNew ) {
            $productData['date_add'] = date( 'Y-m-d H:i:s' );
            $this->db->insert( 'pp_shop_products', $productData );
            $productId = (int) $this->db->id();

            if ( !$productId ) {
                return null;
            }
        } else {
            $this->db->update( 'pp_shop_products', $productData, [ 'id' => $productId ] );

            $this->db->update( 'pp_shop_products', [
                'additional_message' => $productData['additional_message'],
            ], [ 'parent_id' => $productId ] );

            $this->updateCombinationPricesFromBase(
                $productId,
                $d['price_brutto'] ?? 0,
                $d['vat'] ?? 0,
                $d['price_brutto_promo'] ?? null
            );
        }

        $this->saveLanguages( $productId, $d, $isNew );
        $this->saveCategories( $productId, $d['categories'] ?? null );
        $this->saveRelatedProducts( $productId, $d['products_related'] ?? null );
        $this->moveTemporaryFiles( $productId );
        $this->moveTemporaryImages( $productId );

        if ( !empty( $d['gallery_order'] ) ) {
            $this->saveImagesOrder( $productId, $d['gallery_order'] );
        }

        $this->saveCustomFields( $productId, $d['custom_field_name'] ?? [], $d['custom_field_type'] ?? [], $d['custom_field_required'] ?? [] );

        if ( !$isNew ) {
            $this->cleanupDeletedFiles( $productId );
            $this->cleanupDeletedImages( $productId );
        }

        \Shared\Helpers\Helpers::htacces();
        \Shared\Helpers\Helpers::delete_dir( '../temp/' );
        \Shared\Helpers\Helpers::delete_dir( '../thumbs/' );

        if ( !$isNew ) {
            $redis = \Shared\Cache\RedisConnection::getInstance()->getConnection();
            if ( $redis ) {
                $redis->flushAll();
            }
        }

        return $productId;
    }

    private function saveLanguages(int $productId, array $d, bool $isNew): void
    {
        $langs = ( new \Domain\Languages\LanguagesRepository( $this->db ) )->languagesList( true );

        foreach ( $langs as $lg ) {
            $lid = $lg['id'];
            $langData = [
                'name' => $this->nullIfEmpty( $d['name'][$lid] ?? null ),
                'short_description' => $this->nullIfEmpty( $d['short_description'][$lid] ?? null ),
                'description' => $this->nullIfEmpty( $d['description'][$lid] ?? null ),
                'meta_description' => $this->nullIfEmpty( $d['meta_description'][$lid] ?? null ),
                'meta_keywords' => $this->nullIfEmpty( $d['meta_keywords'][$lid] ?? null ),
                'seo_link' => !empty( $d['seo_link'][$lid] ) ? \Shared\Helpers\Helpers::seo( $d['seo_link'][$lid] ) : null,
                'copy_from' => $this->nullIfEmpty( $d['copy_from'][$lid] ?? null ),
                'warehouse_message_zero' => $this->nullIfEmpty( $d['warehouse_message_zero'][$lid] ?? null ),
                'warehouse_message_nonzero' => $this->nullIfEmpty( $d['warehouse_message_nonzero'][$lid] ?? null ),
                'tab_name_1' => $this->nullIfEmpty( $d['tab_name_1'][$lid] ?? null ),
                'tab_description_1' => $this->nullIfEmpty( $d['tab_description_1'][$lid] ?? null ),
                'tab_name_2' => $this->nullIfEmpty( $d['tab_name_2'][$lid] ?? null ),
                'tab_description_2' => $this->nullIfEmpty( $d['tab_description_2'][$lid] ?? null ),
                'canonical' => $this->nullIfEmpty( $d['canonical'][$lid] ?? null ),
                'meta_title' => $this->nullIfEmpty( $d['meta_title'][$lid] ?? null ),
                'security_information' => $this->nullIfEmpty( $d['security_information'][$lid] ?? null ),
            ];

            if ( $isNew ) {
                $langData['product_id'] = $productId;
                $langData['lang_id'] = $lid;
                $this->db->insert( 'pp_shop_products_langs', $langData );
            } else {
                $translationId = $this->db->get( 'pp_shop_products_langs', 'id', [
                    'AND' => [ 'product_id' => $productId, 'lang_id' => $lid ]
                ] );

                if ( $translationId ) {
                    $currentSeoLink = $this->db->get( 'pp_shop_products_langs', 'seo_link', [ 'id' => $translationId ] );
                    $newSeoLink = $langData['seo_link'] ?: \Shared\Helpers\Helpers::seo( 'p-' . $productId . '-' . ( $d['name'][$lid] ?? '' ) );

                    if ( $newSeoLink !== $currentSeoLink && $currentSeoLink != '' ) {
                        $this->handleSeoRedirects( $productId, $lid, $newSeoLink, $currentSeoLink );
                    }

                    $this->db->update( 'pp_shop_products_langs', $langData, [ 'id' => $translationId ] );
                } else {
                    $langData['product_id'] = $productId;
                    $langData['lang_id'] = $lid;
                    $this->db->insert( 'pp_shop_products_langs', $langData );
                }
            }
        }
    }

    private function handleSeoRedirects(int $productId, string $langId, string $newSeoLink, string $currentSeoLink): void
    {
        if ( $this->db->count( 'pp_redirects', [ 'from' => $newSeoLink, 'to' => $currentSeoLink, 'lang_id' => $langId, 'product_id' => $productId ] ) ) {
            $this->db->delete( 'pp_redirects', [ 'from' => $newSeoLink, 'to' => $currentSeoLink, 'lang_id' => $langId, 'product_id' => $productId ] );
        }

        $this->db->delete( 'pp_redirects', [
            'AND' => [
                'product_id' => $productId,
                'lang_id' => $langId,
                'from' => $currentSeoLink,
                'to[!]' => $newSeoLink,
            ],
        ] );

        $seoUsed = (bool) $this->db->count( 'pp_shop_products_langs', [
            'AND' => [
                'lang_id' => $langId,
                'seo_link' => $currentSeoLink,
                'product_id[!]' => $productId,
            ],
        ] );

        if ( !$seoUsed ) {
            $this->db->delete( 'pp_redirects', [
                'AND' => [
                    'from' => $currentSeoLink,
                    'lang_id' => $langId,
                    'product_id[!]' => $productId,
                ],
            ] );

            if ( !$this->db->count( 'pp_redirects', [ 'from' => $currentSeoLink, 'to' => $newSeoLink, 'lang_id' => $langId, 'product_id' => $productId ] ) ) {
                if ( \Shared\Helpers\Helpers::canAddRedirect( $currentSeoLink, $newSeoLink, $langId ) ) {
                    $this->db->insert( 'pp_redirects', [ 'from' => $currentSeoLink, 'to' => $newSeoLink, 'lang_id' => $langId, 'product_id' => $productId ] );
                }
            }
        } else {
            $this->db->delete( 'pp_redirects', [ 'AND' => [ 'product_id' => $productId, 'lang_id' => $langId, 'from' => $currentSeoLink ] ] );
        }
    }

    private function saveCategories(int $productId, $categories): void
    {
        if ( !is_array( $categories ) ) {
            $categories = $categories ? [ $categories ] : [];
        }

        $notIn = array_merge( [0], $categories );
        $this->db->delete( 'pp_shop_products_categories', [ 'AND' => [ 'product_id' => $productId, 'category_id[!]' => $notIn ] ] );

        $existing = $this->db->select( 'pp_shop_products_categories', 'category_id', [ 'product_id' => $productId ] );
        $existing = is_array( $existing ) ? $existing : [];
        $toAdd = array_diff( $categories, $existing );

        foreach ( $toAdd as $categoryId ) {
            if ( $productId && $categoryId ) {
                $order = (int) $this->db->max( 'pp_shop_products_categories', 'o' ) + 1;
                $this->db->insert( 'pp_shop_products_categories', [
                    'product_id' => $productId,
                    'category_id' => (int) $categoryId,
                    'o' => $order,
                ] );
            }
        }
    }

    private function saveRelatedProducts(int $productId, $products): void
    {
        if ( !is_array( $products ) ) {
            $products = $products ? [ $products ] : [];
        }

        $notIn = array_merge( [0], $products );
        $this->db->delete( 'pp_shop_products_related', [ 'AND' => [ 'product_id' => $productId, 'product_related_id[!]' => $notIn ] ] );

        $existing = $this->db->select( 'pp_shop_products_related', 'product_related_id', [ 'product_id' => $productId ] );
        $existing = is_array( $existing ) ? $existing : [];
        $toAdd = array_diff( $products, $existing );

        foreach ( $toAdd as $relatedId ) {
            if ( $productId && $relatedId ) {
                $this->db->insert( 'pp_shop_products_related', [
                    'product_id' => $productId,
                    'product_related_id' => (int) $relatedId,
                ] );
            }
        }
    }

    private function moveTemporaryFiles(int $productId): void
    {
        $results = $this->db->select( 'pp_shop_products_files', '*', [ 'product_id' => null ] );
        if ( !is_array( $results ) ) return;

        $created = false;
        foreach ( $results as $row ) {
            $dir = '/upload/product_files/product_' . $productId;
            $newName = str_replace( '/upload/product_files/tmp', $dir, $row['src'] );

            if ( file_exists( '..' . $row['src'] ) ) {
                if ( !is_dir( '../' . $dir ) && !$created ) {
                    if ( mkdir( '../' . $dir, 0755, true ) ) $created = true;
                }
                rename( '..' . $row['src'], '..' . $newName );
            }

            $this->db->update( 'pp_shop_products_files', [ 'src' => $newName, 'product_id' => $productId ], [ 'id' => $row['id'] ] );
        }
    }

    private function moveTemporaryImages(int $productId): void
    {
        $results = $this->db->select( 'pp_shop_products_images', '*', [ 'product_id' => null ] );
        if ( !is_array( $results ) ) return;

        $created = false;
        foreach ( $results as $row ) {
            $dir = '/upload/product_images/product_' . $productId;
            $newName = str_replace( '/upload/product_images/tmp', $dir, $row['src'] );

            if ( file_exists( '..' . $newName ) ) {
                $ext = strrpos( $newName, '.' );
                $base = substr( $newName, 0, $ext );
                $extension = substr( $newName, $ext );
                $count = 1;
                while ( file_exists( '..' . $base . '_' . $count . $extension ) ) {
                    ++$count;
                }
                $newName = $base . '_' . $count . $extension;
            }

            if ( file_exists( '..' . $row['src'] ) ) {
                if ( !is_dir( '../' . $dir ) && !$created ) {
                    if ( mkdir( '../' . $dir, 0755, true ) ) $created = true;
                }
                rename( '..' . $row['src'], '..' . $newName );
            }

            $this->db->update( 'pp_shop_products_images', [ 'src' => $newName, 'product_id' => $productId ], [ 'id' => $row['id'] ] );
        }
    }

    private function saveCustomFields(int $productId, array $names, array $types, array $required): void
    {
        $existingIds = [];
        foreach ( $names as $i => $name ) {
            if ( !empty( $name ) ) {
                $id = (int) $this->db->get( 'pp_shop_products_custom_fields', 'id_additional_field', [
                    'AND' => [ 'id_product' => $productId, 'name' => $name ]
                ] );
                if ( $id ) $existingIds[] = $id;
            }
        }

        if ( !empty( $existingIds ) ) {
            $this->db->delete( 'pp_shop_products_custom_fields', [
                'AND' => [ 'id_product' => $productId, 'id_additional_field[!]' => $existingIds ]
            ] );
        } else {
            $this->db->delete( 'pp_shop_products_custom_fields', [ 'id_product' => $productId ] );
        }

        foreach ( $names as $i => $name ) {
            if ( empty( $name ) ) continue;

            $typeVal = $types[$i] ?? 'text';
            $isRequired = !empty( $required[$i] ) ? 1 : 0;

            if ( !$this->db->count( 'pp_shop_products_custom_fields', [ 'AND' => [ 'id_product' => $productId, 'name' => $name ] ] ) ) {
                $this->db->insert( 'pp_shop_products_custom_fields', [
                    'id_product' => $productId,
                    'name' => $name,
                    'type' => $typeVal,
                    'is_required' => $isRequired,
                ] );
            } else {
                $this->db->update( 'pp_shop_products_custom_fields', [
                    'type' => $typeVal,
                    'is_required' => $isRequired,
                ], [ 'AND' => [ 'id_product' => $productId, 'name' => $name ] ] );
            }
        }
    }

    private function cleanupDeletedFiles(int $productId): void
    {
        $results = $this->db->select( 'pp_shop_products_files', '*', [ 'AND' => [ 'product_id' => $productId, 'to_delete' => 1 ] ] );
        if ( is_array( $results ) ) {
            foreach ( $results as $row ) {
                if ( file_exists( '../' . $row['src'] ) ) {
                    unlink( '../' . $row['src'] );
                }
            }
        }
        $this->db->delete( 'pp_shop_products_files', [ 'AND' => [ 'product_id' => $productId, 'to_delete' => 1 ] ] );
    }

    private function cleanupDeletedImages(int $productId): void
    {
        $results = $this->db->select( 'pp_shop_products_images', '*', [ 'AND' => [ 'product_id' => $productId, 'to_delete' => 1 ] ] );
        if ( is_array( $results ) ) {
            foreach ( $results as $row ) {
                if ( file_exists( '../' . $row['src'] ) ) {
                    unlink( '../' . $row['src'] );
                }
            }
        }
        $this->db->delete( 'pp_shop_products_images', [ 'AND' => [ 'product_id' => $productId, 'to_delete' => 1 ] ] );
    }

    private function nullIfEmpty($value)
    {
        return ( $value !== null && $value !== '' && $value !== 0.00 ) ? $value : null;
    }

    // ─── Koniec kroku 2 ──────────────────────────────────────────────

    // ─── Krok 3: Operacje na produktach ──────────────────────────────

    /**
     * Usuwa produkt i wszystkie powiązane dane.
     *
     * @param int $productId ID produktu
     * @return bool
     */
    public function delete(int $productId): bool
    {
        // Usuń kombinacje (produkty z parent_id = productId)
        $combinations = $this->db->select( 'pp_shop_products', 'id', [ 'parent_id' => $productId ] );
        if ( $combinations ) {
            foreach ( $combinations as $combId ) {
                $this->db->delete( 'pp_shop_products_attributes', [ 'product_id' => $combId ] );
                $this->db->delete( 'pp_shop_products', [ 'id' => $combId ] );
            }
        }

        $this->db->delete( 'pp_shop_products_categories', [ 'product_id' => $productId ] );
        $this->db->delete( 'pp_shop_products_langs', [ 'product_id' => $productId ] );
        $this->db->delete( 'pp_shop_products_images', [ 'product_id' => $productId ] );
        $this->db->delete( 'pp_shop_products_files', [ 'product_id' => $productId ] );
        $this->db->delete( 'pp_shop_products_attributes', [ 'product_id' => $productId ] );
        $this->db->delete( 'pp_shop_products', [ 'id' => $productId ] );
        $this->db->delete( 'pp_shop_product_sets_products', [ 'product_id' => $productId ] );
        $this->db->delete( 'pp_routes', [ 'product_id' => $productId ] );
        $this->db->delete( 'pp_redirects', [ 'product_id' => $productId ] );

        \Shared\Helpers\Helpers::delete_dir( '../upload/product_images/product_' . $productId . '/' );
        \Shared\Helpers\Helpers::delete_dir( '../upload/product_files/product_' . $productId . '/' );

        return true;
    }

    /**
     * Duplikuje produkt z opcjonalnym kopiowaniem kombinacji.
     *
     * @param int $productId ID produktu do zduplikowania
     * @param bool $withCombinations Czy kopiować kombinacje
     * @return bool
     */
    public function duplicate(int $productId, bool $withCombinations = false): bool
    {
        $product = $this->db->get( 'pp_shop_products', '*', [ 'id' => $productId ] );
        if ( !$product ) {
            return false;
        }

        $this->db->insert( 'pp_shop_products', [
            'price_netto'          => $product['price_netto'],
            'price_brutto'         => $product['price_brutto'],
            'price_netto_promo'    => $product['price_netto_promo'],
            'price_brutto_promo'   => $product['price_brutto_promo'],
            'vat'                  => $product['vat'],
            'promoted'             => $product['promoted'],
            'layout_id'            => $product['layout_id'],
            'new_to_date'          => $product['new_to_date'],
            'stock_0_buy'          => $product['stock_0_buy'],
            'wp'                   => $product['wp'],
            'custom_label_0'       => $product['custom_label_0'],
            'custom_label_1'       => $product['custom_label_1'],
            'custom_label_2'       => $product['custom_label_2'],
            'custom_label_3'       => $product['custom_label_3'],
            'custom_label_4'       => $product['custom_label_4'],
            'additional_message'   => $product['additional_message'],
        ] );

        $newProductId = $this->db->id();
        if ( !$newProductId ) {
            return false;
        }

        // Atrybuty
        $attributes = $this->db->select( 'pp_shop_products_attributes', '*', [ 'product_id' => $productId ] );
        if ( \Shared\Helpers\Helpers::is_array_fix( $attributes ) ) {
            foreach ( $attributes as $row ) {
                $this->db->insert( 'pp_shop_products_attributes', [
                    'product_id'   => $newProductId,
                    'attribute_id' => $row['attribute_id'],
                    'value_id'     => $row['value_id'],
                ] );
            }
        }

        // Kategorie
        $categories = $this->db->select( 'pp_shop_products_categories', '*', [ 'product_id' => $productId ] );
        if ( \Shared\Helpers\Helpers::is_array_fix( $categories ) ) {
            foreach ( $categories as $row ) {
                $this->db->insert( 'pp_shop_products_categories', [
                    'product_id'  => $newProductId,
                    'category_id' => $row['category_id'],
                    'o'           => $row['o'],
                ] );
            }
        }

        // Języki
        $langs = $this->db->select( 'pp_shop_products_langs', '*', [ 'product_id' => $productId ] );
        if ( \Shared\Helpers\Helpers::is_array_fix( $langs ) ) {
            foreach ( $langs as $row ) {
                $this->db->insert( 'pp_shop_products_langs', [
                    'product_id'               => $newProductId,
                    'lang_id'                  => $row['lang_id'],
                    'name'                     => $row['name'] . ' - kopia',
                    'short_description'        => $row['short_description'],
                    'description'              => $row['description'],
                    'tab_name_1'               => $row['tab_name_1'],
                    'tab_description_1'        => $row['tab_description_1'],
                    'tab_name_2'               => $row['tab_name_2'],
                    'tab_description_2'        => $row['tab_description_2'],
                    'meta_description'         => $row['meta_description'],
                    'meta_keywords'            => $row['meta_keywords'],
                    'copy_from'                => $row['copy_from'],
                    'warehouse_message_zero'   => $row['warehouse_message_zero'],
                    'warehouse_message_nonzero' => $row['warehouse_message_nonzero'],
                ] );
            }
        }

        // Custom fields
        $customFields = $this->db->select( 'pp_shop_products_custom_fields', '*', [ 'id_product' => $productId ] );
        if ( \Shared\Helpers\Helpers::is_array_fix( $customFields ) ) {
            foreach ( $customFields as $row ) {
                $this->db->insert( 'pp_shop_products_custom_fields', [
                    'id_product' => $newProductId,
                    'name'       => $row['name'],
                ] );
            }
        }

        // Duplikowanie kombinacji
        if ( $withCombinations ) {
            $productCombinations = $this->db->select( 'pp_shop_products', '*', [ 'parent_id' => $productId ] );
            if ( \Shared\Helpers\Helpers::is_array_fix( $productCombinations ) ) {
                foreach ( $productCombinations as $comb ) {
                    $this->db->insert( 'pp_shop_products', [
                        'parent_id'                    => $newProductId,
                        'permutation_hash'             => $comb['permutation_hash'],
                        'price_netto'                  => $comb['price_netto'],
                        'price_brutto'                 => $comb['price_brutto'],
                        'price_netto_promo'            => $comb['price_netto_promo'],
                        'price_brutto_promo'           => $comb['price_brutto_promo'],
                        'vat'                          => $comb['vat'],
                        'stock_0_buy'                  => $comb['stock_0_buy'],
                        'quantity'                     => $comb['quantity'],
                        'wp'                           => $comb['wp'],
                        'additional_message'           => $comb['additional_message'],
                        'additional_message_text'      => $comb['additional_message_text'],
                        'additional_message_required'  => $comb['additional_message_required'],
                    ] );

                    $combNewId = $this->db->id();
                    if ( $combNewId ) {
                        $combAttrs = $this->db->select( 'pp_shop_products_attributes', '*', [ 'product_id' => $comb['id'] ] );
                        if ( \Shared\Helpers\Helpers::is_array_fix( $combAttrs ) ) {
                            foreach ( $combAttrs as $attr ) {
                                $this->db->insert( 'pp_shop_products_attributes', [
                                    'product_id'   => $combNewId,
                                    'attribute_id' => $attr['attribute_id'],
                                    'value_id'     => $attr['value_id'],
                                ] );
                            }
                        }
                    }
                }
            }
        }

        return true;
    }

    /**
     * Przełącza status produktu (aktywny/nieaktywny).
     *
     * @param int $productId ID produktu
     * @return bool
     */
    public function toggleStatus(int $productId): bool
    {
        $status = $this->db->get( 'pp_shop_products', 'status', [ 'id' => $productId ] );
        $newStatus = $status == 1 ? 0 : 1;
        $this->db->update( 'pp_shop_products', [ 'status' => $newStatus ], [ 'id' => $productId ] );
        return true;
    }

    /**
     * Szybka zmiana ceny brutto produktu.
     *
     * @param int $productId ID produktu
     * @param mixed $price Nowa cena brutto
     * @return bool
     */
    public function updatePriceBrutto(int $productId, $price): bool
    {
        $vat = (float) $this->db->get( 'pp_shop_products', 'vat', [ 'id' => $productId ] );
        $priceNetto = \Shared\Helpers\Helpers::normalize_decimal( (float) $price / ( 100 + $vat ) * 100, 2 );

        $this->db->update( 'pp_shop_products', [
            'price_brutto' => $price != 0.00 ? $price : null,
            'price_netto'  => $priceNetto != 0.00 ? $priceNetto : null,
        ], [ 'id' => $productId ] );

        return true;
    }

    /**
     * Szybka zmiana ceny promocyjnej brutto produktu.
     *
     * @param int $productId ID produktu
     * @param mixed $price Nowa cena promo brutto
     * @return bool
     */
    public function updatePriceBruttoPromo(int $productId, $price): bool
    {
        $vat = (float) $this->db->get( 'pp_shop_products', 'vat', [ 'id' => $productId ] );
        $priceNetto = \Shared\Helpers\Helpers::normalize_decimal( (float) $price / ( 100 + $vat ) * 100, 2 );

        $this->db->update( 'pp_shop_products', [
            'price_brutto_promo' => $price != 0.00 ? $price : null,
            'price_netto_promo'  => $priceNetto != 0.00 ? $priceNetto : null,
        ], [ 'id' => $productId ] );

        return true;
    }

    /**
     * Szybka zmiana custom label produktu.
     *
     * @param int $productId ID produktu
     * @param string $label Numer labela (0-4)
     * @param mixed $value Wartość
     * @return bool
     */
    public function updateCustomLabel(int $productId, string $label, $value): bool
    {
        $this->db->update( 'pp_shop_products', [
            'custom_label_' . $label => $value ? $value : null,
        ], [ 'id' => $productId ] );

        return true;
    }

    // ─── Koniec kroku 3 ──────────────────────────────────────────────

    // ─── Krok 4: Kombinacje ──────────────────────────────────────────

    /**
     * Pobiera kombinacje produktu — lekka wersja do tabeli admin.
     * Jedno zapytanie SQL zamiast N×7 (findForAdmin per wiersz).
     *
     * @param int $productId ID produktu nadrzędnego
     * @return array<int, array{id: int, permutation_hash: string, sku: ?string, quantity: ?int, price_netto: ?float, stock_0_buy: int, parent_id: int}>
     */
    public function getCombinationsForTable(int $productId): array
    {
        $stmt = $this->db->query(
            'SELECT id, permutation_hash, sku, quantity, price_netto, stock_0_buy, parent_id
             FROM pp_shop_products
             WHERE parent_id = :pid
             ORDER BY id ASC',
            [ ':pid' => $productId ]
        );

        return $stmt ? $stmt->fetchAll( \PDO::FETCH_ASSOC ) : [];
    }

    /**
     * Pobiera permutacje (kombinacje) produktu z pełnymi danymi.
     *
     * @param int $productId ID produktu nadrzędnego
     * @return array
     */
    public function getPermutations(int $productId): array
    {
        $products = [];
        $results = $this->db->select( 'pp_shop_products', 'id', [ 'parent_id' => $productId ] );
        if ( \Shared\Helpers\Helpers::is_array_fix( $results ) ) {
            foreach ( $results as $row ) {
                $detail = $this->findForAdmin( (int) $row );
                if ( $detail ) {
                    $products[] = $detail;
                }
            }
        }
        return $products;
    }

    /**
     * Generuje kombinacje produktu na podstawie atrybutów.
     *
     * @param int $productId ID produktu nadrzędnego
     * @param array $attributes Tablica atrybutów [attribute_id => [value_id, ...], ...]
     * @return bool
     */
    public function generateCombinations(int $productId, array $attributes): bool
    {
        $vat = $this->db->get( 'pp_shop_products', 'vat', [ 'id' => $productId ] );
        $attributeRepository = new \Domain\Attribute\AttributeRepository( $this->db );

        $permutations = \shop\Product::array_cartesian( $attributes );
        if ( !\Shared\Helpers\Helpers::is_array_fix( $permutations ) ) {
            return true;
        }

        foreach ( $permutations as $permutation ) {
            $product = null;
            ksort( $permutation );

            $permutationHash = '';

            if ( \Shared\Helpers\Helpers::is_array_fix( $permutation ) ) {
                foreach ( $permutation as $key => $val ) {
                    if ( $permutationHash ) {
                        $permutationHash .= '|';
                    }
                    $permutationHash .= $key . '-' . $val;

                    $valueDetails = $attributeRepository->valueDetails( (int) $val );
                    $impactOnPrice = $valueDetails['impact_on_the_price'];

                    if ( $impactOnPrice > 0 ) {
                        if ( !$product ) {
                            $product = $this->findForAdmin( $productId );
                        }

                        $productPriceBrutto = $product['price_brutto'] + $impactOnPrice;
                        $productPriceNetto = $productPriceBrutto / ( 1 + ( $product['vat'] / 100 ) );

                        if ( $product['price_brutto_promo'] ) {
                            $productPriceBruttoPromo = $product['price_brutto_promo'] + $impactOnPrice;
                            $productPriceNettoPromo = $productPriceBruttoPromo / ( 1 + ( $product['vat'] / 100 ) );
                        } else {
                            $productPriceBruttoPromo = null;
                            $productPriceNettoPromo = null;
                        }
                    }

                    if ( $permutationHash && !$this->db->count( 'pp_shop_products', [ 'AND' => [ 'parent_id' => $productId, 'permutation_hash' => $permutationHash ] ] ) ) {
                        if ( $this->db->insert( 'pp_shop_products', [ 'parent_id' => $productId, 'permutation_hash' => $permutationHash, 'vat' => $vat ] ) ) {
                            $combinationId = $this->db->id();
                            if ( $product ) {
                                $this->db->update( 'pp_shop_products', [
                                    'price_netto'        => $productPriceNetto,
                                    'vat'                => $product['vat'],
                                    'price_brutto'       => $productPriceBrutto,
                                    'price_netto_promo'  => $productPriceNettoPromo ?? null,
                                    'price_brutto_promo' => $productPriceBruttoPromo ?? null,
                                ], [ 'id' => $combinationId ] );
                            }

                            $hashRows = explode( '|', $permutationHash );
                            foreach ( $hashRows as $hashRow ) {
                                $attrRev = explode( '-', $hashRow );
                                $this->db->insert( 'pp_shop_products_attributes', [
                                    'product_id'   => $combinationId,
                                    'attribute_id' => $attrRev[0],
                                    'value_id'     => $attrRev[1],
                                ] );
                            }
                        }
                    }
                }
            }
        }
        return true;
    }

    /**
     * Usuwa kombinację produktu.
     *
     * @param int $combinationId ID kombinacji
     * @return bool
     */
    public function deleteCombination(int $combinationId): bool
    {
        $this->db->delete( 'pp_shop_products', [ 'id' => $combinationId ] );
        $this->db->delete( 'pp_shop_products_attributes', [ 'product_id' => $combinationId ] );
        return true;
    }

    /**
     * Liczy kombinacje produktu.
     *
     * @param int $productId ID produktu nadrzędnego
     * @return int
     */
    public function countCombinations(int $productId): int
    {
        return (int) $this->db->count( 'pp_shop_products', [ 'parent_id' => $productId ] );
    }

    /**
     * Zapisuje stock_0_buy kombinacji.
     */
    public function saveCombinationStock0Buy(int $productId, $value): bool
    {
        $this->db->update( 'pp_shop_products', [ 'stock_0_buy' => $value == 'true' ? 1 : 0 ], [ 'id' => $productId ] );
        return true;
    }

    /**
     * Zapisuje SKU kombinacji.
     */
    public function saveCombinationSku(int $productId, $sku): bool
    {
        $this->db->update( 'pp_shop_products', [ 'sku' => $sku ], [ 'id' => $productId ] );
        return true;
    }

    /**
     * Zapisuje ilość kombinacji.
     */
    public function saveCombinationQuantity(int $productId, $quantity): bool
    {
        $this->db->update( 'pp_shop_products', [
            'quantity' => $quantity == '' ? null : (int) $quantity,
        ], [ 'id' => $productId ] );
        return true;
    }

    /**
     * Zapisuje cenę kombinacji (z przeliczeniem netto -> brutto).
     */
    public function saveCombinationPrice(int $productId, $priceNetto): bool
    {
        $vat = $this->db->get( 'pp_shop_products', 'vat', [ 'id' => $productId ] );
        $priceBrutto = (float) $priceNetto * ( 1 + ( (float) $vat / 100 ) );

        $this->db->update( 'pp_shop_products', [
            'price_netto'  => $priceNetto == '' ? null : (float) $priceNetto,
            'price_brutto' => $priceBrutto == '' ? null : (float) $priceBrutto,
        ], [ 'id' => $productId ] );

        return true;
    }

    // ─── Koniec kroku 4 ──────────────────────────────────────────────

    // ─── Krok 5: Zdjęcia / Pliki / XML ──────────────────────────────

    /**
     * Oznacza zdjęcie do usunięcia.
     */
    public function deleteImage(int $imageId): bool
    {
        $this->db->update( 'pp_shop_products_images', [ 'to_delete' => 1 ], [ 'id' => $imageId ] );
        return true;
    }

    /**
     * Zmienia atrybut alt zdjęcia.
     */
    public function updateImageAlt(int $imageId, string $alt): bool
    {
        $result = $this->db->update( 'pp_shop_products_images', [ 'alt' => $alt ], [ 'id' => $imageId ] );
        \Shared\Helpers\Helpers::delete_cache();
        return (bool) $result;
    }

    /**
     * Zapisuje kolejność zdjęć.
     */
    public function saveImagesOrder(int $productId, string $order): bool
    {
        $orderArr = explode( ';', $order );
        if ( is_array( $orderArr ) && !empty( $orderArr ) ) {
            $i = 0;
            foreach ( $orderArr as $imageId ) {
                $this->db->update( 'pp_shop_products_images', [
                    'o' => $i++,
                ], [
                    'AND' => [
                        'product_id' => $productId,
                        'id'         => $imageId,
                    ],
                ] );
            }
        }
        return true;
    }

    /**
     * Usuwa nieprzypisane zdjęcia (product_id = null).
     */
    public function deleteNonassignedImages(): void
    {
        $results = $this->db->select( 'pp_shop_products_images', '*', [ 'product_id' => null ] );
        if ( is_array( $results ) ) {
            foreach ( $results as $row ) {
                if ( file_exists( '../' . $row['src'] ) ) {
                    unlink( '../' . $row['src'] );
                }
            }
        }
        $this->db->delete( 'pp_shop_products_images', [ 'product_id' => null ] );
    }

    /**
     * Oznacza plik do usunięcia.
     */
    public function deleteFile(int $fileId): bool
    {
        $this->db->update( 'pp_shop_products_files', [ 'to_delete' => 1 ], [ 'id' => $fileId ] );
        return true;
    }

    /**
     * Zmienia nazwę pliku.
     */
    public function updateFileName(int $fileId, string $name): bool
    {
        $this->db->update( 'pp_shop_products_files', [ 'name' => $name ], [ 'id' => $fileId ] );
        return true;
    }

    /**
     * Usuwa nieprzypisane pliki (product_id = null).
     */
    public function deleteNonassignedFiles(): void
    {
        $results = $this->db->select( 'pp_shop_products_files', '*', [ 'product_id' => null ] );
        if ( is_array( $results ) ) {
            foreach ( $results as $row ) {
                if ( file_exists( '../' . $row['src'] ) ) {
                    unlink( '../' . $row['src'] );
                }
            }
        }
        $this->db->delete( 'pp_shop_products_files', [ 'product_id' => null ] );
    }

    /**
     * Pobiera zdjęcia produktu.
     */
    public function getProductImages(int $productId): array
    {
        return $this->db->select( 'pp_shop_products_images', 'src', [
            'product_id' => $productId,
            'ORDER'      => [ 'o' => 'ASC', 'id' => 'ASC' ],
        ] ) ?: [];
    }

    /**
     * Zapisuje nazwę XML produktu.
     */
    public function saveXmlName(int $productId, string $xmlName, string $langId): bool
    {
        return (bool) $this->db->update( 'pp_shop_products_langs', [
            'xml_name' => $xmlName,
        ], [
            'AND' => [ 'product_id' => $productId, 'lang_id' => $langId ],
        ] );
    }

    /**
     * Pobiera sugestie custom label.
     */
    public function customLabelSuggestions(string $customLabel, string $labelType): array
    {
        $output = [];
        $results = $this->db->query(
            'SELECT DISTINCT ' . $labelType . ' AS label FROM pp_shop_products WHERE ' . $labelType . ' LIKE :custom_label LIMIT 10',
            [ ':custom_label' => '%' . $customLabel . '%' ]
        );
        if ( $results ) {
            foreach ( $results->fetchAll( \PDO::FETCH_ASSOC ) as $row ) {
                $output[] = $row;
            }
        }
        return $output;
    }

    /**
     * Zapisuje custom label produktu.
     */
    public function saveCustomLabel(int $productId, string $customLabel, string $labelType): bool
    {
        return (bool) $this->db->update( 'pp_shop_products', [ $labelType => $customLabel ], [ 'id' => $productId ] );
    }

    /**
     * Generuje kod EAN.
     */
    public function generateEAN(string $number): string
    {
        $code = '200' . str_pad( $number, 9, '0' );
        $weightflag = true;
        $sum = 0;

        for ( $i = strlen( $code ) - 1; $i >= 0; $i-- ) {
            $sum += (int) $code[$i] * ( $weightflag ? 3 : 1 );
            $weightflag = !$weightflag;
        }

        $code .= ( 10 - ( $sum % 10 ) ) % 10;
        return $code;
    }

    /**
     * Generuje Google Feed XML.
     */
    public function generateGoogleFeedXml(): void
    {
        global $lang_id;

        $settings = ( new \Domain\Settings\SettingsRepository( $this->db ) )->allSettings( true );

        $domainPrefix = 'https';
        $url = preg_replace( '#^(http(s)?://)?w{3}\.#', '$1', $_SERVER['SERVER_NAME'] );

        $doc = new \DOMDocument( '1.0', 'UTF-8' );
        $xmlRoot = $doc->createElement( 'rss' );
        $doc->appendChild( $xmlRoot );
        $xmlRoot->setAttribute( 'version', '2.0' );
        $xmlRoot->setAttributeNS( 'http://www.w3.org/2000/xmlns/', 'xmlns:g', 'http://base.google.com/ns/1.0' );
        $channelNode = $xmlRoot->appendChild( $doc->createElement( 'channel' ) );
        $channelNode->appendChild( $doc->createElement( 'title', $settings['firm_name'] ) );
        $channelNode->appendChild( $doc->createElement( 'link', $domainPrefix . '://' . $url ) );

        $rows = $this->db->select( 'pp_shop_products', 'id', [
            'AND' => [ 'status' => '1', 'archive' => 0, 'parent_id' => null ],
        ] );

        if ( \Shared\Helpers\Helpers::is_array_fix( $rows ) ) {
            foreach ( $rows as $productId ) {
                $product = \shop\Product::getFromCache( $productId, $lang_id );

                if ( is_array( $product->product_combinations ) && count( $product->product_combinations ) ) {
                    foreach ( $product->product_combinations as $productCombination ) {
                        if ( $productCombination->quantity !== null || $productCombination->stock_0_buy ) {
                            $this->appendCombinationToXml( $doc, $channelNode, $product, $productCombination, $domainPrefix, $url );
                        }
                    }
                } else {
                    $this->appendProductToXml( $doc, $channelNode, $product, $domainPrefix, $url );
                }
            }
        }

        file_put_contents( '../google-feed.xml', $doc->saveXML() );
    }

    /**
     * Dodaje kombinację produktu do XML feed.
     */
    private function appendCombinationToXml(\DOMDocument $doc, \DOMElement $channelNode, $product, $combination, string $domainPrefix, string $url): void
    {
        $itemNode = $channelNode->appendChild( $doc->createElement( 'item' ) );
        $itemNode->appendChild( $doc->createElement( 'g:id', $combination->id ) );
        $itemNode->appendChild( $doc->createElement( 'g:item_group_id', $product->id ) );

        for ( $l = 0; $l <= 4; $l++ ) {
            $label = 'custom_label_' . $l;
            if ( $product->$label ) {
                $itemNode->appendChild( $doc->createElement( 'g:' . $label, $product->$label ) );
            }
        }

        $title = $product->language['xml_name'] ?: $product->language['name'];
        $itemNode->appendChild( $doc->createElement( 'title', str_replace( '&', '&amp;', $title ) . ' - ' . $product->generateSubtitleFromAttributes( $combination->permutation_hash ) ) );

        $gtin = $product->ean ?: $this->generateEAN( $product->id );
        $itemNode->appendChild( $doc->createElement( 'g:gtin', $gtin ) );

        $desc = $product->language['short_description'] ?: $product->language['name'];
        $itemNode->appendChild( $doc->createElement( 'g:description', html_entity_decode( strip_tags( $desc ) ) ) );

        if ( $product->language['seo_link'] ) {
            $link = $domainPrefix . '://' . $url . '/' . \Shared\Helpers\Helpers::seo( $product->language['seo_link'] ) . '/' . str_replace( '|', '/', $combination->permutation_hash );
        } else {
            $link = $domainPrefix . '://' . $url . '/p-' . $product->id . '-' . \Shared\Helpers\Helpers::seo( $product->language['name'] ) . '/' . str_replace( '|', '/', $combination->permutation_hash );
        }
        $itemNode->appendChild( $doc->createElement( 'link', $link ) );

        $this->appendImagesToXml( $doc, $itemNode, $product, $domainPrefix, $url );

        $itemNode->appendChild( $doc->createElement( 'g:condition', 'new' ) );

        if ( $combination->quantity !== null ) {
            if ( $combination->quantity > 0 ) {
                $itemNode->appendChild( $doc->createElement( 'g:availability', 'in stock' ) );
                $itemNode->appendChild( $doc->createElement( 'g:quantity', $combination->quantity ) );
            } else {
                $itemNode->appendChild( $doc->createElement( 'g:availability', $combination->stock_0_buy ? 'in stock' : 'out of stock' ) );
            }
        } else {
            if ( $product->quantity > 0 ) {
                $itemNode->appendChild( $doc->createElement( 'g:availability', 'in stock' ) );
                $itemNode->appendChild( $doc->createElement( 'g:quantity', $product->quantity ) );
            } else {
                $itemNode->appendChild( $doc->createElement( 'g:availability', $product->stock_0_buy ? 'in stock' : 'out of stock' ) );
                $itemNode->appendChild( $doc->createElement( 'g:quantity', $product->stock_0_buy ? 999 : 0 ) );
            }
        }

        if ( $combination->price_brutto ) {
            $itemNode->appendChild( $doc->createElement( 'g:price', $combination->price_brutto . ' PLN' ) );
            if ( $combination->price_brutto_promo ) {
                $itemNode->appendChild( $doc->createElement( 'g:sale_price', $combination->price_brutto_promo . ' PLN' ) );
            }
        } else {
            $itemNode->appendChild( $doc->createElement( 'g:price', $product->price_brutto . ' PLN' ) );
            if ( $product->price_brutto_promo ) {
                $itemNode->appendChild( $doc->createElement( 'g:sale_price', $product->price_brutto_promo . ' PLN' ) );
            }
        }

        $this->appendShippingToXml( $doc, $itemNode, $product );
    }

    /**
     * Dodaje produkt (bez kombinacji) do XML feed.
     */
    private function appendProductToXml(\DOMDocument $doc, \DOMElement $channelNode, $product, string $domainPrefix, string $url): void
    {
        $itemNode = $channelNode->appendChild( $doc->createElement( 'item' ) );
        $itemNode->appendChild( $doc->createElement( 'g:id', $product->id ) );
        $itemNode->appendChild( $doc->createElement( 'g:item_group_id', $product->id ) );

        if ( $product->google_xml_label ) {
            $itemNode->appendChild( $doc->createElement( 'g:custom_label_0', $product->google_xml_label ) );
        }

        $title = $product->language['xml_name'] ?: $product->language['name'];
        $itemNode->appendChild( $doc->createElement( 'title', str_replace( '&', '&amp;', $title ) ) );

        $gtin = $product->ean ?: $this->generateEAN( $product->id );
        $itemNode->appendChild( $doc->createElement( 'g:gtin', $gtin ) );

        $desc = $product->language['short_description'] ?: $product->language['name'];
        $itemNode->appendChild( $doc->createElement( 'g:description', html_entity_decode( strip_tags( $desc ) ) ) );

        if ( $product->language['seo_link'] ) {
            $link = $domainPrefix . '://' . $url . '/' . \Shared\Helpers\Helpers::seo( $product->language['seo_link'] );
        } else {
            $link = $domainPrefix . '://' . $url . '/p-' . $product->id . '-' . \Shared\Helpers\Helpers::seo( $product->language['name'] );
        }
        $itemNode->appendChild( $doc->createElement( 'link', $link ) );

        for ( $l = 0; $l <= 4; $l++ ) {
            $label = 'custom_label_' . $l;
            if ( $product->$label ) {
                $itemNode->appendChild( $doc->createElement( 'g:' . $label, $product->$label ) );
            }
        }

        $this->appendImagesToXml( $doc, $itemNode, $product, $domainPrefix, $url );

        $itemNode->appendChild( $doc->createElement( 'g:condition', 'new' ) );

        if ( $product->quantity ) {
            $itemNode->appendChild( $doc->createElement( 'g:availability', 'in stock' ) );
            $itemNode->appendChild( $doc->createElement( 'g:quantity', $product->quantity ) );
        } else {
            if ( $product->stock_0_buy ) {
                $itemNode->appendChild( $doc->createElement( 'g:availability', 'in stock' ) );
                $itemNode->appendChild( $doc->createElement( 'g:quantity', 999 ) );
            } else {
                $itemNode->appendChild( $doc->createElement( 'g:availability', 'out of stock' ) );
                $itemNode->appendChild( $doc->createElement( 'g:quantity', 0 ) );
            }
        }

        $itemNode->appendChild( $doc->createElement( 'g:price', $product->price_brutto . ' PLN' ) );
        if ( $product->price_brutto_promo ) {
            $itemNode->appendChild( $doc->createElement( 'g:sale_price', $product->price_brutto_promo . ' PLN' ) );
        }

        $this->appendShippingToXml( $doc, $itemNode, $product );
    }

    /**
     * Dodaje zdjęcia do node XML.
     */
    private function appendImagesToXml(\DOMDocument $doc, \DOMElement $itemNode, $product, string $domainPrefix, string $url): void
    {
        if ( isset( $product->images[0] ) ) {
            $itemNode->appendChild( $doc->createElement( 'g:image_link', $domainPrefix . '://' . $url . $product->images[0]['src'] ) );
        }
        if ( count( $product->images ) > 1 ) {
            for ( $i = 1; $i < count( $product->images ); ++$i ) {
                $itemNode->appendChild( $doc->createElement( 'g:additional_image_link', $domainPrefix . '://' . $url . $product->images[$i]['src'] ) );
            }
        }
    }

    /**
     * Dodaje sekcję shipping do node XML.
     */
    private function appendShippingToXml(\DOMDocument $doc, \DOMElement $itemNode, $product): void
    {
        $shippingNode = $itemNode->appendChild( $doc->createElement( 'g:shipping' ) );
        $shippingNode->appendChild( $doc->createElement( 'g:country', 'PL' ) );
        $shippingNode->appendChild( $doc->createElement( 'g:service', '1 dzień roboczy' ) );
        $shippingNode->appendChild( $doc->createElement( 'g:price',
            ( new \Domain\Transport\TransportRepository( $this->db ) )->lowestTransportPrice( (int) $product->wp ) . ' PLN'
        ) );
    }

    // ─── Koniec kroku 5 ──────────────────────────────────────────────

    /**
     * Aktualizuje ceny kombinacji produktu na podstawie cen brutto bazowych.
     * Wywoływane z saveProduct() i z cron.php.
     *
     * @param int $productId ID produktu nadrzędnego
     * @param float $priceBrutto Cena brutto bazowa
     * @param float $vat Stawka VAT
     * @param float|null $priceBruttoPromo Cena promo brutto bazowa (null = brak)
     */
    public function updateCombinationPricesFromBase(int $productId, $priceBrutto, $vat, $priceBruttoPromo): void
    {
        $priceBrutto = (float) $priceBrutto;
        $vat = (float) $vat;
        $priceBruttoPromo = $priceBruttoPromo ? (float) $priceBruttoPromo : null;

        $combinations = $this->db->query(
            'SELECT psp.id '
            . 'FROM pp_shop_products AS psp '
            . 'INNER JOIN pp_shop_products_attributes AS pspa ON psp.id = pspa.product_id '
            . 'INNER JOIN pp_shop_attributes_values AS psav ON pspa.value_id = psav.id '
            . 'WHERE psav.impact_on_the_price > 0 AND psp.parent_id = :product_id',
            [ ':product_id' => $productId ]
        );

        if ( !$combinations ) {
            return;
        }

        $rows = $combinations->fetchAll( \PDO::FETCH_ASSOC );
        foreach ( $rows as $row ) {
            $combBrutto = $priceBrutto;
            $combBruttoPromo = $priceBruttoPromo;

            $values = $this->db->query(
                'SELECT impact_on_the_price FROM pp_shop_attributes_values AS psav '
                . 'INNER JOIN pp_shop_products_attributes AS pspa ON pspa.value_id = psav.id '
                . 'WHERE impact_on_the_price IS NOT NULL AND product_id = :product_id',
                [ ':product_id' => $row['id'] ]
            );

            if ( $values ) {
                foreach ( $values->fetchAll( \PDO::FETCH_ASSOC ) as $value ) {
                    $combBrutto += $value['impact_on_the_price'];
                    if ( $combBruttoPromo !== null ) {
                        $combBruttoPromo += $value['impact_on_the_price'];
                    } else {
                        $combBruttoPromo = null;
                    }
                }
            }

            $combNetto = \Shared\Helpers\Helpers::normalize_decimal( $combBrutto / ( 100 + $vat ) * 100, 2 );
            $combNettoPromo = $combBruttoPromo !== null
                ? \Shared\Helpers\Helpers::normalize_decimal( $combBruttoPromo / ( 100 + $vat ) * 100, 2 )
                : null;

            $this->db->update( 'pp_shop_products', [
                'price_netto' => $combNetto,
                'price_brutto' => $combBrutto,
                'price_netto_promo' => $combNettoPromo,
                'price_brutto_promo' => $combBruttoPromo
            ], [ 'id' => $row['id'] ] );
        }
    }

    /**
     * Aktualizuje ceny kombinacji produktu uwzględniając wpływ na cenę (impact_on_the_price).
     * Wersja prywatna — przyjmuje ceny netto (dla mass_edit / applyDiscountPercent).
     *
     * @param int $productId ID produktu nadrzędnego
     * @param float $priceNetto Cena netto bazowa
     * @param float $vat Stawka VAT
     * @param float|null $priceNettoPromo Cena promo netto bazowa (null = brak)
     */
    private function updateCombinationPrices(int $productId, float $priceNetto, float $vat, ?float $priceNettoPromo): void
    {
        $priceBrutto = \Shared\Helpers\Helpers::normalize_decimal( $priceNetto * ( 100 + $vat ) / 100, 2 );
        $priceBruttoPromo = $priceNettoPromo !== null
            ? \Shared\Helpers\Helpers::normalize_decimal( $priceNettoPromo * ( 100 + $vat ) / 100, 2 )
            : null;

        $combinations = $this->db->query(
            'SELECT psp.id '
            . 'FROM pp_shop_products AS psp '
            . 'INNER JOIN pp_shop_products_attributes AS pspa ON psp.id = pspa.product_id '
            . 'INNER JOIN pp_shop_attributes_values AS psav ON pspa.value_id = psav.id '
            . 'WHERE psav.impact_on_the_price > 0 AND psp.parent_id = :product_id',
            [ ':product_id' => $productId ]
        );

        if ( !$combinations ) {
            return;
        }

        $rows = $combinations->fetchAll( \PDO::FETCH_ASSOC );
        foreach ( $rows as $row ) {
            $combBrutto = $priceBrutto;
            $combBruttoPromo = $priceBruttoPromo;

            $values = $this->db->query(
                'SELECT impact_on_the_price FROM pp_shop_attributes_values AS psav '
                . 'INNER JOIN pp_shop_products_attributes AS pspa ON pspa.value_id = psav.id '
                . 'WHERE impact_on_the_price IS NOT NULL AND product_id = :product_id',
                [ ':product_id' => $row['id'] ]
            );

            if ( $values ) {
                foreach ( $values->fetchAll( \PDO::FETCH_ASSOC ) as $value ) {
                    $combBrutto += $value['impact_on_the_price'];
                    if ( $combBruttoPromo !== null ) {
                        $combBruttoPromo += $value['impact_on_the_price'];
                    }
                }
            }

            $combNetto = \Shared\Helpers\Helpers::normalize_decimal( $combBrutto / ( 100 + $vat ) * 100, 2 );
            $combNettoPromo = $combBruttoPromo !== null
                ? \Shared\Helpers\Helpers::normalize_decimal( $combBruttoPromo / ( 100 + $vat ) * 100, 2 )
                : null;

            $this->db->update( 'pp_shop_products', [
                'price_netto' => $combNetto,
                'price_brutto' => $combBrutto,
                'price_netto_promo' => $combNettoPromo,
                'price_brutto_promo' => $combBruttoPromo
            ], [ 'id' => $row['id'] ] );
        }
    }
}
